# 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<AuthResultInternal | SignupWithEligibilityResult>;
}
```

**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
