9.9 KiB
Consolidated Type System - Complete Solution
Problem Analysis
You identified a critical issue: order items, catalog items, and product types were all trying to encapsulate the same Salesforce Product2 data, leading to:
- Type duplication across contexts
- Inconsistent pricing models (sometimes
price, sometimesmonthlyPrice/oneTimePrice) - Enhanced Order Summary creating its own conflicting
OrderIteminterface - Missing PricebookEntry representation in the type system
Solution: Unified Type Architecture
Core Principle
One Salesforce Object = One TypeScript Type Structure
Since both catalog items and order items ultimately represent Salesforce Product2 objects (with PricebookEntry for pricing), we created a unified type system that directly maps to the Salesforce/WHMCS object structure.
New Type Hierarchy
1. Base Product Structure
// packages/domain/src/entities/product.ts
// Maps directly to Salesforce Product2 fields
interface BaseProduct {
// Standard Salesforce fields
id: string; // Product2.Id
name: string; // Product2.Name
sku: string; // Product2.StockKeepingUnit
// Custom portal fields
category: ProductCategory; // Product2Categories1__c
itemClass: ItemClass; // Item_Class__c
billingCycle: BillingCycle; // Billing_Cycle__c
portalCatalog: boolean; // Portal_Catalog__c
portalAccessible: boolean; // Portal_Accessible__c
// WHMCS integration
whmcsProductId: number; // WH_Product_ID__c
whmcsProductName: string; // WH_Product_Name__c
}
// Represents Salesforce PricebookEntry structure
interface PricebookEntry {
id: string; // PricebookEntry.Id
name: string; // PricebookEntry.Name
unitPrice: number; // PricebookEntry.UnitPrice
pricebook2Id: string; // PricebookEntry.Pricebook2Id
product2Id: string; // PricebookEntry.Product2Id
isActive: boolean; // PricebookEntry.IsActive
}
// Product with proper pricing structure
interface ProductWithPricing extends BaseProduct {
pricebookEntry?: PricebookEntry;
// Convenience fields derived from pricebookEntry and billingCycle
unitPrice?: number; // PricebookEntry.UnitPrice
monthlyPrice?: number; // UnitPrice if billingCycle === "Monthly"
oneTimePrice?: number; // UnitPrice if billingCycle === "Onetime"
}
2. Specialized Product Types
// Category-specific extensions
interface InternetProduct extends ProductWithPricing {
category: "Internet";
internetPlanTier?: "Silver" | "Gold" | "Platinum";
internetOfferingType?: string;
internetMonthlyPrice?: number;
}
interface SimProduct extends ProductWithPricing {
category: "SIM";
simDataSize?: string;
simPlanType?: "DataOnly" | "DataSmsVoice" | "VoiceOnly";
simHasFamilyDiscount?: boolean;
}
// Union type for all products
type Product = InternetProduct | SimProduct | VpnProduct | ProductWithPricing;
3. Order Item Types
// For new orders (before Salesforce creation)
interface OrderItemRequest extends ProductWithPricing {
quantity: number;
autoAdded?: boolean;
}
// Actual Salesforce OrderItem structure
interface SalesforceOrderItem {
id: string; // OrderItem.Id
orderId: string; // OrderItem.OrderId
quantity: number; // OrderItem.Quantity
unitPrice: number; // OrderItem.UnitPrice
totalPrice: number; // OrderItem.TotalPrice
pricebookEntry: PricebookEntry & {
product2: Product; // Full product data
};
whmcsServiceId?: string; // WHMCS_Service_ID__c
billingCycle?: BillingCycle; // Derived from Product2
}
4. Order Types
// Salesforce Order structure
interface SalesforceOrder {
id: string; // Order.Id
orderNumber: string; // Order.OrderNumber
status: string; // Order.Status
type: string; // Order.Type
effectiveDate: string; // Order.EffectiveDate
totalAmount: number; // Order.TotalAmount
accountId: string; // Order.AccountId
pricebook2Id: string; // Order.Pricebook2Id
// Custom fields
orderType?: string; // Order_Type__c
activationStatus?: string; // Activation_Status__c
whmcsOrderId?: string; // WHMCS_Order_ID__c
orderItems?: SalesforceOrderItem[];
}
// WHMCS Order structure (separate for existing orders)
interface WhmcsOrder extends WhmcsEntity {
orderNumber: string;
status: OrderStatus;
items: WhmcsOrderItem[];
}
// Union types for different contexts
type Order = WhmcsOrder | SalesforceOrder;
type OrderItem = WhmcsOrderItem | OrderItemRequest | SalesforceOrderItem;
Key Benefits
1. Single Source of Truth
- All types map directly to Salesforce object structure
- No more guessing which type to use in which context
- Consistent field names across the application
2. Proper PricebookEntry Representation
- Pricing is now properly modeled as PricebookEntry structure
- Convenience fields (
monthlyPrice,oneTimePrice) derived fromunitPriceandbillingCycle - No more confusion between
pricevsmonthlyPricevsunitPrice
3. Type Safety
- TypeScript discrimination based on
categoryfield - Proper type guards for business logic
- Compile-time validation of field access
4. Maintainability
- Changes to Salesforce fields only need updates in one place
- Clear transformation functions between Salesforce API and TypeScript types
- Backward compatibility through type aliases
Transformation Functions
// Transform Salesforce Product2 + PricebookEntry to unified Product
function fromSalesforceProduct2(sfProduct: any, pricebookEntry?: any): Product {
const billingCycle = sfProduct.Billing_Cycle__c || "Onetime";
const unitPrice = pricebookEntry?.UnitPrice;
return {
id: sfProduct.Id,
name: sfProduct.Name,
sku: sfProduct.StockKeepingUnit,
category: sfProduct.Product2Categories1__c,
itemClass: sfProduct.Item_Class__c,
billingCycle: billingCycle,
portalCatalog: sfProduct.Portal_Catalog__c,
portalAccessible: sfProduct.Portal_Accessible__c,
whmcsProductId: sfProduct.WH_Product_ID__c,
whmcsProductName: sfProduct.WH_Product_Name__c,
// PricebookEntry structure
pricebookEntry: pricebookEntry
? {
id: pricebookEntry.Id,
name: pricebookEntry.Name,
unitPrice: pricebookEntry.UnitPrice,
pricebook2Id: pricebookEntry.Pricebook2Id,
product2Id: sfProduct.Id,
isActive: pricebookEntry.IsActive !== false,
}
: undefined,
// Convenience pricing fields
unitPrice: unitPrice,
monthlyPrice: billingCycle === "Monthly" ? unitPrice : undefined,
oneTimePrice: billingCycle === "Onetime" ? unitPrice : undefined,
// Include all other fields for dynamic access
...sfProduct,
};
}
// Transform Salesforce OrderItem to unified structure
function fromSalesforceOrderItem(sfOrderItem: any): SalesforceOrderItem {
const product = fromSalesforceProduct2(
sfOrderItem.PricebookEntry?.Product2,
sfOrderItem.PricebookEntry
);
return {
id: sfOrderItem.Id,
orderId: sfOrderItem.OrderId,
quantity: sfOrderItem.Quantity,
unitPrice: sfOrderItem.UnitPrice,
totalPrice: sfOrderItem.TotalPrice,
pricebookEntry: {
...product.pricebookEntry!,
product2: product,
},
whmcsServiceId: sfOrderItem.WHMCS_Service_ID__c,
billingCycle: product.billingCycle,
...sfOrderItem,
};
}
Usage Examples
Catalog Context
const product = fromSalesforceProduct2(salesforceProduct, pricebookEntry);
if (isCatalogVisible(product)) {
displayInCatalog(product);
}
Order Context
const orderItem: OrderItemRequest = {
...product,
quantity: 1,
autoAdded: false,
};
Enhanced Order Summary
// Now uses consistent unified types
interface OrderItem extends OrderItemRequest {
id?: string;
description?: string;
}
// Access unified pricing fields
const price =
item.billingCycle === "Monthly"
? item.monthlyPrice || item.unitPrice || 0
: item.oneTimePrice || item.unitPrice || 0;
Migration Strategy
-
Backward Compatibility: Legacy type aliases maintained
export type InternetPlan = InternetProduct; export type CatalogItem = Product; -
Gradual Adoption: Existing code continues to work while new code uses unified types
-
Clear Documentation: This document explains the new structure and migration path
Files Modified
packages/domain/src/entities/product.ts- New unified product typespackages/domain/src/entities/order.ts- Updated order typespackages/domain/src/entities/catalog.ts- Re-exports with legacy aliasesapps/portal/src/features/catalog/components/base/EnhancedOrderSummary.tsx- Updated to use unified types
Enhanced Order Summary
The Enhanced Order Summary was a UI component that created its own OrderItem interface, leading to type conflicts. It has been updated to:
- Extend unified types: Now extends
OrderItemRequestinstead of creating conflicting interfaces - Use consistent pricing: Uses
monthlyPrice/oneTimePrice/unitPricefrom unified structure - Proper field access: Uses
itemClass,billingCyclefrom unified product structure
This eliminates the confusion about "which OrderItem type should I use?" because there's now a clear hierarchy:
OrderItemRequest- for new orders in the portalSalesforceOrderItem- for existing orders from SalesforceWhmcsOrderItem- for existing orders from WHMCS
The Enhanced Order Summary now properly represents the unified product structure while adding UI-specific fields like description and optional id.
Conclusion
This consolidated type system eliminates the original problem of multiple types trying to represent the same Salesforce data. Now there's one unified type system that properly maps to your Salesforce/WHMCS object structure, with clear transformation functions and proper PricebookEntry representation.
The solution maintains backward compatibility while providing a clear path forward for consistent type usage across your entire application.