Assist_Design/docs/plans/2026-03-03-signup-flow-simplification-design.md
barsa 9b7cbcf78f refactor: improve error handling and caching in BFF services
- 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.
2026-03-03 17:27:50 +09:00

12 KiB

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:

class AccountCreationWorkflowService {
  execute(
    request: CompleteAccountRequest | SignupWithEligibilityRequest,
    options?: { withEligibility?: boolean }
  ): Promise<AuthResultInternal | SignupWithEligibilityResult>;
}

Routing:

  • POST /auth/get-started/complete-accountexecute(request) (no eligibility)
  • POST /auth/get-started/signup-with-eligibilityexecute(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:

  • BirthdatedateOfBirth (YYYY-MM-DD)
  • Sex__c → mapped from gender (male男性, female女性, otherOther) This is degradable — account creation continues if it fails.

Rename: AccountMigrationWorkflowService

Rename WhmcsMigrationWorkflowServiceAccountMigrationWorkflowService. 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