- Enhanced UnifiedExceptionFilter to handle ZodValidationException, extracting field errors for better user feedback. - Updated WhmcsRequestQueueService and WhmcsHttpClientService to use logger.warn for non-critical errors, improving log clarity. - Introduced Redis-backed caching in JapanPostFacade for postal code lookups, reducing redundant API calls. - Refactored address handling in AddressWriterService to deduplicate concurrent Japan Post lookups, optimizing API usage. - Improved error parsing in various forms and hooks to provide clearer error messages and field-specific feedback.
17 KiB
Auth Flow Simplification Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Simplify auth flows: merge duplicate signup workflows, fix missing SF Birthdate/Sex__c, remove legacy migrate endpoint, clean up OTP infrastructure, delete dead code.
Architecture: 3 phases (can stop after any phase). Phase 1 merges signup workflows + cleans up. Phase 2 removes legacy WHMCS migration. Phase 3 extracts shared OTP pattern.
Tech Stack: NestJS 11, TypeScript, Prisma, Salesforce jsforce, WHMCS API, argon2, BullMQ
Design doc: docs/plans/2026-03-03-signup-flow-simplification-design.md
Phase 1: Merge signup workflows + cleanup
Task 1: Move signup shared services into steps/
Move SignupUserCreationService and SignupWhmcsService from signup/ into steps/, renaming to remove the "Signup" prefix. Update all imports.
Files:
- Create:
apps/bff/src/modules/auth/infra/workflows/steps/portal-user-creation.service.ts - Create:
apps/bff/src/modules/auth/infra/workflows/steps/whmcs-cleanup.service.ts - Modify:
apps/bff/src/modules/auth/infra/workflows/steps/create-portal-user.step.ts - Modify:
apps/bff/src/modules/auth/infra/workflows/steps/index.ts - Modify:
apps/bff/src/modules/auth/infra/workflows/whmcs-migration-workflow.service.ts - Modify:
apps/bff/src/modules/auth/get-started/get-started.module.ts
Step 1: Create portal-user-creation.service.ts
Copy signup/signup-user-creation.service.ts to steps/portal-user-creation.service.ts:
- Rename class:
SignupUserCreationService→PortalUserCreationService - Update import:
SignupWhmcsService→WhmcsCleanupServicefrom./whmcs-cleanup.service.js
Step 2: Create whmcs-cleanup.service.ts
Copy signup/signup-whmcs.service.ts to steps/whmcs-cleanup.service.ts:
- Rename class:
SignupWhmcsService→WhmcsCleanupService - Keep only
markClientForCleanup()method - Delete:
createClient(),checkExistingClient(),validateAddressData()(unused by active code) - Remove unused imports:
SignupRequest,serializeWhmcsKeyValueMap,DomainHttpException,ErrorCode,ConfigService,MappingsService,WhmcsAccountDiscoveryService - Constructor only needs:
WhmcsClientService+Logger
Step 3: Update create-portal-user.step.ts import
Line 3 — FROM: import { SignupUserCreationService } from "../signup/signup-user-creation.service.js";
Line 3 — TO: import { PortalUserCreationService } from "./portal-user-creation.service.js";
Update constructor param: signupUserCreation → portalUserCreation, update usage on line 36.
Step 4: Update steps/index.ts barrel
Add at end:
export { PortalUserCreationService } from "./portal-user-creation.service.js";
export type { UserCreationParams, CreatedUserResult } from "./portal-user-creation.service.js";
export { WhmcsCleanupService } from "./whmcs-cleanup.service.js";
Step 5: Update whmcs-migration-workflow.service.ts import
Line 24 — FROM: import { SignupUserCreationService } from "./signup/signup-user-creation.service.js";
Line 24 — TO: import { PortalUserCreationService } from "./steps/portal-user-creation.service.js";
Update constructor param and usage accordingly. (Task 3 will refactor this further to use CreatePortalUserStep.)
Step 6: Update get-started.module.ts
- Remove imports:
SignupAccountResolverService,SignupValidationService,SignupWhmcsService,SignupUserCreationService - Add imports:
PortalUserCreationService,WhmcsCleanupServicefrom../infra/workflows/steps/index.js - Remove from providers:
SignupAccountResolverService,SignupValidationService,SignupWhmcsService,SignupUserCreationService - Add to providers:
PortalUserCreationService,WhmcsCleanupService
Step 7: Run type-check
Run: pnpm type-check
Expected: PASS
Step 8: Commit
git add apps/bff/src/modules/auth/infra/workflows/steps/portal-user-creation.service.ts \
apps/bff/src/modules/auth/infra/workflows/steps/whmcs-cleanup.service.ts \
apps/bff/src/modules/auth/infra/workflows/steps/create-portal-user.step.ts \
apps/bff/src/modules/auth/infra/workflows/steps/index.ts \
apps/bff/src/modules/auth/infra/workflows/whmcs-migration-workflow.service.ts \
apps/bff/src/modules/auth/get-started/get-started.module.ts
git commit -m "refactor: move signup shared services into steps directory"
Task 2: Create merged AccountCreationWorkflowService
Merge SfCompletionWorkflowService + NewCustomerSignupWorkflowService into one workflow. Fix Birthdate/Sex__c gap.
Files:
- Create:
apps/bff/src/modules/auth/infra/workflows/account-creation-workflow.service.ts
Step 1: Create the merged workflow
Constructor dependencies (union of both old workflows):
ConfigService(email template IDs, APP_BASE_URL)GetStartedSessionServiceDistributedLockServiceUsersServiceEmailService(welcome email in eligibility path)SalesforceAccountService(forcreateContact()— Birthdate/Sex__c)AddressWriterServiceResolveSalesforceAccountStepCreateEligibilityCaseStepCreateWhmcsClientStepCreatePortalUserStepUpdateSalesforceFlagsStepGenerateAuthResultStepLogger
Public API:
execute(
request: CompleteAccountRequest | SignupWithEligibilityRequest,
options?: { withEligibility?: boolean }
): Promise<AuthResultInternal | SignupWithEligibilityResult>
Step sequence in executeCreation():
- Check existing accounts (Portal user duplicate check)
- Hash password (argon2)
- Resolve address + names (from request or session prefill — port
resolveAddress()andresolveNames()from sf-completion) - Step 1: Resolve SF Account (CRITICAL) —
sourcedepends onwithEligibility - Step 1.5: Write SF Address (DEGRADABLE via
safeOperation) - Step 1.6: Create SF Contact (DEGRADABLE via
safeOperation) — NEW: fixes Birthdate/Sex__c- Call
salesforceAccountService.createContact({ accountId, firstName, lastName, email, phone, gender, dateOfBirth })
- Call
- Step 2 (conditional): Create Eligibility Case (DEGRADABLE, only if
withEligibility) - Step 3: Create WHMCS Client (CRITICAL)
- Step 4: Create Portal User (CRITICAL, rollback WHMCS on fail)
- Step 5: Update SF Flags (DEGRADABLE)
- Step 6: Generate Auth Result
- (conditional) Send welcome email (DEGRADABLE, only if
withEligibility) — portsendWelcomeWithEligibilityEmail()from new-customer-signup - Invalidate session
Error handling per path:
withEligibility=true: wrap in try/catch, return{ success: false, errorCategory, message }usingclassifyError()(fromworkflow-error.util.ts)withEligibility=false(default): let exceptions propagate directly (matching current sf-completion behavior)
Session handling:
- Use
acquireAndMarkAsUsed(sessionToken, withEligibility ? "signup_with_eligibility" : "complete_account") isNewCustomer = !session.sfAccountId(same logic as sf-completion)- For
isNewCustomer: require firstName, lastName, address from request - For SF_UNMAPPED: use session prefill data as fallback
Port private helpers from sf-completion:
validateRequest()— checks isNewCustomer needs firstName/lastName/addressensureNoExistingAccounts()— Portal user duplicate checkresolveAddress()— from request or sessionresolveNames()— from request or session
Step 2: Run type-check
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/bff/src/modules/auth/infra/workflows/account-creation-workflow.service.ts
git commit -m "feat: add merged AccountCreationWorkflowService with Birthdate/Sex__c fix"
Task 3: Rename and refactor AccountMigrationWorkflowService
Rename WhmcsMigrationWorkflowService → AccountMigrationWorkflowService. Refactor to use CreatePortalUserStep instead of SignupUserCreationService directly.
Files:
- Create:
apps/bff/src/modules/auth/infra/workflows/account-migration-workflow.service.ts
Step 1: Create renamed + refactored file
Copy whmcs-migration-workflow.service.ts → account-migration-workflow.service.ts:
- Rename class:
WhmcsMigrationWorkflowService→AccountMigrationWorkflowService - Replace
PortalUserCreationService(orSignupUserCreationServiceif Task 1 import not yet updated) withCreatePortalUserStep - Constructor: remove
userCreation: PortalUserCreationService, addportalUserStep: CreatePortalUserStep - In
executeMigration(): replacethis.userCreation.createUserWithMapping({...})withthis.portalUserStep.execute({...}) - Update imports accordingly
Step 2: Run type-check
Run: pnpm type-check
Expected: PASS
Step 3: Commit
git add apps/bff/src/modules/auth/infra/workflows/account-migration-workflow.service.ts
git commit -m "refactor: rename WhmcsMigration to AccountMigration, use CreatePortalUserStep"
Task 4: Update coordinator and module
Wire new services, remove old references.
Files:
- Modify:
apps/bff/src/modules/auth/infra/workflows/get-started-coordinator.service.ts - Modify:
apps/bff/src/modules/auth/get-started/get-started.module.ts
Step 1: Update coordinator
Imports:
- Remove:
NewCustomerSignupWorkflowService,SfCompletionWorkflowService,WhmcsMigrationWorkflowService - Add:
AccountCreationWorkflowService,AccountMigrationWorkflowService
Constructor:
- Remove:
newCustomerSignup,sfCompletion,whmcsMigration - Add:
accountCreation: AccountCreationWorkflowService,accountMigration: AccountMigrationWorkflowService
Methods:
completeAccount():return this.accountCreation.execute(request);signupWithEligibility():return this.accountCreation.execute(request, { withEligibility: true });migrateWhmcsAccount():return this.accountMigration.execute(request);
Step 2: Update module providers
Imports:
- Remove:
NewCustomerSignupWorkflowService,SfCompletionWorkflowService,WhmcsMigrationWorkflowService - Add:
AccountCreationWorkflowService,AccountMigrationWorkflowService
Providers:
- Remove:
NewCustomerSignupWorkflowService,SfCompletionWorkflowService,WhmcsMigrationWorkflowService - Add:
AccountCreationWorkflowService,AccountMigrationWorkflowService
Step 3: Run type-check
Run: pnpm type-check
Expected: PASS
Step 4: Commit
git add apps/bff/src/modules/auth/infra/workflows/get-started-coordinator.service.ts \
apps/bff/src/modules/auth/get-started/get-started.module.ts
git commit -m "refactor: wire AccountCreation and AccountMigration into coordinator and module"
Task 5: Delete old files
Step 1: Delete old workflow files
rm apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts
rm apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts
rm apps/bff/src/modules/auth/infra/workflows/whmcs-migration-workflow.service.ts
Step 2: Delete entire signup/ directory
rm -rf apps/bff/src/modules/auth/infra/workflows/signup/
Step 3: Run type-check and tests
Run: pnpm type-check && pnpm --filter @customer-portal/bff test
Expected: PASS
Step 4: Commit
git add -A
git commit -m "chore: delete merged/deprecated signup workflow files and signup/ directory"
Task 6: Verify no stale references (Phase 1 checkpoint)
Step 1: Search for stale references in source code
Search apps/ and packages/ for:
SfCompletionWorkflow,NewCustomerSignupWorkflow,WhmcsMigrationWorkflowSignupAccountResolver,SignupValidation,SignupWhmcsService,SignupUserCreationsignup-account-resolver,signup-validation,signup-whmcs,signup-user-creation
Expected: No matches in apps/ or packages/ (docs/ matches are fine)
Step 2: Fix any remaining references
Step 3: Run full type-check and tests
Run: pnpm type-check && pnpm --filter @customer-portal/bff test
Expected: PASS
Step 4: Commit if fixes were needed
git add -A
git commit -m "fix: update remaining references to old workflow service names"
Phase 2: Remove legacy migrate endpoint
Task 7: Remove WhmcsLinkWorkflowService and /auth/migrate
Remove the legacy password-based WHMCS migration. All WHMCS migration now goes through get-started OTP-based flow.
Files:
- Delete:
apps/bff/src/modules/auth/infra/workflows/whmcs-link-workflow.service.ts - Modify:
apps/bff/src/modules/auth/auth.module.ts(remove provider + import) - Modify:
apps/bff/src/modules/auth/application/auth-orchestrator.service.ts(removelinkWhmcsUser()+ dependency) - Modify:
apps/bff/src/modules/auth/presentation/http/auth.controller.ts(removePOST /auth/migrateendpoint)
Step 1: Update AuthOrchestrator
- Remove
linkWhmcsUser()method - Remove
WhmcsLinkWorkflowServicefrom constructor injection - Remove import
Step 2: Update AuthController
- Remove the
POST /auth/migrateendpoint handler - Remove related imports (request DTO, etc.)
Step 3: Update AuthModule
- Remove
WhmcsLinkWorkflowServicefrom providers - Remove import
Step 4: Delete the workflow file
rm apps/bff/src/modules/auth/infra/workflows/whmcs-link-workflow.service.ts
Step 5: Search for stale references
Search for WhmcsLinkWorkflow, linkWhmcsUser, /auth/migrate in source code.
Step 6: Run type-check and tests
Run: pnpm type-check && pnpm --filter @customer-portal/bff test
Expected: PASS
Step 7: Commit
git add -A
git commit -m "refactor: remove legacy /auth/migrate endpoint and WhmcsLinkWorkflowService"
Phase 3: Clean up OTP infrastructure
Task 8: Extract shared OTP email-sending pattern
Both LoginOtpWorkflowService.sendOtpEmail() and VerificationWorkflowService.sendOtpEmail() implement the same pattern: look up template ID from config, send via template or fallback HTML.
Files:
- Create:
apps/bff/src/modules/auth/infra/otp/otp-email.service.ts - Modify:
apps/bff/src/modules/auth/infra/workflows/login-otp-workflow.service.ts - Modify:
apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts - Modify:
apps/bff/src/modules/auth/otp/otp.module.ts(add provider)
Step 1: Create OtpEmailService
Extract the shared email-sending pattern:
@Injectable()
export class OtpEmailService {
constructor(
private readonly config: ConfigService,
private readonly emailService: EmailService
) {}
async sendOtpCode(email: string, code: string, context: "login" | "verification"): Promise<void> {
// Look up template ID based on context
// Send via template or fallback HTML
// Both current implementations do the same thing with different template IDs
}
}
Step 2: Refactor LoginOtpWorkflowService
Replace inline sendOtpEmail() with otpEmailService.sendOtpCode(email, code, 'login').
Step 3: Refactor VerificationWorkflowService
Replace inline sendOtpEmail() with otpEmailService.sendOtpCode(email, code, 'verification').
Step 4: Update OtpModule
Add OtpEmailService to providers and exports.
Step 5: Run type-check and tests
Run: pnpm type-check && pnpm --filter @customer-portal/bff test
Expected: PASS
Step 6: Commit
git add -A
git commit -m "refactor: extract shared OTP email service from login and verification workflows"
Task 9: Improve PORTAL_EXISTS redirect UX
When get-started verification detects PORTAL_EXISTS, the frontend redirects to login without context. Improve this by passing the verified email so it can be prefilled.
Files:
- Modify:
apps/portal/src/features/get-started/machines/get-started.machine.ts - Potentially: login page component (to accept prefilled email from query param or state)
Step 1: Update get-started machine
When accountStatus === PORTAL_EXISTS, redirect to login with email as query param:
// Instead of: router.push('/login')
// Do: router.push(`/login?email=${encodeURIComponent(verifiedEmail)}`)
Step 2: Update login page to accept prefilled email
Read email from URL query params and prefill the email input field.
Step 3: Test manually
- Go to get-started with an existing account email
- Verify via OTP
- Should redirect to login with email prefilled
Step 4: Commit
git add -A
git commit -m "feat: prefill email on login page when redirected from get-started"
Notes
Error handling difference between merged paths
The completeAccount endpoint throws exceptions directly. The signupWithEligibility endpoint returns { success: false, message, errorCategory }. The merged workflow preserves this:
if (options?.withEligibility) {
try { return await this.executeCreation(...); }
catch (error) {
const classified = classifyError(error);
return { success: false, errorCategory: classified.errorCategory, message: classified.message };
}
} else {
return await this.executeCreation(...); // exceptions propagate
}
createContact() for Birthdate/Sex__c
New degradable step using existing SalesforceAccountService.createContact(). Gender mapping:
male→男性female→女性other→Other
Sessions stay separate
Login sessions (LoginSessionService) and get-started sessions (GetStartedSessionService) have fundamentally different data models, TTLs, and lifecycle requirements. Only the OTP email-sending pattern is worth extracting.