# T513 — radar-watch-api-crud-quota-gate

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

## Goal

完成後系統應：watch CRUD／pause／resume／archive API 可用，且**未建服務檔案不得建 active watch**、**超過方案 `max_active_watches` 明確拒絕**。

## Depends on

- T511、T512

## Inputs

- Spec：`../spec.md` §3.1、§4.9、§5.2、§5.6、§9.1（SP-01、RW-01、RW-02、RW-04）
- 既有方案／配額讀取：`apps/backend/internal/module/usage/**`、`billing` plan 定義

## Outputs

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

| 路徑 | 動作 | 說明 |
|------|------|------|
| `apps/backend/generate/api/radar.api` | edit | watches req／resp type 定稿 |
| `apps/backend/internal/logic/radar/*Watch*Logic.go` | edit | list／create／get／update／delete(archive)／purge(archived only)／pause／resume |
| `apps/backend/internal/module/radar/usecase/watch_quota.go` | add | 方案上限查詢與判斷（Free 1／Starter 5／Pro 20） |
| `apps/backend/internal/module/billing/**` 或方案常數檔 | edit | 新增 `max_active_watches`、`max_daily_opportunities` 欄位（**不改價格、不動 `soft_caps`**） |

### 行為變更

- 建立或 resume 成 `active` 前檢查兩道閘：服務檔案存在、active 數 < 上限。
- 兩種拒絕皆為明確錯誤：前者 message 指向服務檔案；後者 message 含「目前上限 N」與升級提示。
- 既有超額者不強制降級（只擋新增）。
- `DELETE` 語意為封存（軟刪），非實刪。
- `DELETE /watches/:id/purge` 僅接受 `archived`，永久移除 watch 設定；既有 sweeps、opportunities、回覆與統計不刪。

### API / 契約

- `GET /watches` 支援 `page`／`pageSize`、`status` filter，回 `pagination` + `list`。

## Out of scope

- 手動觸發 sweep（T533）
- 關鍵字建議（T514）

## Acceptance

- [x] SP-01：無服務檔案建 active watch → 明確錯誤且指向服務檔案
- [x] RW-01：Free 已有 1 個 active，再建 → 明確錯誤含上限與升級提示
- [x] RW-02／RW-04：pause 後不在 active 清單；archive 後不可 resume
- [x] 指令：

```bash
cd apps/backend && make gen-api && go test ./internal/logic/radar/... ./internal/module/radar/... -count=1
```

## Notes

配額欄位加在既有方案定義上，**不要**新增第五個 usage meter（RG-04）。

## Notes（2026-07-31）

| 路徑 | 動作 |
|------|------|
| `internal/module/usage/domain/usage.go` | edit（`PlanDef` 加 `MaxActiveWatches`／`MaxDailyOpportunities`；Free 1/5、Starter 5/30、Pro 20/100） |
| `internal/module/usage/usecase/service.go` | edit（新增 `PlanFor`） |
| `internal/module/radar/usecase/watch_quota.go` | add（`PlanQuota` 介面、`FixedQuota`、`assertCanActivate`） |
| `internal/logic/radar/{list,create,get,update,archive,pause,resume}_*_logic.go` | edit |
| `internal/svc/service_context.go` | edit（`radarQuotaBridge` 橋接既有方案） |
| `internal/logic/radar/watch_logic_test.go` | add（8 則） |

- **價格與 `soft_caps` 未動**，也沒有新增第五個 meter：兩個新欄位只是方案的規模上限，AI 判定與回覆仍計既有四個 meter。
- **配額介面留在 radar module，實作橋接在 `internal/svc`。** radar 不反向依賴 usage module。
- **沒接上 `PlanQuota` 時退回最低方案（1／5），不是無上限。** 漏接線的後果應該是使用者看到上限提示來問，而不是所有人都拿到 Pro 的常駐監控量。
- **`resume` 會重驗兩道閘。** 暫停期間方案可能已降級，不重驗就等於「暫停再恢復」是繞過上限的後門。測試明確蓋住這條路。
- **既有超額訂閱不強制降級**（spec §3.1）：降級後只擋新增，已經在跑的照跑。
- **`DELETE` 是軟刪**：archive 後列表仍看得到、`active_count` 不含它，歷史商機與統計不動。
- **`purge` 與 archive 分離**：使用者確認後才刪除 archived 設定，避免把「停止巡邏」誤當成「刪除歷史」。
- `GET /watches` 額外回 `active_count`／`max_active`／`profile_exists`，讓雷達頁在使用者按下建立之前就能顯示引導或升級提示，而不是等 POST 被拒才說。
