790 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
// ============================================================================
// Business Types
// ============================================================================
/**
* Opportunity record as returned from Salesforce
*/
export interface OpportunityRecord {
id: string;
name: string;
accountId: string;
stage: OpportunityStageValue;
closeDate: string;
/** CommodityType - existing Salesforce field for product categorization */
commodityType?: CommodityTypeValue;
/** Simplified product type - derived from commodityType */
productType?: OpportunityProductTypeValue;
source?: OpportunitySourceValue;
applicationStage?: ApplicationStageValue;
isClosed: boolean;
// Linked entities
// Note: Cases and Orders link TO Opportunity (not stored here)
// - Case.OpportunityId → for eligibility, ID verification, cancellation
// - Order.OpportunityId → for order tracking
whmcsServiceId?: number;
// Cancellation fields (updated by CS when processing cancellation Case)
cancellationNotice?: CancellationNoticeValue;
scheduledCancellationDate?: string;
lineReturnStatus?: LineReturnStatusValue;
// NOTE: alternativeContactEmail and cancellationComments are on the Cancellation Case
// Metadata
createdDate: string;
lastModifiedDate: string;
}
/**
* Request to create a new Opportunity
*
* Note: Opportunity Name is auto-generated by Salesforce workflow,
* so we don't need to provide account name. The service will use
* a placeholder that gets overwritten by Salesforce.
*/
export interface CreateOpportunityRequest {
accountId: string;
productType: OpportunityProductTypeValue;
stage: OpportunityStageValue;
source: OpportunitySourceValue;
/** Application stage, defaults to INTRO-1 */
applicationStage?: ApplicationStageValue;
/** Expected close date, defaults to 30 days from now */
closeDate?: string;
}
/**
* Request to update Opportunity stage
*/
export interface UpdateOpportunityStageRequest {
opportunityId: string;
stage: OpportunityStageValue;
/** Optional: reason for stage change (for audit) */
reason?: string;
}
/**
* Cancellation form data from customer
*/
export interface CancellationFormData {
/**
* Selected cancellation month (YYYY-MM format)
* Service ends at end of this month
*/
cancellationMonth: string;
/** Customer confirms they have read cancellation terms */
confirmTermsRead: boolean;
/** Customer confirms they understand month-end cancellation */
confirmMonthEndCancellation: boolean;
/** Optional alternative email for post-cancellation communication */
alternativeEmail?: string;
/** Optional customer comments/notes */
comments?: string;
}
/**
* 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;
}
/**
* Cancellation eligibility check result
*/
export interface CancellationEligibility {
/** Whether cancellation can be requested now */
canCancel: boolean;
/** Earliest month available for cancellation (YYYY-MM) */
earliestCancellationMonth: string;
/** Available cancellation months (up to 12 months ahead) */
availableMonths: CancellationMonthOption[];
/** Deadline for requesting cancellation this month */
currentMonthDeadline: string | null;
/** If cannot cancel, the reason why */
reason?: string;
}
/**
* A month option for cancellation selection
*/
export interface CancellationMonthOption {
/** Value in YYYY-MM format */
value: string;
/** Display label (e.g., "January 2025") */
label: string;
/** End date of the month (service end date) */
serviceEndDate: string;
/** Rental return deadline (10th of following month) */
rentalReturnDeadline: string;
/** Whether this is the current month (may have deadline) */
isCurrentMonth: boolean;
}
/**
* Cancellation status for display in portal
*/
export interface CancellationStatus {
/** Current stage */
stage: OpportunityStageValue;
/** Whether cancellation is pending */
isPending: boolean;
/** Whether cancellation is complete */
isComplete: boolean;
/** Scheduled end date */
scheduledEndDate?: string;
/** Rental return status */
rentalReturnStatus?: LineReturnStatusValue;
/** Rental return deadline */
rentalReturnDeadline?: string;
/** Whether rental equipment needs to be returned */
hasRentalEquipment: boolean;
}
/**
* Result of Opportunity matching/resolution
*/
export interface OpportunityMatchResult {
/** The Opportunity ID (existing or newly created) */
opportunityId: string;
/** Whether a new Opportunity was created */
wasCreated: boolean;
/** Previous stage if updated */
previousStage?: OpportunityStageValue;
}
/**
* Lookup criteria for finding existing Opportunities
*/
export interface OpportunityLookupCriteria {
accountId: string;
productType: OpportunityProductTypeValue;
/** Only match Opportunities in these stages */
allowedStages?: OpportunityStageValue[];
}
// ============================================================================
// 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;
}