SA-1380 — FX Outstanding Notification: Handoff
ส่งมอบงานฝั่ง NotificationService: PR ที่ต้อง review, 3 จุดที่ควรดู, สิ่งที่ต้องบอก Frontend, งานที่เหลือหลัง merge และกับดักที่เจอมาแล้ว
อัปเดต: 2026-08-06
HANDOFF — SA-1380 FX Outstanding Notification (NotificationService)
สถานะ: PR เปิดแล้ว รอ review · งานฝั่ง FxOrchestrator แยกไปอยู่กับ developer เจ้าของงานนั้นแล้ว ไม่อยู่ใน handoff นี้
1. รับอะไรไป
PR ที่ต้อง review: Backend_NotificationService #1949 → development
- branch
feature/sprint-08/SA-1380-fx-inapp· HEAD0ed0838 - worktree:
C:\Source\AzureDevOps_SuperAPP\Backend_NotificationService\.worktrees\sa1380-fx-inapp - 939/940 ผ่าน (1 skip เดิมของ repo) · build 0 error ทั้ง Debug และ Release
เอกสารอ่านประกอบ — private-docs group Pending:
/docs/pending/fx-outstanding-notification-architecture//docs/pending/fx-outstanding-notification-implementation-plan/
ต้นฉบับ: Atlas/docs/notification-service/sa-1380-fx-outstanding-notification/20260806/
2. งานคืออะไร (สรุปสั้น)
สัญญา Forward ใกล้ครบกำหนด → แจ้งเตือนในแอปลูกค้า
FxOrchestrator scheduler → ASB topic fx-inapp-event → NotificationService → กระดิ่ง
(ส่ง "ข้อมูลสัญญา") (FxInApp-Sub) (ประกอบข้อความ + เก็บ + push)
PR นี้คือครึ่งปลายทาง — consumer + dispatcher ที่รับจาก topic ใหม่ เขียนลง inbox เดิม แล้ว push SignalR
ไฟล์ที่แตะ (src เพียง 4 ไฟล์):
| ไฟล์ | หน้าที่ |
|---|---|
Notification04.Domain/Messages/FxOutstandingNotificationMessage.cs | wire contract |
Notification02.Infrastructure/Messaging/FxInAppEventConsumer.cs | ASB plumbing (topic/sub เป็น const) |
Notification02.Infrastructure/Messaging/FxInAppNotificationDispatcher.cs | validate → ประกอบข้อความ → เขียน → settle |
Notification02.Infrastructure/DependencyInjection.cs | register 2 จุด (Mode A + Mode B) |
ไม่มี migration · ไม่แตะ PersonalSignalConsumer, InAppNotificationService, InAppNotificationRepository, NotificationsController, AppHub
3. 3 จุดที่ควรดูตอน review
3.1 unique violation 23505 → Complete ไม่ใช่ Abandon
InAppNotificationService เช็คซ้ำแบบ read-then-write ซึ่งไม่ atomic ⇒ ถ้า ASB redeliver พร้อมกัน ใบที่แพ้ race จะชน IX_in_app_notifications_external_message_dedup จริง
ถ้า abandon ตรงนั้น = retry จนครบ MaxDeliveryCount แล้ว dead-letter ข้อความที่ส่งสำเร็จไปแล้ว
filter ผูกกับชื่อ index เจาะจง — 23505 บน constraint อื่นยัง propagate ตามปกติ (test D-13c)
3.2 นี่คือ in-app path แรกที่มี dedup ระดับ DB
dispatcher ส่ง sourceService + externalMessageId เข้า SendPersonalAsync
PersonalSignalConsumer ที่ใช้ร่วมกันไม่เคยส่งทั้งคู่ ⇒ path นั้นไม่มี dedup ระดับ DB มาตลอด ทั้งที่ unique index มีอยู่แล้ว — ไม่ได้แก้ในงานนี้ (นอก scope) แต่ควรรู้ไว้
3.3 DaysLeft ห้ามคำนวณใหม่
คำนวณที่ต้นทางด้วย TZ ธุรกิจ แล้วส่งมาเป็นตัวเลขทึบ · NotificationService ไม่มี timezone provider ⇒ ถ้าคำนวณใหม่จะได้เวลาของ pod (UTC) และถ้า message redeliver ข้ามวัน ข้อความจะเพี้ยนจากตอน publish
test D-06 ตั้ง MaturityDate ให้ขัดกับ DaysLeft โดยตั้งใจ — ถ้าใครไปคำนวณใหม่ test พังทันที
4. ⚠️ ต้องส่งต่อให้ทีม Frontend
กระดิ่งในแอป FX ต้องอ่านเลข unread จาก GET /notifications/unread-count?appCode=FX เท่านั้น
3 จุดนี้เป็นยอดรวมทุกแอป ไม่แยกต่อ app:
| จุด | อาการ |
|---|---|
unreadCount ใน SignalR push | บอก appCode:"FX" แต่เลขรวมทุกแอป |
UnreadCount ใน envelope ของ GET /notifications?appCode=FX | รายการ filter ถูก แต่เลขข้าง ๆ รวมทุกแอป |
PATCH /read-all | ไม่รับ appCode — กดแล้ว mark ทุกแอป |
แก้ได้แต่ต้องแตะ shared read path จึงไม่ทำในงานนี้ · test RG-09 ล็อกพฤติกรรมปัจจุบันไว้แล้ว ถ้าอนาคตจะเปลี่ยนจะได้เป็นการตัดสินใจ ไม่ใช่ผลข้างเคียง
5. Prerequisite — เขียวหมดแล้ว
| # | เรื่อง | สถานะ |
|---|---|---|
| 1 | ASB topic fx-inapp-event + sub FxInApp-Sub | ✅ สร้างจริงแล้ว dev / sit / uat (DuplicateDetection=false, MaxDeliveryCount=5 — immutable) |
| 2 | POST /contracts/outstanding/near-expiry | ✅ live ที่ Backend_FX_ThirdPartyService บน development — route/request/response ตรงกับ client ทุก field |
| 3 | GET /companies/cust-codes (UserService) | ✅ merge อยู่ใน development แล้ว |
| 4 | คอลัมน์ AppCode + read filter | ✅ merge แล้ว (migration 20260805045911) |
PR ที่เกี่ยวข้อง (ไม่อยู่ใน handoff นี้):
- Backend_Iac #1951 →
development— provisioning script + config · resolve conflict กับ PR 1947 แล้ว merge clean - Backend_FxOrchestratorService #1950 →
origin/feature/sprint-08/SA-1380-noti-0utstanding— อยู่กับ developer เจ้าของงานแล้ว
6. งานที่เหลือหลัง merge
6.1 E2E บน dev — ยังไม่ได้รัน
prerequisite ครบแล้ว รันได้เลย · ขั้นตอนเต็ม 7 ข้ออยู่ในแผน §4 · สรุป:
- ตั้ง FX Codex config
FxNotificationOutstanding—Enabled=true,StartTimesเวลาที่กำลังจะถึง,ExpireDateครอบสัญญาทดสอบ - รอ slot → ดู log
[FxOutstandingNotification] run complete — Slots=… Found=… Sent=… Skipped=… - เปิด browser จริง (headed) login เป็น user ในบริษัทเจ้าของสัญญา → กระดิ่งต้องเด้ง realtime
- refresh → รายการยังอยู่ (พิสูจน์ว่า persist ไม่ใช่แค่ push)
- กดอ่าน → เช็คเลขจาก
/unread-count?appCode=FXเท่านั้น - รัน slot เดิมซ้ำ → ต้องไม่มีรายการซ้ำ — ข้อนี้พิสูจน์ dedup และยืนยันข้อสมมติเดียวที่เหลือ (ว่า Npgsql ใส่
ConstraintNameจริงสำหรับ filtered unique index) - user บริษัทอื่นต้องไม่เห็น
ห้าม seed ข้อมูลด้วยการยิง API — ข้อ 1 และการอ่าน log เป็น setup/observability ไม่ใช่ผลทดสอบ
6.2 หนี้ที่รู้อยู่ ไม่ได้แก้
| เรื่อง | รายละเอียด |
|---|---|
| config-parity FxOrchestrator dev แดง | 6 key ของ holiday-sync (FxContractApi.ResetHolidaySync, HolidaySync.DailyCheck.MinDaysSinceFirstOfYear, ScheduledJobs.Jobs.HolidayDailyCheckSyncJob.* 4 ตัว) ค้างมาก่อนงานนี้ · บล็อก build ของ dev ต้องให้เจ้าของ feature เติม |
| ไม่มี guard ผูกชื่อ topic ข้าม repo | config ฝั่ง publisher / const ฝั่ง consumer / provisioning script — ตรวจด้วยมือแล้วตรงกันทั้ง 4 ที่ แต่ไม่มีอะไรจับให้อัตโนมัติ · ชื่อผิด = เงียบสนิท ไม่มี error |
PersonalSignalConsumer ไม่มี DB dedup | ดู §3.2 |
| SignalR ล้มหลัง commit = push หาย | row เขียนแล้ว push พัง → redeliver จะโดน pre-check ตัด → Complete โดยไม่ push ซ้ำ · user เห็นตอน fetch ครั้งถัดไป · แก้ไม่ได้ถ้าไม่แตะ InAppNotificationService |
6.3 Owner decision ที่ยังเปิด
| id | เรื่อง |
|---|---|
| G-5 | GET /companies/cust-codes เป็น [AllowAnonymous] — ควรเป็น [RequireApiKey] ไหม (infra wired ไว้แล้วแต่ไม่เคยใช้) · ถ้าเปลี่ยนต้องแนบ header ฝั่ง FxOrchestrator + KV secret + IaC ทุก env → ควรเป็น story แยก |
| G-6 | 3 จุด unread ที่ยังไม่แยกต่อแอป (§4) |
| G-7 | FE render NotificationType="Warning" ถูกไหม (backend รองรับแล้ว) |
7. กับดักที่เจอมาแล้ว — อย่าเสียเวลาซ้ำ
| กับดัก | สาระ |
|---|---|
🔴 verify ต้องใช้ --configuration Release | CI รัน Release ซึ่งเปิด analyzer เป็น error · Debug ผ่านแล้ว CI พังมาแล้ว 2 ครั้ง (CS8603 ที่ NS, Sonar S6562 ที่ FX) |
| 🔴 local checkout ค้างเก่ากว่า origin | ตอนวางแผน NS ตามหลัง development 5 commit และข้อเท็จจริงที่ load-bearing ที่สุด 2 ข้อ (cust-codes endpoint, คอลัมน์ AppCode) มองไม่เห็นจาก working tree เลย · git fetch + git show origin/development:<path> ก่อนสรุปเสมอ |
⚠️ repo มี .worktrees/ | exclude ทุก grep/glob ไม่งั้นเจอไฟล์เดียวกัน 6-8 เวอร์ชัน |
| ⚠️ consumer ต้อง register 2 ที่ | Mode B (Entra RBAC) + Mode A (connection string) · ลืมอันใด = ตายเงียบใน mode นั้น · มี branch ที่สามที่ไม่ register อะไรเลย = กรณีไม่มี ASB config (ปกติของเครื่อง dev) |
⚠️ appsettings.SIT.json ของ FxOrchestrator ตายบน cluster | overlay map sit → "Sit" แต่ repo มีไฟล์ชื่อ SIT ตัวใหญ่ บน agent Linux case-sensitive ⇒ guard ไม่เคยรันให้ sit/uat และ overlay เขียนทับด้วยไฟล์จาก IaC ทุกครั้ง · หนี้ของ repo กระทบทุก feature บันทึกไว้ใน IaC KB แล้ว |
| ⚠️ provisioning script เป็น catalog ทั้งระบบ | ตอนรันที่ sit มันสร้างไป 20 entity ไม่ใช่ 2 เพราะ sit ขาด filter-event/flow-*/workflow-* อยู่ก่อน · ไม่ใช่ของ SA-1380 แต่เกิดขึ้นจริง แจ้งไว้ใน PR 1951 แล้ว |
8. ของอ้างอิง
- run record ครบ (intake / decision / 4 รอบ review / 2 รอบ evaluation / diffs / logs):
Atlas/docs/.dev-team/runs/sa1380-fx-outstanding-inapp-noti-plan/ - KB ที่อัปเดตในงานนี้:
Atlas/docs/{iac,notification-service}/repo-knowledge/20260804/ - Jira SA-1380 — AC1 in-app = งานนี้ · AC2 highlight หน้า Outstanding = FE นอก scope · AC3 email = เฟสหน้า (ใช้
fx-email-eventที่มีอยู่แล้ว ไม่ต้องเขียน consumer ใหม่) - Jira SA-1556 — ตั้งค่าจำนวนวัน · ค่าจริงตั้งได้แล้วผ่าน FX Codex
ExpireDateขาดแค่หน้าจอ