# API Property Name Guessing Audit > **Created:** 2026-02-03 > **Status:** Needs Resolution > **Priority:** Medium-High ## Overview This document catalogs all instances where the codebase "guesses" which property name an external API will return. These patterns indicate **missing type safety** at system boundaries and were introduced because the exact API response structure was unknown during implementation. --- ## Problem Statement The codebase contains 30+ instances of fallback property access patterns like: ```typescript // We don't know if the API returns "voicemail" or "voiceMail" const value = account.voicemail ?? account.voiceMail; ``` This creates: - **Runtime uncertainty** - code works but we don't know which branch executes - **Dead code potential** - one fallback may never be used - **Maintenance burden** - developers must remember to check both variants - **Type system weakness** - TypeScript can't help us catch mismatches --- ## Complete Inventory ### 1. Freebit Integration (15 instances) The Freebit API returns properties with inconsistent casing. Both variants are defined in types because we observed both in production. #### Voice Options (camelCase vs lowercase) | Location | Pattern | Fields | | ------------------------------------------- | -------------------------------------------- | --------------------- | | `freebit-mapper.service.ts:134` | `account.voicemail ?? account.voiceMail` | Voice mail setting | | `freebit-mapper.service.ts:136` | `account.callwaiting ?? account.callWaiting` | Call waiting setting | | `freebit-mapper.service.ts:140` | `account.worldwing ?? account.worldWing` | International roaming | | `domain/sim/providers/freebit/mapper.ts:89` | `account.voicemail ?? account.voiceMail` | Duplicate | | `domain/sim/providers/freebit/mapper.ts:90` | `account.callwaiting ?? account.callWaiting` | Duplicate | | `domain/sim/providers/freebit/mapper.ts:91` | `account.worldwing ?? account.worldWing` | Duplicate | #### Account Status/Identity | Location | Pattern | Purpose | | ------------------------------- | ----------------------------------------------------- | ------------------ | | `freebit-mapper.service.ts:103` | `account.networkType ?? account.contractLine ?? "4G"` | Network generation | | `freebit-mapper.service.ts:189` | `account.state ?? account.status ?? "pending"` | Account status | | `freebit-mapper.service.ts:195` | `account.msisdn ?? account.account ?? ""` | Phone number | #### Type Definitions Allow Both (Root Cause) **File:** `apps/bff/src/integrations/freebit/interfaces/freebit.types.ts` ```typescript // Lines 31-32: Status field ambiguity state: "active" | "suspended" | "temporary" | "waiting" | "obsolete"; status?: "active" | "suspended" | "temporary" | "waiting" | "obsolete"; // Lines 42-43: SIM size field ambiguity size?: "standard" | "nano" | "micro" | "esim"; simSize?: "standard" | "nano" | "micro" | "esim"; // Lines 52-57: Voice options casing ambiguity voicemail?: "10" | "20" | number | null; voiceMail?: "10" | "20" | number | null; callwaiting?: "10" | "20" | number | null; callWaiting?: "10" | "20" | number | null; worldwing?: "10" | "20" | number | null; worldWing?: "10" | "20" | number | null; ``` --- ### 2. MNP Data Mapping (10 instances) Salesforce stores MNP (Mobile Number Portability) data with different field names than Freebit expects. **File:** `apps/bff/src/modules/orders/services/sim-fulfillment.service.ts` | Line | Salesforce Name | Freebit Name | Purpose | | ---- | -------------------------- | ------------------- | ---------------------- | | 449 | `mnpNumber` | `reserveNumber` | MNP reservation number | | 450 | `mnpExpiry` | `reserveExpireDate` | Reservation expiry | | 451 | `mvnoAccountNumber` | `account` | MVNO account ID | | 452 | `portingFirstName` | `firstnameKanji` | First name (kanji) | | 453 | `portingLastName` | `lastnameKanji` | Last name (kanji) | | 455 | `portingFirstNameKatakana` | `firstnameZenKana` | First name (katakana) | | 458 | `portingLastNameKatakana` | `lastnameZenKana` | Last name (katakana) | | 460 | `portingGender` | `gender` | Gender | | 461 | `portingDateOfBirth` | `birthday` | Date of birth | | 444 | `source["isMnp"]` | `config["isMnp"]` | MNP flag location | --- ### 3. WHMCS Integration (4 instances) Different WHMCS API endpoints return slightly different field names. #### Billing Mapper **File:** `packages/domain/billing/providers/whmcs/mapper.ts` | Line | Pattern | Cause | | ---- | ----------------------------------------------- | ----------------------- | | 80 | `invoicePayload.invoiceid ?? invoicePayload.id` | List vs detail endpoint | | 120 | `listItem.invoiceid ?? listItem.id` | Same issue | #### Customer Mapper **File:** `packages/domain/customer/providers/whmcs/mapper.ts` | Line | Pattern | Cause | | ---- | --------------------------------------------------- | ------------------------- | | 54 | `client.fullstate ?? client.state` | Full name vs abbreviation | | 58 | `client.phonenumberformatted ?? client.phonenumber` | Formatted vs raw | --- ### 4. Order/Product Data (1 instance) **File:** `packages/domain/orders/helpers.ts` | Line | Pattern | Cause | | ---- | ---------------------------------------- | ---------------------- | | 362 | `item.totalPrice ?? item.unitPrice ?? 0` | Different price fields | --- ## Summary Statistics | Source System | Guessing Patterns | Severity | | -------------------- | ----------------- | -------- | | Freebit | 15 | High | | Salesforce ↔ Freebit | 10 | Medium | | WHMCS | 4 | Low | | Internal | 1 | Low | | **Total** | **30** | | --- ## Recommended Actions ### Phase 1: Normalization Layer Create a preprocessing step that normalizes field names at the API boundary: ```typescript // New file: freebit-response-normalizer.ts const FIELD_ALIASES: Record = { voiceMail: "voicemail", callWaiting: "callwaiting", worldWing: "worldwing", status: "state", simSize: "size", }; export function normalizeFreebitResponse(raw: T): T { // Recursively rename aliased fields to canonical names return transformKeys(raw, FIELD_ALIASES); } ``` Apply in `freebit-client.service.ts` immediately after receiving API response. ### Phase 2: Type Strictness Update raw types to allow only ONE canonical name: ```typescript // Before (allows both) export interface FreebitAccountDetail { voicemail?: "10" | "20" | number | null; voiceMail?: "10" | "20" | number | null; // DELETE THIS } // After (single source of truth) export interface FreebitAccountDetail { voicemail?: "10" | "20" | number | null; // Canonical name only } ``` ### Phase 3: Cleanup Remove all fallback patterns from mappers: ```typescript // Before const voiceMailEnabled = parseFlag(account.voicemail ?? account.voiceMail); // After (normalizer already handled this) const voiceMailEnabled = parseFlag(account.voicemail); ``` ### Phase 4: MNP Field Standardization For MNP data, choose ONE canonical naming convention and update Salesforce field mappings: | Canonical Name | Use This Everywhere | | ------------------- | ------------------------------ | | `reserveNumber` | Not `mnpNumber` | | `reserveExpireDate` | Not `mnpExpiry` | | `firstnameKanji` | Not `portingFirstName` | | `lastnameKanji` | Not `portingLastName` | | `firstnameZenKana` | Not `portingFirstNameKatakana` | | `lastnameZenKana` | Not `portingLastNameKatakana` | | `gender` | Not `portingGender` | | `birthday` | Not `portingDateOfBirth` | Map explicitly in the Salesforce mapper, not with fallbacks in sim-fulfillment. --- ## File Checklist Files requiring updates after resolution: ### Freebit - [ ] `apps/bff/src/integrations/freebit/interfaces/freebit.types.ts` - [ ] `apps/bff/src/integrations/freebit/services/freebit-mapper.service.ts` - [ ] `packages/domain/sim/providers/freebit/raw.types.ts` - [ ] `packages/domain/sim/providers/freebit/mapper.ts` ### WHMCS - [ ] `packages/domain/billing/providers/whmcs/mapper.ts` - [ ] `packages/domain/billing/providers/whmcs/raw.types.ts` - [ ] `packages/domain/customer/providers/whmcs/mapper.ts` - [ ] `packages/domain/customer/providers/whmcs/raw.types.ts` ### Orders - [ ] `apps/bff/src/modules/orders/services/sim-fulfillment.service.ts` - [ ] `packages/domain/orders/helpers.ts` --- ## Success Criteria 1. **Zero fallback patterns** - No `a ?? b` for same conceptual field 2. **Strict raw types** - Each field has exactly one name 3. **Normalization at edge** - Field name mapping happens once, at API boundary 4. **Type safety** - TypeScript catches field name mismatches at compile time