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 |
Recommended Actions
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.tsapps/bff/src/integrations/freebit/services/freebit-mapper.service.tspackages/domain/sim/providers/freebit/raw.types.tspackages/domain/sim/providers/freebit/mapper.ts
WHMCS
packages/domain/billing/providers/whmcs/mapper.tspackages/domain/billing/providers/whmcs/raw.types.tspackages/domain/customer/providers/whmcs/mapper.tspackages/domain/customer/providers/whmcs/raw.types.ts
Orders
apps/bff/src/modules/orders/services/sim-fulfillment.service.tspackages/domain/orders/helpers.ts
Success Criteria
- Zero fallback patterns - No
a ?? bfor same conceptual field - Strict raw types - Each field has exactly one name
- Normalization at edge - Field name mapping happens once, at API boundary
- Type safety - TypeScript catches field name mismatches at compile time