790 lines
23 KiB
TypeScript
790 lines
23 KiB
TypeScript
/**
|
||
* 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;
|
||
}
|