# Customer Portal – Technical Operations Guide A comprehensive guide for supervisors explaining how the Customer Portal integrates with WHMCS, Salesforce, and other systems. --- ## Contents 1. [System Architecture](#system-architecture) 2. [Data Ownership and Flow](#data-ownership-and-flow) 3. [Account Creation and Linking](#account-creation-and-linking) 4. [Profile and Address Management](#profile-and-address-management) 5. [Password Management](#password-management) 6. [Product Catalog and Eligibility](#product-catalog-and-eligibility) 7. [Order Creation](#order-creation) 8. [Order Fulfillment and Provisioning](#order-fulfillment-and-provisioning) 9. [Billing and Payments](#billing-and-payments) 10. [Subscriptions and Services](#subscriptions-and-services) 11. [SIM Management](#sim-management) 12. [Support Cases](#support-cases) 13. [Dashboard and Summary Data](#dashboard-and-summary-data) 14. [Realtime Events](#realtime-events) 15. [Caching Strategy](#caching-strategy) 16. [Error Handling](#error-handling) 17. [Rate Limiting and Security](#rate-limiting-and-security) --- ## System Architecture The portal consists of two main components: - **Frontend**: Next.js application serving the customer UI - **BFF (Backend-for-Frontend)**: NestJS API that orchestrates calls to external systems ### Connected Systems | System | Role | Integration Method | | ---------------------- | --------------------------------------- | ------------------------------------ | | **WHMCS** | Billing system of record | REST API (API actions) | | **Salesforce** | CRM and order management | REST API + Change Data Capture (CDC) | | **Freebit** | SIM/MVNO provisioning | REST API | | **SFTP (fs.mvno.net)** | Call/SMS detail records | SFTP file download | | **PostgreSQL** | Portal user accounts and ID mappings | Direct connection | | **Redis** | Caching and pub/sub for realtime events | Direct connection | ### ID Mapping The portal maintains a mapping table (`id_mappings` in PostgreSQL) linking: - `user_id` (portal UUID) ↔ `whmcs_client_id` (integer) ↔ `sf_account_id` (Salesforce 18-char ID) This mapping is validated on every cross-system operation. The mapping is cached in Redis and can be looked up by any of the three IDs. --- ## Data Ownership and Flow | Data | System of Record | Portal Behavior | | -------------------------- | ------------------------- | ------------------------------------------------------------------------- | | User credentials | Portal (PostgreSQL) | Passwords hashed with Argon2; no WHMCS/SF credentials stored | | Client profile & address | WHMCS | Portal reads/writes to WHMCS; clears cache on update | | Product catalog & prices | Salesforce (Pricebook) | Portal reads from portal pricebook (configured via `PORTAL_PRICEBOOK_ID`) | | Orders & order status | Salesforce (Order object) | Portal creates orders; Salesforce CDC triggers fulfillment | | Invoices & payment methods | WHMCS | Portal reads only; payments via WHMCS SSO | | Subscriptions/Services | WHMCS (tblhosting) | Portal reads only | | Support cases | Salesforce (Case object) | Portal creates/reads cases with Origin = "Portal Website" | | SIM details & usage | Freebit | Portal reads/writes via Freebit API | --- ## Account Creation and Linking ### Salesforce Account Lookup The portal finds a Salesforce Account using the Customer Number: ```sql SELECT Id, Name, WH_Account__c FROM Account WHERE SF_Account_No__c = '{customerNumber}' ``` | Field | Purpose | | ------------------ | ----------------------------------------------------------- | | `SF_Account_No__c` | Customer Number field used for lookup | | `Id` | The Salesforce Account ID (18-char) stored in `id_mappings` | | `WH_Account__c` | If populated, indicates account is already linked to WHMCS | ### New Customer Sign-Up **Validation Steps:** 1. Check if email already exists in portal `users` table - If exists with mapping → "You already have an account. Please sign in." - If exists without mapping → "Please sign in to continue setup." 2. Query Salesforce Account by Customer Number (`SF_Account_No__c`) - If not found → "Salesforce account not found for Customer Number" 3. Check if `WH_Account__c` field is already populated on Salesforce Account - If populated → "You already have an account. Please use the login page." 4. Check if email exists in WHMCS via `GetClientsDetails` - If found with portal mapping → "You already have an account. Please sign in." - If found without mapping → "We found an existing billing account. Please link your account instead." **Creation Steps (if validation passes):** 1. Create WHMCS client via `AddClient` API action with: - Contact info: `firstname`, `lastname`, `email`, `phonenumber` - Address fields: `address1`, `address2`, `city`, `state`, `postcode`, `country` - Custom field 198 (configurable): Customer Number - Optional custom fields: DOB, Gender, Nationality (field IDs configurable) - Password (synced to WHMCS for SSO compatibility) 2. Create portal user record in PostgreSQL (password hashed with Argon2) 3. Create ID mapping in same transaction: `user_id` ↔ `whmcs_client_id` ↔ `sf_account_id` 4. Update Salesforce Account with portal fields (see below) **Salesforce Account Fields Updated:** | Field | Value | Notes | | ------------------------------- | --------------- | ------------------------------------------------------ | | `Portal_Status__c` | "Active" | Configurable via `ACCOUNT_PORTAL_STATUS_FIELD` | | `Portal_Registration_Source__c` | "Portal" | Configurable via `ACCOUNT_PORTAL_STATUS_SOURCE_FIELD` | | `Portal_Last_SignIn__c` | ISO timestamp | Configurable via `ACCOUNT_PORTAL_LAST_SIGNED_IN_FIELD` | | `WH_Account__c` | WHMCS Client ID | Configurable via `ACCOUNT_WHMCS_FIELD` | **Rollback Behavior:** - If WHMCS client creation fails → no portal record created - If portal DB transaction fails after WHMCS creation → WHMCS client marked `Inactive` for manual cleanup ### Linking Existing WHMCS Account **Flow:** 1. Customer submits WHMCS email and password 2. Portal validates credentials via WHMCS `ValidateLogin` API action 3. Check if WHMCS client is already mapped → "This billing account is already linked. Please sign in." 4. Portal reads Customer Number from WHMCS custom field 198 via `GetClientsDetails` 5. Portal queries Salesforce Account by Customer Number (`SF_Account_No__c`) 6. Portal creates local user (without password – needs to be set) 7. Portal inserts ID mapping 8. Portal updates Salesforce Account with portal flags (`Portal_Status__c` = "Active", etc.) 9. Customer is prompted to set a portal password --- ## Profile and Address Management ### Data Source - All profile/address data is read from WHMCS via `GetClientsDetails` API action - Portal caches client profile for 30 minutes ### Updates - Profile updates are written to WHMCS via `UpdateClient` API action - Cache is invalidated immediately after successful update - Salesforce only receives address snapshots at order creation time (not live synced) ### Available Fields | Field | WHMCS Field | Notes | | ---------------- | ------------- | -------------------- | | First Name | `firstname` | | | Last Name | `lastname` | | | Email | `email` | | | Phone | `phonenumber` | Format: +CC.NNNNNNNN | | Address Line 1 | `address1` | | | Address Line 2 | `address2` | Optional | | City | `city` | | | State/Prefecture | `state` | | | Postcode | `postcode` | | | Country | `country` | 2-letter ISO code | --- ## Password Management ### Portal Password - Stored in PostgreSQL hashed with Argon2 - Completely separate from WHMCS credentials after initial sync - WHMCS password is set during signup for SSO compatibility but not synced thereafter ### Password Reset Flow 1. Customer requests reset via email 2. Portal generates time-limited token (stored in DB) 3. Email sent with reset link 4. Customer submits new password with token 5. All existing sessions are invalidated (token blacklist) 6. Customer must log in again ### Rate Limiting - Password reset requests: 5 per 15 minutes per IP - Response always: "If an account exists, a reset email has been sent" (prevents enumeration) --- ## Product Catalog and Eligibility ### Catalog Source - Products pulled from Salesforce Pricebook configured via `PORTAL_PRICEBOOK_ID` - Only products marked for portal visibility are shown - Categories: Internet, SIM/Mobile, VPN ### Caching - Catalog cached in Redis without TTL - Invalidated via Salesforce CDC when `PricebookEntry` records change - Volatile/dynamic data uses 60-second TTL ### SIM Family Plans - Portal checks WHMCS for existing active SIM subscriptions via `GetClientsProducts` - Filter: `status = Active` AND product in SIM group - If active SIM exists → family/discount SIM plans are shown - If no active SIM → family plans are hidden ### Internet Eligibility - Portal queries Salesforce for account-specific eligibility - Eligibility result cached per `sf_account_id` (no TTL, invalidated on Salesforce change) - Checks for duplicate active Internet services in WHMCS before allowing order --- ## Order Creation ### Pre-Checkout Validation 1. User must have valid ID mapping (`whmcs_client_id` exists) 2. User must have at least one payment method in WHMCS (via `GetPayMethods`) 3. For Internet orders: no existing active Internet service in WHMCS ### Salesforce Order Structure **Order object fields:** | Field | Value | | ------------------------ | --------------------------------- | | `AccountId` | From ID mapping (`sf_account_id`) | | `EffectiveDate` | Today's date | | `Status` | "Pending Review" | | `Pricebook2Id` | Portal pricebook ID | | `Type__c` | Internet / SIM / VPN | | `Activation_Type__c` | Immediate / Scheduled | | `Activation_Schedule__c` | Date if scheduled | | Address fields | Snapshot from WHMCS profile | **OrderItem records:** - Created via Salesforce Composite API - `PricebookEntryId` from catalog lookup by SKU - `Quantity` and `UnitPrice` from pricebook ### No Payment Storage - Portal verifies payment method exists but does not store or process card data - Actual payment occurs in WHMCS after fulfillment creates invoice --- ## Order Fulfillment and Provisioning ### Trigger - Salesforce CDC detects Order status change (e.g., to "Approved" or "Reactivate") - Event is published and picked up by the portal's provisioning queue (BullMQ) - Idempotency key prevents duplicate processing ### Provisioning Steps 1. Update Salesforce `Activation_Status__c` = "Activating" 2. Map Salesforce OrderItems to WHMCS products 3. Call WHMCS `AddOrder` API action: - Creates WHMCS order, invoice, and hosting/subscription records - `paymentmethod`: "stripe" - `promocode`: applied if configured - `noinvoiceemail`: true (portal handles notifications) 4. For SIM orders: call Freebit activation API 5. Update Salesforce Order with: - `WHMCS_Order_ID__c` - `Activation_Status__c` = "Active" or error status - Error codes/messages if failed 6. Invalidate order cache 7. Publish realtime event for UI live update ### Distributed Transaction The fulfillment uses a distributed transaction pattern with rollback: - If WHMCS creation fails after Salesforce status update → rollback Salesforce status - No partial orders are left in WHMCS ### Error Handling | Scenario | Behavior | | ---------------------- | ------------------------------------------------- | | Missing payment method | Pause with `PAYMENT_METHOD_MISSING` in Salesforce | | WHMCS API failure | Mark failed; rollback Salesforce status | | Freebit failure | Mark failed; error written to Salesforce | | Transient failure | Retry via BullMQ queue with backoff | --- ## Billing and Payments ### Invoice Data - Fetched from WHMCS via `GetInvoices` and `GetInvoice` API actions - List cached 90 seconds; individual invoice cached 5 minutes - Invalidated by WHMCS webhooks and portal write operations ### Payment Methods - Fetched from WHMCS via `GetPayMethods` API action - Cached 15 minutes per user - Portal transforms WHMCS response to normalized format - First payment method marked as default ### Payment Gateways - Fetched from WHMCS via `GetPaymentMethods` API action - Cached 1 hour (rarely changes) ### SSO Links for Payment - Portal generates WHMCS SSO token via `CreateSsoToken` API action - Destination: `index.php?rp=/invoice/{id}/pay` - Token valid ~60 seconds - Payment method or gateway can be pre-selected via URL params - Portal normalizes redirect URL to configured WHMCS base URL --- ## Subscriptions and Services ### Data Source - WHMCS `GetClientsProducts` API action - Returns hosting/subscription records from `tblhosting` ### Cached Fields | Field | WHMCS Source | | ----------------- | ------------------------------------------------------ | | ID | `id` | | Product Name | `name` / `groupname` | | Status | `status` (Active, Pending, Suspended, Cancelled, etc.) | | Registration Date | `regdate` | | Next Due Date | `nextduedate` | | Amount | `amount` | | Billing Cycle | `billingcycle` | ### Caching - List cached 5 minutes - Individual subscription cached 10 minutes - Invalidated on WHMCS webhooks or profile updates --- ## SIM Management For subscriptions identified as SIM products, additional management is available via Freebit API. ### Identifying SIM Subscriptions - Portal checks if product name contains "SIM" (case-insensitive) - SIM management UI only shown for matching subscriptions ### Data Retrieval **Account Details (PA05-01 getAcnt / master/getAcnt):** | Portal Field | Freebit Field | | --------------------- | --------------------------------------------- | | MSISDN | `msisdn` | | ICCID | `iccid` | | IMSI | `imsi` | | EID | `eid` | | Plan Code | `planCode` | | Status | `status` (active/suspended/cancelled/pending) | | SIM Type | `simType` (physical/esim/standard/nano/micro) | | Remaining Quota (KB) | `remainingQuotaKb` | | Voice Mail | `voiceMailEnabled` | | Call Waiting | `callWaitingEnabled` | | International Roaming | `internationalRoamingEnabled` | | Network Type | `networkType` (4G/5G) | **Usage (getTrafficInfo):** - Today's usage (MB) - Monthly usage (MB) - Recent daily breakdown ### Available Operations | Operation | Freebit API | Effect Timing | | --------------------- | --------------------------------- | ------------------------------------------------------- | | Top-Up Data | `addSpec` / `eachQuota` | Immediate or scheduled | | Change Plan | PA05-21 `changePlan` | 1st of following month (requires `runTime` in YYYYMMDD) | | Update Voice Features | PA05-06 `talkoption/changeOrder` | Immediate | | Update Network Type | PA05-38 `contractline/change` | Immediate | | Cancel SIM Plan | PA05-04 `releasePlan` | Scheduled | | Cancel SIM Account | PA02-04 `cnclAcnt` | Scheduled (requires `runDate`) | | Reissue eSIM | `reissueEsim` / PA05-41 `addAcct` | As scheduled | ### Voice Feature Values | Feature | Enable Value | Disable Value | | -------------------- | ------------ | ------------- | | Voice Mail | "10" | "20" | | Call Waiting | "10" | "20" | | World Wing (Roaming) | "10" | "20" | When enabling World Wing, `worldWingCreditLimit` is set to "50000" (minimum permitted). ### Operation Timing Constraints Critical rule: **30-minute minimum gap** between these operations: - Voice feature changes (PA05-06) - Network type changes (PA05-38) - Plan changes (PA05-21) Additional constraints: - PA05-21 (plan change) and PA02-04 (cancellation) cannot coexist - After PA02-04 cancellation is sent, any PA05-21 call will cancel the cancellation - Voice/network changes should be made before 25th of month for billing cycle alignment The portal enforces these constraints with in-memory operation timestamps per Freebit account. Stale entries are cleaned up periodically (entries older than 35 minutes, except cancellations which persist). ### Call/SMS History - Retrieved via SFTP from `fs.mvno.net` - Files: `PASI_talk-detail-YYYYMM.csv`, `PASI_sms-detail-YYYYMM.csv` - Available 2 months behind current date (e.g., November can access September) - Connection uses SSH key fingerprint verification in production --- ## Support Cases ### Salesforce Case Integration **Create Case:** | Field | Value | | ------------- | --------------------------------- | | `AccountId` | From ID mapping (`sf_account_id`) | | `Origin` | "Portal Website" | | `Subject` | Customer input (required) | | `Description` | Customer input (required) | | `Type` | Customer selection (optional) | | `Priority` | Customer selection (optional) | | `Status` | "New" | **Read Cases:** - Portal queries Cases where `AccountId` matches mapped account - No caching – always live read from Salesforce ### Security - Cases are strictly filtered to the customer's linked Account - If case not found or belongs to different account → "case not found" response --- ## Dashboard and Summary Data The dashboard aggregates data from multiple sources: | Metric | Source | Query | | ---------------- | ---------- | --------------------------------------- | | Recent Orders | Salesforce | Orders for account, last 30 days | | Pending Invoices | WHMCS | Invoices with status Unpaid/Overdue | | Active Services | WHMCS | Subscriptions with status Active | | Open Cases | Salesforce | Cases for account with Status ≠ Closed | | Next Invoice | WHMCS | First unpaid invoice by due date | | Activity Feed | Aggregated | Recent invoices, orders, cases combined | --- ## Realtime Events ### SSE Endpoint - Single endpoint: `GET /api/events` - Server-Sent Events (SSE) connection - Requires authentication - Backed by Redis pub/sub for multi-instance delivery ### Event Streams | Channel | Events | | ---------------------------- | ---------------------------------------- | | `account:sf:{sf_account_id}` | Order status updates, fulfillment events | | `global:catalog` | Catalog/pricebook invalidation | ### Connection Management - Heartbeat every 30 seconds (`account.stream.heartbeat`) - Ready event on connection (`account.stream.ready`) - Per-user connection limit enforced - Rate limit: 30 connection attempts per minute - Connection limiter prevents resource exhaustion --- ## Caching Strategy ### Redis Key Scoping All cache keys include user identifier to prevent data mix-ups between customers. ### TTL Summary | Data | TTL | Invalidation Trigger | | --------------------- | ------------------- | --------------------------------- | | Catalog | None (event-driven) | Salesforce CDC on PricebookEntry | | Eligibility | None | Salesforce CDC on Account changes | | Orders | None (event-driven) | Salesforce CDC on Order/OrderItem | | ID Mappings | None | Create/update/delete operations | | Invoices (list) | 90 seconds | WHMCS webhook, write operations | | Invoice (detail) | 5 minutes | WHMCS webhook, write operations | | Subscriptions (list) | 5 minutes | WHMCS webhook, profile update | | Subscription (single) | 10 minutes | WHMCS webhook, profile update | | Payment Methods | 15 minutes | Add/remove operations | | Payment Gateways | 1 hour | Rarely changes | | Client Profile | 30 minutes | Profile/address update | | Signup Account Lookup | 30 seconds | N/A | | Support Cases | None (live) | N/A | ### Fallback Behavior - If cache read fails → fall back to live API call - Failures are not cached (prevents stale error states) --- ## Error Handling ### General Principles - Fail safe with clear messages - No partial writes – operations are atomic where possible - Errors written back to Salesforce for visibility (fulfillment) - Generic error messages to customers to avoid information leakage ### Error Responses by Area | Area | Error Scenario | Response | | ----------- | ---------------------------- | ------------------------------------------------------------------------- | | Sign-up | Email exists with mapping | "You already have an account. Please sign in." | | Sign-up | Email exists without mapping | "Please sign in to continue setup." | | Sign-up | Customer Number not found | "Salesforce account not found for Customer Number" | | Sign-up | `WH_Account__c` already set | "You already have an account. Please use the login page." | | Sign-up | Email exists in WHMCS | "We found an existing billing account. Please link your account instead." | | Sign-up | WHMCS client creation fails | "Failed to create billing account" (no portal record created) | | Sign-up | DB transaction fails | WHMCS client marked Inactive for cleanup | | Link | WHMCS client already mapped | "This billing account is already linked. Please sign in." | | Checkout | No payment method | Block with "Add payment method" prompt | | Checkout | Duplicate Internet service | Block with explanation | | Fulfillment | Payment method missing | Pause, write `PAYMENT_METHOD_MISSING` to Salesforce | | Billing | Invoice not found | "Invoice not found" (no data leakage) | | Billing | WHMCS unavailable | "Billing system unavailable, try later" | | SIM | 30-minute rule violation | "Please wait 30 minutes between changes" | | SIM | Pending cancellation | "Plan changes not allowed after cancellation" | | SIM | Not a SIM subscription | "This subscription is not a SIM service" | | Support | Case not found/wrong account | "Case not found" | --- ## Rate Limiting and Security ### API Rate Limits | Endpoint Category | Limit | Window | | ----------------- | ------------ | ---------- | | General API | 100 requests | 60 seconds | | Login | 3 attempts | 15 minutes | | Signup | 5 attempts | 15 minutes | | Password Reset | 5 attempts | 15 minutes | | Token Refresh | 10 attempts | 5 minutes | | SSE Connect | 30 attempts | 60 seconds | | Order Creation | 5 attempts | 60 seconds | | Signup Validation | 20 attempts | 10 minutes | ### Rate Limit Key - Composed of: IP address + User-Agent hash - Prevents bypass via User-Agent rotation alone - Uses Redis-backed `rate-limiter-flexible` ### Upstream Throttling - WHMCS requests queued with concurrency limit - Salesforce requests queued with concurrency limit - Respects Salesforce daily API limits - Graceful degradation when limits approached ### Security Features - CAPTCHA integration available (Turnstile/hCaptcha) - Configurable via `AUTH_CAPTCHA_PROVIDER`, `AUTH_CAPTCHA_SECRET` - Can be enabled after threshold of failed auth attempts - Password reset responses are generic (prevents account enumeration) - Cross-account data access returns "not found" (prevents data leakage) - Token blacklist for invalidated sessions - CSRF protection on state-changing operations --- ## Environment Configuration Key environment variables for system integration: | Variable | Purpose | Default | | -------------------------------- | -------------------------------- | -------------------- | | `WHMCS_API_URL` | WHMCS API endpoint | - | | `WHMCS_API_IDENTIFIER` | WHMCS API credentials | - | | `WHMCS_API_SECRET` | WHMCS API credentials | - | | `WHMCS_CUSTOMER_NUMBER_FIELD_ID` | Custom field for Customer Number | "198" | | `SALESFORCE_LOGIN_URL` | Salesforce auth endpoint | - | | `PORTAL_PRICEBOOK_ID` | Salesforce Pricebook for catalog | - | | `ACCOUNT_PORTAL_STATUS_FIELD` | SF Account field for status | "Portal_Status\_\_c" | | `ACCOUNT_WHMCS_FIELD` | SF Account field for WHMCS ID | "WH_Account\_\_c" | | `FREEBIT_API_URL` | Freebit API endpoint | - | | `SFTP_HOST` | MVNO SFTP server | "fs.mvno.net" | --- _Last updated: December 2025_