# 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
