- Added support for custom fields in WHMCS, including customer number, gender, and date of birth, to the environment validation schema. - Updated the signup workflow to handle new fields for date of birth and gender, ensuring they are included in the client creation process. - Implemented email update functionality in the user profile service, allowing users to change their email while ensuring uniqueness across the portal. - Enhanced the profile edit form to include fields for date of birth and gender, with appropriate validation. - Updated the UI to reflect changes in profile management, ensuring users can view and edit their information seamlessly. - Improved error handling and validation for user profile updates, ensuring a smoother user experience.
28 KiB
Customer Portal – Technical Operations Guide
A comprehensive guide for supervisors explaining how the Customer Portal integrates with WHMCS, Salesforce, and other systems.
Contents
- System Architecture
- Data Ownership and Flow
- Account Creation and Linking
- Profile and Address Management
- Password Management
- Product Catalog and Eligibility
- Order Creation
- Order Fulfillment and Provisioning
- Billing and Payments
- Subscriptions and Services
- SIM Management
- Support Cases
- Dashboard and Summary Data
- Realtime Events
- Caching Strategy
- Error Handling
- 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:
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:
- Check if email already exists in portal
userstable- If exists with mapping → "You already have an account. Please sign in."
- If exists without mapping → "Please sign in to continue setup."
- Query Salesforce Account by Customer Number (
SF_Account_No__c)- If not found → "Salesforce account not found for Customer Number"
- Check if
WH_Account__cfield is already populated on Salesforce Account- If populated → "You already have an account. Please use the login page."
- 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):
- Create WHMCS client via
AddClientAPI 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)
- Contact info:
- Create portal user record in PostgreSQL (password hashed with Argon2)
- Create ID mapping in same transaction:
user_id↔whmcs_client_id↔sf_account_id - 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
Inactivefor manual cleanup
Linking Existing WHMCS Account
Flow:
- Customer submits WHMCS email and password
- Portal validates credentials via WHMCS
ValidateLoginAPI action - Check if WHMCS client is already mapped → "This billing account is already linked. Please sign in."
- Portal reads Customer Number from WHMCS custom field 198 via
GetClientsDetails - Portal queries Salesforce Account by Customer Number (
SF_Account_No__c) - Portal creates local user (without password – needs to be set)
- Portal inserts ID mapping
- Portal updates Salesforce Account with portal flags (
Portal_Status__c= "Active", etc.) - Customer is prompted to set a portal password
Profile and Address Management
Data Source
- All profile/address data is read from WHMCS via
GetClientsDetailsAPI action - Portal caches client profile for 30 minutes
Updates
- Profile updates are written to WHMCS via
UpdateClientAPI 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 |
||
| 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
- Customer requests reset via email
- Portal generates time-limited token (stored in DB)
- Email sent with reset link
- Customer submits new password with token
- All existing sessions are invalidated (token blacklist)
- 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
PricebookEntryrecords change - Volatile/dynamic data uses 60-second TTL
SIM Family Plans
- Portal checks WHMCS for existing active SIM subscriptions via
GetClientsProducts - Filter:
status = ActiveAND 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
- User must have valid ID mapping (
whmcs_client_idexists) - User must have at least one payment method in WHMCS (via
GetPayMethods) - 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
PricebookEntryIdfrom catalog lookup by SKUQuantityandUnitPricefrom 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
- Update Salesforce
Activation_Status__c= "Activating" - Map Salesforce OrderItems to WHMCS products
- Call WHMCS
AddOrderAPI action:- Creates WHMCS order, invoice, and hosting/subscription records
paymentmethod: "stripe"promocode: applied if configurednoinvoiceemail: true (portal handles notifications)
- For SIM orders: call Freebit activation API
- Update Salesforce Order with:
WHMCS_Order_ID__cActivation_Status__c= "Active" or error status- Error codes/messages if failed
- Invalidate order cache
- 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
GetInvoicesandGetInvoiceAPI actions - List cached 90 seconds; individual invoice cached 5 minutes
- Invalidated by WHMCS webhooks and portal write operations
Payment Methods
- Fetched from WHMCS via
GetPayMethodsAPI 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
GetPaymentMethodsAPI action - Cached 1 hour (rarely changes)
SSO Links for Payment
- Portal generates WHMCS SSO token via
CreateSsoTokenAPI 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
GetClientsProductsAPI 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
AccountIdmatches 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