- Updated opportunity field mappings to replace deprecated fields with new ones for SIM and Internet cancellations, enhancing clarity and consistency. - Introduced separate data structures for Internet and SIM cancellation data, improving type safety and validation. - Refactored SalesforceOpportunityService to handle updates for both Internet and SIM cancellations, ensuring accurate data handling. - Enhanced cancellation query fields to support new SIM cancellation requirements, improving the overall cancellation process. - Cleaned up the portal integration to reflect changes in opportunity source fields, promoting better data integrity and tracking.
60 KiB
Customer Portal Flow Review: Shop, Eligibility, ID Verification & Opportunity Management
Review Date: December 23, 2025
Last Updated: December 23, 2025
Scope: Complete end-to-end analysis of customer acquisition flows
Priority Focus: Customer Experience (CX)
Table of Contents
- Executive Summary
- System Architecture Overview
- Flow Diagrams
- Detailed Behavior Analysis
- Salesforce Field Changes Reference
- Agent Workflow & Checklist
- Critical Issues & Recommendations
- Customer Experience Analysis
- Implementation Improvements
Executive Summary
Current State Assessment
| Area | Status | CX Impact | Notes |
|---|---|---|---|
| Shop/Catalog | ✅ Good | Low Risk | Well-structured, cached |
| Internet Eligibility | ✅ Good | Low Risk | Manual NTT check, email notifications |
| ID Verification | ✅ Good | Low Risk | Integrated into Profile page |
| Opportunity Management | ✅ Good | Low Risk | Fields exist, WHMCS linking works |
| Order Fulfillment | ✅ Good | Low Risk | Distributed transaction support |
| Profile Data | ✅ Fixed | Low Risk | Customer number, DOB, gender now display |
Key Findings
-
Internet Subscription Detection - Now matches "SonixNet via NTT Optical Fiber" products in addition to "Internet" named products.
-
ID Verification Integrated - Upload functionality is now built into the Profile page (
/account/settings) rather than requiring a separate page. -
Opportunity Lifecycle Fields Exist -
Opportunity_Source__c(with portal picklist values) andWHMCS_Service_ID__care in place and working. -
SIM vs Internet Flows Have Different Requirements - SIM requires ID verification but not eligibility; Internet requires eligibility but not ID verification.
-
Opportunity ↔ WHMCS Linking - After provisioning, Opportunity is linked via
WHMCS_Service_ID__cto enable cancellation workflows. -
Cancellation is NOT Automated to WHMCS - Portal updates Salesforce Opportunity and creates Case, but WHMCS service termination requires agent action.
System Architecture Overview
Three-Tier Integration
┌─────────────────────────────────────────────────────────────────────────────────┐
│ CUSTOMER PORTAL │
│ (Next.js Frontend) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ Shop/Catalog │ Checkout │ Verification │ Dashboard │ Service Mgmt │
└───────┬────────┴─────┬──────┴───────┬────────┴──────┬──────┴─────────┬─────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ BFF (NestJS) │
├─────────────────────────────────────────────────────────────────────────────────┤
│ CatalogService │ CheckoutService │ VerificationService │ OrderService │
│ │ │ │ │
│ Opportunity Resolution │ OrderOrchestrator │ FulfillmentOrchestrator │
└───────┬──────────────────────┴─────────┬──────────┴────────────┬───────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌──────────────┐
│ SALESFORCE │ │ WHMCS │ │ PRISMA │
│ (CRM/Orders) │ │ (Billing) │ │ (Portal DB) │
└───────────────┘ └─────────────────┘ └──────────────┘
Data Flow by Product Type
| Product | Eligibility Required | ID Verification Required | Opportunity Created At |
|---|---|---|---|
| Internet | ✅ Yes (Manual NTT check) | ❌ Not enforced | Eligibility Request |
| SIM | ❌ No | ✅ Yes (Residence Card) | Order Placement |
| VPN | ❌ No | ❌ No | Order Placement |
Flow Diagrams
1. Internet Customer Journey
┌─────────────────────────────────────────────────────────────────────────────────┐
│ INTERNET CUSTOMER JOURNEY │
└─────────────────────────────────────────────────────────────────────────────────┘
CUSTOMER ACTION SYSTEM BEHAVIOR SALESFORCE CHANGES
═══════════════ ═══════════════ ══════════════════
1. Browse Internet Plans
└─► Catalog loads from SF GET /api/catalog/internet (none - read only)
(cached 5 min)
2. Click "Select Plan"
└─► Redirect to checkout User must be authenticated (none)
3. Enter Service Address
└─► Address validation Frontend validation (none)
4. Submit Eligibility Request
└─► API call triggered POST /eligibility-request ┌─────────────────────┐
│ CASE: │
│ • Created │
│ • Subject=Internet │
│ availability check│
│ • Origin=Portal │
├─────────────────────┤
│ ACCOUNT: │
│ • Internet_ │
│ Eligibility_ │
│ Status__c=Pending │
│ • Internet_ │
│ Eligibility_ │
│ Request_Date_ │
│ Time__c=NOW() │
│ • Internet_ │
│ Eligibility_ │
│ Case_Id__c= │
│ (Case ID) │
├─────────────────────┤
│ NOTE: Opportunity │
│ is NOT created at │
│ this step - only │
│ at order placement │
└─────────────────────┘
⏳ CUSTOMER WAITS (no SLA shown)
════════════════════════════════
5. Agent Processes Case (MANUAL AGENT WORK) ┌─────────────────────┐
└─► NTT Check Checks serviceability │ ACCOUNT: │
└─► Update Account │ • Internet_ │
└─► SF Flow sends email │ Eligibility__c= │
│ "Home 1G" (or │
│ other result) │
│ • Internet_ │
│ Eligibility_ │
│ Status__c= │
│ Eligible/ │
│ Ineligible │
│ • Internet_ │
│ Eligibility_ │
│ Checked_Date_ │
│ Time__c=NOW() │
├─────────────────────┤
│ SALESFORCE FLOW: │
│ • Sends email to │
│ customer with │
│ eligibility result│
└─────────────────────┘
6. Customer Returns/Refreshes
└─► Sees eligible plans GET /api/catalog/internet (read only)
filtered by eligibility /eligibility
7. Proceed to Payment
└─► Add payment method Stripe integration (none)
8. Place Order
└─► Order created POST /api/orders ┌─────────────────────┐
│ ORDER: │
│ • Created │
│ • OpportunityId= │
│ (linked) │
│ • Status=Created │
├─────────────────────┤
│ OPPORTUNITY: │
│ • Stage=Post │
│ Processing │
└─────────────────────┘
9. Agent Approves Order
└─► Fulfillment triggered Order.Status='Approved' ┌─────────────────────┐
triggers CDC event │ ORDER: │
│ • Activation_ │
│ Status__c= │
│ Activating │
└─────────────────────┘
10. WHMCS Provisioning
└─► Service created addOrder → acceptOrder ┌─────────────────────┐
│ ORDER: │
│ • Status=Completed │
│ • Activation_ │
│ Status__c= │
│ Activated │
│ • WHMCS_Order_ID__c │
│ =(WHMCS ID) │
├─────────────────────┤
│ OPPORTUNITY: │
│ • Stage=Active │
│ • WHMCS_Service_ │
│ ID__c=(service ID)│
└─────────────────────┘
11. Customer Has Active Service
└─► Service visible in GET /subscriptions (read only)
portal dashboard
2. SIM Customer Journey
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SIM CUSTOMER JOURNEY │
└─────────────────────────────────────────────────────────────────────────────────┘
CUSTOMER ACTION SYSTEM BEHAVIOR SALESFORCE CHANGES
═══════════════ ═══════════════ ══════════════════
1. Browse SIM Plans
└─► Catalog loads GET /api/catalog/sim (none - read only)
Family plans shown if
user has existing SIM
2. Click "Select Plan"
└─► Redirect to login if /auth/login?returnTo=... (none - auth required)
unauthenticated Standard auth flow creates
SF Account + WHMCS Client
3. Configure & Checkout (Authenticated)
└─► AccountCheckoutContainer /account/shop/sim/configure (cart stored locally)
handles full flow /account/order
4. Upload Residence Card (ID Verification)
└─► File upload POST /verification/ ┌─────────────────────┐
residence-card │ ACCOUNT: │
│ • Id_Verification_ │
│ Status__c= │
│ Submitted │
│ • Id_Verification_ │
│ Submitted_Date_ │
│ Time__c=NOW() │
├─────────────────────┤
│ CONTENT VERSION: │
│ • File uploaded │
│ • FirstPublish │
│ LocationId= │
│ Account │
└─────────────────────┘
⏳ CUSTOMER WAITS (manual review)
═════════════════════════════════
5. Agent Reviews ID (MANUAL AGENT WORK) ┌─────────────────────┐
└─► Verify document │ ACCOUNT: │
└─► Update status │ • Id_Verification_ │
│ Status__c= │
│ Verified/Rejected │
│ • Id_Verification_ │
│ Verified_Date_ │
│ Time__c=NOW() │
│ • Id_Verification_ │
│ Rejection_ │
│ Message__c= │
│ (if rejected) │
└─────────────────────┘
6. Customer Returns (If Verified)
└─► Can proceed to payment Verification status checked (none)
7-11. (Same as Internet steps 7-11)
3. Opportunity Lifecycle State Machine
┌─────────────────────────────────────────────────────────────────────────────────┐
│ OPPORTUNITY STAGE TRANSITIONS │
└─────────────────────────────────────────────────────────────────────────────────┘
┌───────────────────┐
│ POST PROCESSING │ ◄─── Created at ORDER PLACEMENT
│ (75% prob) │ (Not at eligibility request)
└─────────┬─────────┘
│
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌───────────────┐ ┌─────────────┐
│ VOID │ │ ACTIVE │ │ PENDING │
│ (Closed, │ │ (90% prob) │ │ (On hold) │
│ 0% prob) │ └───────┬───────┘ └─────────────┘
└───────────┘ │
Failed │ Set when:
provisioning │ • Service provisioned in WHMCS
│ • WHMCS_Service_ID__c populated
│
▼
┌───────────────┐
│ △CANCELLING │ ◄─── Set when:
│ (100% prob) │ • Customer submits cancellation
└───────┬───────┘ • ScheduledCancellationDateAndTime__c set
│ • CancellationNotice__c = 有
│ • LineReturn__c = NotYet
▼
┌───────────────┐
│ 〇CANCELLED │ ◄─── Set when:
│ (Closed,Won) │ • Service terminated
└───────────────┘ • Equipment returned (if applicable)
NOTE: Introduction/Ready stages may be used by agents for pre-order tracking,
but portal creates Opportunities starting at Post Processing.
Detailed Behavior Analysis
Shop/Catalog Module
Location: apps/bff/src/modules/catalog/
Key Services:
InternetCatalogService- Plans, installations, addonsSimCatalogService- Plans, activation fees, addonsVpnCatalogService- Plans
Caching Strategy:
- Catalog items cached via
CatalogCacheService - Cache keys built by product type + item type
- Cache invalidation on product updates
Eligibility-Filtered Plans:
// InternetCatalogService.getPlansForUser()
// Filters plans based on Account.Internet_Eligibility__c value
// e.g., if eligibility = "Home 1G", only Home 1G plans shown
Family Discount Logic (SIM):
// SimCatalogService.getPlansForUser()
// Checks WHMCS for existing active SIM services
// Shows family discount plans only if user has existing SIM
Eligibility Validation Module
Location: apps/bff/src/modules/catalog/services/internet-catalog.service.ts
Flow Summary:
- Customer requests eligibility check
- System creates Case for agent to process
- System updates Account eligibility fields to "Pending"
- Manual Agent Work Required - Agent checks NTT serviceability
- Agent updates Account with eligibility result
- Salesforce Flow sends email notification to customer
- Note: Opportunity is created/reused during eligibility (Stage = Introduction) so the Case can link to it (
Case.OpportunityId) and the journey can be reused at order placement.
Account Fields Updated:
| Field | Value Set | When |
|---|---|---|
Internet_Eligibility_Status__c |
"Pending" | On request |
Internet_Eligibility_Request_Date_Time__c |
NOW() | On request |
Internet_Eligibility_Case_Id__c |
Case ID | On request |
Internet_Eligibility__c |
Result value | By agent |
Internet_Eligibility_Checked_Date_Time__c |
NOW() | By agent |
ID Verification Module
Location: apps/bff/src/modules/verification/residence-card.service.ts
Flow Summary:
- Customer uploads residence card image
- File stored as Salesforce ContentVersion (linked to Account)
- Account verification status updated to "Submitted"
- Manual Agent Work Required
- Agent reviews document and updates status
Account Fields Updated:
| Field | Value Set | When |
|---|---|---|
Id_Verification_Status__c |
"Submitted" | On upload |
Id_Verification_Submitted_Date_Time__c |
NOW() | On upload |
Id_Verification_Rejection_Message__c |
null | Cleared on resubmit |
Id_Verification_Note__c |
null | Cleared on resubmit |
Id_Verification_Status__c |
"Verified"/"Rejected" | By agent |
Id_Verification_Verified_Date_Time__c |
NOW() | By agent |
Supported File Types:
- PNG
- JPG/JPEG
User Registration Flow
IMPORTANT: Guest checkout has been removed. All checkout flows now require authentication first.
Authentication-First Checkout Flow:
- User browses public catalog at
/shop - When proceeding to checkout, unauthenticated users are redirected to
/auth/login - After login/registration, users continue checkout at
/account/order - Checkout is handled by
AccountCheckoutContainer(single-page flow)
Registration Location: apps/portal/src/app/auth/register/ (standard auth flow)
Benefits of Auth-First Approach:
- Simpler code: Removed
checkout-registrationmodule entirely - Better UX: Users complete registration once, then shop freely
- Cleaner architecture: No multi-step guest registration with partial rollback
- Consistent: All users have accounts before interacting with Salesforce/WHMCS
Opportunity Management Module
Location (current code paths):
apps/bff/src/modules/catalog/services/internet-catalog.service.ts(eligibility request creates/reuses Opportunity + Case)apps/bff/src/modules/orders/services/order-orchestrator.service.ts(order placement resolves Opportunity)apps/bff/src/integrations/salesforce/services/salesforce-opportunity.service.ts(Salesforce query/create/update)
Matching Rules:
| Scenario | Action |
|---|---|
Order has opportunityId |
Use it directly |
| Internet order without Opp | Find Introduction/Ready stage or create new |
| SIM order without Opp | Find Introduction/Ready stage or create new |
| VPN order | Always create new Opportunity |
Stage Transitions by Trigger:
| Trigger | From Stage(s) | To Stage |
|---|---|---|
| Eligibility Request | (new) | Introduction |
| Eligibility Confirmed | Introduction | Ready |
| Eligibility Denied | Introduction | Void |
| Order Placed | Introduction, Ready | Post Processing |
| Service Provisioned | Post Processing | Active |
| Cancellation Requested | Active | △Cancelling |
| Cancellation Complete | △Cancelling | 〇Cancelled |
Salesforce Field Changes Reference
Complete Field Map by Object
Account Object
| Field API Name | Label | Type | Who Updates | When |
|---|---|---|---|---|
SF_Account_No__c |
Customer Number | Text | Portal | Registration |
Portal_Status__c |
Portal Status | Picklist | Portal | Registration |
Portal_Registration_Source__c |
Registration Source | Text | Portal | Registration |
Portal_Last_SignIn__c |
Last Sign In | DateTime | Portal | Each login |
WH_Account__c |
WHMCS Account | Text | Portal | Registration |
Internet_Eligibility__c |
Internet Eligibility | Text | Agent | After check |
Internet_Eligibility_Status__c |
Eligibility Status | Picklist | Portal/Agent | Request/Check |
Internet_Eligibility_Request_Date_Time__c |
Eligibility Requested | DateTime | Portal | Request |
Internet_Eligibility_Checked_Date_Time__c |
Eligibility Checked | DateTime | Agent | After check |
Internet_Eligibility_Notes__c |
Eligibility Notes | Text | Agent | After check |
Internet_Eligibility_Case_Id__c |
Eligibility Case | Lookup | Portal | Request |
Id_Verification_Status__c |
ID Status | Picklist | Portal/Agent | Upload/Review |
Id_Verification_Submitted_Date_Time__c |
ID Submitted | DateTime | Portal | Upload |
Id_Verification_Verified_Date_Time__c |
ID Verified | DateTime | Agent | Review |
Id_Verification_Note__c |
ID Notes | Text | Agent | Review |
Id_Verification_Rejection_Message__c |
Rejection Reason | Text | Agent | If rejected |
Opportunity Object
| Field API Name | Label | Type | Who Updates | When |
|---|---|---|---|---|
StageName |
Stage | Picklist | Portal/Agent | Throughout lifecycle |
CommodityType |
Commodity Type | Picklist | Portal | Creation |
Application_Stage__c |
Application Stage | Picklist | Portal | Creation (INTRO-1) |
Opportunity_Source__c |
Opportunity Source | Picklist | Portal | Creation |
WHMCS_Service_ID__c |
WHMCS Service ID | Number | Portal | After provisioning |
CancellationNotice__c |
Cancellation Notice | Picklist | Portal | Cancellation request |
ScheduledCancellationDateAndTime__c |
Scheduled Cancellation | DateTime | Portal | Cancellation request |
LineReturn__c |
Line Return Status | Picklist | Agent | Equipment tracking |
Order Object
| Field API Name | Label | Type | Who Updates | When |
|---|---|---|---|---|
OpportunityId |
Opportunity | Lookup | Portal | Order creation |
Status |
Status | Picklist | Portal/Agent | Throughout |
Activation_Status__c |
Activation Status | Picklist | Portal | Fulfillment |
Activation_Error_Code__c |
Error Code | Text | Portal | If failed |
Activation_Error_Message__c |
Error Message | Text | Portal | If failed |
WHMCS_Order_ID__c |
WHMCS Order ID | Text | Portal | After provisioning |
Case Object
| Field API Name | Label | Type | Who Updates | When |
|---|---|---|---|---|
OpportunityId |
Opportunity | Lookup | Portal | Eligibility/Cancel |
Origin |
Origin | Picklist | Portal | "Portal" |
Status |
Status | Picklist | Agent | Processing |
Priority |
Priority | Picklist | Portal | Creation |
Agent Workflow & Checklist
Internet Eligibility Processing
Queue: Cases with Subject = "Internet availability check request (Portal)"
Agent Checklist:
-
1. Open the Case
- Note the OpportunityId (related Opportunity)
- Review Description for address details
-
2. Perform NTT Serviceability Check
- Check FLET'S光 availability for the address
- Determine offering type (Home 1G, Mansion 1G, etc.)
-
3. Update Account Fields
Internet_Eligibility__c = "[Offering Type]" (e.g., "Home 1G", "Mansion 1G") Internet_Eligibility_Status__c = "Eligible" or "Ineligible" Internet_Eligibility_Checked_Date_Time__c = NOW() Internet_Eligibility_Notes__c = [Any relevant notes] -
4. Close the Case
- Status = "Closed"
- Resolution notes
ℹ️ Automatic Actions:
- Salesforce Flow automatically sends email to customer when eligibility fields are updated
- Portal polls Account for eligibility changes (customer sees result on next visit)
- Opportunity is created/reused at eligibility request (Stage = Introduction) and then reused at order placement when possible
ID Verification Processing
Queue: Accounts with Id_Verification_Status__c = "Submitted"
Agent Checklist:
-
1. Find the Account
- Filter by
Id_Verification_Status__c = "Submitted"
- Filter by
-
2. Download & Review Document
- Go to Account → Files → Most Recent
- Verify: Name matches account, document is valid, not expired
-
3. Update Account (Approved)
Id_Verification_Status__c = "Verified" Id_Verification_Verified_Date_Time__c = NOW() Id_Verification_Note__c = "Verified by [Agent Name]" -
3. Update Account (Rejected)
Id_Verification_Status__c = "Rejected" Id_Verification_Verified_Date_Time__c = NOW() Id_Verification_Rejection_Message__c = "[Specific reason - customer will see this]"
⚠️ Critical: Rejection message is shown to customer. Be specific but professional.
Order Approval Processing
Queue: Orders with Status = "Pending Review" or triggered via automation
Agent Checklist:
-
1. Verify Customer Prerequisites
- Internet:
Account.Internet_Eligibility__cis set - SIM:
Account.Id_Verification_Status__c = "Verified"
- Internet:
-
2. Review Order Details
- Check order items are correct
- Verify pricing
-
3. Approve Order
- Set
Order.Status = "Approved" - This triggers CDC event → BFF provisioning
- Set
-
4. Monitor Activation
- Check
Activation_Status__cupdates to "Activated" - If "Failed", check error code/message
- Check
Cancellation Processing
Queue: Cases with Subject = "Cancellation Request - ..."
Agent Checklist:
- 1. Open the Case
- Note the OpportunityId
- Review cancellation month and service details
- 2. Verify the 25th Rule
- If requested before 25th: Can cancel THIS month
- If requested on/after 25th: Must be NEXT month
- 3. Check Opportunity Fields (portal sets these)
ScheduledCancellationDateAndTime__c= End of cancellation monthCancellationNotice__c= "有"LineReturn__c= "NotYet"
- 4. Process Equipment Return (if Internet)
- Send return kit
- Update
LineReturn__c= "SentKit" - Track return: "Returned1", "Returned2"
- 5. Terminate Service in WHMCS
- On the scheduled date
- 6. Complete Cancellation
Opportunity.Stage = "〇Cancelled"
Critical Issues & Recommendations
🔴 Critical Issues
1. Internet Eligibility Has No SLA Visibility
Problem: Customers submit eligibility requests and see "Pending" indefinitely. No estimated time, no progress indicator.
Impact: High abandonment rate, customer frustration, support inquiries.
Recommendation:
SHORT TERM:
- Add expected timeframe messaging: "Usually completed within 2-3 business days"
- Send email when eligibility is updated
LONG TERM:
- Implement automated NTT API check where possible
- Add SLA tracking on Cases with escalation rules
2. ID Verification Rejection Lacks Guidance
Problem: When rejected, customers see the rejection message but no clear next steps.
Impact: Customers don't know how to fix the issue, leading to repeated failures.
Recommendation:
- Create structured rejection reasons with remediation steps
- Add "Re-submit" button that clears previous submission
- Show example of correct document format
3. Salesforce Fields Not Created (RESOLVED)
Status: ✅ Confirmed fields exist:
Opportunity_Source__c- Picklist with portal valuesWHMCS_Service_ID__c- Number field for WHMCS linking
Note: Emails for eligibility and ID verification status changes are sent automatically from Salesforce (via Flow/Process Builder).
🟡 Medium Issues
4. No Internet ID Verification Requirement
Problem: Internet orders don't require ID verification, but SIM orders do. This may be intentional but creates inconsistency.
Recommendation:
- Confirm business requirement
- If ID needed for Internet, add gating step in checkout
- Document the decision either way
5. WHMCS Rollback is Manual
Problem: If checkout registration fails after WHMCS client creation, the WHMCS client cannot be auto-deleted.
Impact: Orphaned WHMCS accounts require manual cleanup.
Recommendation:
- Add monitoring/alerting for failed registrations
- Create weekly cleanup process for orphaned WHMCS clients
- Consider delayed WHMCS creation (after all validations pass)
6. Opportunity Matching Could Create Duplicates
Problem: The matching query only looks for open Opportunities. If two eligibility requests happen quickly, two Opportunities could be created.
Recommendation:
- Add optimistic locking or unique constraint
- Consider using Salesforce duplicate rules
🟢 Minor Issues
7. Cache Invalidation on Eligibility Update
Problem: After agent updates eligibility, customer might see cached "Pending" status.
Current: Cache key includes account ID, TTL-based expiry.
Recommendation:
- Reduce eligibility cache TTL during "Pending" status
- Consider CDC-triggered cache invalidation
Customer Experience Analysis
Current CX Pain Points (Ranked)
| Rank | Issue | User Impact | Frequency |
|---|---|---|---|
| 1 | No eligibility timeline | High frustration | Every Internet customer |
| 2 | Unclear ID rejection reasons | High frustration | ~10% of SIM customers |
| 3 | No email notifications | Medium annoyance | All customers |
| 4 | Must return to check status | Medium annoyance | All customers |
| 5 | Address entry before eligibility | Low friction | Internet customers |
Recommended CX Improvements
Tier 1: Quick Wins (Days)
-
Add Timeline Messaging
// AvailabilityStep.tsx <AlertBanner variant="info"> Our team will verify NTT serviceability for your address. <strong>This usually takes 1-2 business days.</strong> We'll email you at {user.email} when complete. </AlertBanner> -
Improve Rejection Messages
// Structured rejection reasons const REJECTION_REASONS = { EXPIRED: "Document has expired. Please upload a valid, unexpired residence card.", BLURRY: "Image is not clear enough. Please retake the photo in good lighting.", WRONG_TYPE: "This document type is not accepted. Please upload your residence card (在留カード).", NAME_MISMATCH: "Name on document doesn't match account. Please contact support.", }; -
Add Status Polling with User Feedback
// Show last checked time <p className="text-sm text-muted-foreground"> Last checked: {formatRelativeTime(lastCheckedAt)} <button onClick={refetch}>Check now</button> </p>
Tier 2: Medium Effort (Weeks)
-
Email Notifications
- Eligibility confirmed/denied
- ID verification approved/rejected
- Order status changes
-
Progress Indicators
// Visual progress for Internet checkout <StepIndicator steps={[ { label: "Address", status: "complete" }, { label: "Eligibility Check", status: "in_progress" }, { label: "Payment", status: "pending" }, { label: "Order", status: "pending" }, ]} /> -
Save & Resume Checkout
- Store checkout session in database
- Allow customers to return and complete after eligibility
Tier 3: Strategic (Months)
-
Automated NTT Check
- Integrate with NTT API (where available)
- Instant eligibility for covered areas
- Fallback to manual for edge cases
-
Real-time Status Updates
- WebSocket notifications when status changes
- Push notifications for mobile
Implementation Improvements
Code Quality Observations
✅ What's Working Well
-
Domain-Driven Design
- Clear separation in
packages/domain/ - Zod schemas for validation
- Type-safe contracts
- Clear separation in
-
Error Handling memory:6689308
- Production-ready error messages
- No sensitive data exposure
- Structured logging
-
Distributed Transactions
DistributedTransactionServicefor fulfillment- Step tracking with rollback support
- Idempotency keys for retry safety
-
Caching Strategy
- Catalog caching with cache keys
- Eligibility caching per account
- Invalidation on updates
✅ Areas for Improvement (Resolved)
-
Opportunity Matching Race Condition → ✅ FIXED
// Now uses distributed lock via DistributedLockService // Located: apps/bff/src/infra/cache/distributed-lock.service.ts return this.lockService.withLock( `opportunity:${accountId}:${productType}`, async () => { const existingOppId = await this.opportunityService.findOpenOpportunityForAccount(...); if (!existingOppId) return this.createNewOpportunity(...); return this.useExistingOpportunity(existingOppId); }, { ttlMs: 10_000 } ); -
Hardcoded Eligibility Field Names → ✅ FIXED
// Created centralized field maps in domain package // Located: packages/domain/salesforce/field-maps.ts import { ACCOUNT_FIELDS } from "@customer-portal/domain/salesforce"; const eligibilityValue = account[ACCOUNT_FIELDS.eligibility.value]; const eligibilityStatus = account[ACCOUNT_FIELDS.eligibility.status]; -
Missing Opportunity Creation for SIM → ✅ FIXED
// CheckoutRegistrationService now creates Opportunity for SIM orders // Added orderType parameter to registration request if (data.orderType === "SIM" && this.opportunityMatchingService) { opportunityId = await this.opportunityMatchingService.createOpportunityForCheckoutRegistration(sfAccountId); }
Suggested Refactors (Partially Implemented)
-
Create Unified Status Service → 📋 FUTURE
// CustomerStatusService - not yet implemented // Consider creating for dashboard aggregation class CustomerStatusService { async getCustomerStatus(userId: string): Promise<CustomerStatus> { return { eligibility: await this.getEligibilityStatus(userId), verification: await this.getVerificationStatus(userId), pendingOrders: await this.getPendingOrders(userId), activeServices: await this.getActiveServices(userId), }; } } -
Add Notification Service → ✅ IMPLEMENTED
// Located: apps/bff/src/modules/notifications/notifications.service.ts // Creates in-app notifications from Platform Events // Emails handled by Salesforce Flows -
Centralize Field Maps → ✅ IMPLEMENTED
// Located: packages/domain/salesforce/field-maps.ts import { ACCOUNT_FIELDS, SALESFORCE_FIELDS } from "@customer-portal/domain/salesforce"; // Full field maps for Account, Opportunity, Order, and Case objects
Salesforce Email Configuration (Required)
Overview
Salesforce sends emails automatically when eligibility and ID verification statuses change. The Portal creates matching in-app notifications via Platform Events.
Flow Triggers to Create
1. Internet Eligibility Status Change Flow
Trigger: Record-Triggered Flow on Account
When: Internet_Eligibility_Status__c changes AND is not null AND is not "Pending"
Flow Name: Portal - Eligibility Status Email
Object: Account
Trigger: When a record is updated
Entry Conditions:
- Internet_Eligibility_Status__c IS CHANGED
- Internet_Eligibility_Status__c NOT EQUALS "Pending"
- Internet_Eligibility_Status__c IS NOT NULL
Actions:
1. Decision: Check Status Value
- If "Eligible" → Send Eligible Email
- If "Ineligible" → Send Ineligible Email
2. Send Email (Eligible):
Template: Portal_Eligibility_Eligible
To: {!$Record.PersonEmail} OR Contact.Email
Subject: "Good news! Internet service is available at your address"
3. Send Email (Ineligible):
Template: Portal_Eligibility_Ineligible
To: {!$Record.PersonEmail} OR Contact.Email
Subject: "Update on your internet availability request"
2. ID Verification Status Change Flow
Trigger: Record-Triggered Flow on Account
When: Id_Verification_Status__c changes to "Verified" or "Rejected"
Flow Name: Portal - ID Verification Status Email
Object: Account
Trigger: When a record is updated
Entry Conditions:
- Id_Verification_Status__c IS CHANGED
- Id_Verification_Status__c IN ("Verified", "Rejected")
Actions:
1. Decision: Check Status Value
- If "Verified" → Send Verified Email
- If "Rejected" → Send Rejected Email
2. Send Email (Verified):
Template: Portal_ID_Verified
To: {!$Record.PersonEmail} OR Contact.Email
Subject: "Your identity has been verified"
3. Send Email (Rejected):
Template: Portal_ID_Rejected
To: {!$Record.PersonEmail} OR Contact.Email
Subject: "Action required: ID verification needs attention"
Body should include: {!$Record.Id_Verification_Rejection_Message__c}
Email Templates to Create
Template: Portal_Eligibility_Eligible
Subject: Good news! Internet service is available at your address Hi {{{Recipient.FirstName}}},
Great news! We've confirmed that internet service is available at your address. Your eligible
offering: {{{Account.Internet_Eligibility__c}}} You can now complete your order: [Complete Your
Order] → https://portal.example.com/shop/internet If you have any questions, please contact our
support team. Best regards, The SonixNet Team
Template: Portal_Eligibility_Ineligible
Subject: Update on your internet availability request Hi {{{Recipient.FirstName}}}, Thank you for
your interest in our internet service. Unfortunately, after checking with NTT, we've determined that
internet service is not currently available at your address. {{{#if
Account.Internet_Eligibility_Notes__c}}} Notes: {{{Account.Internet_Eligibility_Notes__c}}}
{{{/if}}} If you believe this is an error or your situation changes, please contact our support
team. Best regards, The SonixNet Team
Template: Portal_ID_Verified
Subject: Your identity has been verified Hi {{{Recipient.FirstName}}}, Your identity verification is
complete! You can now proceed with your order. [Continue to Checkout] →
https://portal.example.com/checkout Best regards, The SonixNet Team
Template: Portal_ID_Rejected
Subject: Action required: ID verification needs attention Hi {{{Recipient.FirstName}}}, We were
unable to verify your identity based on the document you submitted. Reason:
{{{Account.Id_Verification_Rejection_Message__c}}} Please resubmit a clearer image of your residence
card: [Resubmit Document] → https://portal.example.com/account/verification Tips for a successful
submission: - Ensure the entire card is visible - Take the photo in good lighting - Make sure the
image is not blurry - The document must not be expired If you need assistance, please contact our
support team. Best regards, The SonixNet Team
Platform Event Configuration
Portal uses Platform Events to receive real-time updates and create in-app notifications.
Existing Platform Event: Account_Internet_Eligibility_Update__e
Environment Variable: SF_ACCOUNT_EVENT_CHANNEL=/event/Account_Internet_Eligibility_Update__e
Platform Event Fields (Required)
| Field API Name | Type | Description |
|---|---|---|
AccountId__c |
Text | Salesforce Account ID |
Internet_Eligibility__c |
Text | Eligibility value (e.g., "Home 1G") |
Internet_Eligibility_Status__c |
Text | Status (Pending, Eligible, Ineligible) |
Internet_Eligibility_Request_Date_Time__c |
DateTime | When requested |
Internet_Eligibility_Checked_Date_Time__c |
DateTime | When checked |
Internet_Eligibility_Notes__c |
Text | Agent notes |
Internet_Eligibility_Case_Id__c |
Text | Related Case ID |
Id_Verification_Status__c |
Text | ID status (Submitted, Verified, Rejected) |
Id_Verification_Rejection_Message__c |
Text | Rejection reason |
Flow to Publish Platform Event
Trigger: Record-Triggered Flow on Account
When: Any of the eligibility or verification fields change
Flow Name: Portal - Publish Account Status Update
Object: Account
Trigger: When a record is updated
Entry Conditions (OR):
- Internet_Eligibility_Status__c IS CHANGED
- Id_Verification_Status__c IS CHANGED
Actions:
1. Create Platform Event Record:
Object: Account_Internet_Eligibility_Update__e
Fields:
- AccountId__c = {!$Record.Id}
- Internet_Eligibility__c = {!$Record.Internet_Eligibility__c}
- Internet_Eligibility_Status__c = {!$Record.Internet_Eligibility_Status__c}
- Id_Verification_Status__c = {!$Record.Id_Verification_Status__c}
- Id_Verification_Rejection_Message__c = {!$Record.Id_Verification_Rejection_Message__c}
... (other fields as needed)
Notification Architecture (Implemented)
Design Principle: Email + In-App Notifications
Salesforce sends emails automatically, and the Portal displays in-app notifications for the same events. This provides:
- Push notification via email (customer's inbox)
- Pull notification via in-app (when customer logs in)
Implementation Details
- Detection Method: Platform Events (
Account_Internet_Eligibility_Update__e) - Storage: Portal PostgreSQL database (30-day expiry)
- Duplication: Same notification content in email AND in-app
Architecture Diagram
┌─────────────────────────────────────────────────────────────────────────────────┐
│ NOTIFICATION ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────────┘
SALESFORCE
┌──────────────────────────────┐
│ Account Field Change │
│ (Eligibility, Verification) │
└─────────────┬────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ SF FLOW │ │ PLATFORM EVENT │
│ Sends Email │ │ (Pub/Sub API) │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Customer's │ │ Portal BFF │
│ Email Inbox │ │ (Subscriber) │
└─────────────────┘ │ │
│ Creates notif. │
└────────┬────────┘
│
▼
┌─────────────────┐
│ NOTIFICATION │
│ TABLE (Prisma) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Portal UI │
│ 🔔 (2) Badge │
└─────────────────┘
Notification Types
| Type | Trigger | Email? | In-App? | Action |
|---|---|---|---|---|
eligibility_eligible |
Eligibility = Eligible | ✅ SF | ✅ Portal | View Plans |
eligibility_ineligible |
Eligibility = Ineligible | ✅ SF | ✅ Portal | Contact Support |
verification_verified |
ID Status = Verified | ✅ SF | ✅ Portal | Continue Checkout |
verification_rejected |
ID Status = Rejected | ✅ SF | ✅ Portal | Resubmit |
order_activated |
Order Activated | ✅ SF | ✅ Portal | View Service |
cancellation_scheduled |
Cancellation Requested | ✅ SF | ✅ Portal | View Status |
Database Schema (Implemented)
Located in apps/bff/prisma/schema.prisma:
model Notification {
id String @id @default(uuid())
userId String @map("user_id")
// Notification content
type NotificationType
title String
message String?
// Action (optional CTA button)
actionUrl String? @map("action_url")
actionLabel String? @map("action_label")
// Source tracking for deduplication
source NotificationSource @default(SALESFORCE)
sourceId String? @map("source_id") // SF Account ID, etc.
// Status
read Boolean @default(false)
readAt DateTime? @map("read_at")
dismissed Boolean @default(false)
// Timestamps
createdAt DateTime @default(now()) @map("created_at")
expiresAt DateTime @map("expires_at") // 30 days from creation
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId, read, dismissed])
@@index([userId, createdAt])
@@index([expiresAt])
@@map("notifications")
}
enum NotificationType {
ELIGIBILITY_ELIGIBLE
ELIGIBILITY_INELIGIBLE
VERIFICATION_VERIFIED
VERIFICATION_REJECTED
ORDER_APPROVED
ORDER_ACTIVATED
ORDER_FAILED
CANCELLATION_SCHEDULED
CANCELLATION_COMPLETE
PAYMENT_METHOD_EXPIRING
INVOICE_DUE
SYSTEM_ANNOUNCEMENT
}
enum NotificationSource {
SALESFORCE
WHMCS
PORTAL
SYSTEM
}
Case Management Strategy
| Case Type | Customer Visible? | Show In Portal? |
|---|---|---|
| Support Tickets (Origin="Portal Website") | ✅ Yes | Show in Support section |
| Eligibility Check (Origin="Portal") | ❌ No | Internal workflow only |
| ID Verification (Origin="Portal") | ❌ No | Internal workflow only |
| Cancellation Request (Origin="Portal") | Status only | Show via Opportunity/Service status |
Summary
This implementation provides a complete foundation for customer acquisition and service management flows.
All Features Working
| Feature | Status | Location |
|---|---|---|
| Notification Database Schema | ✅ Done | apps/bff/prisma/schema.prisma |
| NotificationService | ✅ Done | apps/bff/src/modules/notifications/notifications.service.ts |
| Notification API | ✅ Done | apps/bff/src/modules/notifications/notifications.controller.ts |
| Platform Event Integration | ✅ Done | Extended CatalogCdcSubscriber + AccountNotificationHandler |
| Cleanup Scheduler | ✅ Done | notification-cleanup.service.ts (30 day expiry) |
| Frontend Bell Icon | ✅ Done | apps/portal/src/features/notifications/components/ |
| Frontend Hooks | ✅ Done | apps/portal/src/features/notifications/hooks/ |
| Distributed Lock Service | ✅ Done | apps/bff/src/infra/cache/distributed-lock.service.ts |
| Centralized SF Field Maps | ✅ Done | packages/domain/salesforce/field-maps.ts |
| Guest Checkout Removal | ✅ Done | Removed checkout-registration module, redirect to login |
| Checkout Store Simplification | ✅ Done | apps/portal/src/features/checkout/stores/checkout.store.ts |
| OrderType Standardization | ✅ Done | PascalCase ("Internet", "SIM", "VPN") across all layers |
| Internet Subscription Match | ✅ Done | Matches "SonixNet via NTT Optical Fiber" products |
| ID Verification in Profile | ✅ Done | apps/portal/src/features/account/views/ProfileContainer.tsx |
| Profile Data Display | ✅ Done | Customer number, DOB, gender now load correctly |
| Opportunity ↔ WHMCS Linking | ✅ Done | WHMCS_Service_ID__c field links after provisioning |
Key Architecture Points
-
Subscription Type Detection - Uses product name matching:
- Internet: matches "internet", "sonixnet", or "ntt" + "fiber"
- SIM: matches "sim" in product/group name
-
ID Verification - Integrated into Profile page (
/account/settings), standalone page kept for checkout redirects -
Opportunity ↔ WHMCS Link -
WHMCS_Service_ID__cstores WHMCS service ID after provisioning -
Cancellation Flow - Portal updates Salesforce, WHMCS termination is manual (agent action)
-
Profile Data - Fetched from WHMCS custom fields (IDs: 198, 200, 201 by default)
Important Notes
WHMCS Cancellation is NOT Automated: When a customer requests cancellation, the portal:
- Updates Salesforce Opportunity (stage, scheduled date, etc.)
- Creates Salesforce Case for agent
- Does NOT automatically terminate WHMCS service
Agent must manually terminate WHMCS service on the scheduled date.
Profile Data Field IDs: If customer number, DOB, or gender don't display, verify these env variables match your WHMCS:
WHMCS_CUSTOMER_NUMBER_FIELD_ID=198
WHMCS_DOB_FIELD_ID=201
WHMCS_GENDER_FIELD_ID=200