Assist_Design/docs/CONSOLIDATED-TYPE-SYSTEM.md
T. Narantuya a95ec60859 Refactor address management and update related services for improved clarity and functionality
- Updated address retrieval in user service to replace billing info with a dedicated address method.
- Adjusted API endpoints to use `PATCH /api/me/address` for address updates instead of billing updates.
- Enhanced documentation to reflect changes in address management processes and API usage.
- Removed deprecated types and services related to billing address handling, streamlining the codebase.
2025-09-17 18:43:43 +09:00

10 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:

  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

// 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 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

// 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

  1. Backward Compatibility: Legacy type aliases maintained

    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.