barsa 7abd433d95 Refactor conditional rendering and improve code readability across multiple components
- Simplified conditional rendering in OrderSummary, ProductCard, InstallationOptions, InternetOfferingCard, DeviceCompatibility, SimPlansContent, and other components by removing unnecessary parentheses.
- Enhanced clarity in the use of ternary operators for better maintainability.
- Updated documentation to reflect changes in development setup for skipping OTP verification during login.
- Removed outdated orchestrator refactoring plan document.
- Added new environment variable for skipping OTP verification in development.
- Minor adjustments in domain contracts and mappers for consistency in conditional checks.
2026-02-03 18:28:38 +09:00

611 lines
18 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;
// ============================================================================
// 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;
}