Project

General

Profile

CR #201 » SCS-016_通知服務程式設計規格.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:39 AM

 

SCS-016 通知服務(notification-service)程式設計規格

項目 內容
文件編號 SCS-016
版本 1.8(2026-10-02,#A-153 併辦:新增 §7.13.1「官方 API 文件(廠商來信)之逐項核對」——BEAM PMO 轉寄 SureNotify 官方來信。結論:未改變任何已實作欄位(我方 payload 與文件逐欄一致);新增可選能力 variables/fromName/unsubscribedLink 三項我方未用且暫不需用(已記錄理由);★ 最大實務價值=取得 Email 寄件人網域驗證之正式聯絡管道 [email protected](§7.13 樣本 5 之阻塞有了申辦路徑,惟仍須人工向廠商提出)。未解除 Email 阻塞;Push/App Push 仍未提供)。前版 1.7(CR-034/#A-151:§7.14 由「規格先行」轉為「已實作並真機驗證」——新增 §7.14.4b 驗證結果表(8 項:正確/錯誤/無簽章、重送去重、bounce 字串與數字型別、暫時退信不停用、投訴、寄送前攔阻);§7.14.1 之回應碼定案(簽章不符 401/已驗章之異常 200);§7.14.5 由「設計」改為「已實作」。★ 實作:POST /api/v1/notifications/surenotify/webhook(permit-all,簽章為唯一身分依據)/V5 遷移(notification_delivery_events+notification_suppressions)/SureNotifyChannel 寄送前查停用名單。前版 1.6(CR-034:新增 §7.14「SureNotify 回報事件(Webhook)」規格先行(尚未實作)——BEAM PMO 提供廠商之事件格式文件。含我方端點建議、★ 簽章驗證(HMACSHA256,<mail.id><event>,API Key 為 secret,Hex 輸出)、Email 5 事件 + SMS 2 事件之 payload、★ delivery 之語意澄清(廠商明載「寄出即先定義為 delivery」,之後仍可能 bounce → delivery ≠ 送達)、★ 外部前置條件(公開可達之 HTTPS URL——本地 localhost 不可達,故尚不可實作)、以及收到後應做什麼(bounce/complaint 應停用地址)。★ 併誠實修正 §7.13 原敘「3 秒送達」語意過強。前版 1.5(CR-034:新增 §7.13 SureNotify(電子豹)真實通道——BEAM PMO 提供 API Key,解除鐵則 3 類真阻塞之一「通知發送通道(僅發送)」。★ 資安硬性規定:外部通道不得使用 ServiceClientHttpClient(會對第三方洩漏內部 Bearer 權杖)→ 改用獨立 HTTP 客戶端、只帶 x-api-key。★ 通道優先序以 @Order 明定(SureNotify 10/Logging 100);無金鑰時完全退回現行行為。★ 收件人抽出 RecipientResolverPort(預設誠實失敗、不假造;test-recipient 僅供本地驗證)。★ 如實揭露:廠商不支援 Push/LINE(仍為真阻塞);NCC 實名制與網域白名單屬業務範疇。★ §8 降級表同步更新。前版 1.4(CR-029 批次 C:新增 §7.11 繳款入帳事件消費(payment.payment.posted.v1 → 樣板 PAYMENT_RECEIVED)與 §7.12 帳單出帳事件消費(statement.statement.issued.v1 → 樣板 STATEMENT_ISSUED)——兩樣板皆為既有種子資料(非臆造);★ 前置:兩事件 payload 原缺 currency,已依 SAD-004 §5.3 補為可選(SCS-008 §7.2/SCS-011 §7.2);★ 缺 currency 時不建立通知並記 WARN(不臆造幣別);★ 併將 AuthApprovedNotificationPort 通用化為 EventConsumptionPort)。前版 1.3(CR-027 批次 A-4:新增 §7.10 事件輸出——notification.sent.v1;★ 僅「全數通道成功」發射;★ sentAt 取自 markSent 回傳值)前版 1.2(GOV-006 4.8 W-2 推廣・CR-027 批次 A-2:新增 §7.9 授權核准事件消費(authorization.authorization.approved.v1 → 通知)——補 §3/§10 #7 早已載明卻從未實作之事件驅動;★ 樣板 AUTH_APPROVED 為既有種子資料(非臆造);★ partyId 傳 null(契約無收件人欄位,不臆造);★ 冪等雙重保險。併擴充生產端 payload 2 個可選欄位(cardNoMasked/merchantId)。更前版 1.1(CR-017 已執行:模板/通知兩表+通道抽象先行(本地 Log 通道 stand-in)+2 端點)
狀態 已核定(規格先行)
對應需求 ISS-2.10(信用卡通知服務)
服務埠號 8097(已產生骨架並實測;CSP-006 §4)
套件根 com.beam.cardcore.notif
權威 schema CC_NOTIF(V2 已建表;SDD-001 §3.18)
關聯文件 SDD-006 §6、BRD-002、GOV-005、CR-017 §5/§9.4

1. 對應需求

FR 編號 需求名稱 來源
ISS-2.10 信用卡通知服務 BRD-002

1.1 上層設計依據(本規格不重複這些圖,僅引用)

圖種 文件 數量(實測) 本服務相關
ER-Diagram SDD-001_領域模型與ER-Diagram 16 個 erDiagram(涵蓋 17 個 bounded context) ✅ SDD-001 §3.18 已補 DDL 章節(CR-017 裁定 G);⚠️ 循 V2 補充區慣例,ER 圖未動(僅補 DDL)
DFD SDD-002_DFD與流程分解 10 個 flowchart 通知流
Sequence SDD-003_Sequence-Diagrams 15 個 sequenceDiagram 通知觸發時序
狀態機 SDD-005_狀態機設計 通知狀態未建模為正式狀態機 本期狀態轉換由領域層守衛(見 §7.6)
API Contract SDD-006 + contracts/openapi/notification-service.yaml 18 份 本服務 2 支端點

2. 服務職責與邊界

承作:

  • 通知模板(NotificationTemplate:多通道、多語、變數)
  • 通知發送(Notification:SMS/Email/Push/LINE BC/LINE AOA,狀態與重試)
  • 由領域事件觸發(授權結果、繳款、爭議、額度異動等)

不做(屬他服務):

  • 業務邏輯 → 各領域服務(僅消費事件)
  • 實際訊息遞送 → 通道閘道(本期未接,見 §8)

3. 領域模型

  • 聚合(依 services.yaml):Notification(通知)、NotificationTemplate(模板)
  • 值物件:Channel(SMS/EMAIL/PUSH/LINE_BC/LINE_AOA)、NotificationStatus、TemplateCode
  • 狀態(以契約為權威):Notification.status ∈ {QUEUED, SENT, FAILED}

⚠️ 值域更正(2026-09-22,CR-017 執行時):本節原載設計意圖
{PENDING, SENT, FAILED, RETRYING},與契約 NotificationView.status
(QUEUED/SENT/FAILED)完全脫鉤——PENDING ≠ QUEUED,且 RETRYING
契約未定義。依本專案「API 曝露之列舉以契約為權威」之慣例(前例:CR-015/CR-016、
C-5 成功碼依契約更正)更正為契約之 3 值。
重試不以獨立狀態表示:失敗時 retryCount + 1 且狀態維持 QUEUED,達上限方轉 FAILED
——此設計使重試可達且可測,且不需引入契約未定義之狀態(避免無對應語意之死狀態=假綠)。


4. 程式結構

套件/類別 職責 對應需求
domain/model/Notification NotificationTemplate 聚合與值物件 ISS-2.10
domain/model/NotificationMessage DeliveryResult 遞送之值物件(通道埠之輸入/輸出) ISS-2.10
domain/port/NotificationRepository 通知持久化埠 —
domain/port/NotificationTemplateRepository 模板持久化埠 —
domain/port/NotificationChannelPort 通道抽象(CR-017 裁定 F) —
application/NotificationService 受理/渲染驗證/遞送/重試 ISS-2.10
infrastructure/jdbc/Jdbc*Repository JDBC 實作 —
infrastructure/channel/LoggingNotificationChannel 本地 Log 通道(stand-in) —
infrastructure/scheduler/NotificationDispatchScheduler 取件遞送排程 —
interfaces/rest/NotificationController REST 端點 ISS-2.10

5. 資料模型

CR-017 裁定 F/G(BEAM PMO 2026-09-22):本節為設計權威,實體交付為
V2__notification_templates_and_notifications.sql(CC_NOTIF 於 V1 基線完全無表)。

5.1 表定義(Oracle CC_NOTIF,實測回填)

表 欄位 值域/備註
NOTIFICATION_TEMPLATES ID RAW(16) PK/TEMPLATE_CODE/CHANNEL/LANGUAGE/SUBJECT/BODY_TEMPLATE/VARIABLES JSON/ENABLED CHANNEL IN ('SMS','EMAIL','PUSH','LINE_BC','LINE_AOA');ENABLED IN (0,1);唯一鍵 UQ_NOTIF_TPL_CODE_CHANNEL_LANG (TEMPLATE_CODE, CHANNEL, LANGUAGE)——多通道、多語
NOTIFICATIONS ID RAW(16) PK/NOTIFICATION_NO UNIQUE/ACCOUNT_ID/PARTY_ID/TEMPLATE_CODE/LANGUAGE/VARIABLES JSON/PREFERRED_CHANNELS JSON/CHANNELS JSON/AMOUNT/STATUS/RETRY_COUNT/FAIL_REASON/SENT_AT STATUS IN ('QUEUED','SENT','FAILED')(對齊契約);NOTIFICATION_NO 由 notification_no_seq 產生(NOTIF + 年月 + 6 碼)

5.2 內建模板(種子)

AUTH_APPROVED(SMS/EMAIL/PUSH/LINE_BC)、PAYMENT_RECEIVED、STATEMENT_ISSUED
——僅建表+內建少量種子,不做 CRUD(契約無模板端點;CR-017 §5 (d) 建議)。

5.3 交付

項 位置
建表+內建模板 V2__notification_templates_and_notifications.sql(+notification_no_seq;模板種子以 NOT EXISTS 守衛,冪等)
設計權威 SDD-001 §3.18

無獨立之本地種子檔:模板為產品內建(非環境特定),隨 V2 交付;
通知則由 API 受理產生。故不另立 ops/local/init/oracle/13-*.sql
(避免「同一資料兩處定義」之漂移)。

5.4 ⚠️ 本期語意(誠實揭露)

  1. 實際發送未接——NotificationChannelPort 之唯一實作為 LoggingNotificationChannel
    (本地 Log 通道 stand-in),不呼叫任何簡訊/電郵/推播閘道。
    依鐵則,「通知發送通道(僅發送)」屬真阻塞之 3 類之一(GOV-006 2.3.7)。
  2. 收件人聯絡方式未解析——契約 NotificationRequest 無收件人欄位(實測),
    且 party-service 契約之 4 端點皆非聯絡資訊(實測)→ PARTY_ID 僅為參照,
    實際手機/電郵之解析待契約與通道就緒。
  3. 無 PII 欄位——因第 2 點,本 schema 刻意不含加密聯絡欄(AesGcmFieldEncryptor 路徑
    本期無適用對象)。硬加一個無來源之密文欄位=假綠,故不為。
  4. 多語——模板以 (TEMPLATE_CODE, CHANNEL, LANGUAGE) 為鍵,惟契約無語言欄位,
    本期 NOTIFICATIONS.LANGUAGE 固定預設 zh-TW;多語選定待契約擴充。

6. API 設計(來自 contracts/openapi/notification-service.yaml)

端點 方法 說明 成功碼 對應 FR
/api/v1/notifications POST 發送通知(數位優先決) 201 ISS-2.10
/api/v1/notifications/{notificationId} GET 通知查詢 200 ISS-2.10

必填標頭:X-Channel(WEB/MOBILE/PARTNER/BATCH/SYSTEM)、
X-Idempotency-Key(8–120 字元)。
錯誤碼:CC-NOT-1001(樣板不存在,422)、CC-NOT-1002(無可用發送通道,422)、
CC-NOT-1003(樣板變數不足,422)、CC-NOT-4040(查無通知,404)——
後 3 碼為本輪新增(SDD-006 §3.4,CR-017 §9.4)。


7. 關鍵邏輯

  • 模板渲染:依 TemplateCode 與事件變數渲染多語內文。
  • 重試:發送失敗依 RETRYING 重試(冪等),超次標 FAILED 轉人工。
  • PII:收件人聯絡資訊經 common-security 加密(AesGcmFieldEncryptor)。

CR-017(BEAM PMO 2026-09-22 裁定 F)後定案之實作要點

  1. 通道抽象先行(裁定 F):NotificationChannelPort 定義 supports(Channel) 與
    deliver(NotificationMessage) → DeliveryResult;實作可多個(Spring 注入 List<…>,
    依通道擇一)。本期唯一實作為 LoggingNotificationChannel(stand-in)。
    → 正式環境僅需新增閘道實作,不需改動領域或應用層(此即「抽象先行」之目的)。
  2. 數位優先決:契約載「LINE_BC/LINE_AOA > PUSH > EMAIL > SMS」。
    實際通道=請求 preferredChannels 與該樣板「已啟用通道」之交集,並依上式排序;
    preferredChannels 未提供時取該樣板全部已啟用通道(同序)。
    交集為空 → 422 CC-NOT-1002(不靜默改送其他通道)。
  3. 受理即驗證(fail-fast):受理時驗證 ①樣板存在(否則 CC-NOT-1001)
    ②通道交集非空(CC-NOT-1002)③逐通道試渲染,任一變數未提供 → CC-NOT-1003
    ——不把可於受理期發現的錯誤延後到遞送期。
  4. 非同步遞送:受理僅建立 QUEUED 即回 201(契約);實際遞送由排程取件
    (NotificationDispatchScheduler)→ SENT/重試/FAILED——狀態由流程推進。
  5. 重試(以 QUEUED + RETRY_COUNT 表示):遞送失敗 → RETRY_COUNT + 1;
    未達上限 → 維持 QUEUED(下一輪重試);達上限 → FAILED 並必載 FAIL_REASON
    (領域層強制,未載即拒)。
  6. FAILED 之可達性(避免假綠):本地 Log 通道不是「永遠成功」——遞送前檢查
    各通道之內容長度上限(SMS 160/PUSH 200/LINE_BC/LINE_AOA 1000/EMAIL 2000
    字元,為真實閘道限制之近似值),超過即遞送失敗。
    → 目的:使 FAILED 與重試路徑可達且可測;否則「永遠成功」之實作無法證明失敗處理正確。
    ⚠️ 此為本地 stand-in 之遞送規則,非契約或 SDD 之規定。
  7. 冪等:受理以冪等鍵派生主鍵(UUID v3,前綴 notif:)——同鍵重送回同一通知。
  8. 狀態機:本期狀態轉換由領域層守衛(QUEUED → SENT/QUEUED → FAILED;
    終態不可再轉)並拋 CC-NOT-3001/CC-NOT-3002;未建模為 SDD-005 正式狀態機
    (見 §10)。

7.9 授權核准事件消費(authorization.authorization.approved.v1)——2026-10-01 新增(GOV-006 4.8 W-2 推廣・CR-027 批次 A-2)

緣由:SAD-002/SAD-004 §5.2 均載本服務為 authorization.authorization.approved.v1 之消費者;
§3 亦明載「由領域事件觸發(授權結果、繳款、爭議、額度異動等)」,
惟 §8/§10 #7 如實揭露「消費端 W-2 ⬜ 未實作;本期僅 REST 受理」——本節即補上該片拼圖。

訂閱:主題 cc.authorization.authorization.approved;群組 notification-service。

處理:

  1. 冪等:eventId 記入 consumed_events(與通知建立同交易);已存在 → 略過。
    ★ 雙重保險:NotificationService.submit 之冪等鍵即 eventId(確定性 UUID)——
    即使 consumed_events 遺失,重放亦不會產生第二筆通知。
  2. 亂序防護:sequenceNo 落後於該 aggregateId 已消費之最大值 → 略過並 WARN。
  3. 未知 eventType:略過並 WARN(不拋錯,SAD-004 §5.4)。
  4. 建立通知:以樣板 AUTH_APPROVED(既有種子資料,非臆造)呼叫 submit。

樣板變數(★ 恰為 AUTH_APPROVED 所宣告之 4 項):

變數 來源 說明
cardLast4 事件之 cardNoMasked 取末 4 碼 遮罩值,非明文
merchant 事件之 merchantId ⚠️ 2026-10-01 新增之可選欄位(見下)
amount 事件之 amount 最小單位(分)
currency 事件之 currency ISO 4217

★ 本次併擴充生產端 payload(CR-027 §5 #11 待裁定):

AUTH_APPROVED 樣板需要 cardLast4 與 merchant,惟 authorization.authorization.approved.v1
之原 payload(SDD-006 §4.2)未帶 cardNoMasked/merchantId——故於
AuthEventFactory 新增兩個可選欄位(依 SAD-004 §5.3「新增可選欄位不升版」→ 不升 v2)。
先例:card.card.status-changed.v1 早已帶 cardNoMasked(同為遮罩值)。
⚠️ 仍屬契約擴充,已登錄待裁定。

設計要點:

決策 理由
partyId 傳 null 契約無收件人欄位;party-service 之聯絡資訊端點不存在(§5.4 ②)——不臆造收件人
preferredChannels 傳 null 取樣板之全部已啟用通道(AUTH_APPROVED 現有 EMAIL/PUSH/SMS/LINE_BC,其中 LINE_AOA 停用)
不寫自有讀模型 本服務無業務表;消費之效果即「建立一筆 QUEUED 通知」
不解析 holdId/holdAmount/standinFlag/authNo 本消費者只建「消費通知」——不多取無用之資料
樣板代碼可由組態覆寫 cardcore.notification.auth-approved.template-code(預設 AUTH_APPROVED)

⚠️ 遞送仍為真阻塞(§8):本節只保證「通知被正確建立」,不保證送達客戶——
通道抽象目前唯一實作為本地 Log stand-in。SMS/Push 通道申請中(約兩天後開通)。

真機驗證(2026-10-01;真實 Oracle + Kafka):

驗證 結果
訂閱 ✅ Subscribed to topic(s): cc.authorization.authorization.approved;partitions assigned: [cc.authorization.authorization.approved-0]
遷移 ✅ CC_NOTIF.consumed_events(V3)建立成功
端到端 ✅ 送授權(15,000 分、APPROVED、merchantId=MCH-001)→ 事件 → 通知 NOTIF202610000003 建立(AUTH_APPROVED、SENT、amount=15000)
通道 ✅ ["LINE_BC","PUSH","EMAIL","SMS"](樣板全部已啟用通道;LINE_AOA 停用故未選)
變數正確性 ✅ {"cardLast4":"1111","merchant":"MCH-001","amount":15000,"currency":"TWD"}——cardLast4 由遮罩卡號取末 4 碼
冪等 ✅ 重送同 eventId → 通知總數不變(仍 3)、consumed_events 仍 1
亂序防護 ✅ 送 sequenceNo=1(落後)→ 略過,通知總數不變
⚠️ 舊事件(payload 擴充前) ✅ 如實略過:#A-121 時期發布之事件無 cardNoMasked/merchantId → 樣板變數不足 → CC-NOT-1003 → WARN 略過(不建半殘通知)。已知落差:該類事件不會產生通知(見 §10 #10)
單元測試 ✅ AuthApprovedNotificationServiceTest 6 例(守門邏輯 + last4/variables 純函式)

7.10 事件輸出(Transactional Outbox;CR-027 批次 A-4)

掛點:NotificationService.deliver 於全部通道成功(failures.isEmpty())→
repository.update(n.markSent(...)) 之後、同一交易內 append。

⚠️ 交易邊界:deliver 為私有方法,由 dispatchQueued(@Transactional)呼叫
→ 交易由 dispatchQueued 開啟,本處 append 確實同交易(不經代理亦無妨,
因交易已在外部開啟)。全批共用一個交易為既有設計(§7 實作要點 ④)。

事件:notification.sent.v1(SAD-002 §3.15、SDD-006 §4.2)

項目 值 依據
Kafka 主題 cc.notification.sent OutboxPublisher.topicFor
分區鍵 aggregateId = notificationId SAD-004 §5.4(同聚合有序)
aggregate_type notification SAD-002 §3.15(聚合 NotificationJob)
event_type(outbox 欄) sent(不含版本) SDD-001 §3.15
event_version 1 —
payload notificationId/channels[]/sentAt(恰 3 欄) SDD-006 §4.2

主題/event_type 之推導:依 SAD-004 §5.4 既有慣例——事件全名去 .vN 後,
再移除一層與 aggregate_type 相同之首段(notification.sent.v1 → sent)。

payload 欄位之來源對映:

payload 欄 來源 說明
notificationId n.id() 通知主鍵(亦為分區鍵)
channels[] parseChannels(n.channels()) 之通道代碼 與 NotificationView.channels 同源、同序——順序為通道擇定之結果(數位優先決,§7.2),非請求之偏好順序
sentAt n.markSent(now).sentAt() 與落庫之 SENT_AT 同一時間值(不得另行 Instant.now())

⚠️ sentAt 必須取自 markSent 之回傳值——若在 append 時另取 Instant.now(),
事件與資料庫將出現毫秒級落差,稽核對不上。

發射條件:僅「全數通道成功」(狀態 → SENT)時發射恰一則。
不發射之情形:

情形 原因
任一通道失敗(未達重試上限,維持 QUEUED) 尚未送出——下一輪重試;發射會造成「未送出卻宣稱已送出」
任一通道失敗且達上限(→ FAILED) 同左;FAILED 無對應事件(契約無)
channels 為空(→ 記失敗) 同左
受理(submit) 受理 ≠ 送出——QUEUED 階段不發射
冪等重播 早退,且不涉及遞送

⚠️ 本服務「遞送成功」=本地 Log 通道成功(§8:真實閘道未接,屬真阻塞)。
故此事件目前之意義為「已交付通道抽象」,非「客戶已收到」——
待真實閘道接入後語意方為完整(誠實揭露)。

消費者:party(SAD-002 §3.15)——⚠️ 尚未實作;且 SAD-004 §5.2「主要事件」未列
本事件(兩份清單不一致,見 CR-027 §5 #5)。

7.11 繳款入帳事件消費(payment.payment.posted.v1)——2026-10-01 新增(CR-029 批次 C-1)

訂閱主題:cc.payment.payment.posted(SAD-004 §5.4 命名規則)。

項目 值
事件型別 payment.payment.posted.v1
依據 SAD-004 §5.2:消費者含 notification(用途:繳款入帳與恢復額度)
樣板 PAYMENT_RECEIVED(既有種子資料,V2__notification_templates_and_notifications.sql;EMAIL/SMS,皆 enabled=1)
消費效果 建立一筆 QUEUED 通知(本服務無自有讀模型)

樣板變數對映(恰為樣板所宣告者):

變數 來源(事件 payload) 說明
amount amount 繳款金額(最小單位=分)
currency currency ⚠️ 見下方「前置契約變更」

⚠️ 前置契約變更(本節之關鍵):payment.payment.posted.v1 之 payload 原無 currency,
而 PAYMENT_RECEIVED 樣板需要它 → 變數不足即無法建立通知(CC-NOT-1003)。
已依 SAD-004 §5.3(新增可選欄位不升版)於 SCS-008 §7.2 補為可選(此即 CR-027 §5 #10 之建議選項 (b))。
本消費者之容忍行為:缺 currency 時不建立通知、記 WARN 並略過——不臆造幣別
(比照 §7.9 對「擴充前舊事件」之處理:不建半殘通知)。

處理(複用 §7.9 之形狀):

  1. 未知型別 → 略過 + WARN(不拋錯——否則新增事件會毒化消費者)。
  2. 冪等:consumed_events 以 event_id 去重。
  3. 亂序:sequence_no 落後於 MAX(sequence_no) WHERE aggregate_id → 略過 + WARN。
  4. 建立通知:NotificationService.submit,以事件之 eventId 為冪等鍵(雙重保險)。
  5. 記錄已消費:與 ④ 同交易。
  6. partyId 傳 null——契約無收件人欄位、party-service 無聯絡資訊端點(§5.4 ②),不臆造收件人。

⚠️ 語意僅為「通知被正確建立」:遞送通道仍為真阻塞(§8:唯一實作為本地 Log stand-in)
——不宣稱「客戶已收到」(與 §7.9 同一立場)。

7.12 帳單出帳事件消費(statement.statement.issued.v1)——2026-10-01 新增(CR-029 批次 C-2)

訂閱主題:cc.statement.statement.issued(SAD-004 §5.4 命名規則)。

項目 值
事件型別 statement.statement.issued.v1
依據 SAD-004 §5.2:消費者含 notification(用途:帳單出帳)
樣板 STATEMENT_ISSUED(既有種子資料;EMAIL,enabled=1)
消費效果 建立一筆 QUEUED 通知(本服務無自有讀模型)

樣板變數對映(恰為樣板所宣告者):

變數 來源(事件 payload) 說明
statementMonth statementMonth 帳單月份(YYYY-MM)
totalAmount totalAmount 本期應繳(最小單位=分)
currency currency ⚠️ 同 §7.11:原缺,已依 SAD-004 §5.3 於 SCS-011 §7.2 補為可選
paymentDueDate paymentDueDate 繳款截止日(YYYY-MM-DD)

處理:同 §7.11 之 ①~⑥(未知型別略過/冪等/亂序/submit/同交易記錄/partyId=null)。
缺 currency 之容忍行為:不建立通知 + WARN(不臆造幣別)。

⚠️ totalAmount 之語意:為本期應繳總額(Statement.newBalance()),
非「消費合計」——對映依據見 SCS-011 §7.1。本服務不自行加總。


7.13 SureNotify(電子豹)真實通道——2026-10-02 新增(CR-034)

緣由:BEAM PMO 提供 SureNotify 測試 API Key → 解除鐵則 3 類真阻塞之一
「通知發送通道(僅發送)」(GOV-006 §3.D)。
規格先行:CR-034(計畫與裁定)→ 本節 → 實作 → 真機驗證。
官方文件:https://newsleopard.com/surenotify/api/v1/

端點與認證

項 值
Base URL https://mail.surenotifyapi.com(cardcore.notification.surenotify.base-url)
認證 request header x-api-key(cardcore.notification.surenotify.api-key)
認證失敗 HTTP 403 {"message":"Forbidden"}
憑證存放 ~/.config/cardcore/notify.env(repo 之外、權限 600;ops/local/env.sh 若存在才載入)

★★ 資安硬性規定:外部通道不得使用 ServiceClientHttpClient

common-spring 之 ServiceClientHttpClient 一律附加出站 Bearer 權杖
(OutboundTokenProvider;CR-010),係為服務間呼叫而設。
若以之呼叫 SureNotify,將把本專案之 Keycloak 權杖送交外部廠商——對第三方洩漏內部憑證。

故:外部通道一律使用獨立之 java.net.http.HttpClient(自帶連線/請求逾時),
只帶 x-api-key,不帶任何內部憑證。
⚠️ 日後不得以「統一改用共用客戶端」為由改動本處——那會造成憑證外洩。

SMS:POST /v1/sms/messages

{ "content": "<渲染後內容>",
  "recipients": [ { "address": "<門號>", "country_code": "886" } ] }
欄位 必填 本服務之來源
content ★ NotificationMessage.body()(模板渲染後)
recipients[].address ★ RecipientResolverPort(見下)
recipients[].country_code ★ cardcore.notification.surenotify.sms-country-code(預設 886)
from 不帶(未申請專屬門號)
alive_mins 不帶(取廠商預設=最小值 5 分;避免拉長 webhook 等待)

Email:POST /v1/messages

{ "subject": "<主旨>", "fromAddress": "<寄件人>", "content": "<渲染後內容>",
  "recipients": [ { "name": "<收件人名稱>", "address": "<電郵>" } ] }
欄位 必填 本服務之來源
subject ★ NotificationMessage.subject()(模板之 subject)
fromAddress ★ cardcore.notification.surenotify.email-from-address(必設;未設 → 該通道 failed)
content ★ NotificationMessage.body()
recipients[].name ★ recipientRef(如 party:{uuid})——僅為標識,非 PII
recipients[].address ★ RecipientResolverPort

回應與判定

{ "id": "…", "success": [ { "id": "…", "address": "…" } ], "failure": [] }

⚠️ 實測揭露(2026-10-02):官方文件示例之 failure 為物件 {},
惟真實回應為陣列——實測以無效門號送出得
{"id":"…","success":[],"failure":[{"abc":"invalid address"}]}(HTTP 200)。
實作須同時容忍兩者(只認物件會在真實環境退化成印出原始 JSON)。

情境 判定
failure 非空(陣列或物件) failed(<第一筆「地址:原因」,原樣保留>)
success 非空且 failure 空 ok()
HTTP 403 failed("SureNotify 認證失敗(403 Forbidden)")
其他非 2xx failed("SureNotify HTTP <code>")
逾時/IO failed(<原因>)
無收件人可解析 failed("收件人聯絡資訊不可得…")——不假造收件人

⚠️ 埠契約(NotificationChannelPort)明訂:實作不得拋例外表達業務性失敗。
上列一律轉為 DeliveryResult.failed,由 NotificationService 記 FAILED 並依重試上限處理。

★ 收件人來源:RecipientResolverPort(新增埠)

項 內容
埠 Optional<String> resolve(String recipientRef, Channel channel)
為何要抽埠 「地址從哪來」是產品決策(CR-030 §4.2:party 聯絡資訊端點不存在);「怎麼呼叫閘道」是技術。兩者解耦後,端點一旦就位只需換實作,通道與應用層不動
預設實作 ConfiguredRecipientResolver——未設 test-recipient → 回 empty → 通道 誠實失敗(不假造)

⚠️ test-recipient 之定位(必須揭露,避免成為假綠)

  • cardcore.notification.test-recipient.sms/.email——預設為空(=不啟用)
  • 一旦設定,所有通知之該通道皆送往同一地址 → 絕不可用於 SIT/UAT/PROD
  • 目的:使真實閘道在「收件人來源未定」之現況下仍可端到端驗證
  • 不得隨環境檔流入上層環境(比照 ops/local/.env 之慣例)

通道優先序(@Order)

實作 @Order 承作通道
SureNotifyChannel 10 SMS/EMAIL(僅當 surenotify.enabled=true)
LoggingNotificationChannel 100(fallback) 任何通道(enabled 時)

→ 有金鑰:SMS/EMAIL 走真實閘道;LINE/PUSH 仍走 Logging(無真實實作)。
→ 無金鑰(enabled=false):完全退回現行行為(零破壞)。

★ 廠商支援範圍(如實揭露)

通道 支援 影響
SMS/Email ✅ 解阻塞
Push/App Push ❌ 文件無對應端點 ⚠️ 仍為真阻塞
LINE_BC/LINE_AOA ❌ ⚠️ 仍為真阻塞

★ NCC 法規(業務面,非本服務可解):簡訊內容須標示實名制身份;
含網址者須事先申請網域白名單(5–7 工作天)。→ 實名制字樣由模板提供(業務文案,CR-030 §4.2)。
⚠️ 現況:種子 SMS 模板(PAYMENT_RECEIVED/AUTH_APPROVED)皆未含實名制字樣 → 上線前必須補。

★★ 真實發送實測結果(2026-10-02;CR-034 §4)

# 樣本 結果
1 真實簡訊(<手機已遮蔽>) 通知 SENT;SureNotify messageId=20261002033020-0-…;事件紀錄 accept 03:30:20 → delivery 03:30:23;★ 收件人實機確認收到
2 真實簡訊(<手機已遮蔽>) 通知 SENT;messageId=20261002033153-0-…;accept 03:31:53 → delivery 03:31:57;★ 收件人實機確認收到
3 真實 Email(<信箱已遮蔽>) ⚠️ 失敗,原因為外部前置條件:SureNotify HTTP 400:Invalid: sender domain unverified——寄件人網域須先於 SureNotify 完成 DNS 驗證(POST /v1/domains/{domain} → 設 DNS → PUT 驗證),需自有網域與 DNS 控制權。★ 本服務正確呈現廠商原文(非原始 JSON)
4 Email 阻塞之精確定位(2026-10-02 探測矩陣) (a) fromAddress 為必填(不帶 → Invalid request body … missing required properties (["fromAddress"]));(b) 任何寄件人網域皆回 sender domain unverified——含廠商自家之 [email protected] → 該帳號目前「沒有任何已驗證之寄件人網域」;(c) 寄件人檢查先於收件人檢查 → 收件人網域(Gmail/公司網域)之規則目前無法測試
5 指定網域 dxccard.com 試寄(2026-10-02) ⚠️ 仍回 Invalid: sender domain unverified——該網域尚未於本帳號完成驗證。★ 併發現:本 API 無任何網域管理端點——POST/GET /v1/domains[/{domain}]、/v1/messages/domains、/v1/email/domains 皆 404;api.newsleopard.com 回 403 Missing Authentication Token(不同閘道)。→ 文件所載之 POST /v1/domains/{domain} 在此帳號/方案不存在 → 網域驗證只能於 SureNotify 後台或請廠商設定,無法由程式完成

⚠️ ★ 重大業務限制(廠商告知,2026-10-02):SureNotify 不發送給 Gmail/Outlook/iCloud/Yahoo 等免費信箱。
→ 對本專案影響極大:信用卡持卡人多數使用免費信箱 → Email 通道之適用對象將大幅受限。
須於 BRD/通路選型層面確認(是否改採其他 ESP、或僅以簡訊/App 推播為主)——此為產品決策,非技術可解。

⚠️ ★ delivery 之語意澄清(誠實修正;2026-10-02,依廠商文件):
廠商明載——Email 之 delivery 係「寄出即先定義為 delivery」(各家 mail server 回應時間不一),
故收到 delivery 之後仍可能收到 bounce。→ delivery ≠ 「已進到收件匣」。
本案對 SMS 之送達,另有「收件人實機確認」作為獨立佐證,故 SMS 之送達已由人工證實;
惟 SMS delivery 之精確語意仍待與廠商確認(是否為電信端送達回條)。
⚠️ 本節原先之敘述「3 秒送達」語意過強,已更正為僅陳述事件時序。

★ 樣本 1/2 為完整的端到端證明:notification-service → 事件 → 排程 → SureNotifyChannel →
真實閘道 → 電信網路 → 真實手機收到(★ 由收件人實機確認,非僅憑事件狀態)。
⚠️ 樣本 3 之阻塞不在本服務:程式與對映已完備,缺的是寄件人網域(外部前置條件)。
故 Email 之真實發送仍為阻塞;SMS 已完全打通。

7.13.1 官方 API 文件(廠商來信)之逐項核對——2026-10-02 新增

緣由:BEAM PMO 2026-10-02 轉寄 SureNotify(電子豹)官方來信,內含 API 使用說明。
本節為逐項核對結果(是否影響既有實作)。

官方文件項 我方實作 結論
Endpoint https://mail.surenotifyapi.com cardcore.notification.surenotify.base-url 預設即此 ✅ 一致
認證:request header x-api-key SureNotifyChannel 只帶 x-api-key(不帶內部 Bearer,CR-034) ✅ 一致
SMS:content + recipients[].address + recipients[].country_code buildBody 逐欄相同(另刻意不帶 from/alive_mins) ✅ 一致
Email:subject/fromAddress/content/recipients[].name/recipients[].address buildBody 逐欄相同(name 以 recipientRef 填,非 PII) ✅ 一致
variables(收件人層級變數替換,如 {{name}}) 未使用——本服務之通知內容於 application 層已渲染為成品字串,無需閘道端替換 ➖ 不需變更(如日後要做個人化,應改為「傳模板+變數」模式,另提 CR)
fromName(寄件人顯示名稱) 未帶 ⚠️ 可選;未帶時由廠商採預設。待產品決策是否補
unsubscribedLink(取消訂閱連結) 未帶 ⚠️ 可選;行銷類郵件之合規欄位。本專案通知屬交易類(帳單/繳款),現階段不適用
備註:變數名稱不得含特殊符號 未使用變數 → 不適用 ➖ 已記錄,供日後採用 variables 時遵循
「如何申請 API KEY」:註冊後寄信至 [email protected] — ★ 本封來信之最大實務價值:為樣本 5 之寄件人網域驗證阻塞提供了正式聯絡管道(原僅知「須後台或廠商處理」,不知找誰)。⚠️ 仍屬外部前置條件——須由本專案人員以信件向廠商提出,程式無法代辦

小結:官方來信未改變任何已實作之欄位(我方 payload 與文件逐欄一致),
亦未解除 Email 之阻塞(文件同樣未載「網域驗證」之自助端點);
但確認了阻塞之申辦路徑([email protected]),並釐清 variables/fromName/
unsubscribedLink 三項我方未用且暫不需用之可選能力。Push/App Push 仍未提供(文件無對應端點)。


7.14 SureNotify 回報事件(Webhook)——2026-10-02 規格先行 + 已實作(#A-151)

緣由:BEAM PMO 2026-10-02 提供廠商之 Webhook 事件格式文件,並指示「同意選 (a)」
(先實作、後驗證)。本節為規格;實作已完成並以自簽 HMAC 完成真機驗證(§7.14.4b)。
⚠️ 真實廠商事件仍待公開可達之 URL(§7.14.4)。
官方文件:https://newsleopard.com/surenotify/api/v1/

7.14.1 我方端點(建議)

項 值
方法/路徑 POST /api/v1/notifications/surenotify/webhook
認證 不用我方 JWT(廠商無法取得)——改用簽章驗證(§7.14.2)
冪等 廠商可能重送 → 以 mail.id + event + timestamp 去重(⚠️ 必須含 timestamp:open/click 可合法重複發生,僅以 mail.id+event 去重會把第二次開信誤判為重送而丟失)
回應 ★ 簽章不符 → 401(必須讓廠商看得見——若回 200,則「金鑰設錯」將表現為「所有回報悄悄消失」=靜默資料遺失);
★ 已驗章但內容異常(含未知事件型別)→ 200 + WARN(對「已確認是廠商」之請求回 4xx 只會招來重試風暴且無助解決)

7.14.2 簽章驗證(必做)

步驟 內容
1 取 request header x-surenotify-signature
2 取 body 之 mail.id 與 event
3 串接為 <mail.id><event>(廠商示例:…804ab7 + delivery → …804ab7delivery)
4 以 API Key 為 secret、HmacSHA256 計算,輸出 Hex 字串
5 比對;不符 → 拒收(記 WARN,不得當成有效事件)

⚠️ 實作時須以真實 webhook 校驗此演算法(文件所述之串接來源為 id/event,
示例與 mail.id 一致,惟未經真實事件驗證)——不得僅憑文件即視為正確。

7.14.3 事件型別與 payload(Email)

事件 關鍵欄位 對本服務之意義
delivery delivery.timestamp ⚠️ 「寄出即定義」——不等於已進收件匣(見下)
open open.timestamp、open.client_headers 開信(含 UA/國別等)——行銷分析用
click click.timestamp、click.sort、click.link_url 點擊(含連結序號)——行銷分析用
bounce bounce.timestamp/code(SMTP)/type(0 暫時/1 永久)/reason ★ 退信——永久退信應停用該地址(重要)
complaint complaint.timestamp 投訴(應立即停止寄送)

共同信封:{ "event": "<型別>", "mail": { "id", "subject", "content", "from", "to", "variables" }, "<型別>": { … } }

簡訊(SMS):廠商文件載 僅支援 delivery(代碼 3)與 bounce(代碼 6)。

⚠️ ★ delivery 之語意(廠商明載,必讀):
「鑒於每家 mail server 回應的時間不盡相同,Surenotify 設計上,寄信寄出即先定義為 delivery,
故有可能收到 delivery 事件後,又收到 bounce 事件的情況
」。
→ delivery ≠ 送達;收到 delivery 後仍須等待 bounce 才能判定最終結果。
→ 本服務不得以 delivery 作為「已送達客戶」之唯一依據。

7.14.4 ⚠️ 外部前置條件(尚無法收到「真實廠商事件」之原因)

前置條件 說明
★ 公開可達之 HTTPS URL 廠商須能主動 POST 至我方端點。本地開發環境(localhost)不可達 → 需部署至可公開存取之環境(或使用臨時通道)。此為外部條件,非程式問題
廠商後台登錄 Webhook URL POST /v1/webhooks//v1/sms/webhooks——⚠️ 實測 GET 該二路徑回 404,POST 是否可用待驗
真實 webhook 樣本 用以校驗簽章演算法(§7.14.2)——⚠️ 現以自簽 HMAC 驗證(§7.14.4b),真實樣本仍待

7.14.4b ★ 真機驗證結果(2026-10-02;以自簽 HMAC 模擬廠商 POST)

⚠️ 驗證方式之誠實揭露:廠商無法 POST 至本地 localhost,
故以自簽 HMAC(同一演算法)對本地服務發真實 HTTP POST——
驗證「端點/簽章/解析/落庫/停用名單/寄送前攔阻」全鏈路。
真實廠商事件仍待公開 URL(§7.14.4)。

# 樣本 結果
1 正確簽章(delivery) HTTP 200、ACCEPTED;★ 未帶 JWT → 證明 permit-all 生效
2 錯誤簽章 HTTP 401 CC-PLT-4010
3 無簽章 HTTP 401
4 重送同一則 HTTP 200、DUPLICATE(未重複落庫)
5 bounce type="1"(字串) 落庫 bounce_type=1;★ 寫入停用名單(PERMANENT_BOUNCE)
6 bounce type="0"(暫時) 落庫 bounce_type=0;★ 未寫入停用名單(正確)
7 complaint 落庫;★ 因地址已被 5 停用 → suppress 冪等回 false(正確)
8 ★★ 寄送前攔阻 對已停用地址寄送 → 通知 FAILED、原因「收件人已停用(曾永久退信或投訴)——依規範不再寄送」;★ 原因不含地址(PII 安全);未呼叫閘道

★ 樣本 5/6 同時證明廠商文件之型別不一致已被正確處理(bounce.type 表列 number、範例為字串 → 兩者皆容)。
★ 樣本 8 為本節之目的所在:停用名單真的會擋下寄送,不是只寫進資料庫。

7.14.5 本服務收到後應做什麼(已實作)

事件 建議處理
delivery 記錄時序(不改通知狀態為「已送達」——見 §7.14.3)
bounce(type=1 永久) ★ 標記該地址無效並停止後續寄送(需收件人地址,與 RecipientResolverPort 之產品決策連動)
complaint ★ 立即停止對該地址寄送(法遵要求)
open/click 目前不做(行銷分析範疇,非交易通知所需)

8. 外部依賴與降級

依賴 現況 失敗處理(Stand-in)
SMS/Email 閘道(SureNotify) ✅ 已解阻塞(§7.13;CR-034,2026-10-02,BEAM PMO 提供 API Key) 認證失敗/非 2xx/failure 非空/逾時 → DeliveryResult.failed(不拋例外);enabled=false 或無金鑰 → 退回本地 Log 通道
Push/App Push/LINE 閘道 🔴 仍為真阻塞——SureNotify 不支援(文件無對應端點) 仍由 LoggingNotificationChannel(@Order(100))承作——遞送不離開本機
收件人聯絡資訊(party-service) ⚠️ 端點不存在(實測 4 端點皆非聯絡資訊);需產品決策(CR-030 §4.2) 已抽出 RecipientResolverPort(§7.13)預留接點;未設 test-recipient 時回 empty → 誠實失敗(不假造收件人)。⚠️ test-recipient 僅供本地/驗證,不得流入上層環境
Kafka(事件觸發) ✅ 消費端已實作(§7.9,2026-10-01,#A-125) 事件驅動與 REST 受理並存(REST 仍為對外入口)

9. 測試案例(對應 TRP-001)

  • 通知發送/查詢(ISS-2.10)、模板渲染、重試與失敗轉人工
  • PII 加密欄位、多通道/多語

本期實作之測試(C-9)

層 檔案 覆蓋
領域 NotificationDomainTest 狀態守衛(終態不可轉)、重試上限轉 FAILED、FAIL_REASON 必載、通道優先序
領域 NotificationTemplateTest 渲染(變數代入)、未提供變數之偵測、Channel 長度上限
應用 NotificationServiceTest 受理(樣板不存在/通道交集為空/變數不足)、冪等、遞送成功/重試/達上限、查詢 404
架構 LayeringTest DDD 四層依賴方向(既有)

10. 已知限制(誠實揭露)

  • CC_NOTIF 尚未建表 → 已建(V2__notification_templates_and_notifications.sql;
    SDD-001 §3.18 已補 DDL 章節)。
  • 通知狀態機待 SDD-005 正式化 → 仍未建模為正式狀態機(本期由領域層守衛);
    值域已更正為契約之 3 值(見 §3)。
  • notification-service 骨架與埠號尚未產生(規格先行) → 已產生,埠號 8097,已啟動實測。

CR-017 執行後新增之限制(2026-09-22)

  1. 實際發送未接(真阻塞):唯一實作為本地 Log 通道;SMS/Email/Push/LINE 閘道未接。
  2. 收件人未解析:契約無收件人欄位、party-service 無聯絡端點 → PARTY_ID 僅為參照。
  3. 無 PII 加密:因第 2 點,AesGcmFieldEncryptor 路徑本期無適用對象(刻意不設欄位)。
  4. 多語未啟用:語言固定 zh-TW(契約無語言欄位)。
  5. 無模板管理端點:模板由遷移內建(契約未定義 CRUD)。
  6. 通道長度上限為近似值:SMS 160 等為本地 stand-in 之規則(真實閘道限制待接入後校準)。
  7. 無事件觸發:W-2 Kafka 消費端未實作 → 本期僅 REST 受理。
    → ✅ 已解除(2026-10-01,#A-125):新增 §7.9 授權核准事件消費——
    訂閱 cc.authorization.authorization.approved,以樣板 AUTH_APPROVED 建立通知;
    冪等(consumed_events + submit 之確定性冪等鍵雙重保險)/亂序防護/未知型別略過。
    ⚠️ 惟仍非全事件驅動:SAD-002 之其他訂閱關係(如繳款、爭議、額度異動)尚未實作——
    目前僅授權核准一條。且 partyId 仍傳 null(見 #2)。
  8. 重試為整筆重送:一筆通知含多通道,任一通道失敗即全部通道重送
    (未做「僅重送失敗通道」之精細化)——正式環境宜改為逐通道狀態(需契約擴充)。
  9. 事件驅動僅涵蓋「授權核准」(§7.9,#A-125):SAD-002 載本服務訂閱之其餘事件
    (繳款、爭議、額度異動等)尚未接;且消費端不解析收件人(同 #2),
    故建立之通知無法保證可送達(通道亦為真阻塞,同 #1)。
  10. authorization.authorization.approved.v1 之 payload 已擴充 2 個可選欄位(§7.9,#A-125):
    cardNoMasked/merchantId(樣板需要)。依 SAD-004 §5.3 不升版,惟仍屬契約擴充,
    已列 CR-027 §5 #11 待裁定。
    ⚠️ 併揭露:擴充前發布之舊事件(無此二欄)將因樣板變數不足而略過
    (CC-NOT-1003 → WARN)——不會產生通知。實務上事件流為即時、歷史事件無需補建,
    惟重放歷史事件時該類事件不會補出通知(已知落差)。
    正式解法:待 #11 裁定後,考慮升 v2 雙軌或消費端容忍缺欄(以空值渲染)。

CR-027 批次 A-4 執行後新增之限制(2026-10-01)

  1. notification.sent.v1 之語意僅為「已交付通道抽象」:真實簡訊/Email/Push/LINE 閘道
    未接(同 #1,真阻塞)→ 目前之「送出」實為本地 Log 通道成功。
    待閘道接入後語意方為完整。見 §7.10。
  2. notification.sent.v1 之消費者未實作,且兩份清單不一致:SAD-002 §3.15 載消費者為
    party,惟 SAD-004 §5.2「主要事件」未列本事件 → 屬 CR-027 §5 #5
    (「事件清單以哪份為權威」)之具體案例。本期依 SAD-002(服務矩陣)標註。
  3. FAILED 無對應事件:契約之 notification.* 僅有 sent.v1 → 遞送永久失敗
    不會有任何事件(作業面僅能由 GET 查詢或日誌察覺)。待裁定是否另立事件。
  4. sequenceNo 為「同 JVM 內遞增之時間戳基底序號」:多實例部署可能重複;
    正解為「聚合層級持久化序號」(SAD-004 §5.4),本期未做(與 CR-027 §5 #12 同型)。

CR-029 批次 C 執行後新增之限制(2026-10-01)

  1. 僅涵蓋 3 個事件:authorization.approved(§7.9)/payment.posted(§7.11)/statement.issued(§7.12)。
    SAD-004 §5.2 載本服務尚應訂閱 rewards.points.accrued/rewards.points.redeemed/dispute.case.opened
    ——三者無對應樣板(種子僅 AUTH_APPROVED/PAYMENT_RECEIVED/STATEMENT_ISSUED),
    不自行編寫業務文案 → 列 CR-029 §5 C-6 待裁定(若要做,樣板由誰提供)。
  2. partyId 仍傳 null(同 #2):三個消費者皆不解析收件人 → 建立之通知無法保證可送達
    (且通道仍為真阻塞,同 #1)。
  3. 缺 currency 之舊事件不產生通知(§7.11/§7.12):擴充前發布之事件將因變數不足而略過(WARN)。
    實務上事件流為即時,惟重放歷史事件時該類事件不會補出通知(已知落差,同 #10 之型)。

11. 修訂記錄

版本 日期 修訂者 說明
1.8 2026-10-02 WorkBuddy AI #A-153 併辦:新增 §7.13.1「官方 API 文件(廠商來信)之逐項核對」——BEAM PMO 轉寄 SureNotify(電子豹)官方 API 使用說明。逐項核對結果:Endpoint/x-api-key/SMS 之 content+recipients[].address+country_code/Email 之 subject+fromAddress+content+recipients[].name+address —— 與 SureNotifyChannel.buildBody 逐欄一致,無需變更。新增可選能力:variables(閘道端變數替換——本服務於 application 層已渲染成品字串,故不需)、fromName(可選,待產品決策)、unsubscribedLink(行銷類合規欄位;本專案通知屬交易類,現階段不適用);「變數名稱不得含特殊符號」已記錄供日後遵循。★ 本封來信之最大實務價值:申請 API KEY 之流程為「註冊 → 寄信至 [email protected]」→ 為 §7.13 樣本 5 之寄件人網域驗證阻塞提供正式聯絡管道(原僅知「須後台或廠商處理」而不知找誰);⚠️ 仍屬外部前置條件,程式無法代辦。未解除 Email 阻塞(文件亦未載自助網域端點);Push/App Push 仍未提供
1.0 — 開發團隊 初版(規格先行;CC_NOTIF DDL 待補)
1.1 2026-09-22 WorkBuddy AI CR-017 裁定 F/G 執行完成——①**§3 值域更正**:{PENDING,SENT,FAILED,RETRYING} → 契約之 {QUEUED,SENT,FAILED}(並說明重試改以 QUEUED+RETRY_COUNT 表示之理由);②**§5 改寫為設計權威(兩表定義、內建模板、交付清單、四項誠實揭露);③§7** 新增 8 項實作要點(通道抽象/數位優先決/受理即驗證/非同步/重試/FAILED 可達性/冪等/狀態守衛);④**§8** 改為「真阻塞」之如實記載+通道抽象之降級設計;⑤**§9** 補本期實作之測試矩陣;⑥**§10** 移除三項已解除限制、新增 8 項;⑦**§4** 補通道埠與 stand-in 實作;⑧**§6** 補成功碼與新增錯誤碼。單測 31 例 0 失敗(全模組 35 例,含既有 LayeringTest 4 例);真機 e2e 七項
1.2 2026-10-01 WorkBuddy AI GOV-006 4.8 W-2 推廣・CR-027 批次 A-2:新增 §7.9 授權核准事件消費(authorization.authorization.approved.v1)——§3 早已載「由領域事件觸發(授權結果…)」、§10 #7 亦如實揭露「消費端未實作」,本版補上。實作:V3__event_consumption.sql(consumed_events)+ AuthApprovedNotificationPort/AuthApprovedNotificationService/JdbcAuthApprovedNotificationRepository/AuthApprovedListener。★ 樣板 AUTH_APPROVED 為既有種子資料(非臆造);★ partyId 傳 null(契約無收件人欄位,不臆造);★ 冪等雙重保險(consumed_events + submit 之確定性鍵)。併擴充生產端 payload 2 個可選欄位(cardNoMasked/merchantId,依 SAD-004 §5.3 不升版)——已列 CR-027 §5 #11 待裁定。§8 依賴表更新(Kafka 消費端 ✅);§10 #7 改為已解除、新增 #9/#10
1.3 2026-10-01 WorkBuddy AI CR-027 批次 A-4(#A-138):①**§7.10 新增事件輸出規格**——notification.sent.v1:主題(cc.notification.sent)/分區鍵/payload 逐欄來源對映(★ sentAt 必須取自 markSent 之回傳值,不得另取 Instant.now())/發射與否(僅「全數通道成功」發射;QUEUED 重試中、FAILED、空通道、受理階段皆不發射);②**§10 新增 4 項**(#11 語意僅為「已交付通道抽象」/#12 消費者未實作且兩份清單不一致/#13 FAILED 無事件/#14 sequenceNo 多實例風險)
1.4 2026-10-01 WorkBuddy AI CR-029 批次 C(#A-141):新增 §7.11/§7.12 兩個事件消費端——payment.payment.posted.v1 → 樣板 PAYMENT_RECEIVED(EMAIL/SMS)、statement.statement.issued.v1 → 樣板 STATEMENT_ISSUED(EMAIL);兩樣板皆為既有種子資料(V2 遷移),未新增任何業務文案(不臆造)。複用 §7.9 之形狀(未知型別略過/consumed_events 冪等/sequenceNo 亂序/submit 以 eventId 為冪等鍵之雙重保險/markConsumed 同交易/partyId=null)。★ 前置契約變更:兩事件 payload 原缺 currency 而樣板需要 → 依 SAD-004 §5.3 補為可選(不升版;SCS-008 §7.2/SCS-011 §7.2)——此即 CR-027 §5 #10 之建議 (b),比照 #11/#14 之先例。★ 缺 currency 之容忍:不建立通知 + WARN(不臆造幣別、不建半殘通知)。★ 併將 AuthApprovedNotificationPort 通用化為 EventConsumptionPort(consumed_events 表本即通用,僅名稱綁 auth)——三個消費端共用一份,避免複製三份。§10 新增 3 項(#15~#17)
1.5 2026-10-02 WorkBuddy AI CR-034:新增 §7.13「SureNotify(電子豹)真實通道」+ §8 降級表更新——BEAM PMO 提供 SureNotify 測試 API Key → 解除鐵則 3 類真阻塞之一「通知發送通道(僅發送)」(GOV-006 §3.D)。①端點與認證:Base https://mail.surenotifyapi.com、header x-api-key、失敗 403 {"message":"Forbidden"};憑證存 ~/.config/cardcore/notify.env(repo 之外、600)。②SMS POST /v1/sms/messages(content+recipients[].address/country_code;不帶 from/alive_mins);Email POST /v1/messages(subject/fromAddress/content/recipients[].name/.address)。③判定:failure 非空 → failed(原因原樣保留);403 → 認證失敗;非 2xx/逾時 → failed;無收件人 → 誠實失敗。★ 埠契約:不得拋例外表達業務性失敗。④★★ 資安硬性規定:外部通道不得使用 ServiceClientHttpClient——其一律附加出站 Bearer 權杖(CR-010),用於外部呼叫即對第三方洩漏內部憑證;故改用獨立 java.net.http.HttpClient、只帶 x-api-key,並明令日後不得以「統一改用共用客戶端」為由改動。⑤通道優先序:@Order——SureNotifyChannel 10/LoggingNotificationChannel 100(fallback);無金鑰(enabled=false)時完全退回現行行為(零破壞)。⑥★ 收件人:抽出 RecipientResolverPort(解耦「地址從哪來」=產品決策 vs「怎麼呼叫閘道」=技術);預設 ConfiguredRecipientResolver 未設 test-recipient 即回 empty → 誠實失敗、不假造。⚠️ test-recipient 僅供本地/驗證、預設為空、不得流入上層環境。⑦★ 如實揭露:廠商不支援 Push/App Push/LINE(文件無對應端點)→ 仍為真阻塞;NCC 實名制與網域白名單(5–7 工作天)屬業務文案範疇(CR-030 §4.2)。
1.6 2026-10-02 WorkBuddy AI CR-034:新增 §7.14「SureNotify 回報事件(Webhook)規格」+ 修正 §7.13 之 delivery 語意——BEAM PMO 提供廠商之 Webhook 事件格式文件(delivery/open/click/bounce/complaint 之完整 payload,Email 5 種;SMS 文件載僅 delivery(3)/bounce(6))。①我方端點建議:POST /api/v1/notifications/surenotify/webhook(不用我方 JWT——廠商無法取得 → 改簽章驗證;以 mail.id+event 去重;一律回 200,處理失敗記 WARN 不拋錯)。②**★ 簽章驗證(必做):header x-surenotify-signature;message = <mail.id><event>;secret = API Key;HmacSHA256 → Hex;不符即拒收。⚠️ 須以真實 webhook 校驗,不得僅憑文件。③★ delivery 語意澄清(廠商明載):「寄出即先定義為 delivery,故有可能收到 delivery 事件後又收到 bounce」→ delivery ≠ 送達,本服務不得以之作為「已送達客戶」之唯一依據。④★ 外部前置條件(本節尚不可實作之原因):廠商須能主動 POST** → 需公開可達之 HTTPS URL;本地 localhost 不可達 → 需部署至可公開存取環境(外部條件,非程式問題);另需廠商後台登錄 Webhook URL(⚠️ 實測 GET /v1/webhooks//v1/sms/webhooks 回 404,POST 是否可用待驗)。⑤收到後應做什麼:bounce(type=1 永久)→ ★ 標記地址無效並停止寄送;complaint → ★ 立即停止寄送(法遵);open/click → 目前不做(行銷範疇)。⑥★ 併誠實修正 §7.13:原敘「3 秒送達」語意過強,已改為僅陳述事件時序,並註明 SMS 之送達係由「收件人實機確認」證實(非僅憑事件狀態)。
1.7 2026-10-02 WorkBuddy AI CR-034/#A-151:§7.14 Webhook 由「規格先行」轉為「已實作並真機驗證」(BEAM PMO 指示「同意選 (a)」)。①實作:POST /api/v1/notifications/surenotify/webhook(免認證——廠商取不到我方 JWT,故列於 cardcore.security.permit-all;簽章驗證為唯一身分依據);V5__surenotify_webhook.sql(notification_delivery_events append-only + notification_suppressions);SureNotifyWebhookService/SureNotifyWebhookController/JdbcDeliveryEventRepository/JdbcSuppressionRepository;SureNotifyChannel 於解析地址後、呼叫閘道前查停用名單。②回應碼定案:簽章不符 → 401(必須讓廠商看得見——回 200 會使「金鑰設錯」表現為「所有回報悄悄消失」);已驗章但內容異常(含未知事件型別)→ 200 + WARN(避免重試風暴)。③冪等鍵=mail.id+event+timestamp——⚠️ 必須含 timestamp:open/click 可合法重複發生,僅以 mail.id+event 去重會把第二次開信誤判為重送而丟失(已補單測)。④★ 型別不一致之處理:廠商文件之欄位表載 bounce.type 為 number、範例為字串 "1" → 兩者皆容(只認一種則真實事件可能判不出永久退信→ 該地址不會被停用)。⑤安全:未設 API Key 一律拒收(fail-closed);比對採 MessageDigest.isEqual(常數時間);Hex 大小寫正規化。⑥★ 真機驗證 8 項(以自簽 HMAC 模擬廠商 POST):正確簽章 200/錯誤 401/無簽章 401/重送去重/bounce 字串與數字型別皆正確解析/暫時退信不停用/投訴冪等/★ 寄送前攔阻(通知 FAILED「收件人已停用…」,未呼叫閘道,原因不含地址)。⑦測試:notification-service 105 測 0 失敗(新增 18)。⚠️ 仍待:公開可達之 HTTPS URL(廠商無法 POST 至 localhost)+ 廠商後台登錄 + 真實事件樣本。
(67-67/67)