# T502 — fe-domain-types-repo-interfaces

> Status: `done`（2026-07-31）  
> Milestone: `M0`  
> Kind: `feat`  
> Est. change: `~180 lines`

## Goal

完成後系統應：前端有 Radar／CRM 型別與 `radar`、`crm` repository 介面，mock 與 live 兩實作同介面，live 先回未就緒錯誤。

## Depends on

- T500

## Inputs

- Spec：`../spec.md` §5.2、§5.3、§8
- 既有：`apps/web/src/domain/types.ts`、`src/data/repos.ts`、`src/data/live/repos.ts`、`src/data/mock/`
- `AGENTS.md`：mock | live 同一 repository 介面

## Outputs

### 程式變更（預期路徑）

| 路徑 | 動作 | 說明 |
|------|------|------|
| `apps/web/src/domain/types.ts` | edit | `ServiceProfile`／`RadarWatch`／`RadarSweep`／`Opportunity`／`ReplyVariant`／`Contact`／`ContactTouch`／`FollowUp` 與 enum |
| `apps/web/src/data/repos.ts` | edit | 新增 `radar`、`crm` repository 介面 |
| `apps/web/src/data/live/radarRepos.ts` | add | live 實作分檔，先 throw 未就緒錯誤 |
| `apps/web/src/data/createRepos.ts` | edit | 掛上兩個新 repo |
| `apps/web/src/data/mock/` | edit | radar／crm mock 種子與實作 |

### 行為變更

- `useRepos().radar` / `.crm` 在 mock 模式可回假資料；live 模式呼叫未實作端點時拋明確錯誤，不吞掉。

### API / 契約

- 型別欄位與 spec §8 一致；時間欄位型別為 `number`（ns），畫面換算在頁面層。
- `intent_band`：`high | mid | low`；`stage`：`new_found | engaged | dm_sent | replied | quoted | won | lost`；`needs_follow_up` 為獨立 boolean。

## Out of scope

- 任何頁面 UI（T515／T516／T551 起）
- live HTTP 實作細節（各 M 的 FE task 補上）

## Acceptance

- [x] `tsc` 無錯；~~mock 模式可讀到 radar／crm 假資料~~ → 本 repo 已無業務 mock 層，改以單元測試覆蓋 live 對應（見 Notes）
- [x] 全新型別中**無** `lead` 指稱銷售線索
- [x] 指令：

```bash
cd apps/web && npm run build && npm run test && npm run lint
```

## Notes

`repos.ts` 已 649 行，新介面請分區塊註記，live 實作一律進 `radarRepos.ts` 不再灌 `live/repos.ts`。

### 實作結果（2026-07-31）

| 路徑 | 動作 | 說明 |
|------|------|------|
| `apps/web/src/domain/types.ts` | edit | +287 行；spec §8 全部 collection 的前端型別 |
| `apps/web/src/data/repos.ts` | edit | `RadarRepo`（21 方法）／`CrmRepo`（15 方法）＋掛進 `Repos` |
| `apps/web/src/data/live/radarRepos.ts` | add | `createLiveRadarRepo` / `createLiveCrmRepo`，含 mapper |
| `apps/web/src/data/live/repos.ts` | edit | `radar` / `crm` 兩行組裝 |
| `apps/web/src/lib/apiErrors.ts` | edit | `501010` / `400100` 進 code→i18n 表 |
| `apps/web/src/lib/i18n/messages.ts` | edit | zh-TW／en 各兩條文案 |
| `apps/web/src/data/live/radarRepos.test.ts` | add | 4 則測試 |

### 與 task 描述的兩處偏離

1. **沒有 mock 實作。** `src/data/mock/store.ts` 開頭已寫明「業務 mock seed / HarborStore 已移除」，`createRepos()` 也是無條件 `createLiveRepos()`，沒有 mock | live 切換點。硬造一份 mock 等於復活已被移除的層，所以只做 live，另外用 `radarRepos.test.ts` 補住「介面行為可驗證」這件事。`createRepos.ts` 因此未改（live 組裝在 `live/repos.ts`）。
2. **live 不自行 throw「未就緒」。** task 寫「先 throw 未就緒錯誤」，但 T500 已讓後端對未實作能力回 501010，`apiRequest` 遇非 102000 就丟 `ApiError`。前端再寫死一層 throw 只會讓 M1 每實作一支就要回頭刪一行，而且會蓋掉後端真正的錯誤碼。所以 live 直接打真 endpoint，未就緒行為由後端決定，測試裡明確驗證 501010 會冒出來、不會被吞成空清單。

### 契約細節

- `ServiceProfile.exists`：後端沒給就是 `false`。T513 的「無服務檔案不得建 active 訂閱」閘門靠這個欄位，所以缺欄位不能讀成「檔案已就緒」。
- `CrmRepo.listContacts` 的 `follow_up` 只在明確給 `true`／`false` 時才進 query，`undefined` 代表不篩選（避免把「不篩」送成 `follow_up=false`）。
- 時間欄位一律 `number`（unix ns）；`optNum` 把 0 視為未設定，對齊後端 `optional` int64 的零值語意。
