- Added an availability confirmation step in the checkout process for internet orders, ensuring users verify service eligibility before proceeding to payment. - Updated the CheckoutWizard component to conditionally include the new AvailabilityStep based on the order type. - Refactored AddressStep to navigate to the AvailabilityStep for internet orders, improving user flow. - Enhanced the checkout store to track internet availability requests and updated the state management for better handling of checkout steps. - Updated relevant schemas and documentation to reflect the new checkout flow and requirements.
1431 lines
53 KiB
Markdown
1431 lines
53 KiB
Markdown
# 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<GuestInfo> | 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<GuestInfo>) => void;
|
|
setAddress: (address: Address) => void;
|
|
setRegistrationComplete: (userId: string) => void;
|
|
setPaymentVerified: (verified: boolean) => void;
|
|
setCurrentStep: (step: CheckoutState["currentStep"]) => void;
|
|
clear: () => void;
|
|
}
|
|
|
|
export const useCheckoutStore = create<CheckoutState>()(
|
|
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 (
|
|
<CheckoutLayout>
|
|
<CheckoutWizard />
|
|
</CheckoutLayout>
|
|
);
|
|
}
|
|
|
|
// 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 <EmptyCartRedirect />;
|
|
}
|
|
|
|
// Skip account step if already authenticated
|
|
const effectiveStep = isAuthenticated && currentStep === 'account'
|
|
? 'address'
|
|
: currentStep;
|
|
|
|
return (
|
|
<div className="max-w-3xl mx-auto">
|
|
<CheckoutProgress currentStep={effectiveStep} />
|
|
|
|
<div className="mt-8 grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
<div className="lg:col-span-2">
|
|
{effectiveStep === 'account' && <AccountStep />}
|
|
{effectiveStep === 'address' && <AddressStep />}
|
|
{effectiveStep === 'payment' && <PaymentStep />}
|
|
{effectiveStep === 'review' && <ReviewStep />}
|
|
</div>
|
|
|
|
<div className="lg:col-span-1">
|
|
<OrderSummaryCard item={cartItem} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<div className="space-y-6">
|
|
{/* Sign-in prompt */}
|
|
<div className="bg-muted/50 rounded-xl p-6 border border-border">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="font-semibold">Already have an account?</h3>
|
|
<p className="text-sm text-muted-foreground">
|
|
Sign in to use your saved information
|
|
</p>
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setMode('signin')}
|
|
>
|
|
Sign In
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{mode === 'signin' ? (
|
|
<SignInForm
|
|
onSuccess={handleSignInSuccess}
|
|
onCancel={() => setMode('new')}
|
|
embedded
|
|
/>
|
|
) : (
|
|
<>
|
|
<div className="relative">
|
|
<div className="absolute inset-0 flex items-center">
|
|
<div className="w-full border-t border-border" />
|
|
</div>
|
|
<div className="relative flex justify-center text-sm">
|
|
<span className="bg-background px-4 text-muted-foreground">
|
|
Or continue as new customer
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<GuestInfoForm onSubmit={handleContinue} />
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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 (
|
|
<div className="space-y-6">
|
|
<div className="bg-card rounded-xl border border-border p-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<CreditCardIcon className="h-6 w-6 text-primary" />
|
|
<h2 className="text-lg font-semibold">Payment Method</h2>
|
|
</div>
|
|
|
|
{hasPaymentMethod ? (
|
|
<div className="space-y-4">
|
|
<PaymentMethodCard
|
|
method={paymentMethods.paymentMethods[0]}
|
|
/>
|
|
<div className="flex items-center gap-2 text-sm text-success">
|
|
<CheckCircleIcon className="h-5 w-5" />
|
|
Payment method verified
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="text-center py-8">
|
|
<CreditCardIcon className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
|
|
|
|
{isWaiting ? (
|
|
<>
|
|
<h3 className="font-semibold mb-2">Waiting for payment method...</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
Complete the payment setup in the new tab, then return here.
|
|
</p>
|
|
<Spinner className="mx-auto" />
|
|
</>
|
|
) : (
|
|
<>
|
|
<h3 className="font-semibold mb-2">Add a payment method</h3>
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
We'll open our secure payment portal to add your card.
|
|
</p>
|
|
<Button onClick={handleAddPayment}>
|
|
Add Payment Method
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-between">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={() => setCurrentStep('address')}
|
|
>
|
|
← Back
|
|
</Button>
|
|
<Button
|
|
onClick={() => setCurrentStep('review')}
|
|
disabled={!hasPaymentMethod}
|
|
>
|
|
Continue to Review
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 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<CheckoutRegisterResponse> {
|
|
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<PaymentStatusResponse> {
|
|
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<CheckoutCart> {
|
|
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<OrderCreateResponse> {
|
|
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<string> {
|
|
// 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<CheckoutRegisterResponse> {
|
|
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 <CatalogContent />;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Document History
|
|
|
|
| Version | Date | Author | Changes |
|
|
| ------- | ---------- | ------ | --------------- |
|
|
| 1.0 | 2024-12-17 | - | Initial version |
|