250 lines
9.4 KiB
Markdown
250 lines
9.4 KiB
Markdown
|
|
# 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<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:
|
||
|
|
|
||
|
|
```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
|