# Public Catalog & Unified Checkout - Development Plan > **Status**: Planning > **Created**: 2024-12-17 > **Epic**: Transform Portal into Public-Facing Website with E-commerce Checkout ## Executive Summary This document outlines the development plan to transform the customer portal from an authenticated-only application into a public-facing website where users can: 1. **Browse catalog without authentication** 2. **Configure products without an account** 3. **Complete checkout with seamless account creation** 4. **Place orders in a single, unified flow** The goal is to eliminate friction in the customer acquisition funnel by making registration feel like a natural part of the ordering process, rather than a prerequisite. ### Implementation Notes (Current Codebase) - Public catalog routes are implemented under `/shop` (not `/catalog`). - Unified checkout is implemented under `/order` (not `/checkout`). - Internet orders require an availability/eligibility confirmation step (between Address and Payment). --- ## Table of Contents 1. [Current State Analysis](#current-state-analysis) 2. [Target Architecture](#target-architecture) 3. [User Journeys](#user-journeys) 4. [Technical Design](#technical-design) 5. [Development Phases](#development-phases) 6. [API Changes](#api-changes) 7. [Database Changes](#database-changes) 8. [Testing Strategy](#testing-strategy) 9. [Rollout Plan](#rollout-plan) 10. [Risks & Mitigations](#risks--mitigations) 11. [Success Metrics](#success-metrics) --- ## Current State Analysis ### Current Architecture ``` ┌─────────────────────────────────────────────────────────────────────┐ │ CURRENT FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ 1. User visits portal │ │ 2. Must sign up (requires sfNumber from Salesforce) │ │ 3. Must add payment method (WHMCS SSO) │ │ 4. Can browse catalog │ │ 5. Can configure and order │ │ │ │ Problems: │ │ • sfNumber requirement blocks new customers │ │ • Registration is separate from ordering intent │ │ • High friction = low conversion │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ### Critical Integration Requirements **Salesforce is the source of truth** for customer data and must be created for every new customer: | System | Role | Requirement | | -------------- | ------------------------------ | ------------------------------------------------------- | | **Salesforce** | CRM, customer tracking, orders | Account + Contact MUST be created for every customer | | **WHMCS** | Billing, invoices, payments | Client MUST be linked to SF Account via `WH_Account__c` | | **Portal DB** | Authentication, session | User record links all systems via `id_mappings` | The relationship chain: ``` Salesforce Account (has SF_Account_No__c) └── WH_Account__c → WHMCS Client ID └── Portal User → id_mappings → whmcsClientId + sfAccountId ``` ### Current Route Structure ``` apps/portal/src/app/ ├── (public)/ # Only auth pages │ ├── auth/ │ │ ├── login/ │ │ ├── signup/ # Requires sfNumber │ │ └── ... │ └── page.tsx # Landing page │ └── (authenticated)/ # Everything else requires auth ├── catalog/ # ← Should be public! ├── checkout/ ├── dashboard/ ├── billing/ ├── subscriptions/ ├── orders/ └── support/ ``` ### Current Dependencies | Component | Current Behavior | Issue | | --------------- | ------------------------------------------ | ---------------------------- | | Catalog API | Works without auth (returns generic plans) | ✅ Ready | | Catalog UI | Requires authentication | ❌ Needs change | | Checkout | Requires auth + payment method | ❌ Needs redesign | | Signup | Requires sfNumber | ❌ Needs to be optional | | Payment Methods | WHMCS SSO only | ⚠️ Constraint to work around | --- ## Target Architecture ### New Route Structure ``` apps/portal/src/app/ ├── (public)/ │ ├── page.tsx # Homepage/Landing │ ├── layout.tsx # PublicShell │ │ │ ├── catalog/ # ★ PUBLIC CATALOG │ │ ├── page.tsx # Catalog home │ │ ├── layout.tsx # CatalogLayout │ │ ├── internet/ │ │ │ ├── page.tsx # Internet plans │ │ │ └── configure/page.tsx # Configure internet │ │ ├── sim/ │ │ │ ├── page.tsx # SIM plans │ │ │ └── configure/page.tsx # Configure SIM │ │ └── vpn/ │ │ └── page.tsx # VPN plans │ │ │ ├── checkout/ # ★ UNIFIED CHECKOUT │ │ ├── page.tsx # Multi-step checkout │ │ └── complete/page.tsx # Order confirmation │ │ │ ├── support/ # ★ PUBLIC SUPPORT │ │ ├── page.tsx # FAQ/Help center │ │ └── contact/page.tsx # Contact form │ │ │ └── auth/ │ ├── login/page.tsx # Simplified login │ └── forgot-password/page.tsx │ ├── (authenticated)/ │ ├── layout.tsx # AppShell │ ├── dashboard/ # Customer dashboard │ ├── orders/ # Order history │ ├── subscriptions/ # Manage subscriptions │ ├── billing/ # Invoices & payments │ └── support/ │ └── cases/ # Support case management ``` ### Unified Checkout Flow ``` ┌─────────────────────────────────────────────────────────────────────┐ │ NEW FLOW │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ PUBLIC CATALOG │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Browse Plans → Configure → "Proceed to Checkout" │ │ │ │ (saves to localStorage) │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ UNIFIED CHECKOUT (/order) │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ Step 1: Account │ │ │ │ • "Already have account? Sign in" OR │ │ │ │ • Collect: email, name, phone, password │ │ │ ├────────────────────────────────────────────────────────────┤ │ │ │ Step 2: Address │ │ │ │ • Collect service/shipping address │ │ │ │ (Account created in background after this step) │ │ │ ├────────────────────────────────────────────────────────────┤ │ │ │ Step 2.5: Availability (Internet only) │ │ │ │ • Request/confirm serviceability before payment │ │ │ ├────────────────────────────────────────────────────────────┤ │ │ │ Step 3: Payment │ │ │ │ • Open WHMCS to add payment method │ │ │ │ • Poll for completion, show confirmation │ │ │ ├────────────────────────────────────────────────────────────┤ │ │ │ Step 4: Review & Submit │ │ │ │ • Order summary, T&C acceptance, submit │ │ │ └────────────────────────────────────────────────────────────┘ │ │ ↓ │ │ ORDER CONFIRMATION → Redirect to /orders/[id] │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` --- ## User Journeys ### Journey 1: New Customer - Internet Order ```mermaid journey title New Customer Orders Internet section Browse Visit homepage: 5: Customer View internet plans: 5: Customer Select 10Gbps plan: 5: Customer Configure addons: 4: Customer section Checkout Enter email & name: 4: Customer Enter address: 4: Customer Add payment (WHMCS): 3: Customer Review order: 5: Customer Submit order: 5: Customer section Post-Order View confirmation: 5: Customer Access dashboard: 5: Customer ``` ### Journey 2: Existing Customer - Quick Order ```mermaid journey title Existing Customer Quick Order section Browse Visit catalog: 5: Customer Select SIM plan: 5: Customer Configure: 5: Customer section Checkout Click "Sign In": 5: Customer Login: 4: Customer Verify address: 5: Customer Confirm payment: 5: Customer Submit: 5: Customer ``` ### Journey 3: Abandoned Cart Recovery ```mermaid journey title Cart Recovery section Initial Configure product: 5: Customer Start checkout: 4: Customer Enter email: 4: Customer Leave site: 1: Customer section Recovery Receive email: 3: Customer Click link: 4: Customer Resume checkout: 5: Customer Complete order: 5: Customer ``` --- ## Technical Design ### 1. Checkout State Store ```typescript // Location: apps/portal/src/features/checkout/stores/checkout.store.ts import { create } from "zustand"; import { persist } from "zustand/middleware"; interface CartItem { orderType: "INTERNET" | "SIM" | "VPN"; planSku: string; planName: string; addonSkus: string[]; configuration: { installationType?: string; simType?: string; activationType?: string; mnpDetails?: MnpDetails; [key: string]: unknown; }; pricing: { monthlyTotal: number; oneTimeTotal: number; breakdown: PriceBreakdownItem[]; }; } interface GuestInfo { email: string; firstName: string; lastName: string; phone: string; phoneCountryCode: string; dateOfBirth?: string; gender?: "male" | "female" | "other"; password: string; } interface CheckoutState { // Cart data cartItem: CartItem | null; // Guest info (pre-registration) guestInfo: Partial | null; // Address address: Address | null; // Registration state registrationComplete: boolean; userId: string | null; // Payment state paymentMethodVerified: boolean; // Checkout step currentStep: "account" | "address" | "payment" | "review"; // Actions setCartItem: (item: CartItem) => void; updateGuestInfo: (info: Partial) => void; setAddress: (address: Address) => void; setRegistrationComplete: (userId: string) => void; setPaymentVerified: (verified: boolean) => void; setCurrentStep: (step: CheckoutState["currentStep"]) => void; clear: () => void; } export const useCheckoutStore = create()( persist( (set, get) => ({ cartItem: null, guestInfo: null, address: null, registrationComplete: false, userId: null, paymentMethodVerified: false, currentStep: "account", setCartItem: item => set({ cartItem: item }), updateGuestInfo: info => set(state => ({ guestInfo: { ...state.guestInfo, ...info }, })), setAddress: address => set({ address }), setRegistrationComplete: userId => set({ registrationComplete: true, userId, }), setPaymentVerified: verified => set({ paymentMethodVerified: verified }), setCurrentStep: step => set({ currentStep: step }), clear: () => set({ cartItem: null, guestInfo: null, address: null, registrationComplete: false, userId: null, paymentMethodVerified: false, currentStep: "account", }), }), { name: "checkout-store", version: 1, } ) ); ``` ### 2. Checkout Page Component Structure ```typescript // Location: apps/portal/src/app/(public)/checkout/page.tsx export default function CheckoutPage() { return ( ); } // Location: apps/portal/src/features/checkout/components/CheckoutWizard.tsx export function CheckoutWizard() { const { currentStep, cartItem } = useCheckoutStore(); const { isAuthenticated } = useAuthSession(); // Redirect if no cart if (!cartItem) { return ; } // Skip account step if already authenticated const effectiveStep = isAuthenticated && currentStep === 'account' ? 'address' : currentStep; return (
{effectiveStep === 'account' && } {effectiveStep === 'address' && } {effectiveStep === 'payment' && } {effectiveStep === 'review' && }
); } ``` ### 3. Account Step with Sign-In Option ```typescript // Location: apps/portal/src/features/checkout/components/steps/AccountStep.tsx export function AccountStep() { const [mode, setMode] = useState<'new' | 'signin'>('new'); const { updateGuestInfo, setCurrentStep } = useCheckoutStore(); const handleContinue = async (data: GuestFormData) => { updateGuestInfo(data); setCurrentStep('address'); }; const handleSignInSuccess = () => { // User is now authenticated, skip to address setCurrentStep('address'); }; return (
{/* Sign-in prompt */}

Already have an account?

Sign in to use your saved information

{mode === 'signin' ? ( setMode('new')} embedded /> ) : ( <>
Or continue as new customer
)}
); } ``` ### 4. Payment Step with WHMCS Integration ```typescript // Location: apps/portal/src/features/checkout/components/steps/PaymentStep.tsx export function PaymentStep() { const { setPaymentVerified, setCurrentStep, registrationComplete } = useCheckoutStore(); const { data: paymentMethods, refetch } = usePaymentMethods(); const createSsoLink = useCreatePaymentMethodsSsoLink(); const [isWaiting, setIsWaiting] = useState(false); // Poll for payment method after opening WHMCS useEffect(() => { if (!isWaiting) return; const interval = setInterval(async () => { const result = await refetch(); if (result.data?.paymentMethods?.length > 0) { setPaymentVerified(true); setIsWaiting(false); } }, 3000); return () => clearInterval(interval); }, [isWaiting, refetch, setPaymentVerified]); // Focus listener for when user returns useEffect(() => { const handleFocus = () => { if (isWaiting) { void refetch(); } }; window.addEventListener('focus', handleFocus); return () => window.removeEventListener('focus', handleFocus); }, [isWaiting, refetch]); const handleAddPayment = async () => { const { url } = await createSsoLink.mutateAsync(); window.open(url, '_blank'); setIsWaiting(true); }; const hasPaymentMethod = paymentMethods?.paymentMethods?.length > 0; return (

Payment Method

{hasPaymentMethod ? (
Payment method verified
) : (
{isWaiting ? ( <>

Waiting for payment method...

Complete the payment setup in the new tab, then return here.

) : ( <>

Add a payment method

We'll open our secure payment portal to add your card.

)}
)}
); } ``` ### 5. New BFF Endpoints ```typescript // Location: apps/bff/src/modules/checkout/checkout.controller.ts @Controller("checkout") export class CheckoutController { constructor( private readonly checkoutService: CheckoutService, private readonly logger: Logger ) {} /** * Register a new user during checkout * * IMPORTANT: This creates accounts in ALL systems synchronously: * 1. Salesforce Account + Contact (for CRM tracking) * 2. WHMCS Client (for billing) * 3. Portal User (for authentication) * * Returns auth tokens so user is immediately logged in */ @Post("register") @Public() @RateLimit({ limit: 5, ttl: 60 }) @UsePipes(new ZodValidationPipe(checkoutRegisterSchema)) async register(@Body() body: CheckoutRegisterRequest): Promise { this.logger.log("Checkout registration request", { email: body.email }); return this.checkoutService.registerForCheckout(body); } /** * Check if current user has valid payment method * Used by checkout to gate the review step */ @Get("payment-status") async getPaymentStatus(@Request() req: RequestWithUser): Promise { return this.checkoutService.getPaymentStatus(req.user.id); } /** * Build cart preview from configuration * Works for both authenticated and anonymous users */ @Post("cart") @Public() @UsePipes(new ZodValidationPipe(checkoutBuildCartRequestSchema)) async buildCart( @Request() req: RequestWithUser, @Body() body: CheckoutBuildCartRequest ): Promise { const userId = req.user?.id; return this.checkoutService.buildCart(body, userId); } /** * Submit order - requires authentication */ @Post("submit") @UsePipes(new ZodValidationPipe(checkoutSubmitSchema)) async submitOrder( @Request() req: RequestWithUser, @Body() body: CheckoutSubmitRequest ): Promise { return this.checkoutService.submitOrder(req.user.id, body); } } ``` ### 6. Salesforce Account Creation Service **CRITICAL**: Every new customer MUST have a Salesforce Account for business tracking. ```typescript // Location: apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts // ADD these new methods to the existing service /** * Create a new Salesforce Account for a new customer * This is used when customer signs up through checkout (no existing sfNumber) */ async createAccount(data: CreateSalesforceAccountRequest): Promise<{ accountId: string; accountNumber: string; }> { this.logger.log('Creating new Salesforce Account', { email: data.email }); // Generate unique account number (SF_Account_No__c) const accountNumber = await this.generateAccountNumber(); const accountPayload = { Name: `${data.firstName} ${data.lastName}`, SF_Account_No__c: accountNumber, BillingStreet: data.address.address1, BillingCity: data.address.city, BillingState: data.address.state, BillingPostalCode: data.address.postcode, BillingCountry: data.address.country, Phone: data.phone, // Portal tracking fields [this.portalStatusField]: 'Active', [this.portalSourceField]: 'Portal Checkout', // Record type for individual customers RecordTypeId: this.configService.get('SF_PERSON_ACCOUNT_RECORD_TYPE_ID'), }; try { const result = await this.connection.sobject('Account').create(accountPayload); if (!result.id) { throw new Error('Salesforce Account creation failed - no ID returned'); } this.logger.log('Salesforce Account created', { accountId: result.id, accountNumber }); return { accountId: result.id, accountNumber }; } catch (error) { this.logger.error('Failed to create Salesforce Account', { error: getErrorMessage(error), email: data.email, }); throw new Error('Failed to create customer account in CRM'); } } /** * Create a Contact associated with an Account */ async createContact(data: CreateSalesforceContactRequest): Promise<{ contactId: string }> { this.logger.log('Creating Salesforce Contact', { accountId: data.accountId, email: data.email }); const contactPayload = { AccountId: data.accountId, FirstName: data.firstName, LastName: data.lastName, Email: data.email, Phone: data.phone, MailingStreet: data.address.address1, MailingCity: data.address.city, MailingState: data.address.state, MailingPostalCode: data.address.postcode, MailingCountry: data.address.country, }; try { const result = await this.connection.sobject('Contact').create(contactPayload); if (!result.id) { throw new Error('Salesforce Contact creation failed - no ID returned'); } this.logger.log('Salesforce Contact created', { contactId: result.id }); return { contactId: result.id }; } catch (error) { this.logger.error('Failed to create Salesforce Contact', { error: getErrorMessage(error), accountId: data.accountId, }); throw new Error('Failed to create customer contact in CRM'); } } /** * Generate a unique customer number for new accounts * Format: PNNNNNNN (P = Portal, 7 digits) */ private async generateAccountNumber(): Promise { // Query for max existing portal account number const result = await this.connection.query( `SELECT SF_Account_No__c FROM Account WHERE SF_Account_No__c LIKE 'P%' ORDER BY SF_Account_No__c DESC LIMIT 1`, { label: 'auth:getMaxAccountNumber' } ); let nextNumber = 1000001; // Start from P1000001 if (result.totalSize > 0) { const lastNumber = result.records[0]?.SF_Account_No__c; if (lastNumber) { const numPart = parseInt(lastNumber.substring(1), 10); if (!isNaN(numPart)) { nextNumber = numPart + 1; } } } return `P${nextNumber}`; } ``` ### 7. Checkout Registration Service - Full Flow ```typescript // Location: apps/bff/src/modules/checkout/checkout.service.ts @Injectable() export class CheckoutService { constructor( private readonly salesforceAccountService: SalesforceAccountService, private readonly whmcsService: WhmcsService, private readonly usersFacade: UsersFacade, private readonly mappingsService: MappingsService, private readonly tokenService: AuthTokenService, private readonly prisma: PrismaService, @Inject(Logger) private readonly logger: Logger ) {} /** * Register a new customer during checkout * * This is a critical workflow that creates accounts in ALL systems: * * 1. Create Salesforce Account (generates SF_Account_No__c) * 2. Create Salesforce Contact (linked to Account) * 3. Create WHMCS Client (for billing) * 4. Update SF Account with WH_Account__c * 5. Create Portal User * 6. Create ID Mapping (links all systems) * 7. Generate auth tokens * * If any step fails, we attempt rollback of previous steps. */ async registerForCheckout(data: CheckoutRegisterRequest): Promise { this.logger.log("Starting checkout registration", { email: data.email }); // Track created resources for rollback let sfAccountId: string | null = null; let sfContactId: string | null = null; let sfAccountNumber: string | null = null; let whmcsClientId: number | null = null; let portalUserId: string | null = null; try { // Step 1: Create Salesforce Account this.logger.log("Step 1: Creating Salesforce Account"); const sfAccount = await this.salesforceAccountService.createAccount({ firstName: data.firstName, lastName: data.lastName, email: data.email, phone: this.formatPhone(data.phoneCountryCode, data.phone), address: data.address, }); sfAccountId = sfAccount.accountId; sfAccountNumber = sfAccount.accountNumber; // Step 2: Create Salesforce Contact this.logger.log("Step 2: Creating Salesforce Contact"); const sfContact = await this.salesforceAccountService.createContact({ accountId: sfAccountId, firstName: data.firstName, lastName: data.lastName, email: data.email, phone: this.formatPhone(data.phoneCountryCode, data.phone), address: data.address, }); sfContactId = sfContact.contactId; // Step 3: Create WHMCS Client this.logger.log("Step 3: Creating WHMCS Client"); const whmcsClient = await this.whmcsService.addClient({ firstname: data.firstName, lastname: data.lastName, email: data.email, phonenumber: this.formatPhone(data.phoneCountryCode, data.phone), address1: data.address.address1, address2: data.address.address2 || "", city: data.address.city, state: data.address.state, postcode: data.address.postcode, country: data.address.country, password2: data.password, customfields: this.buildWhmcsCustomFields(sfAccountNumber), }); whmcsClientId = whmcsClient.clientId; // Step 4: Update Salesforce Account with WHMCS ID this.logger.log("Step 4: Linking Salesforce to WHMCS"); await this.salesforceAccountService.updatePortalFields(sfAccountId, { whmcsAccountId: whmcsClientId, status: "Active", source: "Portal Checkout", }); // Step 5: Create Portal User (in transaction) this.logger.log("Step 5: Creating Portal User"); const user = await this.prisma.$transaction(async tx => { const passwordHash = await argon2.hash(data.password); const newUser = await tx.user.create({ data: { email: data.email, passwordHash, isActive: true, emailVerified: false, }, }); // Step 6: Create ID Mapping await tx.idMapping.create({ data: { userId: newUser.id, whmcsClientId: whmcsClientId!, sfAccountId: sfAccountId!, sfContactId: sfContactId!, }, }); return newUser; }); portalUserId = user.id; // Step 7: Generate auth tokens this.logger.log("Step 6: Generating auth tokens"); const tokens = await this.tokenService.generateTokenPair({ sub: user.id, email: user.email, }); this.logger.log("Checkout registration completed successfully", { userId: user.id, sfAccountId, sfAccountNumber, whmcsClientId, }); return { success: true, user: { id: user.id, email: user.email, firstname: data.firstName, lastname: data.lastName, }, session: { expiresAt: tokens.accessTokenExpiresAt, refreshExpiresAt: tokens.refreshTokenExpiresAt, }, sfAccountNumber, // Return so it can be shown to user }; } catch (error) { this.logger.error("Checkout registration failed, initiating rollback", { error: getErrorMessage(error), sfAccountId, whmcsClientId, portalUserId, }); // Rollback in reverse order await this.rollbackRegistration({ portalUserId, whmcsClientId, sfAccountId, }); throw new BadRequestException("Registration failed. Please try again or contact support."); } } private async rollbackRegistration(resources: { portalUserId: string | null; whmcsClientId: number | null; sfAccountId: string | null; }) { // Best-effort rollback - log failures but don't throw if (resources.portalUserId) { try { await this.prisma.user.delete({ where: { id: resources.portalUserId } }); this.logger.log("Rollback: Deleted portal user", { userId: resources.portalUserId }); } catch (e) { this.logger.error("Rollback failed: Portal user", { error: getErrorMessage(e) }); } } if (resources.whmcsClientId) { try { await this.whmcsService.deleteClient(resources.whmcsClientId); this.logger.log("Rollback: Deleted WHMCS client", { clientId: resources.whmcsClientId }); } catch (e) { this.logger.error("Rollback failed: WHMCS client", { error: getErrorMessage(e) }); } } // Note: We intentionally do NOT delete the Salesforce Account // It's better to have an orphaned SF Account that can be cleaned up // than to lose potential customer data if (resources.sfAccountId) { this.logger.warn("Salesforce Account not rolled back (intentional)", { sfAccountId: resources.sfAccountId, action: "Manual cleanup may be required", }); } } private formatPhone(countryCode: string, phone: string): string { const cc = countryCode.replace(/\D/g, ""); const num = phone.replace(/\D/g, ""); return `+${cc}.${num}`; } private buildWhmcsCustomFields(sfAccountNumber: string): string { const customerNumberFieldId = this.configService.get("WHMCS_CUSTOMER_NUMBER_FIELD_ID"); if (!customerNumberFieldId) return ""; return `${customerNumberFieldId}|${sfAccountNumber}`; } } ``` --- ## Development Phases ### Phase 0: Preparation (3 days) | Task | Owner | Effort | Dependencies | | ---------------------------------------- | ----- | ------ | ------------ | | Create feature branch | Dev | 0.5d | - | | Document current catalog/checkout flows | Dev | 0.5d | - | | Set up feature flags for gradual rollout | Dev | 1d | - | | Create test accounts for each scenario | QA | 1d | - | ### Phase 1: Public Catalog (5 days) | Task | Description | Effort | Priority | | ---------------------------------------------------------- | ------------------------------------ | ------ | -------- | | **1.1** Create `CatalogLayout` component | Hybrid header for public/auth states | 1d | P0 | | **1.2** Move catalog routes to `(public)/catalog/` | Copy and adapt existing pages | 1d | P0 | | **1.3** Add `@Public()` decorator to BFF catalog endpoints | Make API publicly accessible | 0.5d | P0 | | **1.4** Update catalog service for anonymous users | Handle missing userId gracefully | 0.5d | P0 | | **1.5** Create "Proceed to Checkout" flow | Save config to store, redirect | 1d | P0 | | **1.6** Test all catalog pages without auth | E2E testing | 1d | P0 | **Deliverable**: Users can browse and configure products without logging in. ### Phase 2: Checkout Store & Cart (3 days) | Task | Description | Effort | Priority | | ----------------------------------------- | ------------------------------------ | ------ | -------- | | **2.1** Create checkout Zustand store | With localStorage persistence | 0.5d | P0 | | **2.2** Implement cart item serialization | Save/restore configuration | 0.5d | P0 | | **2.3** Add cart validation utilities | Ensure cart is valid before checkout | 0.5d | P0 | | **2.4** Create cart recovery hook | Detect and restore abandoned carts | 0.5d | P1 | | **2.5** Unit tests for store | Full coverage | 1d | P0 | **Deliverable**: Cart state persists across page reloads and sessions. ### Phase 3: Unified Checkout UI (8 days) | Task | Description | Effort | Priority | | ------------------------------------------ | --------------------------- | ------ | -------- | | **3.1** Create `/checkout` page shell | Layout, progress indicator | 0.5d | P0 | | **3.2** Build `CheckoutWizard` component | Step management, navigation | 1d | P0 | | **3.3** Build `AccountStep` component | Sign-in + new account form | 1.5d | P0 | | **3.4** Build `AddressStep` component | Reuse existing address form | 1d | P0 | | **3.5** Build `PaymentStep` component | WHMCS SSO + polling | 1.5d | P0 | | **3.6** Build `ReviewStep` component | Summary + terms + submit | 1d | P0 | | **3.7** Build `OrderSummaryCard` component | Sidebar cart display | 0.5d | P0 | | **3.8** Build order confirmation page | Success state, next steps | 1d | P0 | **Deliverable**: Complete checkout UI with all steps functional. ### Phase 4: Backend - Checkout Registration (7 days) | Task | Description | Effort | Priority | | --------------------------------------------------------- | -------------------------------------------------- | ------ | -------- | | **4.1** Update domain schema - make sfNumber optional | Schema changes | 0.5d | P0 | | **4.2** Add `createAccount()` to SalesforceAccountService | Create SF Account for new customers | 1d | P0 | | **4.3** Add `createContact()` to SalesforceAccountService | Create SF Contact linked to Account | 0.5d | P0 | | **4.4** Add `generateAccountNumber()` method | Auto-generate SF_Account_No\_\_c (PNNNNNNN format) | 0.5d | P0 | | **4.5** Create `CheckoutService` in BFF | Main registration orchestration | 1d | P0 | | **4.6** Implement `registerForCheckout()` method | Full 7-step registration flow | 1.5d | P0 | | **4.7** Create `POST /checkout/register` endpoint | Public registration endpoint | 0.5d | P0 | | **4.8** Implement rollback logic | Compensating transactions on failure | 1d | P0 | | **4.9** Add comprehensive logging | Track each step for debugging | 0.5d | P0 | **Deliverable**: Users can create accounts during checkout with **synchronous** Salesforce Account + Contact + WHMCS Client creation. > **IMPORTANT**: Salesforce Account is created FIRST to ensure CRM tracking. The flow is: > > 1. Create SF Account (generates customer number) > 2. Create SF Contact > 3. Create WHMCS Client > 4. Link SF Account to WHMCS (update WH_Account\_\_c) > 5. Create Portal User + ID Mapping > 6. Return auth tokens ### Phase 5: Integration & Testing (5 days) | Task | Description | Effort | Priority | | ------------------------------------------------------- | ------------------------- | ------ | -------- | | **5.1** Integrate checkout UI with BFF endpoints | Wire up API calls | 1d | P0 | | **5.2** Implement auth token handling post-registration | Auto-login after signup | 0.5d | P0 | | **5.3** E2E tests - new customer flow | Full journey testing | 1d | P0 | | **5.4** E2E tests - existing customer flow | Sign-in during checkout | 0.5d | P0 | | **5.5** E2E tests - cart recovery | Abandoned cart scenarios | 0.5d | P1 | | **5.6** Performance testing | Load testing checkout | 0.5d | P1 | | **5.7** Security review | Auth, CSRF, rate limiting | 1d | P0 | **Deliverable**: Fully tested, production-ready checkout flow. ### Phase 6: Public Support (3 days) | Task | Description | Effort | Priority | | --------------------------------------------------- | -------------------------- | ------ | -------- | | **6.1** Create public support landing page | FAQ, contact info | 1d | P1 | | **6.2** Create public contact form | No auth required | 1d | P1 | | **6.3** Add `POST /support/contact` public endpoint | Creates Lead in Salesforce | 1d | P1 | **Deliverable**: Non-authenticated users can get help and contact support. ### Phase 7: Polish & Launch Prep (4 days) | Task | Description | Effort | Priority | | ------------------------------------- | ---------------------------- | ------ | -------- | | **7.1** Error handling polish | User-friendly error messages | 1d | P0 | | **7.2** Loading states and skeletons | Smooth UX during async ops | 0.5d | P0 | | **7.3** Mobile responsiveness testing | All breakpoints | 0.5d | P0 | | **7.4** Analytics integration | Track funnel, conversions | 0.5d | P1 | | **7.5** Documentation update | README, guides | 0.5d | P1 | | **7.6** Feature flag configuration | Gradual rollout setup | 0.5d | P0 | | **7.7** Production deployment prep | Env configs, monitoring | 0.5d | P0 | **Deliverable**: Production-ready feature. --- ## Timeline Summary ``` Week 1: Phase 0 (Prep) + Phase 1 (Public Catalog) Week 2: Phase 2 (Cart) + Phase 3 Start (Checkout UI) Week 3: Phase 3 Complete (Checkout UI) Week 4: Phase 4 (Backend - SF Account Creation) Week 5: Phase 4 Complete + Phase 5 (Integration Testing) Week 6: Phase 6 (Public Support) + Phase 7 (Polish) Week 7: Buffer + Launch Total: ~7 weeks ``` ### Critical Path The **Salesforce Account creation** (Phase 4) is on the critical path because: 1. Orders require a valid SF Account ID 2. WHMCS must be linked to SF Account 3. All tracking/reporting depends on SF data Ensure SF integration user has proper permissions before starting Phase 4. --- ## API Changes ### New Endpoints | Method | Path | Auth | Description | | ------ | ------------------------------ | -------- | --------------------------- | | `POST` | `/api/checkout/register` | Public | Create user during checkout | | `GET` | `/api/checkout/payment-status` | Required | Check payment method status | | `POST` | `/api/checkout/cart` | Public | Build cart from config | | `POST` | `/api/checkout/submit` | Required | Submit order | | `POST` | `/api/support/contact` | Public | Public contact form | ### Modified Endpoints | Method | Path | Change | | ------ | ------------------ | ------------------------- | | `GET` | `/api/catalog/*` | Add `@Public()` decorator | | `POST` | `/api/auth/signup` | Make sfNumber optional | ### New Schemas ```typescript // packages/domain/checkout/schema.ts export const checkoutRegisterSchema = z.object({ email: z.string().email(), firstName: z.string().min(1), lastName: z.string().min(1), phone: z.string().min(1), phoneCountryCode: z.string().regex(/^\+\d{1,4}$/), password: z.string().min(8), address: addressFormSchema, acceptTerms: z.literal(true), marketingConsent: z.boolean().optional(), }); export const checkoutSubmitSchema = z.object({ orderType: orderTypeSchema, skus: z.array(z.string()).min(1), configuration: z.record(z.unknown()).optional(), }); export const publicContactSchema = z.object({ email: z.string().email(), name: z.string().min(1), subject: z.string().min(1), message: z.string().min(10), phone: z.string().optional(), }); ``` --- ## Database Changes ### Portal Database - No Schema Changes Required The current database schema supports this feature: - `User` table already stores auth credentials - `IdMapping` table already has `sfAccountId` and `sfContactId` fields - No new tables needed ### Salesforce - New Field Requirements Ensure these fields exist and are writable by the integration user: | Object | Field | Type | Purpose | | ------- | ------------------------------- | -------- | ---------------------------------- | | Account | `SF_Account_No__c` | Text | Unique customer number | | Account | `WH_Account__c` | Text | WHMCS Client ID link | | Account | `Portal_Status__c` | Picklist | Active/Inactive | | Account | `Portal_Registration_Source__c` | Picklist | Add "Portal Checkout" value | | Contact | Standard fields | - | First, Last, Email, Phone, Address | ### Salesforce - Customer Number Generation New accounts created through checkout will have auto-generated customer numbers: - Format: `PNNNNNNN` (P prefix + 7 digits) - Example: `P1000001`, `P1000002` - The "P" prefix distinguishes portal-originated customers from legacy customers ### WHMCS - No Changes Required WHMCS client creation already works; just need to: - Set the customer number custom field with the generated SF_Account_No\_\_c - Ensure the client is created with proper address data --- ## Testing Strategy ### Unit Tests | Component | Coverage Target | | ------------------------- | --------------- | | CheckoutStore | 100% | | Cart validation utilities | 100% | | Checkout steps | 90% | | BFF CheckoutService | 90% | ### Integration Tests | Flow | Scenarios | | -------------------------- | ---------------------------------------------- | | New customer checkout | Happy path, validation errors, payment failure | | Existing customer checkout | Sign-in flow, pre-filled data | | Cart persistence | Page refresh, browser close, storage limits | ### E2E Tests | Test | Description | | ------------------------------------ | ------------------------------------------------ | | `checkout-new-customer.spec.ts` | Full flow: browse → configure → checkout → order | | `checkout-existing-customer.spec.ts` | Sign-in during checkout | | `checkout-cart-recovery.spec.ts` | Abandoned cart scenarios | | `public-catalog.spec.ts` | Catalog access without auth | ### Manual Testing Checklist - [ ] Mobile responsive design - [ ] Screen reader accessibility - [ ] Keyboard navigation - [ ] WHMCS payment flow in different browsers - [ ] Error recovery scenarios - [ ] Rate limiting behavior - [ ] Session timeout during checkout --- ## Rollout Plan ### Stage 1: Internal Testing (1 week) - Deploy to staging - Internal team testing - Bug fixes ### Stage 2: Beta (1 week) - Enable for 10% of traffic via feature flag - Monitor error rates, conversion - Gather feedback ### Stage 3: Gradual Rollout (2 weeks) - 25% → 50% → 75% → 100% - Monitor metrics at each stage - Rollback plan ready ### Stage 4: Full Launch - Remove feature flag - Update marketing materials - Announce to existing customers --- ## Risks & Mitigations | Risk | Likelihood | Impact | Mitigation | | ---------------------------------------------- | ---------- | ------------ | --------------------------------------------------------------- | | **Salesforce Account creation fails** | Medium | **Critical** | Robust error handling, retry logic, clear user messaging | | **Salesforce API rate limits** | Medium | High | Implement queuing if needed, monitor API usage | | Salesforce permission issues | Medium | High | Pre-validate permissions in Phase 0, test with integration user | | WHMCS SSO breaks checkout flow | Medium | High | Implement iframe fallback, clear error messaging | | Partial registration (SF created, WHMCS fails) | Medium | High | Rollback logic, manual cleanup process documented | | Cart data corruption | Low | Medium | Robust validation, version migration logic | | Rate limiting blocks legitimate users | Low | Medium | Tune limits, add captcha fallback | | Performance degradation (SF calls slow) | Medium | Medium | Add timeouts, show progress indicators | | Duplicate SF Account creation | Low | Medium | Check for existing account by email before creating | ### Salesforce-Specific Risks | Risk | Mitigation | | ---------------------------------------- | ------------------------------------------------------ | | Integration user lacks create permission | Test all CRUD operations in sandbox first | | Required fields missing in payload | Document all required fields, validate before API call | | Customer number collision | Use database sequence or SF auto-number field | | SF sandbox vs production differences | Test in production-like sandbox with same config | --- ## Success Metrics ### Primary Metrics | Metric | Current | Target | Measurement | | ------------------------ | -------- | ------ | ------------------------- | | Catalog page views | N/A | +200% | Analytics | | Checkout start rate | ~20% | 40% | Cart creation / plan view | | Checkout completion rate | ~30% | 50% | Orders / checkout starts | | New customer acquisition | Baseline | +50% | New accounts | ### Secondary Metrics | Metric | Target | | -------------------------- | ------------ | | Time to first order | < 10 minutes | | Cart abandonment rate | < 60% | | Payment step completion | > 70% | | Error rate during checkout | < 2% | --- ## Appendix A: Component File Structure ``` apps/portal/src/features/checkout/ ├── components/ │ ├── CheckoutLayout.tsx │ ├── CheckoutProgress.tsx │ ├── CheckoutWizard.tsx │ ├── OrderSummaryCard.tsx │ ├── EmptyCartRedirect.tsx │ └── steps/ │ ├── AccountStep.tsx │ ├── AddressStep.tsx │ ├── PaymentStep.tsx │ └── ReviewStep.tsx ├── hooks/ │ ├── useCheckout.ts # Main checkout logic │ ├── useCheckoutNavigation.ts # Step navigation │ ├── usePaymentPolling.ts # WHMCS polling │ └── useCartRecovery.ts # Abandoned cart ├── services/ │ ├── checkout.service.ts # API calls │ └── cart.service.ts # Cart utilities ├── stores/ │ └── checkout.store.ts # Zustand store ├── utils/ │ ├── cart-validation.ts │ └── checkout-analytics.ts └── index.ts ``` --- ## Appendix B: Feature Flag Configuration ```typescript // Feature flags for gradual rollout export const FEATURE_FLAGS = { PUBLIC_CATALOG: 'public-catalog-enabled', UNIFIED_CHECKOUT: 'unified-checkout-enabled', CHECKOUT_REGISTRATION: 'checkout-registration-enabled', PUBLIC_SUPPORT: 'public-support-enabled', }; // Usage in components function CatalogPage() { const isPublicCatalogEnabled = useFeatureFlag(FEATURE_FLAGS.PUBLIC_CATALOG); if (!isPublicCatalogEnabled && !isAuthenticated) { redirect('/auth/login'); } return ; } ``` --- ## Document History | Version | Date | Author | Changes | | ------- | ---------- | ------ | --------------- | | 1.0 | 2024-12-17 | - | Initial version |