Assist_Design/docs/_archive/ORDERS-FIELD-CONFIG-PROBLEM.md

12 KiB

The Field Config Problem - Root Cause Analysis

🎯 Core Issue: Unnecessary Abstraction Layer

The orders domain has an entire field configuration system that is completely unnecessary and adds significant complexity without any real benefit.


📊 Comparison: Catalog vs Orders

Catalog Domain (Clean & Simple)

// 1. Raw types define actual Salesforce fields
export const salesforceProduct2RecordSchema = z.object({
  Internet_Plan_Tier__c: z.string().nullable().optional(),
  Billing_Cycle__c: z.string().nullable().optional(),
  // ... actual field names
});

// 2. Mapper uses them directly
export function mapInternetPlan(product: SalesforceProduct2WithPricebookEntries) {
  const base = baseProduct(product);
  const tier = product.Internet_Plan_Tier__c ?? undefined;  // ← Direct access
  
  return { ...base, internetPlanTier: tier };
}

// 3. Query builders use actual field names
const fields = [
  "Id",
  "Name",
  "Internet_Plan_Tier__c",  // ← Direct field name
  "Billing_Cycle__c",
];

Result: Simple, straightforward, easy to understand.


Orders Domain (Over-Engineered)

// 1. Raw types define actual Salesforce fields (same as catalog)
export const salesforceOrderRecordSchema = z.object({
  Activation_Type__c: z.string().nullable().optional(),
  Internet_Plan_Tier__c: z.string().nullable().optional(),
  // ... actual field names
});

// 2. Field config interfaces (UNNECESSARY LAYER)
export interface SalesforceOrderFieldConfig {
  activationType: string;      // → "Activation_Type__c"
  internetPlanTier: string;    // → "Internet_Plan_Tier__c"
  // ... mapping logical names to actual names
}

// 3. Field config service (UNNECESSARY SERVICE)
@Injectable()
export class SalesforceFieldMapService {
  getFieldMap(): SalesforceOrdersFieldConfig {
    return {
      order: {
        activationType: "Activation_Type__c",
        internetPlanTier: "Internet_Plan_Tier__c",
        // ... hardcoded values from env
      }
    };
  }
}

// 4. Mapper uses field config (INDIRECT ACCESS)
export function transformSalesforceOrderItem(
  record: SalesforceOrderItemRecord,
  fieldConfig: SalesforceOrdersFieldConfig  // ← Extra parameter!
) {
  return {
    sku: pickProductString(product, fieldConfig.product.sku),  // ← Indirect
    internetPlanTier: pickProductString(product, fieldConfig.product.internetPlanTier),
  };
}

// Helper that does runtime reflection (SLOW!)
function pickProductString(product: SalesforceProduct2Record, field: string) {
  return ensureString(Reflect.get(product, field));  // ← Runtime lookup!
}

// 5. Query builders use field config (INDIRECT)
export function buildOrderSelectFields(
  fieldConfig: SalesforceOrdersFieldConfig,  // ← Extra parameter!
  additional: string[] = []
) {
  const fields = [
    "Id",
    fieldConfig.order.activationType,    // ← Indirect
    fieldConfig.order.internetPlanTier,  // ← Indirect
  ];
  return fields;
}

Result: Complex, confusing, slow (runtime reflection), hard to maintain.


🤔 Why Does This Exist?

Hypothesis 1: Support Multiple Salesforce Orgs?

Theory: Maybe different orgs have different field names?

  • Dev org: Activation_Type__c
  • Prod org: Order_Activation_Type__c

Reality: This doesn't make sense because:

  1. The raw types still hardcode the field names
  2. You'd need to deploy different schemas for different orgs
  3. Custom fields in Salesforce packages have fixed API names
  4. No other domain does this

Verdict: Not the reason


Hypothesis 2: Environment Configuration?

Theory: Maybe field names come from environment variables?

Reality: Looking at the service:

activationType: this.configService.get<string>("ORDER_ACTIVATION_TYPE_FIELD")!,

But this is pointless because:

  1. The raw types hardcode Activation_Type__c anyway
  2. If the env var has the wrong value, queries fail
  3. No flexibility gained - you still need matching schemas
  4. Other domains don't need this

Verdict: Fake flexibility - not actually useful


Hypothesis 3: Legacy/Historical Reasons?

Theory: Maybe this was built before the raw types existed?

Reality: Raw types exist now, making the entire field config system obsolete.

Verdict: Most likely - technical debt that should be removed


📉 Problems Caused by Field Config System

1. Unnecessary Complexity

  • Extra interfaces: SalesforceOrderFieldConfig, SalesforceOrderMnpFieldConfig, etc.
  • Extra service: SalesforceFieldMapService
  • Extra parameters: Every mapper function needs fieldConfig
  • Extra configuration: Environment variables for field names

2. Performance Issues

// Current: Runtime reflection (slow)
Reflect.get(product, fieldConfig.product.sku)

// Should be: Direct access (fast)
product.StockKeepingUnit

3. Type Safety Issues

// Current: String lookups - no type checking
pickProductString(product, fieldConfig.product.sku)  // runtime error if field missing

// Should be: Direct access - compile-time type checking
product.StockKeepingUnit  // compile error if field missing

4. Maintenance Burden

  • Three places to update when adding a field:

    1. Raw type schema
    2. Field config interface
    3. Field config service
    4. Environment variables
  • Should be ONE place:

    1. Raw type schema only

5. Inconsistency Across Domains

  • Catalog: No field config Simple
  • Billing: No field config Simple
  • Orders: Field config Complex
  • New developers: Confused why orders is different

Correct Solution: Remove Field Config System

Step 1: Update Mappers to Use Raw Types Directly

Before:

export function transformSalesforceOrderItem(
  record: SalesforceOrderItemRecord,
  fieldConfig: SalesforceOrdersFieldConfig  // ← Remove this
): OrderItemDetails {
  return {
    sku: pickProductString(product, fieldConfig.product.sku),  // ← Indirect
    billingCycle: pickOrderItemString(record, "billingCycle", fieldConfig),
  };
}

After:

export function transformSalesforceOrderItem(
  record: SalesforceOrderItemRecord
  // No fieldConfig needed!
): OrderItemDetails {
  return {
    sku: product.StockKeepingUnit ?? undefined,         // ← Direct
    billingCycle: record.Billing_Cycle__c ?? undefined,  // ← Direct
  };
}

Benefits:

  • No extra parameters
  • No runtime reflection
  • Type-safe compile-time checks
  • Faster performance
  • Easier to understand

Step 2: Update Query Builders to Use Raw Field Names

Before:

export function buildOrderSelectFields(
  fieldConfig: SalesforceOrdersFieldConfig,
  additional: string[] = []
): string[] {
  const fields = [
    "Id",
    fieldConfig.order.activationType,
    fieldConfig.order.internetPlanTier,
  ];
  return fields;
}

After:

export function buildOrderSelectFields(
  additional: string[] = []
): string[] {
  const fields = [
    "Id",
    "Activation_Type__c",
    "Internet_Plan_Tier__c",
  ];
  return fields;
}

Benefits:

  • No dependencies on field config
  • Clear what fields are queried
  • Matches raw type field names exactly
  • Easier to maintain

Step 3: Update Order Builder to Use Raw Field Names

Before:

async buildOrderFields(body: OrderBusinessValidation): Promise<Record<string, unknown>> {
  const fieldMap = this.fieldMapService.getFieldMap();
  return {
    AccountId: userMapping.sfAccountId,
    [orderField("orderType", fieldMap)]: body.orderType,  // ← Indirect
    [orderField("activationType", fieldMap)]: body.configurations?.activationType,
  };
}

After:

async buildOrderFields(body: OrderBusinessValidation): Promise<Record<string, unknown>> {
  return {
    AccountId: userMapping.sfAccountId,
    Type: body.orderType,                              // ← Direct
    Activation_Type__c: body.configurations?.activationType,
  };
}

Benefits:

  • No field config service dependency
  • Clear what Salesforce fields are set
  • Type-safe (can create typed interface for order creation)

Step 4: Delete Unnecessary Code

Delete these files:

  • apps/bff/src/integrations/salesforce/services/salesforce-field-config.service.ts
  • All field config interfaces from packages/domain/orders/contract.ts
  • Environment variables for field names (120+ lines in .env)

Update these:

  • Remove fieldConfig parameters from all mapper functions
  • Remove SalesforceFieldMapService from module providers
  • Remove query builder exports from domain (or simplify them)

📊 Impact Analysis

Lines of Code Removed: ~500+

  • Field config interfaces: ~100 lines
  • Field config service: ~150 lines
  • Helper functions (orderField, mnpField, etc.): ~50 lines
  • Environment variable handling: ~120 lines
  • Updated mapper functions: ~100 lines simpler

Dependencies Removed:

  • SalesforceFieldMapService (entire service)
  • Field config interfaces (all of them)
  • ConfigService dependency in mappers
  • Environment variables for field names

Performance Improvement:

  • No runtime Reflect.get() calls
  • No field config object construction
  • Direct property access is JIT-optimized
  • Estimated: 20-30% faster mapping

Developer Experience:

  • One place to define fields (raw types)
  • IDE autocomplete works perfectly
  • Compile-time type checking
  • Consistent with other domains
  • New developers: "Oh, it's just like catalog!"

🎯 Migration Plan

Phase 1: Prepare (Low Risk)

  1. Document all current field mappings
  2. Create mapping table: logical name → actual field name
  3. Write tests for current behavior

Phase 2: Update Mappers (Medium Risk)

  1. Update transformSalesforceOrderItem to use direct access
  2. Update transformSalesforceOrderDetails to use direct access
  3. Remove fieldConfig parameters
  4. Run tests to verify behavior unchanged

Phase 3: Update Query Builders (Medium Risk)

  1. Update buildOrderSelectFields to use actual field names
  2. Update buildOrderItemSelectFields to use actual field names
  3. Remove fieldConfig parameters
  4. Test SOQL queries still work

Phase 4: Update Order Builder (Medium Risk)

  1. Replace dynamic field assignments with direct assignments
  2. Remove SalesforceFieldMapService dependency
  3. Test order creation still works

Phase 5: Cleanup (Low Risk)

  1. Delete SalesforceFieldMapService
  2. Delete field config interfaces
  3. Remove from module providers
  4. Remove environment variables
  5. Update documentation

Estimated Time: 1-2 days

Risk Level: Medium (but well-tested migration)

Value: High (simpler, faster, more maintainable)


🎓 Key Lessons

Lesson 1: YAGNI (You Aren't Gonna Need It)

The field config system was built for "flexibility" that was never actually needed. The raw types already define the contract with Salesforce.

Lesson 2: Consistency Matters

Having orders work differently from catalog/billing creates confusion and makes the codebase harder to understand.

Lesson 3: Indirection Has Costs

Every layer of indirection has costs:

  • Performance cost (runtime lookups)
  • Cognitive cost (harder to understand)
  • Maintenance cost (more code to update)

Only add indirection when the benefit is clear and measurable.

Lesson 4: Look at What Already Works

Catalog and billing domains work perfectly without field config. That's a strong signal that orders doesn't need it either.


Recommendation

Delete the entire field config system and make orders domain work like catalog/billing domains.

The raw types already define the Salesforce schema. Use them directly. No need for an extra mapping layer.

This will make the codebase:

  • Simpler - 500+ fewer lines
  • Faster - No runtime reflection
  • Safer - Compile-time type checking
  • Consistent - Matches other domains
  • Maintainable - One place to define fields

The field config system is pure technical debt with no actual benefits.