CR #235 » CR-034_SureNotify通知通道接入之計劃與裁定.md
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 | ✅ | 解阻塞 |
| ✅ | 解阻塞 | |
| Push / App Push | ❌ 文件完全未提及、無對應端點 | ⚠️ 仍為真阻塞(需另尋廠商或改用 App 內推播) |
| LINE_BC / LINE_AOA | ❌ | ⚠️ 仍為真阻塞 |
1.6 NCC 法規(業務面,非本 CR 可解)
- 簡訊內容須清楚標示實名制身份(公司/品牌/活動名稱)
- 簡訊內若含網址,須向電信業者事先申請網域白名單(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 實名制與網域白名單屬業務範疇 |