Project

General

Profile

CR #235 » CR-034_SureNotify通知通道接入之計劃與裁定.md

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

 

CR-034 SureNotify(電子豹)通知通道接入之計劃與裁定

項目 內容
文件編號 CR-034
版本 1.1(2026-10-02,#A-153 併辦:依廠商官方來信逐項核對 API 文件——實作無需變更;★ 確認 Email 網域驗證之聯絡管道 [email protected]。前版 1.0)
提出日 2026-10-02
提出者 WorkBuddy AI(開發團隊)
緣起 BEAM PMO 2026-10-02:「請您把 SMS/Notification 的 API Key 存好……接下來靠您了」——提供 SureNotify 測試 API Key,解除鐵則 3 類真阻塞之一「通知發送通道(僅發送)」(GOV-006 §3.D)
依據 SCS-016 §8(降級表)/CR-017 裁定 F(通道抽象先行);官方文件 https://newsleopard.com/surenotify/api/v1/
範圍 notification-service 之真實通道實作(SMS + Email)
不在範圍 Push/App Push(廠商不支援,見 §1.4);party 聯絡資訊端點(需產品決策,CR-030 §4.2)

1. 官方 API 要點(實測 + 文件)

1.1 端點與認證

項 值
Base URL https://mail.surenotifyapi.com
認證 request header x-api-key: <金鑰>
認證失敗 HTTP 403 {"message":"Forbidden"}(實測確認:以錯金鑰呼叫得此回應)
憑證存放 ~/.config/cardcore/notify.env(repo 之外,權限 600;由 ops/local/env.sh 若存在才載入)

1.2 簡訊:POST /v1/sms/messages

{ "content": "…", "alive_mins": 5,
  "recipients": [ { "address": "<手機已遮蔽>", "country_code": "886" } ] }
  • 必填:content、recipients[].address、recipients[].country_code
  • 選填:from(專屬門號)、alive_mins(5~480 分)、variables
  • 限制:每請求 ≤ 100 收件人;variables 值 ≤ 100 字元、開頭不得為數字

1.3 電子郵件:POST /v1/messages

{ "subject": "…", "fromAddress": "…", "content": "…",
  "recipients": [ { "name": "…", "address": "…" } ] }
  • 必填:subject、fromAddress、content、recipients[].name、recipients[].address

1.4 回應與判定

{ "id": "…", "success": [ { "id": "…", "address": "…" } ], "failure": [] }
  • failure 非空 → 遞送失敗(值為錯誤原因,如 invalid address)
  • success 非空且 failure 空 → 成功

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

1.5 ★ 通道支援範圍(必須如實揭露)

通道 廠商支援 本案影響
SMS ✅ 解阻塞
Email ✅ 解阻塞
Push / App Push ❌ 文件完全未提及、無對應端點 ⚠️ 仍為真阻塞(需另尋廠商或改用 App 內推播)
LINE_BC / LINE_AOA ❌ ⚠️ 仍為真阻塞

1.6 NCC 法規(業務面,非本 CR 可解)

  1. 簡訊內容須清楚標示實名制身份(公司/品牌/活動名稱)
  2. 簡訊內若含網址,須向電信業者事先申請網域白名單(5~7 個工作天)

→ 屬「業務文案」範疇(CR-030 §4.2):本 CR 不代擬文案;實名制字樣須由模板提供。


2. 設計裁定

2.1 ★★ 不得使用 ServiceClientHttpClient(安全)

common-spring 之 ServiceClientHttpClient 一律附加出站 Bearer 權杖
(OutboundTokenProvider,CR-010)——那是為服務間呼叫設計的。

⚠️ 若用它呼叫 SureNotify,會把本專案的 Keycloak 權杖送給外部廠商——
對第三方洩漏內部權杖,屬嚴重資安問題。
且無權杖時 OutboundTokenProvider 會拋錯(fail-closed),外部呼叫將無故失敗。

裁定:外部通道使用****獨立的 java.net.http.HttpClient(自帶逾時),
只帶 x-api-key,不帶任何內部憑證。此點必須寫入 SCS-016,避免日後被「統一改用共用客戶端」而誤改。

2.2 通道優先序:SureNotify 優先、Logging 降為 fallback

現行 LoggingNotificationChannel.supports(channel) 對任何通道皆回 true
(只要 enabled)→ 若直接新增 SureNotifyChannel,NotificationService.resolvePort
取「注入順序中第一個匹配者」,順序不確定 → 可能永遠走不到 SureNotify。

裁定:以 @Order 明定——SureNotifyChannel 10、LoggingNotificationChannel 100。
效果:

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

2.3 ★ 收件人來源:新增 RecipientResolverPort(誠實失敗,不假造)

現況:NotificationMessage.recipientRef 為 party:{uuid}/account:{uuid}——是參照,不是聯絡方式;
契約 NotificationRequest 無收件人欄位,party-service 無聯絡資訊端點(實測)。

裁定:抽出 RecipientResolverPort(Optional<String> resolve(recipientRef, channel)),
將「地址從哪來」(產品決策)與「怎麼呼叫閘道」(技術)解耦。

實作 行為
ConfiguredRecipientResolver(唯一 bean) ① cardcore.notification.test-recipient.* 未設 → 回 empty → 通道回 DeliveryResult.failed("收件人聯絡資訊不可得…")(誠實失敗,不假造收件人)
② 已設 → 回該值(⚠️ 僅供本地/驗證;見 §2.4)

→ 正式路徑不假造;party 聯絡資訊端點一旦就位,只需換掉本埠之實作,通道與應用層不動。

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

cardcore.notification.test-recipient.sms/.email 為本地/驗證專用:

  • 預設為空(未設=不啟用)
  • 一旦設定,所有通知之該通道皆送往同一個地址 → 絕不可用於 SIT/UAT/PROD
  • 其存在之目的:讓真實閘道在「收件人來源未定」之現況下仍可端到端驗證
  • application.yml 須載明;不隨環境檔流入上層環境(比照 .env 之慣例)

2.5 失敗處理(沿用 SCS-016 §8 之精神)

情境 行為
未帶 x-api-key/金鑰錯誤(403) DeliveryResult.failed("SureNotify 認證失敗(403 Forbidden)")
非 2xx failed("SureNotify HTTP <code>")
failure 非空 failed(<原因>)
連線逾時/IO failed(...)
無收件人可解析 failed("收件人聯絡資訊不可得…")

⚠️ 埠契約明訂:實作不得拋例外表達業務性失敗(NotificationChannelPort)。
故所有失敗皆轉為 DeliveryResult.failed,由 NotificationService 記為 FAILED/重試。


3. 執行步驟(依鐵律)

① 規格先行   SCS-016 §7.13(通道規格)+ §8(降級表更新)
② 實作       RecipientResolverPort / ConfiguredRecipientResolver / SureNotifyChannel / @Order
③ 設定       application.yml:cardcore.notification.surenotify.*(enabled/base-url/api-key/逾時)
④ 單測       請求組裝 + 回應判定(含 403/failure/無收件人)
⑤ 真機驗證   §4
⑥ 追蹤清單   HANDOVER + GOV-006(★ **§3.D 真阻塞解除**)

4. 驗證計畫

# 樣本 預期
1 金鑰存在性 錯金鑰 → 403 Forbidden;正確金鑰 → 認證通過(唯讀端點驗證,不發送)
2 無收件人(未設 test-recipient) 通知 → FAILED,原因「收件人聯絡資訊不可得」——不得宣稱成功、不得假造收件人
3 ★ 真實 API 往返(負樣本) 以無效門號送出 → 真實 SureNotify 回 failure → 適配器判定為 failed 且原因原樣保留(此步驟不實際發送任何簡訊)
4 既有流程不受影響 未設金鑰/enabled=false → 行為與現行完全相同(Logging 通道)
5 真實發送(BEAM PMO 提供門號) 一封真實簡訊 → success 非空 → 通知 SENT

4.1 執行結果(2026-10-02;BEAM PMO 提供測試門號 <手機已遮蔽>/<手機已遮蔽>)

# 樣本 實測結果
1 真實簡訊 <手機已遮蔽> 通知 SENT(sentAt 03:30:20Z);messageId=20261002033020-0-2bb322e8-…;事件紀錄:accept 03:30:20 → delivery 03:30:23(3 秒);收件人實機確認收到
2 真實簡訊 <手機已遮蔽> 通知 SENT(sentAt 03:31:54Z);messageId=20261002033153-0-a654a1c9-…;accept 03:31:53 → delivery 03:31:57;收件人實機確認收到
3 真實 Email <信箱已遮蔽> ⚠️ 失敗——外部前置條件:SureNotify HTTP 400:Invalid: sender domain unverified

★ 樣本 1/2 之閉環程度:notification-service →(QUEUED)→ 排程 → SureNotifyChannel →
真實閘道 → 電信網路 → 真實手機 → 人工確認。
每一環皆有證據:DB 狀態(SENT)+ 服務日誌(messageId)+ 廠商事件紀錄(delivery)+ 收件人回報。

★ 樣本 3 之阻塞歸屬(重要):不在本服務——程式、請求組裝、錯誤呈現皆已完備;
缺的是 SureNotify 帳號側之「寄件人網域驗證」(需自有網域 + DNS 控制權:
POST /v1/domains/{domain} → 於 DNS 設驗證記錄 → PUT 確認)。
→ Email 之真實發送仍為阻塞;SMS 已完全打通。

★ 併修(由樣本 3 驅動):非 2xx 之失敗原因原為原始 JSON,
已改為優先取 error 欄文字(SureNotify HTTP 400:Invalid: sender domain unverified),
使維運無須自行從 JSON 挑重點;已補單測釘住。

★ 樣本 3 之精確定位(2026-10-02 探測矩陣;BEAM PMO 提供 [email protected] 再測)

探測 結果
fromAddress 不帶 Invalid request body … missing required properties (["fromAddress"]) → 必填
[email protected] sender domain unverified
[email protected](廠商自有) sender domain unverified ← ★ 關鍵

→ (a) 該帳號沒有任何已驗證之寄件人網域(連廠商自家網域亦未通過);
(b) 寄件人檢查先於收件人檢查 → 收件人網域(dxc.com vs Gmail)之規則目前無法測試。
→ Email 之唯一阻塞=「完成寄件人網域驗證」(需一個可控制之網域 + DNS 權限)。

★ 指定網域 dxccard.com 試寄(2026-10-02;BEAM PMO 指示)

探測 結果
[email protected]/[email protected] → [email protected] Invalid: sender domain unverified(HTTP 400)
POST /v1/domains/dxccard.com(文件所載之建立驗證) HTTP 404
GET /v1/domains//v1/domains/{domain}//v1/messages/domains//v1/email/domains 皆 404 Resource Not Found
api.newsleopard.com/v1/domains 403 Missing Authentication Token(不同閘道,非本 API)

→ ★ 本 API 無任何網域管理端點:文件所載之 POST /v1/domains/{domain} 在此帳號/方案不存在。
→ 網域驗證只能於 SureNotify 後台或請廠商設定,無法由程式完成(又一項文件與實況不符)。

⚠️ ★ 重大業務限制(廠商告知):SureNotify 不發送給 Gmail/Outlook/iCloud/Yahoo。
信用卡持卡人多數使用免費信箱 → Email 通道之適用對象將大幅受限 →
須於通路選型層面決策(改採其他 ESP/或改以簡訊與 App 推播為主)——非技術可解。


5. 明確不做/留待外部輸入

事項 性質
簡訊實名制字樣與網域白名單 業務(NCC 法規;文案由業務提供、白名單需 5–7 工作天)
Email 寄件人網域驗證(dxccard.com 等) 外部前置條件——本 API 無網域管理端點(/v1/domains* 實測全 404);★ 申辦管道已確認:寄信至 [email protected](廠商官方來信所述之 API KEY 申請流程;SCS-016 §7.13.1)。須人工提出,程式無法代辦
party-service 聯絡資訊端點 產品決策(CR-030 §4.2)——本 CR 以 RecipientResolverPort 預留接點
Push/App Push 廠商不支援 → 仍為真阻塞(需另尋通道)。★ 2026-10-02 官方來信再次確認:文件無對應端點
LINE 通道 廠商不支援 → 同上
variables/fromName/unsubscribedLink 可選、暫不用——variables 因本服務已於 application 層渲染成品字串;unsubscribedLink 屬行銷類合規欄位(本專案通知為交易類)。理由見 SCS-016 §7.13.1

6. 修訂記錄

版本 日期 說明
1.1 2026-10-02 #A-153 併辦:依 BEAM PMO 轉寄之 SureNotify 官方來信,逐項核對官方 API 文件。結論:Endpoint/x-api-key/SMS 與 Email 之請求欄位與 SureNotifyChannel.buildBody 逐欄一致 → 實作無需變更。新增可選能力 variables/fromName/unsubscribedLink(我方未用,理由見 SCS-016 §7.13.1)。★ 最大實務價值:確認 Email 寄件人網域驗證之正式聯絡管道 [email protected](§5 之阻塞由「不知找誰」變為「有申辦路徑」);惟未解除阻塞(仍屬外部前置條件)。詳見 SCS-016 §7.13.1/§11
1.0 2026-10-02 初版:依 BEAM PMO 提供之 SureNotify 測試 API Key 擬定接入計畫。★ 資安裁定:外部通道不得使用 ServiceClientHttpClient(會對第三方洩漏內部 Bearer 權杖)→ 改用獨立 HTTP 客戶端、只帶 x-api-key。★ 通道優先序以 @Order 明定(SureNotify 10/Logging 100),無金鑰時完全退回現行行為。★ 收件人抽出 RecipientResolverPort(預設誠實失敗、不假造;test-recipient 僅供本地驗證)。★ 如實揭露:廠商不支援 Push/LINE;NCC 實名制與網域白名單屬業務範疇
    (1-1/1)