Private Docs

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 #1949development

  • branch feature/sprint-08/SA-1380-fx-inapp · HEAD 0ed0838
  • 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.cswire contract
Notification02.Infrastructure/Messaging/FxInAppEventConsumer.csASB plumbing (topic/sub เป็น const)
Notification02.Infrastructure/Messaging/FxInAppNotificationDispatcher.csvalidate → ประกอบข้อความ → เขียน → settle
Notification02.Infrastructure/DependencyInjection.csregister 2 จุด (Mode A + Mode B)

ไม่มี migration · ไม่แตะ PersonalSignalConsumer, InAppNotificationService, InAppNotificationRepository, NotificationsController, AppHub


3. 3 จุดที่ควรดูตอน review

3.1 unique violation 23505Complete ไม่ใช่ 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 — เขียวหมดแล้ว

#เรื่องสถานะ
1ASB topic fx-inapp-event + sub FxInApp-Sub✅ สร้างจริงแล้ว dev / sit / uat (DuplicateDetection=false, MaxDeliveryCount=5immutable)
2POST /contracts/outstanding/near-expiry✅ live ที่ Backend_FX_ThirdPartyService บน development — route/request/response ตรงกับ client ทุก field
3GET /companies/cust-codes (UserService)✅ merge อยู่ใน development แล้ว
4คอลัมน์ AppCode + read filter✅ merge แล้ว (migration 20260805045911)

PR ที่เกี่ยวข้อง (ไม่อยู่ใน handoff นี้):

  • Backend_Iac #1951development — provisioning script + config · resolve conflict กับ PR 1947 แล้ว merge clean
  • Backend_FxOrchestratorService #1950origin/feature/sprint-08/SA-1380-noti-0utstandingอยู่กับ developer เจ้าของงานแล้ว

6. งานที่เหลือหลัง merge

6.1 E2E บน dev — ยังไม่ได้รัน

prerequisite ครบแล้ว รันได้เลย · ขั้นตอนเต็ม 7 ข้ออยู่ในแผน §4 · สรุป:

  1. ตั้ง FX Codex config FxNotificationOutstandingEnabled=true, StartTimes เวลาที่กำลังจะถึง, ExpireDate ครอบสัญญาทดสอบ
  2. รอ slot → ดู log [FxOutstandingNotification] run complete — Slots=… Found=… Sent=… Skipped=…
  3. เปิด browser จริง (headed) login เป็น user ในบริษัทเจ้าของสัญญา → กระดิ่งต้องเด้ง realtime
  4. refresh → รายการยังอยู่ (พิสูจน์ว่า persist ไม่ใช่แค่ push)
  5. กดอ่าน → เช็คเลขจาก /unread-count?appCode=FX เท่านั้น
  6. รัน slot เดิมซ้ำ → ต้องไม่มีรายการซ้ำ — ข้อนี้พิสูจน์ dedup และยืนยันข้อสมมติเดียวที่เหลือ (ว่า Npgsql ใส่ ConstraintName จริงสำหรับ filtered unique index)
  7. 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 ข้าม repoconfig ฝั่ง 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-5GET /companies/cust-codes เป็น [AllowAnonymous] — ควรเป็น [RequireApiKey] ไหม (infra wired ไว้แล้วแต่ไม่เคยใช้) · ถ้าเปลี่ยนต้องแนบ header ฝั่ง FxOrchestrator + KV secret + IaC ทุก env → ควรเป็น story แยก
G-63 จุด unread ที่ยังไม่แยกต่อแอป (§4)
G-7FE render NotificationType="Warning" ถูกไหม (backend รองรับแล้ว)

7. กับดักที่เจอมาแล้ว — อย่าเสียเวลาซ้ำ

กับดักสาระ
🔴 verify ต้องใช้ --configuration ReleaseCI รัน 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 ตายบน clusteroverlay 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 ขาดแค่หน้าจอ