305 lines
10 KiB
Markdown
305 lines
10 KiB
Markdown
|
|
# 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:
|
||
|
|
|
||
|
|
1. **Type duplication** across contexts
|
||
|
|
2. **Inconsistent pricing models** (sometimes `price`, sometimes `monthlyPrice`/`oneTimePrice`)
|
||
|
|
3. **Enhanced Order Summary** creating its own conflicting `OrderItem` interface
|
||
|
|
4. **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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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 from `unitPrice` and `billingCycle`
|
||
|
|
- No more confusion between `price` vs `monthlyPrice` vs `unitPrice`
|
||
|
|
|
||
|
|
### 3. **Type Safety**
|
||
|
|
- TypeScript discrimination based on `category` field
|
||
|
|
- 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
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
```typescript
|
||
|
|
const product = fromSalesforceProduct2(salesforceProduct, pricebookEntry);
|
||
|
|
|
||
|
|
if (isCatalogVisible(product)) {
|
||
|
|
displayInCatalog(product);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Order Context
|
||
|
|
```typescript
|
||
|
|
const orderItem: OrderItemRequest = {
|
||
|
|
...product,
|
||
|
|
quantity: 1,
|
||
|
|
autoAdded: false
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### Enhanced Order Summary
|
||
|
|
```typescript
|
||
|
|
// 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
|
||
|
|
|
||
|
|
1. **Backward Compatibility**: Legacy type aliases maintained
|
||
|
|
```typescript
|
||
|
|
export type InternetPlan = InternetProduct;
|
||
|
|
export type CatalogItem = Product;
|
||
|
|
```
|
||
|
|
|
||
|
|
2. **Gradual Adoption**: Existing code continues to work while new code uses unified types
|
||
|
|
|
||
|
|
3. **Clear Documentation**: This document explains the new structure and migration path
|
||
|
|
|
||
|
|
## Files Modified
|
||
|
|
|
||
|
|
- `packages/domain/src/entities/product.ts` - New unified product types
|
||
|
|
- `packages/domain/src/entities/order.ts` - Updated order types
|
||
|
|
- `packages/domain/src/entities/catalog.ts` - Re-exports with legacy aliases
|
||
|
|
- `apps/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:
|
||
|
|
|
||
|
|
1. **Extend unified types**: Now extends `OrderItemRequest` instead of creating conflicting interfaces
|
||
|
|
2. **Use consistent pricing**: Uses `monthlyPrice`/`oneTimePrice`/`unitPrice` from unified structure
|
||
|
|
3. **Proper field access**: Uses `itemClass`, `billingCycle` from 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 portal
|
||
|
|
- `SalesforceOrderItem` - for existing orders from Salesforce
|
||
|
|
- `WhmcsOrderItem` - 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.
|