From aa77f23d8597c6697dd1c490e603c5ac0da773d3 Mon Sep 17 00:00:00 2001 From: barsa Date: Thu, 15 Jan 2026 16:11:11 +0900 Subject: [PATCH] Refactor conditional rendering and improve code readability across multiple components - Updated conditional rendering syntax in CheckoutStatusBanners, IdentityVerificationSection, and other components for consistency. - Removed unused quick eligibility and maybe later API functions from get-started API. - Enhanced error handling messages in various subscription views. - Updated documentation to reflect changes in eligibility check flow. - Cleaned up unused fields in Salesforce mapping and raw types. --- .../services/salesforce-account.service.ts | 8 +- .../services/salesforce-case.service.ts | 20 +- .../auth/constants/portal.constants.ts | 4 +- .../infra/otp/get-started-session.service.ts | 217 +++++------------- .../workflows/get-started-workflow.service.ts | 179 +++------------ .../signup/signup-account-resolver.service.ts | 2 + .../http/get-started.controller.ts | 192 +++++++--------- .../get-started/api/get-started.api.ts | 104 +++++---- .../get-started/stores/get-started.store.ts | 2 - docs/features/unified-get-started-flow.md | 19 +- packages/domain/get-started/contract.ts | 11 +- packages/domain/get-started/index.ts | 14 +- packages/domain/get-started/schema.ts | 66 +----- .../support/providers/salesforce/mapper.ts | 3 - .../support/providers/salesforce/raw.types.ts | 4 - 15 files changed, 248 insertions(+), 597 deletions(-) diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts index a9617350..522e5fe7 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts @@ -192,8 +192,8 @@ export class SalesforceAccountService { // Record type for Person Accounts (required) RecordTypeId: personAccountRecordTypeId, // Portal tracking fields - [this.portalStatusField]: "Active", - [this.portalSourceField]: "Portal Checkout", + [this.portalStatusField]: data.portalStatus ?? "Not Yet", + [this.portalSourceField]: data.portalSource, }; this.logger.debug("Person Account creation payload", { @@ -525,6 +525,10 @@ export interface CreateSalesforceAccountRequest { lastName: string; email: string; phone: string; + /** Portal status - defaults to "Not Yet" if not provided */ + portalStatus?: string; + /** Portal registration source - REQUIRED, must be explicitly set by caller */ + portalSource: string; } /** diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts index 88198d94..cc5d7293 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-case.service.ts @@ -166,9 +166,7 @@ export class SalesforceCaseService { * @param params - Case creation parameters * @returns Created case ID and case number */ - async createCase( - params: CreateCaseParams - ): Promise<{ id: string; caseNumber: string; threadId: string | null }> { + async createCase(params: CreateCaseParams): Promise<{ id: string; caseNumber: string }> { const safeAccountId = assertSalesforceId(params.accountId, "accountId"); const safeContactId = params.contactId ? assertSalesforceId(params.contactId, "contactId") @@ -219,19 +217,17 @@ export class SalesforceCaseService { throw new Error("Salesforce did not return a case ID"); } - // Fetch the created case to get the CaseNumber and Thread_Id + // Fetch the created case to get the CaseNumber const createdCase = await this.getCaseByIdInternal(created.id); const caseNumber = createdCase?.CaseNumber ?? created.id; - const threadId = createdCase?.Thread_Id ?? null; this.logger.log("Salesforce case created successfully", { caseId: created.id, caseNumber, - threadId, origin: params.origin, }); - return { id: created.id, caseNumber, threadId }; + return { id: created.id, caseNumber }; } catch (error: unknown) { this.logger.error("Failed to create Salesforce case", { error: extractErrorMessage(error), @@ -248,9 +244,7 @@ export class SalesforceCaseService { * Does not require an Account - uses supplied contact info. * Separate from createCase() because it has a different payload structure. */ - async createWebCase( - params: CreateWebCaseParams - ): Promise<{ id: string; caseNumber: string; threadId: string | null }> { + async createWebCase(params: CreateWebCaseParams): Promise<{ id: string; caseNumber: string }> { this.logger.log("Creating Web-to-Case", { email: params.suppliedEmail }); const casePayload: Record = { @@ -271,19 +265,17 @@ export class SalesforceCaseService { throw new Error("Salesforce did not return a case ID"); } - // Fetch the created case to get the CaseNumber and Thread_Id + // Fetch the created case to get the CaseNumber const createdCase = await this.getCaseByIdInternal(created.id); const caseNumber = createdCase?.CaseNumber ?? created.id; - const threadId = createdCase?.Thread_Id ?? null; this.logger.log("Web-to-Case created successfully", { caseId: created.id, caseNumber, - threadId, email: params.suppliedEmail, }); - return { id: created.id, caseNumber, threadId }; + return { id: created.id, caseNumber }; } catch (error: unknown) { this.logger.error("Failed to create Web-to-Case", { error: extractErrorMessage(error), diff --git a/apps/bff/src/modules/auth/constants/portal.constants.ts b/apps/bff/src/modules/auth/constants/portal.constants.ts index 1adab220..41c50be1 100644 --- a/apps/bff/src/modules/auth/constants/portal.constants.ts +++ b/apps/bff/src/modules/auth/constants/portal.constants.ts @@ -4,6 +4,8 @@ export type PortalStatus = typeof PORTAL_STATUS_ACTIVE | typeof PORTAL_STATUS_NO export const PORTAL_SOURCE_NEW_SIGNUP = "New Signup" as const; export const PORTAL_SOURCE_MIGRATED = "Migrated" as const; +export const PORTAL_SOURCE_INTERNET_ELIGIBILITY = "Internet Eligibility" as const; export type PortalRegistrationSource = | typeof PORTAL_SOURCE_NEW_SIGNUP - | typeof PORTAL_SOURCE_MIGRATED; + | typeof PORTAL_SOURCE_MIGRATED + | typeof PORTAL_SOURCE_INTERNET_ELIGIBILITY; diff --git a/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts b/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts index a6cdefe7..eec3c4d1 100644 --- a/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts +++ b/apps/bff/src/modules/auth/infra/otp/get-started-session.service.ts @@ -22,7 +22,32 @@ interface SessionData extends Omit { /** Timestamp when session was marked as used (for one-time operations) */ usedAt?: string; /** The operation that used this session */ - usedFor?: "guest_eligibility" | "signup_with_eligibility" | "complete_account"; + usedFor?: "signup_with_eligibility" | "complete_account"; +} + +/** + * Build a clean SessionData object from partial data, filtering out undefined values. + * This maintains exactOptionalPropertyTypes compliance. + */ +function buildSessionData( + required: Pick, + optional: Partial> +): SessionData { + const result: SessionData = { ...required }; + + if (optional.usedAt !== undefined) result.usedAt = optional.usedAt; + if (optional.usedFor !== undefined) result.usedFor = optional.usedFor; + if (optional.accountStatus !== undefined) result.accountStatus = optional.accountStatus; + if (optional.firstName !== undefined) result.firstName = optional.firstName; + if (optional.lastName !== undefined) result.lastName = optional.lastName; + if (optional.phone !== undefined) result.phone = optional.phone; + if (optional.address !== undefined) result.address = optional.address; + if (optional.sfAccountId !== undefined) result.sfAccountId = optional.sfAccountId; + if (optional.whmcsClientId !== undefined) result.whmcsClientId = optional.whmcsClientId; + if (optional.eligibilityStatus !== undefined) + result.eligibilityStatus = optional.eligibilityStatus; + + return result; } /** @@ -43,7 +68,10 @@ export class GetStartedSessionService { private readonly HANDOFF_PREFIX = "guest-handoff:"; private readonly SESSION_LOCK_PREFIX = "session-lock:"; private readonly ttlSeconds: number; - private readonly handoffTtlSeconds = 1800; // 30 minutes for handoff tokens + /** TTL for handoff tokens (30 minutes) */ + private readonly handoffTtlSeconds = 1800; + /** Lock TTL must exceed workflow lock (60s) to prevent premature expiry */ + private readonly sessionLockTtlMs = 65_000; constructor( private readonly cache: CacheService, @@ -51,14 +79,11 @@ export class GetStartedSessionService { private readonly config: ConfigService, @Inject(Logger) private readonly logger: Logger ) { - this.ttlSeconds = this.config.get("GET_STARTED_SESSION_TTL", 3600); // 1 hour + this.ttlSeconds = this.config.get("GET_STARTED_SESSION_TTL", 3600); } /** * Create a new session for email verification - * - * @param email - Email address (normalized) - * @returns Session token (UUID) */ async create(email: string): Promise { const sessionId = randomUUID(); @@ -72,7 +97,6 @@ export class GetStartedSessionService { }; await this.cache.set(this.buildKey(sessionId), sessionData, this.ttlSeconds); - this.logger.debug({ email: normalizedEmail, sessionId }, "Get-started session created"); return sessionId; @@ -80,16 +104,10 @@ export class GetStartedSessionService { /** * Get session by token - * - * @param sessionToken - Session token (UUID) - * @returns Session data or null if not found/expired */ async get(sessionToken: string): Promise { const sessionData = await this.cache.get(this.buildKey(sessionToken)); - - if (!sessionData) { - return null; - } + if (!sessionData) return null; return { ...sessionData, @@ -98,7 +116,7 @@ export class GetStartedSessionService { } /** - * Update session with email verification status + * Update session with email verification status and optional prefill data */ async markEmailVerified( sessionToken: string, @@ -114,10 +132,7 @@ export class GetStartedSessionService { } ): Promise { const sessionData = await this.cache.get(this.buildKey(sessionToken)); - - if (!sessionData) { - return false; - } + if (!sessionData) return false; const updatedData: SessionData = { ...sessionData, @@ -126,7 +141,6 @@ export class GetStartedSessionService { ...prefillData, }; - // Calculate remaining TTL const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt); await this.cache.set(this.buildKey(sessionToken), updatedData, remainingTtl); @@ -138,45 +152,6 @@ export class GetStartedSessionService { return true; } - /** - * Update session with quick check data - */ - async updateWithQuickCheckData( - sessionToken: string, - data: { - firstName: string; - lastName: string; - address: GetStartedSession["address"]; - phone?: string; - sfAccountId?: string; - } - ): Promise { - const sessionData = await this.cache.get(this.buildKey(sessionToken)); - - if (!sessionData) { - return false; - } - - const updatedData: SessionData = { - ...sessionData, - firstName: data.firstName, - lastName: data.lastName, - address: data.address, - phone: data.phone, - sfAccountId: data.sfAccountId, - }; - - const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt); - await this.cache.set(this.buildKey(sessionToken), updatedData, remainingTtl); - - this.logger.debug( - { sessionId: sessionToken }, - "Get-started session updated with quick check data" - ); - - return true; - } - /** * Delete session (after successful account creation) */ @@ -187,8 +162,6 @@ export class GetStartedSessionService { /** * Validate that a session exists and email is verified - * - * @returns Session data if valid, null otherwise */ async validateVerifiedSession(sessionToken: string): Promise { const session = await this.get(sessionToken); @@ -216,10 +189,6 @@ export class GetStartedSessionService { * This token allows passing data from guest eligibility check to account creation * without requiring email verification first. The email will be verified when * the user proceeds to account creation. - * - * @param email - Email address (NOT verified) - * @param data - User data from eligibility check - * @returns Handoff token ID */ async createGuestHandoffToken( email: string, @@ -248,42 +217,11 @@ export class GetStartedSessionService { }; await this.cache.set(this.buildHandoffKey(tokenId), tokenData, this.handoffTtlSeconds); - this.logger.debug({ email: normalizedEmail, tokenId }, "Guest handoff token created"); return tokenId; } - /** - * Validate and retrieve guest handoff token data - * - * @param token - Handoff token ID - * @returns Token data if valid, null otherwise - */ - async validateGuestHandoffToken(token: string): Promise { - const data = await this.cache.get(this.buildHandoffKey(token)); - - if (!data) { - this.logger.debug({ tokenId: token }, "Guest handoff token not found or expired"); - return null; - } - - if (data.type !== "guest_handoff") { - this.logger.warn({ tokenId: token }, "Invalid handoff token type"); - return null; - } - - return data; - } - - /** - * Invalidate guest handoff token (after it's been used) - */ - async invalidateHandoffToken(token: string): Promise { - await this.cache.del(this.buildHandoffKey(token)); - this.logger.debug({ tokenId: token }, "Guest handoff token invalidated"); - } - // ============================================================================ // Session Locking (Idempotency Protection) // ============================================================================ @@ -293,22 +231,16 @@ export class GetStartedSessionService { * * This prevents race conditions where the same session could be used * multiple times (e.g., double-clicking "Create Account"). - * - * @param sessionToken - Session token - * @param operation - The operation being performed - * @returns Object with success flag and session data if acquired */ async acquireAndMarkAsUsed( sessionToken: string, - operation: SessionData["usedFor"] + operation: NonNullable ): Promise<{ success: true; session: GetStartedSession } | { success: false; reason: string }> { const lockKey = `${this.SESSION_LOCK_PREFIX}${sessionToken}`; - // Try to acquire lock with no retries (immediate fail if already locked) const lockResult = await this.lockService.tryWithLock( lockKey, async () => { - // Check session state within lock const sessionData = await this.cache.get(this.buildKey(sessionToken)); if (!sessionData) { @@ -330,27 +262,27 @@ export class GetStartedSessionService { }; } - // Mark as used - build object with required fields, then add optional fields - const updatedData = Object.assign( + // Mark session as used + const updatedData = buildSessionData( { id: sessionData.id, email: sessionData.email, emailVerified: sessionData.emailVerified, createdAt: sessionData.createdAt, + }, + { usedAt: new Date().toISOString(), usedFor: operation, - }, - sessionData.accountStatus ? { accountStatus: sessionData.accountStatus } : {}, - sessionData.firstName ? { firstName: sessionData.firstName } : {}, - sessionData.lastName ? { lastName: sessionData.lastName } : {}, - sessionData.phone ? { phone: sessionData.phone } : {}, - sessionData.address ? { address: sessionData.address } : {}, - sessionData.sfAccountId ? { sfAccountId: sessionData.sfAccountId } : {}, - sessionData.whmcsClientId === undefined - ? {} - : { whmcsClientId: sessionData.whmcsClientId }, - sessionData.eligibilityStatus ? { eligibilityStatus: sessionData.eligibilityStatus } : {} - ) as SessionData; + accountStatus: sessionData.accountStatus, + firstName: sessionData.firstName, + lastName: sessionData.lastName, + phone: sessionData.phone, + address: sessionData.address, + sfAccountId: sessionData.sfAccountId, + whmcsClientId: sessionData.whmcsClientId, + eligibilityStatus: sessionData.eligibilityStatus, + } + ); const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt); await this.cache.set(this.buildKey(sessionToken), updatedData, remainingTtl); @@ -365,7 +297,7 @@ export class GetStartedSessionService { }, }; }, - { ttlMs: 65_000, maxRetries: 0 } // TTL must exceed workflow lock (60s) - fail fast + { ttlMs: this.sessionLockTtlMs, maxRetries: 0 } ); if (!lockResult.success) { @@ -379,52 +311,9 @@ export class GetStartedSessionService { return lockResult.result; } - /** - * Check if a session has already been used for an operation - */ - async isSessionUsed(sessionToken: string): Promise { - const sessionData = await this.cache.get(this.buildKey(sessionToken)); - return sessionData?.usedAt != null; - } - - /** - * Clear the "used" status from a session (for recovery after partial failure) - * - * This should only be called when rolling back a failed operation - * to allow the user to retry. - */ - async clearUsedStatus(sessionToken: string): Promise { - const sessionData = await this.cache.get(this.buildKey(sessionToken)); - - if (!sessionData) { - return false; - } - - // Build clean session data without usedAt and usedFor - const cleanSessionData = Object.assign( - { - id: sessionData.id, - email: sessionData.email, - emailVerified: sessionData.emailVerified, - createdAt: sessionData.createdAt, - }, - sessionData.accountStatus ? { accountStatus: sessionData.accountStatus } : {}, - sessionData.firstName ? { firstName: sessionData.firstName } : {}, - sessionData.lastName ? { lastName: sessionData.lastName } : {}, - sessionData.phone ? { phone: sessionData.phone } : {}, - sessionData.address ? { address: sessionData.address } : {}, - sessionData.sfAccountId ? { sfAccountId: sessionData.sfAccountId } : {}, - sessionData.whmcsClientId === undefined ? {} : { whmcsClientId: sessionData.whmcsClientId }, - sessionData.eligibilityStatus ? { eligibilityStatus: sessionData.eligibilityStatus } : {} - ) as SessionData; - - const remainingTtl = this.calculateRemainingTtl(sessionData.createdAt); - await this.cache.set(this.buildKey(sessionToken), cleanSessionData, remainingTtl); - - this.logger.debug({ sessionId: sessionToken }, "Session used status cleared for retry"); - - return true; - } + // ============================================================================ + // Private Helpers + // ============================================================================ private buildKey(sessionId: string): string { return `${this.SESSION_PREFIX}${sessionId}`; diff --git a/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts index f60d88a6..ebcf6a42 100644 --- a/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/get-started-workflow.service.ts @@ -12,14 +12,10 @@ import { type SendVerificationCodeResponse, type VerifyCodeRequest, type VerifyCodeResponse, - type QuickEligibilityRequest, - type QuickEligibilityResponse, type BilingualEligibilityAddress, type GuestEligibilityRequest, type GuestEligibilityResponse, type CompleteAccountRequest, - type MaybeLaterRequest, - type MaybeLaterResponse, type SignupWithEligibilityRequest, } from "@customer-portal/domain/get-started"; @@ -43,7 +39,9 @@ import { SignupWhmcsService } from "./signup/signup-whmcs.service.js"; import { SignupUserCreationService } from "./signup/signup-user-creation.service.js"; import { PORTAL_SOURCE_NEW_SIGNUP, + PORTAL_SOURCE_INTERNET_ELIGIBILITY, PORTAL_STATUS_ACTIVE, + PORTAL_STATUS_NOT_YET, } from "@bff/modules/auth/constants/portal.constants.js"; import { SalesforceService } from "@bff/integrations/salesforce/salesforce.service.js"; @@ -62,8 +60,9 @@ function removeUndefined>(obj: T): Partial * Orchestrates the unified "Get Started" flow: * 1. Email verification via OTP * 2. Account status detection (Portal, WHMCS, SF) - * 3. Quick eligibility check for guests + * 3. Guest eligibility check (creates SF Account + Case without OTP) * 4. Account completion for SF-only users + * 5. Full signup with eligibility (SF + Case + WHMCS + Portal) */ @Injectable() export class GetStartedWorkflowService { @@ -200,103 +199,6 @@ export class GetStartedWorkflowService { }; } - // ============================================================================ - // Quick Eligibility Check (Guest Flow) - // ============================================================================ - - /** - * Quick eligibility check for guests - * Creates SF Account + eligibility case - */ - async quickEligibilityCheck(request: QuickEligibilityRequest): Promise { - const session = await this.sessionService.validateVerifiedSession(request.sessionToken); - if (!session) { - throw new BadRequestException("Invalid or expired session. Please verify your email again."); - } - - const { firstName, lastName, address, phone } = request; - - try { - // Check if SF account already exists for this email - let sfAccountId: string; - - const existingSf = await this.salesforceAccountService.findByEmail(session.email); - - if (existingSf) { - sfAccountId = existingSf.id; - this.logger.log({ email: session.email }, "Using existing SF account for quick check"); - } else { - // Create new SF Account - const { accountId } = await this.salesforceAccountService.createAccount({ - firstName, - lastName, - email: session.email, - phone: phone ?? "", - }); - sfAccountId = accountId; - this.logger.log( - { email: session.email, sfAccountId }, - "Created SF account for quick check" - ); - } - - // Create eligibility case - const { caseId } = await this.createEligibilityCase(sfAccountId, address); - - // Update session with SF account info (clean address to remove undefined values) - await this.sessionService.updateWithQuickCheckData(request.sessionToken, { - firstName, - lastName, - address: removeUndefined(address), - ...(phone && { phone }), - ...(sfAccountId && { sfAccountId }), - }); - - return { - submitted: true, - requestId: caseId, - sfAccountId, - message: "Eligibility check submitted. We'll notify you of the results.", - }; - } catch (error) { - this.logger.error( - { error: extractErrorMessage(error), email: session.email }, - "Quick eligibility check failed" - ); - - return { - submitted: false, - message: "Failed to submit eligibility check. Please try again.", - }; - } - } - - /** - * "Maybe Later" flow - create SF Account + case, customer returns later - */ - async maybeLater(request: MaybeLaterRequest): Promise { - // This is essentially the same as quickEligibilityCheck - // but with explicit intent to not create account now - const result = await this.quickEligibilityCheck(request); - - if (result.submitted) { - // Send confirmation email - await this.sendMaybeLaterConfirmationEmail( - (await this.sessionService.get(request.sessionToken))!.email, - request.firstName, - result.requestId! - ); - } - - return { - success: result.submitted, - requestId: result.requestId, - message: result.submitted - ? "Your eligibility check has been submitted. Check your email for updates." - : result.message, - }; - } - // ============================================================================ // Guest Eligibility Check (No OTP Required) // ============================================================================ @@ -344,13 +246,21 @@ export class GetStartedWorkflowService { { email: normalizedEmail, sfAccountId }, "Using existing SF account for guest eligibility check" ); + + // Set portal source for existing accounts going through eligibility + // This ensures we can track that they came through internet eligibility + await this.salesforceService.updateAccountPortalFields(sfAccountId, { + status: PORTAL_STATUS_NOT_YET, + source: PORTAL_SOURCE_INTERNET_ELIGIBILITY, + }); } else { - // Create new SF Account (email NOT verified) + // Create new SF Account with portal fields set to "Not Yet" + "Internet Eligibility" const { accountId } = await this.salesforceAccountService.createAccount({ firstName, lastName, email: normalizedEmail, phone: phone ?? "", + portalSource: PORTAL_SOURCE_INTERNET_ELIGIBILITY, }); sfAccountId = accountId; this.logger.log( @@ -639,13 +549,20 @@ export class GetStartedWorkflowService { { email: normalizedEmail, sfAccountId }, "Using existing SF account for signup" ); + + // Set portal source for existing accounts going through eligibility signup + // Source will be preserved when we activate the account later + await this.salesforceService.updateAccountPortalFields(sfAccountId, { + source: PORTAL_SOURCE_INTERNET_ELIGIBILITY, + }); } else { - // Create new SF Account + // Create new SF Account with portal fields set to "Not Yet" + "Internet Eligibility" const { accountId, accountNumber } = await this.salesforceAccountService.createAccount({ firstName, lastName, email: normalizedEmail, phone: phone ?? "", + portalSource: PORTAL_SOURCE_INTERNET_ELIGIBILITY, }); sfAccountId = accountId; customerNumber = accountNumber; @@ -787,41 +704,6 @@ export class GetStartedWorkflowService { } } - private async sendMaybeLaterConfirmationEmail( - email: string, - firstName: string, - requestId: string - ): Promise { - const appBase = this.config.get("APP_BASE_URL", "http://localhost:3000"); - const templateId = this.config.get("EMAIL_TEMPLATE_ELIGIBILITY_SUBMITTED"); - - if (templateId) { - await this.emailService.sendEmail({ - to: email, - subject: "We're checking internet availability at your address", - templateId, - dynamicTemplateData: { - firstName, - portalUrl: appBase, - email, - requestId, - }, - }); - } else { - await this.emailService.sendEmail({ - to: email, - subject: "We're checking internet availability at your address", - html: ` -

Hi ${firstName},

-

We received your request to check internet availability.

-

We'll review this and email you the results within 1-2 business days.

-

When ready, create your account at: ${appBase}/get-started

-

Just enter your email (${email}) to continue.

- `, - }); - } - } - private async sendWelcomeWithEligibilityEmail( email: string, firstName: string, @@ -913,8 +795,8 @@ export class GetStartedWorkflowService { private async createEligibilityCase( sfAccountId: string, - address: BilingualEligibilityAddress | QuickEligibilityRequest["address"] - ): Promise<{ caseId: string; caseNumber: string; threadId: string | null }> { + address: BilingualEligibilityAddress | SignupWithEligibilityRequest["address"] + ): Promise<{ caseId: string; caseNumber: string }> { // Find or create Opportunity for Internet eligibility const { opportunityId } = await this.opportunityResolution.findOrCreateForInternetEligibility(sfAccountId); @@ -956,11 +838,7 @@ export class GetStartedWorkflowService { ...(japaneseAddress ? ["", "【Japanese Address】", japaneseAddress] : []), ].join("\n"); - const { - id: caseId, - caseNumber, - threadId, - } = await this.caseService.createCase({ + const { id: caseId, caseNumber } = await this.caseService.createCase({ accountId: sfAccountId, opportunityId, subject: "Internet availability check request (Portal)", @@ -971,7 +849,7 @@ export class GetStartedWorkflowService { // Update Account eligibility status to Pending this.updateAccountEligibilityStatus(sfAccountId); - return { caseId, caseNumber, threadId }; + return { caseId, caseNumber }; } private updateAccountEligibilityStatus(sfAccountId: string): void { @@ -982,12 +860,15 @@ export class GetStartedWorkflowService { private async updateSalesforcePortalFlags( accountId: string, - whmcsClientId: number + whmcsClientId: number, + /** Optional source override - if not provided, source is not updated (preserves existing) */ + source?: typeof PORTAL_SOURCE_NEW_SIGNUP | typeof PORTAL_SOURCE_INTERNET_ELIGIBILITY ): Promise { try { await this.salesforceService.updateAccountPortalFields(accountId, { status: PORTAL_STATUS_ACTIVE, - source: PORTAL_SOURCE_NEW_SIGNUP, + // Only update source if explicitly provided - preserves existing source for eligibility flows + ...(source && { source }), lastSignedInAt: new Date(), whmcsAccountId: whmcsClientId, }); diff --git a/apps/bff/src/modules/auth/infra/workflows/signup/signup-account-resolver.service.ts b/apps/bff/src/modules/auth/infra/workflows/signup/signup-account-resolver.service.ts index 636aa256..8b780636 100644 --- a/apps/bff/src/modules/auth/infra/workflows/signup/signup-account-resolver.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/signup/signup-account-resolver.service.ts @@ -9,6 +9,7 @@ import { SalesforceAccountService } from "@bff/integrations/salesforce/services/ import { extractErrorMessage } from "@bff/core/utils/error.util.js"; import type { SignupRequest } from "@customer-portal/domain/auth"; import type { SignupAccountSnapshot, SignupAccountCacheEntry } from "./signup.types.js"; +import { PORTAL_SOURCE_NEW_SIGNUP } from "@bff/modules/auth/constants/portal.constants.js"; @Injectable() export class SignupAccountResolverService { @@ -90,6 +91,7 @@ export class SignupAccountResolverService { lastName, email: normalizedEmail, phone, + portalSource: PORTAL_SOURCE_NEW_SIGNUP, }); } catch (error) { this.logger.error("Salesforce Account creation failed - blocking signup", { diff --git a/apps/bff/src/modules/auth/presentation/http/get-started.controller.ts b/apps/bff/src/modules/auth/presentation/http/get-started.controller.ts index d191ff66..8474477a 100644 --- a/apps/bff/src/modules/auth/presentation/http/get-started.controller.ts +++ b/apps/bff/src/modules/auth/presentation/http/get-started.controller.ts @@ -11,16 +11,13 @@ import { sendVerificationCodeResponseSchema, verifyCodeRequestSchema, verifyCodeResponseSchema, - quickEligibilityRequestSchema, - quickEligibilityResponseSchema, guestEligibilityRequestSchema, guestEligibilityResponseSchema, completeAccountRequestSchema, - maybeLaterRequestSchema, - maybeLaterResponseSchema, signupWithEligibilityRequestSchema, signupWithEligibilityResponseSchema, } from "@customer-portal/domain/get-started"; +import type { User } from "@customer-portal/domain/customer"; import { GetStartedWorkflowService } from "../../infra/workflows/get-started-workflow.service.js"; @@ -29,27 +26,77 @@ class SendVerificationCodeRequestDto extends createZodDto(sendVerificationCodeRe class SendVerificationCodeResponseDto extends createZodDto(sendVerificationCodeResponseSchema) {} class VerifyCodeRequestDto extends createZodDto(verifyCodeRequestSchema) {} class VerifyCodeResponseDto extends createZodDto(verifyCodeResponseSchema) {} -class QuickEligibilityRequestDto extends createZodDto(quickEligibilityRequestSchema) {} -class QuickEligibilityResponseDto extends createZodDto(quickEligibilityResponseSchema) {} class GuestEligibilityRequestDto extends createZodDto(guestEligibilityRequestSchema) {} class GuestEligibilityResponseDto extends createZodDto(guestEligibilityResponseSchema) {} class CompleteAccountRequestDto extends createZodDto(completeAccountRequestSchema) {} -class MaybeLaterRequestDto extends createZodDto(maybeLaterRequestSchema) {} -class MaybeLaterResponseDto extends createZodDto(maybeLaterResponseSchema) {} class SignupWithEligibilityRequestDto extends createZodDto(signupWithEligibilityRequestSchema) {} class SignupWithEligibilityResponseDto extends createZodDto(signupWithEligibilityResponseSchema) {} +// Cookie configuration const ACCESS_COOKIE_PATH = "/api"; const REFRESH_COOKIE_PATH = "/api/auth/refresh"; const TOKEN_TYPE = "Bearer" as const; -const calculateCookieMaxAge = (isoTimestamp: string): number => { +interface AuthTokens { + accessToken: string; + refreshToken: string; + expiresAt: string; + refreshExpiresAt: string; +} + +interface SessionInfo { + expiresAt: string; + refreshExpiresAt: string; + tokenType: typeof TOKEN_TYPE; +} + +interface AuthSuccessResponse { + user: User; + session: SessionInfo; +} + +/** + * Calculate cookie max age from ISO timestamp + */ +function calculateCookieMaxAge(isoTimestamp: string): number { const expiresAt = Date.parse(isoTimestamp); - if (Number.isNaN(expiresAt)) { - return 0; - } + if (Number.isNaN(expiresAt)) return 0; return Math.max(0, expiresAt - Date.now()); -}; +} + +/** + * Set authentication cookies (httpOnly, secure in production) + */ +function setAuthCookies(res: Response, tokens: AuthTokens): void { + const isProduction = process.env["NODE_ENV"] === "production"; + + res.cookie("access_token", tokens.accessToken, { + httpOnly: true, + secure: isProduction, + sameSite: "lax", + path: ACCESS_COOKIE_PATH, + maxAge: calculateCookieMaxAge(tokens.expiresAt), + }); + + res.cookie("refresh_token", tokens.refreshToken, { + httpOnly: true, + secure: isProduction, + sameSite: "lax", + path: REFRESH_COOKIE_PATH, + maxAge: calculateCookieMaxAge(tokens.refreshExpiresAt), + }); +} + +/** + * Build session info from tokens + */ +function buildSessionInfo(tokens: AuthTokens): SessionInfo { + return { + expiresAt: tokens.expiresAt, + refreshExpiresAt: tokens.refreshExpiresAt, + tokenType: TOKEN_TYPE, + }; +} /** * Get Started Controller @@ -57,8 +104,9 @@ const calculateCookieMaxAge = (isoTimestamp: string): number => { * Handles the unified "Get Started" flow: * - Email verification via OTP * - Account status detection - * - Quick eligibility check (guest) + * - Guest eligibility check (no OTP required) * - Account completion (SF-only users) + * - Signup with eligibility (full flow) * * All endpoints are public (no authentication required) */ @@ -68,8 +116,6 @@ export class GetStartedController { /** * Send OTP verification code to email - * - * Rate limit: 5 codes per 5 minutes per IP */ @Public() @Post("send-code") @@ -86,8 +132,6 @@ export class GetStartedController { /** * Verify OTP code and determine account status - * - * Rate limit: 10 attempts per 5 minutes per IP */ @Public() @Post("verify-code") @@ -102,31 +146,11 @@ export class GetStartedController { return this.workflow.verifyCode(body, fingerprint); } - /** - * Quick eligibility check for guests - * Creates SF Account + eligibility case - * - * Rate limit: 5 per 15 minutes per IP - */ - @Public() - @Post("quick-check") - @HttpCode(200) - @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) - @RateLimit({ limit: 5, ttl: 900 }) - async quickEligibilityCheck( - @Body() body: QuickEligibilityRequestDto - ): Promise { - return this.workflow.quickEligibilityCheck(body); - } - /** * Guest eligibility check - NO email verification required - * Creates SF Account + eligibility case without OTP verification * - * This allows users to check availability without verifying their email first. + * Creates SF Account + eligibility case without OTP verification. * Email verification happens later when they create an account. - * - * Rate limit: 3 per 15 minutes per IP (stricter due to no OTP protection) */ @Public() @Post("guest-eligibility") @@ -141,28 +165,11 @@ export class GetStartedController { return this.workflow.guestEligibilityCheck(body, fingerprint); } - /** - * "Maybe Later" flow - * Creates SF Account + eligibility case, sends confirmation email - * - * Rate limit: 3 per 10 minutes per IP - */ - @Public() - @Post("maybe-later") - @HttpCode(200) - @UseGuards(RateLimitGuard, SalesforceWriteThrottleGuard) - @RateLimit({ limit: 3, ttl: 600 }) - async maybeLater(@Body() body: MaybeLaterRequestDto): Promise { - return this.workflow.maybeLater(body); - } - /** * Complete account for SF-only users - * Creates WHMCS client and Portal user, links to existing SF account * - * Returns auth tokens (sets httpOnly cookies) - * - * Rate limit: 5 per 15 minutes per IP + * Creates WHMCS client and Portal user, links to existing SF account. + * Returns auth tokens (sets httpOnly cookies). */ @Public() @Post("complete-account") @@ -172,49 +179,23 @@ export class GetStartedController { async completeAccount( @Body() body: CompleteAccountRequestDto, @Res({ passthrough: true }) res: Response - ) { + ): Promise { const result = await this.workflow.completeAccount(body); - // Set auth cookies (same pattern as signup) - const accessExpires = result.tokens.expiresAt; - const refreshExpires = result.tokens.refreshExpiresAt; - - res.cookie("access_token", result.tokens.accessToken, { - httpOnly: true, - secure: process.env["NODE_ENV"] === "production", - sameSite: "lax", - path: ACCESS_COOKIE_PATH, - maxAge: calculateCookieMaxAge(accessExpires), - }); - - res.cookie("refresh_token", result.tokens.refreshToken, { - httpOnly: true, - secure: process.env["NODE_ENV"] === "production", - sameSite: "lax", - path: REFRESH_COOKIE_PATH, - maxAge: calculateCookieMaxAge(refreshExpires), - }); + setAuthCookies(res, result.tokens); return { user: result.user, - session: { - expiresAt: accessExpires, - refreshExpiresAt: refreshExpires, - tokenType: TOKEN_TYPE, - }, + session: buildSessionInfo(result.tokens), }; } /** * Full signup with eligibility check (inline flow) - * Creates SF Account + Case + WHMCS + Portal in one operation * - * Used when user clicks "Create Account" on the eligibility check page. - * This is the primary signup path - creates all accounts at once after OTP verification. - * - * Returns auth tokens (sets httpOnly cookies) - * - * Rate limit: 5 per 15 minutes per IP + * Creates SF Account + Case + WHMCS + Portal in one operation. + * This is the primary signup path from the eligibility check page. + * Returns auth tokens (sets httpOnly cookies) on success. */ @Public() @Post("signup-with-eligibility") @@ -224,7 +205,10 @@ export class GetStartedController { async signupWithEligibility( @Body() body: SignupWithEligibilityRequestDto, @Res({ passthrough: true }) res: Response - ): Promise { + ): Promise< + | SignupWithEligibilityResponseDto + | (AuthSuccessResponse & { success: true; eligibilityRequestId?: string }) + > { const result = await this.workflow.signupWithEligibility(body); if (!result.success || !result.authResult) { @@ -234,35 +218,13 @@ export class GetStartedController { }; } - // Set auth cookies (same pattern as complete-account) - const accessExpires = result.authResult.tokens.expiresAt; - const refreshExpires = result.authResult.tokens.refreshExpiresAt; - - res.cookie("access_token", result.authResult.tokens.accessToken, { - httpOnly: true, - secure: process.env["NODE_ENV"] === "production", - sameSite: "lax", - path: ACCESS_COOKIE_PATH, - maxAge: calculateCookieMaxAge(accessExpires), - }); - - res.cookie("refresh_token", result.authResult.tokens.refreshToken, { - httpOnly: true, - secure: process.env["NODE_ENV"] === "production", - sameSite: "lax", - path: REFRESH_COOKIE_PATH, - maxAge: calculateCookieMaxAge(refreshExpires), - }); + setAuthCookies(res, result.authResult.tokens); return { success: true, eligibilityRequestId: result.eligibilityRequestId, user: result.authResult.user, - session: { - expiresAt: accessExpires, - refreshExpiresAt: refreshExpires, - tokenType: TOKEN_TYPE, - }, + session: buildSessionInfo(result.authResult.tokens), }; } } diff --git a/apps/portal/src/features/get-started/api/get-started.api.ts b/apps/portal/src/features/get-started/api/get-started.api.ts index 0c162949..f43aad2f 100644 --- a/apps/portal/src/features/get-started/api/get-started.api.ts +++ b/apps/portal/src/features/get-started/api/get-started.api.ts @@ -8,28 +8,40 @@ import { apiClient, getDataOrThrow } from "@/core/api"; import { sendVerificationCodeResponseSchema, verifyCodeResponseSchema, - quickEligibilityResponseSchema, guestEligibilityResponseSchema, - maybeLaterResponseSchema, signupWithEligibilityResponseSchema, type SendVerificationCodeRequest, type SendVerificationCodeResponse, type VerifyCodeRequest, type VerifyCodeResponse, - type QuickEligibilityRequest, - type QuickEligibilityResponse, type GuestEligibilityRequest, type GuestEligibilityResponse, type CompleteAccountRequest, - type MaybeLaterRequest, - type MaybeLaterResponse, type SignupWithEligibilityRequest, type SignupWithEligibilityResponse, } from "@customer-portal/domain/get-started"; -import { authResponseSchema, type AuthResponse } from "@customer-portal/domain/auth"; +import { + authResponseSchema, + type AuthResponse, + type AuthSession, +} from "@customer-portal/domain/auth"; +import type { User } from "@customer-portal/domain/customer"; const BASE_PATH = "/api/auth/get-started"; +/** + * Response type for signup with eligibility - success case includes auth data + */ +export type SignupWithEligibilitySuccessResponse = SignupWithEligibilityResponse & { + success: true; + user: User; + session: AuthSession; +}; + +export type SignupWithEligibilityApiResponse = + | SignupWithEligibilityResponse + | SignupWithEligibilitySuccessResponse; + /** * Send OTP verification code to email */ @@ -54,23 +66,11 @@ export async function verifyCode(request: VerifyCodeRequest): Promise { - const response = await apiClient.POST(`${BASE_PATH}/quick-check`, { - body: request, - }); - const data = getDataOrThrow(response, "Failed to submit eligibility check"); - return quickEligibilityResponseSchema.parse(data); -} - /** * Guest eligibility check - NO OTP verification required - * Allows users to check availability without verifying email first - * Email verification happens later when user creates an account + * + * Allows users to check availability without verifying email first. + * Email verification happens later when user creates an account. */ export async function guestEligibilityCheck( request: GuestEligibilityRequest @@ -83,20 +83,10 @@ export async function guestEligibilityCheck( return guestEligibilityResponseSchema.parse(data); } -/** - * Maybe later flow - create SF account and eligibility case - */ -export async function maybeLater(request: MaybeLaterRequest): Promise { - const response = await apiClient.POST(`${BASE_PATH}/maybe-later`, { - body: request, - }); - const data = getDataOrThrow(response, "Failed to submit request"); - return maybeLaterResponseSchema.parse(data); -} - /** * Complete account for SF-only users - * Returns auth response with user and session + * + * Returns auth response with user and session. */ export async function completeAccount(request: CompleteAccountRequest): Promise { const response = await apiClient.POST(`${BASE_PATH}/complete-account`, { @@ -108,23 +98,41 @@ export async function completeAccount(request: CompleteAccountRequest): Promise< /** * Full signup with eligibility check (inline flow) - * Creates SF Account + Case + WHMCS + Portal in one operation - * Used when user clicks "Create Account" on eligibility check page + * + * Creates SF Account + Case + WHMCS + Portal in one operation. + * Returns auth data (user + session) on success. */ export async function signupWithEligibility( request: SignupWithEligibilityRequest -): Promise { - const response = await apiClient.POST< - SignupWithEligibilityResponse & { user?: unknown; session?: unknown } - >(`${BASE_PATH}/signup-with-eligibility`, { - body: request, - }); +): Promise { + const response = await apiClient.POST( + `${BASE_PATH}/signup-with-eligibility`, + { body: request } + ); const data = getDataOrThrow(response, "Failed to create account"); - // Parse the base response, but allow extra fields (user, session) + + // Parse base response first const baseResponse = signupWithEligibilityResponseSchema.parse(data); - return { - ...baseResponse, - user: data.user, - session: data.session, - }; + + // If successful, parse the full auth data + if (baseResponse.success && "user" in data && "session" in data) { + const authData = authResponseSchema.parse({ user: data.user, session: data.session }); + return { + ...baseResponse, + success: true as const, + user: authData.user, + session: authData.session, + }; + } + + return baseResponse; +} + +/** + * Type guard to check if signup response was successful + */ +export function isSignupSuccess( + response: SignupWithEligibilityApiResponse +): response is SignupWithEligibilitySuccessResponse { + return response.success === true && "user" in response && "session" in response; } diff --git a/apps/portal/src/features/get-started/stores/get-started.store.ts b/apps/portal/src/features/get-started/stores/get-started.store.ts index 570ef308..c3ed26f4 100644 --- a/apps/portal/src/features/get-started/stores/get-started.store.ts +++ b/apps/portal/src/features/get-started/stores/get-started.store.ts @@ -73,7 +73,6 @@ export interface GetStartedState { // Verification state codeSent: boolean; attemptsRemaining: number | null; - resendDisabled: boolean; // Actions sendVerificationCode: (email: string) => Promise; @@ -124,7 +123,6 @@ const initialState = { error: null, codeSent: false, attemptsRemaining: null, - resendDisabled: false, }; export const useGetStartedStore = create()((set, get) => ({ diff --git a/docs/features/unified-get-started-flow.md b/docs/features/unified-get-started-flow.md index 9648672a..bad7ed9b 100644 --- a/docs/features/unified-get-started-flow.md +++ b/docs/features/unified-get-started-flow.md @@ -102,17 +102,17 @@ A dedicated page for guests to check internet availability. This approach provid 1. Collects name, email, and address (with Japan ZIP code lookup) 2. Verifies email with 6-digit OTP 3. Creates SF Account + Eligibility Case immediately on verification -4. Shows success with options: "Create Account Now" or "Maybe Later" +4. Shows success with options: "Create Account Now" or "View Internet Plans" ## Backend Endpoints -| Endpoint | Rate Limit | Purpose | -| ----------------------------------------- | ---------- | --------------------------------------------- | -| `POST /auth/get-started/send-code` | 5/5min | Send OTP to email | -| `POST /auth/get-started/verify-code` | 10/5min | Verify OTP, return account status | -| `POST /auth/get-started/quick-check` | 5/15min | Guest eligibility (creates SF Account + Case) | -| `POST /auth/get-started/complete-account` | 5/15min | Complete SF-only account | -| `POST /auth/get-started/maybe-later` | 3/10min | Create SF + Case (legacy) | +| Endpoint | Rate Limit | Purpose | +| ------------------------------------------------ | ---------- | --------------------------------------------- | +| `POST /auth/get-started/send-code` | 5/5min | Send OTP to email | +| `POST /auth/get-started/verify-code` | 10/5min | Verify OTP, return account status | +| `POST /auth/get-started/guest-eligibility` | 3/15min | Guest eligibility (no OTP, creates SF + Case) | +| `POST /auth/get-started/complete-account` | 5/15min | Complete SF-only account | +| `POST /auth/get-started/signup-with-eligibility` | 5/15min | Full signup with eligibility (OTP verified) | ## Domain Schemas @@ -122,8 +122,9 @@ Key schemas: - `sendVerificationCodeRequestSchema` - email only - `verifyCodeRequestSchema` - email + 6-digit code -- `quickEligibilityRequestSchema` - sessionToken, name, address +- `guestEligibilityRequestSchema` - email, name, address (no OTP required) - `completeAccountRequestSchema` - sessionToken + password + profile fields +- `signupWithEligibilityRequestSchema` - sessionToken + full account data - `accountStatusSchema` - `portal_exists | whmcs_unmapped | sf_unmapped | new_customer` ## OTP Security diff --git a/packages/domain/get-started/contract.ts b/packages/domain/get-started/contract.ts index d9fcbfc0..7a30a7f6 100644 --- a/packages/domain/get-started/contract.ts +++ b/packages/domain/get-started/contract.ts @@ -4,8 +4,9 @@ * Types and constants for the unified "Get Started" flow that handles: * - Email verification (OTP) * - Account status detection - * - Quick eligibility check + * - Guest eligibility check (no OTP required) * - Account completion for SF-only accounts + * - Signup with eligibility (full flow) */ import type { z } from "zod"; @@ -15,15 +16,11 @@ import type { sendVerificationCodeResponseSchema, verifyCodeRequestSchema, verifyCodeResponseSchema, - quickEligibilityRequestSchema, - quickEligibilityResponseSchema, bilingualEligibilityAddressSchema, guestEligibilityRequestSchema, guestEligibilityResponseSchema, guestHandoffTokenSchema, completeAccountRequestSchema, - maybeLaterRequestSchema, - maybeLaterResponseSchema, signupWithEligibilityRequestSchema, signupWithEligibilityResponseSchema, getStartedSessionSchema, @@ -89,11 +86,9 @@ export type GetStartedErrorCode = export type SendVerificationCodeRequest = z.infer; export type VerifyCodeRequest = z.infer; -export type QuickEligibilityRequest = z.infer; export type BilingualEligibilityAddress = z.infer; export type GuestEligibilityRequest = z.infer; export type CompleteAccountRequest = z.infer; -export type MaybeLaterRequest = z.infer; export type SignupWithEligibilityRequest = z.infer; // ============================================================================ @@ -102,9 +97,7 @@ export type SignupWithEligibilityRequest = z.infer; export type VerifyCodeResponse = z.infer; -export type QuickEligibilityResponse = z.infer; export type GuestEligibilityResponse = z.infer; -export type MaybeLaterResponse = z.infer; export type SignupWithEligibilityResponse = z.infer; // ============================================================================ diff --git a/packages/domain/get-started/index.ts b/packages/domain/get-started/index.ts index 7c5d95ef..7266c08d 100644 --- a/packages/domain/get-started/index.ts +++ b/packages/domain/get-started/index.ts @@ -4,9 +4,9 @@ * Unified "Get Started" flow for: * - Email verification (OTP) * - Account status detection - * - Quick eligibility check (guest) + * - Guest eligibility check (no OTP required) * - Account completion (SF-only → full account) - * - "Maybe Later" flow + * - Signup with eligibility (full flow) */ // ============================================================================ @@ -24,15 +24,11 @@ export { type SendVerificationCodeResponse, type VerifyCodeRequest, type VerifyCodeResponse, - type QuickEligibilityRequest, - type QuickEligibilityResponse, type BilingualEligibilityAddress, type GuestEligibilityRequest, type GuestEligibilityResponse, type GuestHandoffToken, type CompleteAccountRequest, - type MaybeLaterRequest, - type MaybeLaterResponse, type SignupWithEligibilityRequest, type SignupWithEligibilityResponse, type GetStartedSession, @@ -51,9 +47,6 @@ export { verifyCodeRequestSchema, verifyCodeResponseSchema, accountStatusSchema, - // Quick eligibility schemas (OTP-verified) - quickEligibilityRequestSchema, - quickEligibilityResponseSchema, // Guest eligibility schemas (no OTP required) bilingualEligibilityAddressSchema, guestEligibilityRequestSchema, @@ -64,9 +57,6 @@ export { // Signup with eligibility schemas (full inline signup) signupWithEligibilityRequestSchema, signupWithEligibilityResponseSchema, - // Maybe later schemas - maybeLaterRequestSchema, - maybeLaterResponseSchema, // Session schema getStartedSessionSchema, } from "./schema.js"; diff --git a/packages/domain/get-started/schema.ts b/packages/domain/get-started/schema.ts index 1ba1f8ad..5cd247a3 100644 --- a/packages/domain/get-started/schema.ts +++ b/packages/domain/get-started/schema.ts @@ -95,7 +95,7 @@ export const verifyCodeResponseSchema = z.object({ }); // ============================================================================ -// Quick Eligibility Check Schemas +// Helpers // ============================================================================ /** @@ -106,37 +106,6 @@ const isoDateOnlySchema = z .regex(/^\d{4}-\d{2}-\d{2}$/, "Enter a valid date (YYYY-MM-DD)") .refine(value => !Number.isNaN(Date.parse(value)), "Enter a valid date (YYYY-MM-DD)"); -/** - * Request for quick eligibility check (guest flow) - * Minimal data required to create SF Account and check eligibility - */ -export const quickEligibilityRequestSchema = z.object({ - /** Session token from email verification */ - sessionToken: z.string().min(1, "Session token is required"), - /** Customer first name */ - firstName: nameSchema, - /** Customer last name */ - lastName: nameSchema, - /** Full address for eligibility check */ - address: addressFormSchema, - /** Optional phone number */ - phone: phoneSchema.optional(), -}); - -/** - * Response from quick eligibility check - */ -export const quickEligibilityResponseSchema = z.object({ - /** Whether the request was submitted successfully */ - submitted: z.boolean(), - /** Case ID for the eligibility request */ - requestId: z.string().optional(), - /** SF Account ID created */ - sfAccountId: z.string().optional(), - /** Message to display */ - message: z.string(), -}); - // ============================================================================ // Guest Eligibility Check Schemas (No OTP Required) // ============================================================================ @@ -252,39 +221,6 @@ export const completeAccountRequestSchema = z.object({ marketingConsent: z.boolean().optional(), }); -// ============================================================================ -// "Maybe Later" Flow Schemas -// ============================================================================ - -/** - * Request for "Maybe Later" flow - * Creates SF Account and eligibility case, customer can return later - */ -export const maybeLaterRequestSchema = z.object({ - /** Session token from email verification */ - sessionToken: z.string().min(1, "Session token is required"), - /** Customer first name */ - firstName: nameSchema, - /** Customer last name */ - lastName: nameSchema, - /** Full address for eligibility check */ - address: addressFormSchema, - /** Optional phone number */ - phone: phoneSchema.optional(), -}); - -/** - * Response from "Maybe Later" flow - */ -export const maybeLaterResponseSchema = z.object({ - /** Whether the SF account and case were created */ - success: z.boolean(), - /** Case ID for the eligibility request */ - requestId: z.string().optional(), - /** Message to display */ - message: z.string(), -}); - // ============================================================================ // Signup With Eligibility Schema (Full Inline Signup) // ============================================================================ diff --git a/packages/domain/support/providers/salesforce/mapper.ts b/packages/domain/support/providers/salesforce/mapper.ts index 6f5bec90..4382c0b7 100644 --- a/packages/domain/support/providers/salesforce/mapper.ts +++ b/packages/domain/support/providers/salesforce/mapper.ts @@ -260,9 +260,6 @@ export function buildCaseSelectFields(additionalFields: string[] = []): string[] // Flags "IsEscalated", - - // Email-to-Case Threading - "Thread_Id", ]; return [...new Set([...baseFields, ...additionalFields])]; diff --git a/packages/domain/support/providers/salesforce/raw.types.ts b/packages/domain/support/providers/salesforce/raw.types.ts index bc0aaf54..8e91d157 100644 --- a/packages/domain/support/providers/salesforce/raw.types.ts +++ b/packages/domain/support/providers/salesforce/raw.types.ts @@ -114,10 +114,6 @@ export const salesforceCaseRecordSchema = z.object({ MilestoneStatus: z.string().nullable().optional(), // Text(30) Language: z.string().nullable().optional(), // Picklist - // Email-to-Case Threading - // ───────────────────────────────────────────────────────────────────────── - Thread_Id: z.string().nullable().optional(), // Auto-generated thread ID for Email-to-Case - // Web-to-Case fields SuppliedName: z.string().nullable().optional(), // Web Name - Text(80) SuppliedEmail: z.string().nullable().optional(), // Web Email - Email