740 lines
21 KiB
TypeScript
Raw Normal View History

/**
* 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 (Portal_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 Constants
// ============================================================================
/**
* Stages considered "open" for matching purposes
* Opportunities in these stages can be linked to new orders
*/
export const OPEN_OPPORTUNITY_STAGES: OpportunityStageValue[] = [
OPPORTUNITY_STAGE.INTRODUCTION,
OPPORTUNITY_STAGE.READY,
OPPORTUNITY_STAGE.POST_PROCESSING,
OPPORTUNITY_STAGE.ACTIVE,
];
/**
* 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 (transformed from form)
* Only core lifecycle fields - details go on Cancellation Case
*/
export interface CancellationOpportunityData {
/** 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;
}
/**
* 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;
}