# Auth Flow Simplification Design **Date**: 2026-03-03 **Status**: Approved ## Problem The auth system has accumulated duplication and dead code: 1. **Duplicate signup workflows**: `SfCompletionWorkflow` and `NewCustomerSignupWorkflow` are 85% identical (~300 lines each). The only difference is eligibility case creation and a welcome email. 2. **Confusing names**: `SfCompletionWorkflow` doesn't describe what it does (it creates full accounts, not just "completes SF"). 3. **Missing SF fields**: `Birthdate` and `Sex__c` are not set on Salesforce PersonContact during account creation. The only code that called `createContact()` lived in dead legacy code. 4. **Dead code**: `signup/` directory (6 files), `WhmcsLinkWorkflowService` — all unreferenced or superseded. 5. **Legacy duplicate**: `WhmcsLinkWorkflowService` (password-based migration via `POST /auth/migrate`) is superseded by `WhmcsMigrationWorkflowService` (OTP-based migration via get-started). 6. **Scattered shared services**: `SignupUserCreationService` and `SignupWhmcsService` live in `signup/` but are used by other workflows via `CreatePortalUserStep`. They should live alongside the steps they serve. ## Approach — 3 Phases (can stop after any phase) ### Phase 1: Merge signup workflows + cleanup Merge the two near-duplicate workflows. Move shared services. Delete dead code. Fix Birthdate/Sex\_\_c. ### Phase 2: Remove legacy migrate endpoint Delete `WhmcsLinkWorkflowService` and `POST /auth/migrate`. All WHMCS migration goes through the get-started OTP-based flow. ### Phase 3: Clean up OTP infrastructure Extract shared OTP email-sending pattern. Login and signup OTP stay as separate workflows (they serve different purposes: 2FA vs email verification). Keep separate sessions (different data models, TTLs, lifecycle). Improve the PORTAL_EXISTS redirect UX (prefill email on login page). **Why login and signup stay separate**: In the ISP/telecom industry, login (password → OTP as 2FA) and signup (OTP as email verification → form) serve fundamentally different security purposes. Forcing them into one flow either adds friction for existing users or weakens the security model. Major ISP portals keep these flows separate. --- ## Current Structure ``` workflows/ ├── verification-workflow.service.ts # OTP + account status detection ├── guest-eligibility-workflow.service.ts # Guest ISP check (no account created) ├── sf-completion-workflow.service.ts # Creates account (misleading name, 85% overlap) ├── new-customer-signup-workflow.service.ts # Creates account + eligibility (85% overlap) ├── whmcs-migration-workflow.service.ts # OTP-based WHMCS migration (active) ├── whmcs-link-workflow.service.ts # Password-based WHMCS migration (legacy, superseded) ├── login-otp-workflow.service.ts # Login OTP 2FA ├── password-workflow.service.ts # Password operations ├── get-started-coordinator.service.ts # Get-started routing ├── workflow-error.util.ts # Error classification ├── signup/ # Mixed: some active, some dead │ ├── signup-account-resolver.service.ts # Dead (no active caller) │ ├── signup-validation.service.ts # Dead (no active caller) │ ├── signup-whmcs.service.ts # Active (markClientForCleanup used by CreatePortalUserStep) │ ├── signup-user-creation.service.ts # Active (used by CreatePortalUserStep + WhmcsMigration) │ ├── signup.types.ts # Dead (only used by dead validation service) │ └── index.ts # Barrel └── steps/ # Shared step services ├── resolve-salesforce-account.step.ts ├── create-whmcs-client.step.ts ├── create-portal-user.step.ts # Delegates to SignupUserCreationService ├── create-eligibility-case.step.ts ├── update-salesforce-flags.step.ts ├── generate-auth-result.step.ts └── index.ts ``` ## New Structure (after all 3 phases) ``` workflows/ ├── verification-workflow.service.ts # OTP + account detection (unchanged) ├── guest-eligibility-workflow.service.ts # Guest ISP check (unchanged) ├── account-creation-workflow.service.ts # MERGED: replaces sf-completion + new-customer-signup ├── account-migration-workflow.service.ts # RENAMED: from whmcs-migration ├── login-otp-workflow.service.ts # Login OTP 2FA (Phase 3: uses shared OTP orchestration) ├── password-workflow.service.ts # Unchanged ├── get-started-coordinator.service.ts # Updated imports ├── workflow-error.util.ts # Unchanged └── steps/ # Shared steps + moved services ├── resolve-salesforce-account.step.ts ├── create-whmcs-client.step.ts ├── create-portal-user.step.ts ├── create-eligibility-case.step.ts ├── update-salesforce-flags.step.ts ├── generate-auth-result.step.ts ├── portal-user-creation.service.ts # MOVED from signup/ (renamed) ├── whmcs-cleanup.service.ts # MOVED from signup/ (renamed, trimmed) └── index.ts ``` **Deleted (all phases combined):** - `sf-completion-workflow.service.ts` (merged, Phase 1) - `new-customer-signup-workflow.service.ts` (merged, Phase 1) - `whmcs-migration-workflow.service.ts` (renamed, Phase 1) - `whmcs-link-workflow.service.ts` (removed, Phase 2) - `signup/` entire directory (Phase 1) --- ## Phase 1: Merge signup workflows + cleanup ### Merged Workflow: AccountCreationWorkflowService **API:** ```typescript class AccountCreationWorkflowService { execute( request: CompleteAccountRequest | SignupWithEligibilityRequest, options?: { withEligibility?: boolean } ): Promise; } ``` **Routing:** - `POST /auth/get-started/complete-account` → `execute(request)` (no eligibility) - `POST /auth/get-started/signup-with-eligibility` → `execute(request, { withEligibility: true })` **Step Sequence:** ``` 1. Validate + check existing accounts 2. Hash password 3. Resolve address + names (from request or session prefill) 4. Step 1: Resolve SF Account (CRITICAL) 5. Step 1.5: Write SF Address (DEGRADABLE) 6. Step 1.6: Create SF Contact — Birthdate + Sex__c (DEGRADABLE) ← NEW, fixes the bug 7. [if withEligibility] Step 2: Create Eligibility Case (DEGRADABLE) 8. Step 3: Create WHMCS Client (CRITICAL, rollback) 9. Step 4: Create Portal User (CRITICAL, rollback WHMCS on fail) 10. Step 5: Update SF Flags (DEGRADABLE) 11. Step 6: Generate Auth Result 12. [if withEligibility] Send welcome email with eligibility ref (DEGRADABLE) 13. Invalidate session ``` **Error handling difference between paths:** - `completeAccount` path: throws exceptions directly (current sf-completion behavior) - `signupWithEligibility` path: wraps in try/catch, returns `{ success: false, message, errorCategory }` using `classifyError()` **Birthdate/Sex\_\_c Fix:** After resolving the SF account, call `SalesforceAccountService.createContact()` on the PersonContact: - `Birthdate` → `dateOfBirth` (YYYY-MM-DD) - `Sex__c` → mapped from `gender` (`male`→`男性`, `female`→`女性`, `other`→`Other`) This is degradable — account creation continues if it fails. ### Rename: AccountMigrationWorkflowService Rename `WhmcsMigrationWorkflowService` → `AccountMigrationWorkflowService`. Refactor to use `CreatePortalUserStep` instead of calling `SignupUserCreationService` directly (reuse existing step, same as other workflows). ### Move signup/ services into steps/ | Old | New | Changes | | ---------------------------------------- | --------------------------------------- | ------------------------------------------------------------------- | | `signup/signup-user-creation.service.ts` | `steps/portal-user-creation.service.ts` | Rename class to `PortalUserCreationService` | | `signup/signup-whmcs.service.ts` | `steps/whmcs-cleanup.service.ts` | Rename to `WhmcsCleanupService`, keep only `markClientForCleanup()` | ### Delete dead code | File | Reason | | ------------------------------------------- | ------------------------------------ | | `signup/signup-account-resolver.service.ts` | No active caller | | `signup/signup-validation.service.ts` | No active caller | | `signup/signup.types.ts` | Only used by dead validation service | | `signup/index.ts` | Directory being removed | --- ## Phase 2: Remove legacy migrate endpoint ### Delete WhmcsLinkWorkflowService `WhmcsLinkWorkflowService` (`POST /auth/migrate`) is superseded by `AccountMigrationWorkflowService` (`POST /auth/get-started/migrate-whmcs-account`). The new flow uses OTP-based identity verification instead of WHMCS password validation — strictly more secure. **Files to modify:** - `apps/bff/src/modules/auth/auth.module.ts` — remove provider + import - `apps/bff/src/modules/auth/application/auth-orchestrator.service.ts` — remove `linkWhmcsUser()` method + dependency - `apps/bff/src/modules/auth/presentation/http/auth.controller.ts` — remove `POST /auth/migrate` endpoint **File to delete:** - `apps/bff/src/modules/auth/infra/workflows/whmcs-link-workflow.service.ts` --- ## Phase 3: Clean up OTP infrastructure ### Extract shared OTP email-sending pattern Both `LoginOtpWorkflowService` and `VerificationWorkflowService` implement the same pattern: 1. Look up email template ID from config 2. If template exists, send via template with dynamic data 3. If no template, send fallback HTML Extract this into a shared `OtpEmailService` (or method on existing `OtpService`). ### Improve PORTAL_EXISTS redirect UX When get-started verification detects `PORTAL_EXISTS`, pass the verified email to the login page so it's prefilled. This reduces friction for users who accidentally start on the wrong page. ### What stays separate (and why) - **Login OTP workflow**: OTP serves as 2FA after password validation - **Verification OTP workflow**: OTP serves as email ownership proof before signup - **LoginSessionService**: Simple (5 fields, 10min TTL, no prefill) - **GetStartedSessionService**: Complex (10+ fields, 1hr TTL, prefill data, handoff tokens, idempotency locking) These serve fundamentally different security purposes and have different data models. Forcing unification would create a confusing abstraction. --- ## Module Updates (all phases) ### GetStartedModule (Phase 1) - Remove: `NewCustomerSignupWorkflowService`, `SfCompletionWorkflowService`, `WhmcsMigrationWorkflowService` - Remove: `SignupAccountResolverService`, `SignupValidationService`, `SignupWhmcsService`, `SignupUserCreationService` - Add: `AccountCreationWorkflowService`, `AccountMigrationWorkflowService` - Add: `PortalUserCreationService`, `WhmcsCleanupService` ### GetStartedCoordinator (Phase 1) - `completeAccount()` → `accountCreation.execute(request)` - `signupWithEligibility()` → `accountCreation.execute(request, { withEligibility: true })` - `migrateWhmcsAccount()` → `accountMigration.execute(request)` ### AuthModule (Phase 2) - Remove: `WhmcsLinkWorkflowService` from providers and imports ### AuthOrchestrator (Phase 2) - Remove: `linkWhmcsUser()` method and `WhmcsLinkWorkflowService` dependency ### AuthController (Phase 2) - Remove: `POST /auth/migrate` endpoint