Assist_Design/docs/CONSOLIDATED-TYPE-SYSTEM.md

305 lines
10 KiB
Markdown
Raw Normal View History

# 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.