# Notification Service

# Notification Service — SRS/SDD Document

| Version | Version Date | Editor | Memo |
|---------|--------------|--------|------|
| 1.0.0 | 2026/05/21 | Gateway Team | 初版：Gateway Notification 模組 SDD |

---

## 1. Introduction

### 1.1 Purpose

Notification 模組為 TianTing Gateway 的**統一對外通知入口**，支援 Email 與 SMS 的同步寄送、異步佇列、冪等、租戶配額、指數退避重試與 DLQ（Dead Letter Queue）管理。

### 1.2 Scope

**範圍內：**

- 同步 `Send`：渲染模板後立即送達
- 異步 `Enqueue`：Mongo 寫入 pending + Redis ZSET 排程 + RetryWorker 背景投遞
- 冪等（Redis cache + Mongo unique index）
- 租戶每日 Email/SMS 配額
- 模板渲染（embed HTML / SMS / Subject，多語系）
- Provider chain（SMTP、SES、Mitake、Mock）
- Admin DLQ 查詢與手動重試

**範圍外：**

- Push / Webhook（enum 已定義，尚未實作）
- 專用 HTTP API（目前僅內部 logic 呼叫；擴充路徑見 README）

### 1.3 Definitions, Acronyms, and Abbreviation

| 縮寫 | 說明 |
|------|------|
| **NotifyKind** | 通知類型（verify_email、tenant_welcome 等） |
| **Channel** | 通道：email / sms / push / webhook |
| **DLQ** | Dead Letter Queue，超過 MaxRetry 的失敗通知 |
| **IdempotencyKey** | 冪等鍵，同 tenant+kind+key 不重複寄送 |
| **TargetHash** | SHA-256(target)，Mongo 不存明文 email/phone |
| **RetryJob** | Redis ZSET 成員，含 target（僅 Redis，不落 Mongo） |

### 1.4 Technologies to be used

| 項目 | 技術 |
|------|------|
| Application Language | Go 1.22+ |
| Framework | go-zero |
| Database | MongoDB（notifications、notification_dlq） |
| Cache / Queue | Redis（冪等、配額、retry ZSET） |
| Templates | go:embed + Go text/html template |
| Email Providers | SMTP、AWS SES、Mock |
| SMS Providers | Mitake（三竹）、Mock |

### 1.5 Overview

Notification 為 library-style domain package，嵌入 Gateway process 內執行：

1. **Send**：Caller → 冪等檢查 → 配額 → Insert pending → Render → Provider chain → Update sent/failed
2. **Enqueue**：同上至 Insert → ZADD Redis → 回傳 pending；RetryWorker 背景 ClaimDue → 投遞
3. **DLQ**：attempts ≥ MaxRetry → status=dropped + 寫入 notification_dlq

---

## 2. System Overview

Notification 模組解耦業務邏輯與實際寄送：

- Auth 註冊 OTP、Member 業務驗證等 logic 層呼叫 `Notifier.Send`
- 歡迎信等可容忍延遲的通知可走 `Enqueue` + Worker
- 隱私設計：Mongo / DLQ 只存 `target_hash`，明文 target 僅在 Redis RetryJob 生命週期內存在

Worker 由 `ServiceContext.StartWorkers` 啟動，與 Gateway 同 process 運行。

---

## 3. System Architecture

### 3.1 System Architecture

```mermaid
flowchart TB
    subgraph Callers["Internal Callers"]
        AuthLogic[logic/auth]
        MemberLogic[logic/member]
    end

    subgraph NotificationModule["internal/model/notification"]
        Notifier[NotifierUseCase]
        Admin[AdminNotifierUseCase]
        Worker[RetryWorker]
        Factory[Provider Factory]
        Renderer[Template Renderer]
    end

    subgraph Providers["Providers"]
        EmailChain[email.Chain<br/>SMTP / SES / Mock]
        SMSChain[sms.Chain<br/>Mitake / Mock]
    end

    subgraph Storage["Storage"]
        Mongo[(MongoDB)]
        Redis[(Redis)]
    end

    AuthLogic --> Notifier
    MemberLogic --> Notifier
    Notifier --> Renderer
    Notifier --> Factory
    Notifier --> Mongo
    Notifier --> Redis
    Factory --> EmailChain
    Factory --> SMSChain
    Worker --> Redis
    Worker --> Mongo
    Worker --> EmailChain
    Worker --> SMSChain
    Admin --> Mongo
    Admin --> Redis
```

### 3.2 Decomposition Description

| 套件 | 職責 |
|------|------|
| `config/` | Async、Rate、Email、SMS 設定 |
| `domain/entity/` | Notification、NotificationDLQ |
| `domain/enum/` | Channel、NotifyKind、NotifyStatus、Severity |
| `domain/repository/` | Notification、DLQ、Store、RetryQueue 介面 |
| `domain/usecase/` | Notifier、Admin 介面 + DTO |
| `domain/template/` | 模板 Spec、Renderer、Registry 介面 |
| `repository/` | Mongo + Redis 實作 |
| `usecase/` | Notifier、Admin、RetryWorker、factory、module |
| `provider/email/` | SMTP、SES、Mock sender + chain |
| `provider/sms/` | Mitake、Mock sender + chain |
| `template/` | embed 模板 + render |

### 3.3 Send（同步）

```mermaid
sequenceDiagram
    participant C as Caller
    participant N as NotifierUseCase
    participant R as Redis
    participant M as Mongo
    participant P as Provider

    C->>N: Send(SendRequest)
    N->>R: Get idempotency
    alt cache hit
        N-->>C: cached DTO
    else miss
        N->>M: FindByIdempotency / Insert pending
        N->>R: Incr quota
        N->>N: Render template
        N->>P: Email/SMS chain.Send
        N->>M: UpdateDelivery (sent/failed)
        N->>R: Set idempotency cache
        N-->>C: NotificationDTO
    end
```

### 3.4 Enqueue + RetryWorker（異步）

```mermaid
sequenceDiagram
    participant C as Caller
    participant N as NotifierUseCase
    participant M as Mongo
    participant R as Redis
    participant W as RetryWorker
    participant P as Provider

    C->>N: Enqueue(SendRequest)
    N->>M: Insert (pending)
    N->>R: ZADD RetryJob (score=now)
    N-->>C: pending DTO

    loop every 1s
        W->>R: ClaimDue (ZREM)
        W->>M: FindByID
        W->>P: Render + deliver
        alt success
            W->>M: status=sent
        else attempts < MaxRetry
            W->>M: status=retrying
            W->>R: ZADD (score=now+backoff)
        else
            W->>M: status=dropped
            W->>M: Insert DLQ
        end
    end
```

**退避序列（預設）：** `[1, 5, 30, 300, 1800]` 秒

---

## 4. Data Design

### 4.1 Data Dictionary

#### notifications（MongoDB）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| tenant_id | String | 租戶 ID | Unique(tenant_id, kind, idempotency_key) |
| uid | String | 會員 UID | (tenant_id, uid, occurred_at desc) |
| channel | String | email / sms | — |
| kind | String | NotifyKind | Unique(tenant_id, kind, idempotency_key) |
| target_hash | String | SHA-256(target) | — |
| template_key | String | 模板 key | — |
| locale | String | 語系 | — |
| body | String | 渲染後內容（OTP 類可省略） | — |
| provider | String | 實際使用的 provider | — |
| provider_message_id | String | 外部 message ID | — |
| status | String | pending/sent/failed/retrying/dropped | (status, attempts, occurred_at) |
| attempts | Int | 投遞嘗試次數 | — |
| last_error | String | 最後錯誤 | — |
| idempotency_key | String | 冪等鍵 | Unique(tenant_id, kind, idempotency_key) |
| severity | String | info/warn/critical | — |
| occurred_at | Int64 | 事件時間 ms | — |
| delivered_at | Int64 | 送達時間 ms | — |
| create_at | Int64 | 建立時間 ms | — |
| update_at | Int64 | 更新時間 ms | — |

#### notification_dlq（MongoDB）

| Field | Type | Comment | Index |
|-------|------|---------|-------|
| _id | ObjectId | PK | PK |
| notification_id | String | 原 notification ID | — |
| tenant_id | String | 租戶 ID | — |
| uid | String | 會員 UID | — |
| channel | String | 通道 | — |
| kind | String | 通知類型 | — |
| target_hash | String | target hash | — |
| last_error | String | 最後錯誤 | — |
| attempts | Int | 總嘗試次數 | — |
| payload | Object | 重試 metadata（locale、data 等，不含 target） | — |
| occurred_at | Int64 | 事件時間 ms | — |
| create_at | Int64 | 建立時間 ms | — |

#### Redis Keys

| Key Pattern | 用途 | TTL |
|-------------|------|-----|
| notif:idem:{tenant}:{kind}:{key} | 冪等 cache | 24h |
| notif:quota:{tenant}:{channel}:{yyyyMMdd} | 每日配額計數 | 25h |
| notif:retry:zset（可配置） | RetryJob ZSET（score=run_at_ms） | — |

#### NotifyKind 一覽

| Kind | 通道 | 說明 |
|------|------|------|
| verify_email | email | 業務 email OTP |
| verify_phone | sms | 業務 phone OTP |
| verify_registration_email | email | 註冊 email OTP |
| step_up_email | email | Email step-up |
| step_up_phone | sms | Phone step-up |
| account_suspended | email | 帳號停權通知 |
| tenant_welcome | email | 租戶歡迎信 |

---

## 5. API Design

**現況：** 尚無專用 HTTP API。Notification 由內部 logic 呼叫：

| 呼叫方 | 方法 | 用途 |
|--------|------|------|
| `logic/auth/register_logic.go` | `Notifier.Send` | 註冊 email OTP |
| `logic/member/verify_helper.go` | `Notifier.Send` | 業務 email/phone OTP |
| `cmd/notify-test` | Send / Enqueue / Admin | 本機測試 CLI |

**擴充 HTTP API 路徑：**

1. 在 `generate/api/` 定義路由
2. `make gen-api`
3. `internal/logic` 映射 types ↔ DTO

---

## 6. Resource

| 資源 | 路徑 |
|------|------|
| 模組原始碼 | `internal/model/notification/` |
| 開發 README | `internal/model/notification/README.md` |
| 測試指南 | `docs/notification-testing.md` |
| 測試 CLI | `cmd/notify-test`（`make notify-test`） |
| Worker | `internal/worker/notification_retry/` |
| 設定範例 | `etc/gateway.dev.example.yaml` → `Notification` 區塊 |
| 設計參考 | `docs/identity-member-design.md` §11 |
