Assist_Design/docs/refactoring/api-property-guessing-audit.md

9.4 KiB

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:

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

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

Phase 1: Normalization Layer

Create a preprocessing step that normalizes field names at the API boundary:

// New file: freebit-response-normalizer.ts
const FIELD_ALIASES: Record<string, string> = {
  voiceMail: "voicemail",
  callWaiting: "callwaiting",
  worldWing: "worldwing",
  status: "state",
  simSize: "size",
};

export function normalizeFreebitResponse<T>(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:

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

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