# T510 — service-profile-model-repository

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

## Goal

完成後系統應：`radar` module 有 ServiceProfile domain 與 repository（mongo／memory），每會員一份可讀寫。

## Depends on

- T501

## Inputs

- Spec：`../spec.md` §2、§4.1、§8
- 既有 module 模式：`apps/backend/internal/module/scout/{domain,repository,usecase}`

## Outputs

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

| 路徑 | 動作 | 說明 |
|------|------|------|
| `apps/backend/internal/module/radar/domain/service_profile.go` | add | struct＋驗證：`services[]`（項目＋價格區間）、`cases[]`、`forbidden[]`、`faq[]`、`service_areas[]`、`remote_ok`、`availability`、`tone_note` |
| `apps/backend/internal/module/radar/repository/service_profile_mongo.go` | add | `radar_service_profiles`，`_id = owner_uid` upsert |
| `apps/backend/internal/module/radar/repository/service_profile_memory.go` | add | 測試用 |
| `apps/backend/internal/module/radar/usecase/service_profile.go` | add | Get／Upsert usecase |

### 行為變更

- `Upsert` 為整份覆寫（每會員一份），`updated_at` 為 ns UTC。
- `Get` 未建立時回「不存在」語意，供上層判斷 SP-01 前置條件。
- 驗證：價格區間下限 ≤ 上限；`service_areas[]` 為縣市代碼白名單；`forbidden[]` 去空白去重。

## Out of scope

- HTTP logic（T511）
- 判定與回覆如何使用本檔（T529／T549）

## Acceptance

- [x] unit test：upsert → get 欄位完整、`forbidden[]` 可讀、價格區間反向被拒
- [x] 指令：

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

## Notes

縣市代碼白名單放 `domain` 常數，不猜測、不做模糊比對（OP-04 行為靠這個約束）。

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

| 路徑 | 動作 |
|------|------|
| `internal/module/radar/domain/service_profile.go` | add（含 22 個縣市代碼白名單、`Normalize`、`MatchesRegion`） |
| `internal/module/radar/domain/repository.go` | add（`Repository` 介面） |
| `internal/module/radar/repository/service_profile_{mongo,memory}.go` | add |
| `internal/module/radar/usecase/service_profile.go` | add（Get／Has／Upsert） |
| `internal/module/radar/usecase/service_profile_test.go` | add（9 則） |

### 幾個刻意的決定

- **`Get` 未建檔回 `ErrNotFound`，不回零值檔案。** 「沒建檔」與「建了一份空的」必須分得出來，SP-01 的訂閱閘門就是靠這個差別；HTTP 層才把它翻成 `exists=false` 的 200。
- **價格只在兩端都填時比大小。** 只填 `price_min` 是「起價」、只填 `price_max` 是「上限」，都是真實的報價方式，硬要成對會逼使用者亂填。填了價格但沒填幣別時補 `TWD`。
- **`service_areas` 空且 `remote_ok=false` 直接拒絕。** 兩者皆空時，五問判定的地區這一問沒有任何依據可用。
- **`Upsert` 是整份覆寫，但保留 `created_at`。** 不做欄位合併，否則使用者永遠刪不掉一個服務項目或禁語。
