/** * Opportunity Domain - Contract * * Business types and constants for Salesforce Opportunity lifecycle management. * Used to track customer journeys from interest through service cancellation. * * IMPORTANT: Stage values match existing Salesforce picklist values. * See docs/salesforce/OPPORTUNITY-LIFECYCLE-GUIDE.md for complete documentation. */ // ============================================================================ // Opportunity Stage Constants (Existing Salesforce Values) // ============================================================================ /** * Opportunity lifecycle stages - these match existing Salesforce picklist values * * Portal Flow: Introduction -> Ready -> Post Processing -> Active -> △Cancelling -> 〇Cancelled */ export const OPPORTUNITY_STAGE = { // Interest/Lead stages INTRODUCTION: "Introduction", // Initial interest / eligibility pending (30%) WIKI: "WIKI", // Low priority / informational (10%) // Ready to proceed READY: "Ready", // Eligible / ready to order (60%) // Order/Processing stages POST_PROCESSING: "Post Processing", // Order placed, processing (75%) // Active service ACTIVE: "Active", // Service active (90%) // Cancellation stages CANCELLING: "△Cancelling", // Cancellation pending (100%, Open) CANCELLED: "〇Cancelled", // Successfully cancelled (100%, Closed/Won) // Completed/Closed stages COMPLETED: "Completed", // Service completed normally (100%, Closed/Won) VOID: "Void", // Lost / not eligible (0%, Closed/Lost) PENDING: "Pending", // On hold / abandoned (0%, Closed/Lost) } as const; export type OpportunityStageValue = (typeof OPPORTUNITY_STAGE)[keyof typeof OPPORTUNITY_STAGE]; // ============================================================================ // Application Stage Constants (Existing Salesforce Values) // ============================================================================ /** * Application stage picklist (Application_Stage__c) * Tracks application process step * * For portal, we use INTRO_1 (single-step intro, not multi-email legacy flow) */ export const APPLICATION_STAGE = { INTRO_1: "INTRO-1", // Portal default - simple introduction NA: "N/A", // Not applicable } as const; export type ApplicationStageValue = (typeof APPLICATION_STAGE)[keyof typeof APPLICATION_STAGE]; // ============================================================================ // Cancellation Notice Constants (Existing Salesforce Values) // ============================================================================ /** * Cancellation notice status (CancellationNotice__c) * Tracks whether cancellation form has been received */ export const CANCELLATION_NOTICE = { RECEIVED: "有", // Form received NOT_YET: "未", // Not yet received (default) NOT_REQUIRED: "不要", // Not required TRANSFER: "移転", // Transferring (moving) } as const; export type CancellationNoticeValue = (typeof CANCELLATION_NOTICE)[keyof typeof CANCELLATION_NOTICE]; // ============================================================================ // Line Return Status Constants (Existing Salesforce Values) // ============================================================================ /** * Rental equipment return status (LineReturn__c) * Tracks equipment return process after cancellation */ export const LINE_RETURN_STATUS = { NOT_YET: "NotYet", // Not yet initiated (default) SENT_KIT: "SentKit", // Return kit sent to customer PICKUP_SCHEDULED: "AS/Pickup予定", // Pickup scheduled RETURNED_1: "Returned1", // Returned (variant 1) RETURNED: "Returned2", // Returned NTT_DISPATCH: "NTT派遣", // NTT dispatch COMPENSATED: "Compensated", // Compensation fee charged NA: "N/A", // Not applicable (no equipment) } as const; export type LineReturnStatusValue = (typeof LINE_RETURN_STATUS)[keyof typeof LINE_RETURN_STATUS]; // ============================================================================ // Commodity Type Constants (Existing Salesforce Field: CommodityType) // ============================================================================ /** * Commodity types - uses existing Salesforce CommodityType picklist * Maps to WHMCS product categories */ export const COMMODITY_TYPE = { // Internet types PERSONAL_HOME_INTERNET: "Personal SonixNet Home Internet", CORPORATE_HOME_INTERNET: "Corporate SonixNet Home Internet", // SIM SIM: "SIM", // VPN VPN: "VPN", // Other (not used by portal) TECH_SUPPORT: "Onsite Support", } as const; export type CommodityTypeValue = (typeof COMMODITY_TYPE)[keyof typeof COMMODITY_TYPE]; /** * Simplified product types for portal logic * Maps multiple CommodityType values to simple categories */ export const OPPORTUNITY_PRODUCT_TYPE = { INTERNET: "Internet", SIM: "SIM", VPN: "VPN", } as const; export type OpportunityProductTypeValue = (typeof OPPORTUNITY_PRODUCT_TYPE)[keyof typeof OPPORTUNITY_PRODUCT_TYPE]; /** * Map CommodityType to simplified product type */ export function getCommodityTypeProductType( commodityType: CommodityTypeValue ): OpportunityProductTypeValue | null { switch (commodityType) { case COMMODITY_TYPE.PERSONAL_HOME_INTERNET: case COMMODITY_TYPE.CORPORATE_HOME_INTERNET: return OPPORTUNITY_PRODUCT_TYPE.INTERNET; case COMMODITY_TYPE.SIM: return OPPORTUNITY_PRODUCT_TYPE.SIM; case COMMODITY_TYPE.VPN: return OPPORTUNITY_PRODUCT_TYPE.VPN; default: return null; } } /** * Get default CommodityType for a product type * Used when creating new Opportunities * * NOTE: Currently only Personal SonixNet Home Internet is used for Internet. * Corporate can be added later when needed. */ export function getDefaultCommodityType( productType: OpportunityProductTypeValue ): CommodityTypeValue { switch (productType) { case OPPORTUNITY_PRODUCT_TYPE.INTERNET: // Always use Personal for now - Corporate support can be added later return COMMODITY_TYPE.PERSONAL_HOME_INTERNET; case OPPORTUNITY_PRODUCT_TYPE.SIM: return COMMODITY_TYPE.SIM; case OPPORTUNITY_PRODUCT_TYPE.VPN: return COMMODITY_TYPE.VPN; } } /** * Default commodity types used when creating opportunities from portal * These are the only values the portal will create - agents can use others */ export const PORTAL_DEFAULT_COMMODITY_TYPES = { INTERNET: COMMODITY_TYPE.PERSONAL_HOME_INTERNET, SIM: COMMODITY_TYPE.SIM, VPN: COMMODITY_TYPE.VPN, } as const; // ============================================================================ // Opportunity Source Constants // ============================================================================ /** * Sources from which Opportunities are created (Opportunity_Source__c) */ export const OPPORTUNITY_SOURCE = { INTERNET_ELIGIBILITY: "Portal - Internet Eligibility Request", SIM_CHECKOUT_REGISTRATION: "Portal - SIM Checkout Registration", ORDER_PLACEMENT: "Portal - Order Placement", AGENT_CREATED: "Agent Created", } as const; export type OpportunitySourceValue = (typeof OPPORTUNITY_SOURCE)[keyof typeof OPPORTUNITY_SOURCE]; // ============================================================================ // Opportunity Matching / Stage Sets // ============================================================================ /** * Stages considered "open" (i.e. not closed) in our lifecycle. * * IMPORTANT: * - Do NOT use this list to match a *new* order or eligibility request to an existing Opportunity. * For those flows we intentionally match a much smaller set of early-stage Opportunities. * - Use the `OPPORTUNITY_MATCH_STAGES_*` constants below instead. */ export const OPEN_OPPORTUNITY_STAGES: OpportunityStageValue[] = [ OPPORTUNITY_STAGE.INTRODUCTION, OPPORTUNITY_STAGE.READY, OPPORTUNITY_STAGE.POST_PROCESSING, OPPORTUNITY_STAGE.ACTIVE, ]; /** * Stages eligible for matching during an Internet eligibility request. * * We only ever reuse the initial "Introduction" opportunity; later stages indicate the * customer journey has progressed beyond eligibility. */ export const OPPORTUNITY_MATCH_STAGES_INTERNET_ELIGIBILITY: OpportunityStageValue[] = [ OPPORTUNITY_STAGE.INTRODUCTION, ]; /** * Stages eligible for matching during order placement. * * If a customer came from eligibility, the Opportunity will usually be in "Introduction" or "Ready". * We must never match an "Active" Opportunity for a new order (that would corrupt lifecycle tracking). */ export const OPPORTUNITY_MATCH_STAGES_ORDER_PLACEMENT: OpportunityStageValue[] = [ OPPORTUNITY_STAGE.INTRODUCTION, OPPORTUNITY_STAGE.READY, ]; /** * Stages that indicate the Opportunity is closed */ export const CLOSED_OPPORTUNITY_STAGES: OpportunityStageValue[] = [ OPPORTUNITY_STAGE.CANCELLED, OPPORTUNITY_STAGE.COMPLETED, OPPORTUNITY_STAGE.VOID, OPPORTUNITY_STAGE.PENDING, ]; // ============================================================================ // Cancellation Deadline Constants // ============================================================================ /** * Day of month by which cancellation form must be received * If after this day, cancellation applies to next month */ export const CANCELLATION_DEADLINE_DAY = 25; /** * Day of following month by which rental equipment must be returned */ export const RENTAL_RETURN_DEADLINE_DAY = 10; // ============================================================================ // Cancellation Opportunity Data Types // ============================================================================ /** * Cancellation data to populate on Opportunity for Internet services * Only core lifecycle fields - details go on Cancellation Case */ export interface InternetCancellationOpportunityData { /** End of cancellation month (YYYY-MM-DD format) */ scheduledCancellationDate: string; /** Cancellation notice status (always 有 from portal) */ cancellationNotice: CancellationNoticeValue; /** Initial line return status (always NotYet from portal) */ lineReturnStatus: LineReturnStatusValue; } /** * SIM cancellation notice values (SIMCancellationNotice__c) * Tracks whether SIM cancellation form has been received */ export const SIM_CANCELLATION_NOTICE = { RECEIVED: "有", // Form received NOT_YET: "未", // Not yet received (default) NOT_REQUIRED: "不要", // Not required } as const; export type SimCancellationNoticeValue = (typeof SIM_CANCELLATION_NOTICE)[keyof typeof SIM_CANCELLATION_NOTICE]; /** * Cancellation data to populate on Opportunity for SIM services * NOTE: Customer comments go on the Cancellation Case, not Opportunity (same as Internet) */ export interface SimCancellationOpportunityData { /** End of cancellation month (YYYY-MM-DD format) */ scheduledCancellationDate: string; /** SIM Cancellation notice status (always 有 from portal) */ cancellationNotice: SimCancellationNoticeValue; } /** * Data to populate on Cancellation Case * This contains all customer-provided details */ export interface CancellationCaseData { /** Account ID */ accountId: string; /** Linked Opportunity ID (if known) */ opportunityId?: string; /** WHMCS Service ID for reference */ whmcsServiceId: number; /** Product type (Internet, SIM, VPN) */ productType: OpportunityProductTypeValue; /** Selected cancellation month (YYYY-MM) */ cancellationMonth: string; /** End of cancellation month (YYYY-MM-DD) */ cancellationDate: string; /** Optional alternative contact email */ alternativeEmail?: string; /** Optional customer comments */ comments?: string; } // ============================================================================ // Customer-Facing Service Display Model // ============================================================================ /** * Customer-facing service phase * Only shows what's relevant to customers (no internal sales stages) */ export type CustomerServicePhase = | "order" // Order placed, awaiting activation (has SF Order, no WHMCS) | "active" // Service is live (has WHMCS service) | "cancelling" // Cancellation pending | "cancelled"; // Service ended /** * Whether the WHMCS service is linked to an Opportunity */ export type ServiceLinkStatus = | "linked" // Opportunity has WHMCS_Service_ID__c (full features) | "unlinked"; // Legacy - WHMCS exists but Opp not linked /** * Order tracking info (shown after order placed, before WHMCS activates) * Source: Salesforce Order */ export interface OrderTrackingInfo { /** Salesforce Order ID */ orderId: string; /** Order number for display */ orderNumber: string; /** Product being ordered */ productName: string; /** Order status */ status: string; /** When order was placed */ orderDate: string; /** Expected activation date (estimated) */ expectedActivation?: string; /** Progress steps */ steps: Array<{ label: string; status: "completed" | "current" | "upcoming"; }>; } /** * Service details from WHMCS (shown when service is active) * This is the primary data source for customer service page */ export interface WhmcsServiceDetails { /** WHMCS Service ID */ whmcsServiceId: number; /** Product/plan name */ productName: string; /** WHMCS status (Active, Suspended, etc.) */ status: string; /** Service address (for Internet) */ serviceAddress?: string; /** Monthly amount */ monthlyAmount?: number; /** Currency code */ currency?: string; /** Next billing date */ nextDueDate?: string; /** Registration date */ registrationDate?: string; /** Linked Opportunity ID (from WHMCS custom field) */ opportunityId?: string; } /** * Cancellation info for display (shown when cancelling) */ export interface CancellationDisplayInfo { /** When the service will end (end of month) */ serviceEndDate: string; /** Deadline for returning equipment */ equipmentReturnDeadline: string; /** Current return kit status */ returnKitStatus: LineReturnStatusValue; /** Human-readable return status */ returnKitStatusLabel: string; /** Whether equipment needs to be returned */ hasEquipmentToReturn: boolean; /** Days remaining until service ends */ daysUntilServiceEnd: number; /** Days remaining until return deadline */ daysUntilReturnDeadline: number; } /** * Customer-facing service view for the portal * * Combines data from WHMCS (primary) and Opportunity (lifecycle/cancellation) * Handles both linked and legacy (unlinked) services */ export interface CustomerServiceView { // ---- Identification ---- /** Unique ID for this view (WHMCS Service ID as string) */ id: string; /** WHMCS Service ID */ whmcsServiceId: number; /** Linked Opportunity ID (null for legacy services) */ opportunityId?: string; /** Product type (Internet, SIM, VPN) */ productType: OpportunityProductTypeValue; // ---- Display State ---- /** * Customer-facing phase * - order: Waiting for activation * - active: Service is live * - cancelling: Cancellation pending * - cancelled: Service ended */ phase: CustomerServicePhase; /** * Whether this service has full Opportunity linking * - linked: Full features (can show cancellation status, etc.) * - unlinked: Legacy service (limited features) */ linkStatus: ServiceLinkStatus; // ---- Service Data (from WHMCS) ---- /** Product/plan name */ productName: string; /** WHMCS status */ status: string; /** Monthly amount */ monthlyAmount?: number; /** Currency */ currency?: string; /** Next billing date */ nextDueDate?: string; // ---- Cancellation Data (from Opportunity, if linked) ---- /** * Cancellation info (only when phase = 'cancelling' and linked) */ cancellation?: CancellationDisplayInfo; // ---- Actions ---- /** Whether cancel button should be shown */ canCancel: boolean; /** Whether user can view invoices */ canViewInvoices: boolean; /** Message to show if features are limited (for legacy) */ limitedFeaturesMessage?: string; } /** * Map Opportunity stage to customer-facing phase */ export function getCustomerPhaseFromStage( stage: OpportunityStageValue | undefined, whmcsStatus: string ): CustomerServicePhase { // If no Opportunity or WHMCS is active, derive from WHMCS status if (!stage) { if (whmcsStatus === "Active") return "active"; if (whmcsStatus === "Cancelled" || whmcsStatus === "Terminated") return "cancelled"; return "active"; // Default for legacy } switch (stage) { case OPPORTUNITY_STAGE.INTRODUCTION: case OPPORTUNITY_STAGE.READY: case OPPORTUNITY_STAGE.POST_PROCESSING: return "order"; // Customer sees "order in progress" case OPPORTUNITY_STAGE.ACTIVE: return "active"; case OPPORTUNITY_STAGE.CANCELLING: return "cancelling"; case OPPORTUNITY_STAGE.CANCELLED: case OPPORTUNITY_STAGE.COMPLETED: case OPPORTUNITY_STAGE.VOID: case OPPORTUNITY_STAGE.PENDING: return "cancelled"; default: return "active"; } } /** * Get order tracking progress steps * Used for customers who have placed an order but WHMCS is not yet active */ export function getOrderTrackingSteps( currentStage: OpportunityStageValue ): OrderTrackingInfo["steps"] { const stages = [ { stage: OPPORTUNITY_STAGE.POST_PROCESSING, label: "Order Received" }, { stage: OPPORTUNITY_STAGE.POST_PROCESSING, label: "Processing" }, { stage: OPPORTUNITY_STAGE.ACTIVE, label: "Activating" }, { stage: OPPORTUNITY_STAGE.ACTIVE, label: "Active" }, ]; // Simplify to 4 steps for customer clarity let currentStep = 0; if (currentStage === OPPORTUNITY_STAGE.POST_PROCESSING) { currentStep = 1; // Processing } else if (currentStage === OPPORTUNITY_STAGE.ACTIVE) { currentStep = 3; // Active } return stages.map((s, index) => ({ label: s.label, status: index < currentStep ? "completed" : (index === currentStep ? "current" : "upcoming"), })); } /** * Determine if a service can be cancelled */ export function canServiceBeCancelled( whmcsStatus: string, opportunityStage?: OpportunityStageValue ): boolean { // Can't cancel if WHMCS is not active if (whmcsStatus !== "Active") return false; // If linked to Opportunity, check stage if (opportunityStage) { // Can only cancel from Active stage return opportunityStage === OPPORTUNITY_STAGE.ACTIVE; } // Legacy service - allow cancellation (will create Case for agent) return true; }