CR #201 » SCS-016_通知服務程式設計規格.md
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 ⚠️ 本期語意(誠實揭露)
-
實際發送未接——
NotificationChannelPort之唯一實作為LoggingNotificationChannel
(本地 Log 通道 stand-in),不呼叫任何簡訊/電郵/推播閘道。
依鐵則,「通知發送通道(僅發送)」屬真阻塞之 3 類之一(GOV-0062.3.7)。 -
收件人聯絡方式未解析——契約
NotificationRequest無收件人欄位(實測),
且party-service契約之 4 端點皆非聯絡資訊(實測)→PARTY_ID僅為參照,
實際手機/電郵之解析待契約與通道就緒。 -
無 PII 欄位——因第 2 點,本 schema 刻意不含加密聯絡欄(
AesGcmFieldEncryptor路徑
本期無適用對象)。硬加一個無來源之密文欄位=假綠,故不為。 -
多語——模板以
(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)後定案之實作要點
-
通道抽象先行(裁定 F):
NotificationChannelPort定義supports(Channel)與
deliver(NotificationMessage) → DeliveryResult;實作可多個(Spring 注入List<…>,
依通道擇一)。本期唯一實作為LoggingNotificationChannel(stand-in)。
→ 正式環境僅需新增閘道實作,不需改動領域或應用層(此即「抽象先行」之目的)。 -
數位優先決:契約載「
LINE_BC/LINE_AOA>PUSH>EMAIL>SMS」。
實際通道=請求preferredChannels與該樣板「已啟用通道」之交集,並依上式排序;
preferredChannels未提供時取該樣板全部已啟用通道(同序)。
交集為空 → 422CC-NOT-1002(不靜默改送其他通道)。 -
受理即驗證(fail-fast):受理時驗證 ①樣板存在(否則
CC-NOT-1001)
②通道交集非空(CC-NOT-1002)③逐通道試渲染,任一變數未提供 →CC-NOT-1003
——不把可於受理期發現的錯誤延後到遞送期。 -
非同步遞送:受理僅建立
QUEUED即回 201(契約);實際遞送由排程取件
(NotificationDispatchScheduler)→SENT/重試/FAILED——狀態由流程推進。 -
重試(以
QUEUED+RETRY_COUNT表示):遞送失敗 →RETRY_COUNT + 1;
未達上限 → 維持QUEUED(下一輪重試);達上限 →FAILED並必載FAIL_REASON
(領域層強制,未載即拒)。 -
FAILED之可達性(避免假綠):本地 Log 通道不是「永遠成功」——遞送前檢查
各通道之內容長度上限(SMS160/PUSH200/LINE_BC/LINE_AOA1000/EMAIL2000
字元,為真實閘道限制之近似值),超過即遞送失敗。
→ 目的:使FAILED與重試路徑可達且可測;否則「永遠成功」之實作無法證明失敗處理正確。
⚠️ 此為本地 stand-in 之遞送規則,非契約或SDD之規定。 -
冪等:受理以冪等鍵派生主鍵(
UUID v3,前綴notif:)——同鍵重送回同一通知。 -
狀態機:本期狀態轉換由領域層守衛(
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。
處理:
-
冪等:
eventId記入consumed_events(與通知建立同交易);已存在 → 略過。
★ 雙重保險:NotificationService.submit之冪等鍵即eventId(確定性 UUID)——
即使consumed_events遺失,重放亦不會產生第二筆通知。 -
亂序防護:
sequenceNo落後於該aggregateId已消費之最大值 → 略過並 WARN。 -
未知
eventType:略過並 WARN(不拋錯,SAD-004 §5.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 之形狀):
- 未知型別 → 略過 + WARN(不拋錯——否則新增事件會毒化消費者)。
-
冪等:
consumed_events以event_id去重。 -
亂序:
sequence_no落後於MAX(sequence_no) WHERE aggregate_id→ 略過 + WARN。 -
建立通知:
NotificationService.submit,以事件之eventId為冪等鍵(雙重保險)。 - 記錄已消費:與 ④ 同交易。
-
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 之送達已由人工證實;
惟 SMSdelivery之精確語意仍待與廠商確認(是否為電信端送達回條)。
⚠️ 本節原先之敘述「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)。 -
→ 已產生,埠號 8097,已啟動實測。notification-service骨架與埠號尚未產生(規格先行)
CR-017 執行後新增之限制(2026-09-22)
- 實際發送未接(真阻塞):唯一實作為本地 Log 通道;SMS/Email/Push/LINE 閘道未接。
-
收件人未解析:契約無收件人欄位、
party-service無聯絡端點 →PARTY_ID僅為參照。 -
無 PII 加密:因第 2 點,
AesGcmFieldEncryptor路徑本期無適用對象(刻意不設欄位)。 -
多語未啟用:語言固定
zh-TW(契約無語言欄位)。 - 無模板管理端點:模板由遷移內建(契約未定義 CRUD)。
-
通道長度上限為近似值:
SMS160 等為本地 stand-in 之規則(真實閘道限制待接入後校準)。 -
無事件觸發:W-2Kafka 消費端未實作 → 本期僅 REST 受理。
→ ✅ 已解除(2026-10-01,#A-125):新增§7.9授權核准事件消費——
訂閱cc.authorization.authorization.approved,以樣板AUTH_APPROVED建立通知;
冪等(consumed_events+submit之確定性冪等鍵雙重保險)/亂序防護/未知型別略過。
⚠️ 惟仍非全事件驅動:SAD-002之其他訂閱關係(如繳款、爭議、額度異動)尚未實作——
目前僅授權核准一條。且partyId仍傳null(見 #2)。 -
重試為整筆重送:一筆通知含多通道,任一通道失敗即全部通道重送
(未做「僅重送失敗通道」之精細化)——正式環境宜改為逐通道狀態(需契約擴充)。 -
事件驅動僅涵蓋「授權核准」(
§7.9,#A-125):SAD-002載本服務訂閱之其餘事件
(繳款、爭議、額度異動等)尚未接;且消費端不解析收件人(同 #2),
故建立之通知無法保證可送達(通道亦為真阻塞,同 #1)。 -
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)
-
notification.sent.v1之語意僅為「已交付通道抽象」:真實簡訊/Email/Push/LINE 閘道
未接(同 #1,真阻塞)→ 目前之「送出」實為本地 Log 通道成功。
待閘道接入後語意方為完整。見 §7.10。 -
notification.sent.v1之消費者未實作,且兩份清單不一致:SAD-002 §3.15載消費者為
party,惟SAD-004 §5.2「主要事件」未列本事件 → 屬CR-027 §5 #5
(「事件清單以哪份為權威」)之具體案例。本期依SAD-002(服務矩陣)標註。 -
FAILED無對應事件:契約之notification.*僅有sent.v1→ 遞送永久失敗
不會有任何事件(作業面僅能由GET查詢或日誌察覺)。待裁定是否另立事件。 -
sequenceNo為「同 JVM 內遞增之時間戳基底序號」:多實例部署可能重複;
正解為「聚合層級持久化序號」(SAD-004 §5.4),本期未做(與CR-027 §5 #12同型)。
CR-029 批次 C 執行後新增之限制(2026-10-01)
-
僅涵蓋 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待裁定(若要做,樣板由誰提供)。 -
partyId仍傳null(同 #2):三個消費者皆不解析收件人 → 建立之通知無法保證可送達
(且通道仍為真阻塞,同 #1)。 -
缺
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)+ 廠商後台登錄 + 真實事件樣本。 |
- « Previous
- 1
- …
- 65
- 66
- 67
- Next »