AMLO Screening — Scenario + Sequence Diagram
ทุกเส้นทางของการบล็อกบริษัทที่ติด AMLO เป็น Sequence Diagram — รอบอัตโนมัติ, push ขึ้น modal, เลือกบริษัทใหม่, ไม่มีบริษัทสะอาด, login ภายหลัง, sync มือ, การบังคับใช้ที่ชั้น session, เคส push หาย, ปลดบล็อก และ race ตอนสลับบริษัท
อัปเดต: 2026-08-18
อ่านคู่กับ Architecture — หน้านี้คือ “เกิดอะไรขึ้นตามลำดับ” ส่วนหน้าโน้นคือ “ทำไมถึงออกแบบแบบนั้น” 4 scenario แรกคือสิ่งที่ Owner ระบุมา · S5–S10 คือเส้นทางที่ทีมเพิ่มเพราะถ้าไม่ออกแบบไว้ ระบบจะพังเงียบ
ตัวละครในทุกไดอะแกรม
| ย่อ | ตัวจริง |
|---|---|
| FE | Frontend HostApp (Angular shell) |
| APIM | APIM sentinel facade (sentinel-facade-inbound-v6.xml) |
| SEN | Sentinel Gateway (/sentinel/internal/resolve-session) |
| US | UserService |
| CEN | Centralized (เจ้าของข้อมูล AMLO) |
| NS | NotificationService (SignalR AppHub) |
| SB | Service Bus |
S1 — รอบอัตโนมัติตี 5 (Owner ข้อ 1 + 2)
sequenceDiagram
autonumber
participant SFTP
participant CEN
participant US
participant Redis
participant SB
participant NS
participant FE
Note over CEN: BackgroundService ตื่นตาม AmloIngestion:DailyRunHourBangkok = 5
CEN->>Redis: TryAcquireLock amlo-ingest:leader (TTL 30 นาที)
alt ได้ lock
CEN->>SFTP: list ไฟล์ prefix AMLO_
CEN->>CEN: กรองไฟล์ที่นิ่งเกิน StabilityWindowMinutes
loop ทีละไฟล์ = ทีละ batch
CEN->>SFTP: อ่านไฟล์
CEN->>CEN: parse แถว แล้ว insert match/reject
end
CEN-->>US: แจ้งว่ารอบ ingestion จบแล้ว (batchIds, asOfUtc)
else ไม่ได้ lock
CEN-->>CEN: ข้ามรอบนี้ (มีตัวอื่นรันอยู่)
end
Note over US: reconcile — ดึงสถานะทั้งชุดมาคำนวณใหม่ ไม่พึ่ง event ว่าครบ
US->>US: อ่าน (CustCode, CompanyId, UserId) ทั้งระบบ
US->>CEN: ถามผล screening ราย custCode
CEN-->>US: 200 มี match = Flagged / 404 = Clear
US->>US: custCode ว่างหรือรูปแบบไม่ตรง = NotScreenable (ไม่ใช่ Clear)
US->>US: commit สถานะรายบริษัท + asOfUtc + batchId
US->>Redis: invalidate user-info ของ user ที่กระทบ (หลัง commit เท่านั้น)
US->>SB: publish notification-personal (userId, AmloBlocked, payload ไม่มี title)
SB->>NS: PersonalSignalConsumer relay
NS->>FE: SignalR event AmloBlocked ไปที่ group user-{userId}
FE->>FE: เปิด modal แบบปิดไม่ได้
⚠️ ลำดับที่ 12–13 ห้ามสลับ — invalidate ก่อน commit จะทำให้ request ที่วิ่งพร้อมกัน re-warm ค่าเก่ากลับเข้า Redis พร้อม TTL ใหม่ยาวอีกรอบ
S2 — ผู้ใช้ค้าง session อยู่ และมีหลายบริษัท (Owner ข้อ 2.1.1)
sequenceDiagram
autonumber
actor U as ผู้ใช้
participant FE
participant APIM
participant US
Note over FE: กำลังใช้งานอยู่ ได้ event AmloBlocked
FE->>FE: modal เด้ง ปิดไม่ได้ (closeOnBackdrop=false, closeOnEscape=false)
FE->>APIM: GET /v1/users/me (อยู่ใน allowlist จึงไม่โดน 403)
APIM->>US: forward
US-->>FE: companies[] พร้อม amloBlocked ต่อบริษัท + amloAsOfUtc
FE->>FE: แยกบริษัทสะอาดออกมา
alt มีบริษัทสะอาดมากกว่า 1
FE->>U: แสดงรายชื่อให้เลือก
else เหลือบริษัทสะอาดใบเดียว
FE->>U: preselect ให้ แต่ยังต้องกดยืนยัน (ไม่สลับให้อัตโนมัติ)
end
U->>FE: เลือกบริษัท
FE->>APIM: PATCH /v1/companies/default {companyId} (allowlist)
APIM->>US: forward
US->>US: ตรวจเป็นสมาชิกจริง + บริษัทปลายทางต้องไม่ Flagged
US->>US: ตั้ง IsDefault ใหม่ + invalidate user-info
US-->>FE: 200
FE->>APIM: GET /v1/users/me (ดึงสถานะจริงกลับมา ไม่ patch signal เอาเอง)
FE->>FE: ปิด modal
🔴 ทำไมไม่ auto-switch แม้เหลือใบเดียว — ผู้ใช้ต้องรับรู้ว่าบริษัทเดิมถูกบล็อกและตอนนี้กำลังทำงานในนามบริษัทอื่น การสลับให้เงียบ ๆ ทำให้เขาทำรายการในนามบริษัทที่ไม่ได้ตั้งใจ
⚠️ ต่างจากพฤติกรรม FE วันนี้ — CorporateState.switchCompany ปัจจุบัน patch signal ในหน่วยความจำหลังสลับ ไม่ได้ดึง /users/me ใหม่ ในเส้นทางนี้ต้องดึงจริง เพราะสถานะ AMLO อาจเปลี่ยนระหว่างทาง
S3 — ไม่มีบริษัทสะอาดเหลือเลย (Owner ข้อ 2.1.2)
sequenceDiagram
autonumber
actor U as ผู้ใช้
participant FE
participant APIM
participant US
FE->>APIM: GET /v1/users/me
US-->>FE: ทุกบริษัทมี amloBlocked = true
FE->>U: modal "ติดต่อธนาคาร" — ไม่มีปุ่มเลือกบริษัท ไม่มีปุ่มปิด
U->>FE: กดปุ่มเดียวที่มี (ออกจากระบบ)
FE->>APIM: /sentinel/logout (auth path = passthrough เสมอ)
Note over U,FE: ทุก path ที่ทำรายการถูก 403 ที่ชั้น facade อยู่แล้ว<br/>modal เป็นแค่การอธิบายให้ผู้ใช้เข้าใจ
ต้องมีทางออกเสมอ — ถ้าไม่ปล่อย logout ผ่าน ผู้ใช้จะติดค้างในจอที่ทำอะไรไม่ได้เลย และเป็น denial of service ที่เราสร้างเอง
S4 — login ภายหลัง (Owner ข้อ 4)
sequenceDiagram
autonumber
actor U as ผู้ใช้
participant FE
participant APIM
participant SEN
participant US
U->>FE: เปิดเว็บ / login เสร็จ
Note over FE: provideAppInitializer ตามลำดับที่มีอยู่แล้ว
FE->>FE: syncUserProfile()
FE->>FE: realtime.initialize() — ต่อ SignalR
FE->>APIM: GET /v1/users/me
APIM->>SEN: resolve-session (cookie → token + amloBlocked)
APIM->>US: forward พร้อม Bearer
US-->>FE: companies[] + amloBlocked ต่อบริษัท + amloAsOfUtc
alt บริษัทที่ active ติด AMLO
FE->>FE: เปิด modal เดียวกับ S2/S3
else ไม่ติด
FE->>FE: เข้าหน้าปกติ
end
API “1 เส้น” ที่ Owner ขอ = /users/me ที่ FE เรียกอยู่แล้ว ไม่ต้องเพิ่ม endpoint ใหม่ — modal ต้องใช้ทั้ง “ถูกบล็อกไหม” และ “บริษัทไหนสะอาด” ซึ่งอยู่ก้อนเดียวกัน แยกเป็นสองเส้นคือสอง round trip ที่ต้องคอย sync กันเอง
⚠️ race ที่มีอยู่แล้วในโค้ด — SignalR ต่อเสร็จก่อน /users/me จะกลับมา ถ้า event มาถึงในช่วงนั้น FE ต้องเก็บไว้แล้วค่อยประมวลผลหลัง bootstrap เสร็จ ไม่ใช่ทิ้ง
S5 — sync มือ (Owner ข้อ 3)
sequenceDiagram
autonumber
actor A as Admin / Ops
participant ADM as Admin Portal
participant CEN
participant Redis
participant US
A->>ADM: กด "ตรวจ AMLO เดี๋ยวนี้"
ADM->>CEN: POST /v1/admin/amlo-ingestion/run (X-API-Key)
CEN->>Redis: TryAcquireLock amlo-ingest:leader
alt ชนกับรอบอัตโนมัติ
CEN-->>ADM: 409 กำลังรันอยู่ (ไม่ใช่ 200 เงียบแบบวันนี้)
else ได้ lock
CEN->>CEN: ingest ไฟล์ใหม่ถ้ามี
CEN-->>ADM: outcome ที่อ่านออก (ไม่ให้เดาจากตัวเลข 0,0,0,0)
CEN-->>US: trigger reconcile
US->>US: recompute สถานะทุกบริษัท + invalidate + push
end
🔴 ต้องแยกให้ออกและต้อง recompute เสมอ — วันนี้ทั้ง “ชน lock” และ “ไม่มีไฟล์ใหม่” คืน 200 (0,0,0,0) เหมือนกัน และการรันมือแค่ไปอ่าน SFTP ซ้ำ ถ้าไม่มีไฟล์ใหม่ก็ไม่มีอะไรเปลี่ยนเลย ซึ่งไม่ตรงกับสิ่งที่ Owner ต้องการ (ให้ผลการบล็อกอัปเดตเดี๋ยวนี้) — ปุ่มนี้ต้องสั่ง recompute ไม่ว่าจะมีไฟล์ใหม่หรือไม่
S6 — การบังคับใช้จริง (ผู้ใช้ที่ไม่เคยเห็น modal เลย)
sequenceDiagram
autonumber
actor U as ผู้ใช้ที่ถูกบล็อก
participant FE as FE / remote app / curl
participant APIM
participant SEN
participant WF as service ปลายทาง (Workflow/Task/FX)
U->>FE: สั่งทำรายการ
FE->>APIM: POST /api/workflow-service/... (cookie)
APIM->>SEN: POST /sentinel/internal/resolve-session
SEN-->>APIM: accessToken + amloBlocked = true
alt path ไม่อยู่ใน allowlist
APIM-->>FE: 403 COMPANY_AMLO_BLOCKED
Note over APIM,WF: ไม่ถึง backend เลย — ไม่ต้องหวังว่าทุก service<br/>จะไปเขียนโค้ดตรวจเอง
else path อยู่ใน allowlist (/users/me, companies/default, logout, hub)
APIM->>WF: forward ตามปกติ
end
นี่คือเหตุผลที่ชั้น session เป็น “การรับประกัน” — ICompanyAccessGuard ของ UserService ถูกเรียกจาก 4 handler ใน feature เดียว ส่วน WorkflowService/TaskService ไม่เคยอ่านข้อมูลบริษัทจาก blob เลย (grep = 0) ถ้าพึ่งการไล่แก้ทุก service เราจะเปิดช่องโดยไม่รู้ตัว
S7 — เมื่อข้อมูลไม่ชัด (fail-closed)
sequenceDiagram
autonumber
participant SVC as service ที่ทำรายการ
participant Redis
participant US
SVC->>Redis: อ่าน user-info blob
alt blob มี amloBlocked = true
SVC-->>SVC: ปฏิเสธ 403
else blob มี amloBlocked = false และ amloAsOfUtc ยังไม่เกินเกณฑ์
SVC->>US: ยืนยันกับ state จริงก่อนทำรายการที่มีมูลค่า
US-->>SVC: ยืนยัน
SVC-->>SVC: ทำรายการ
else blob ไม่มี field / เป็น null / อ่าน blob ไม่ได้ / ข้อมูลเก่าเกินเกณฑ์
Note over SVC: UNKNOWN
SVC->>US: ไปตรวจของจริง
alt ตรวจได้
US-->>SVC: ผลจริง
else ตรวจไม่ได้
SVC-->>SVC: ปฏิเสธถ้าเป็น action ที่มีมูลค่า<br/>(read-only ปล่อยผ่านได้)
end
end
null ≠ “ไม่ติด” — blob ที่สร้างก่อนมีฟีเจอร์นี้ หรือ service ที่ยังใช้ SupApp_util_lib เก่า จะเห็น null ทั้งคู่ ถ้าตีความว่าไม่ติด ทุกเส้นทางความล้มเหลวจะกลายเป็น fail-open
S8 — push หาย ระบบต้องหายเองได้
sequenceDiagram
autonumber
participant US
participant SB
participant NS
participant FE
US->>SB: publish AmloBlocked
alt ข้อความหาย / เข้า DLQ / ผู้ใช้ offline
Note over SB,NS: ไม่มีใครรู้ — consumer ถือว่างานเสร็จทันที<br/>หลัง NotifyUserAsync ไม่ว่าจะมี connection จริงหรือไม่
Note over FE: ผู้ใช้ไม่เห็น modal
Note over US: แต่ state ใน DB + Redis อัปเดตแล้ว<br/>ทุก request ของเขาจึงถูก 403 อยู่ดี
FE->>FE: SignalR reconnected หรือ bootstrap ใหม่
FE->>US: GET /v1/users/me
US-->>FE: amloBlocked = true
FE->>FE: modal เด้ง (ช้าไปหน่อย แต่ไม่ผิด)
end
เกณฑ์ยอมรับ: push หาย = ผู้ใช้เห็น modal ช้า ห้ามแปลว่า ผู้ใช้ยังทำรายการได้ ถ้าอันหลังเกิดขึ้น แปลว่าการบังคับใช้ไปพึ่ง push ซึ่งผิด design
S9 — ปลดบล็อก (un-flag)
sequenceDiagram
autonumber
actor C as Compliance officer
participant ADM as Admin Portal
participant US
participant Redis
participant FE
Note over US: การหายไปจากไฟล์รอบใหม่ ไม่ปลดบล็อกเอง
C->>ADM: บันทึก clearance (บริษัท, เหตุผล, หลักฐาน)
ADM->>US: ปลดบล็อกบริษัท
US->>US: เขียน clearance + audit event (ใคร เมื่อไหร่ อ้างอิงอะไร)
US->>US: สถานะบริษัท -> Clear
US->>Redis: invalidate user-info ของสมาชิกทุกคน
US->>FE: push ผ่าน SignalR ว่าใช้งานได้แล้ว (best-effort)
FE->>FE: ปิด modal / โหลดสถานะใหม่
🔴 ทำไมปลดอัตโนมัติไม่ได้ (ยัง) — ข้อมูลปัจจุบันไม่มี state “cleared” และ match เก่าไม่เคยถูกลบ จึงพิสูจน์ให้ผู้ตรวจสอบไม่ได้ว่า “หายจากไฟล์” แปลว่าพ้นจริงหรือแค่ไฟล์รอบนั้นเป็น delta ที่ไม่ได้ส่งซ้ำ ต้องตอบคำถาม snapshot-vs-delta ก่อน (ดู Open Items §1) การปลดอัตโนมัติจึงเป็นงานเฟส 2
S10 — race ตอนสลับบริษัท
sequenceDiagram
autonumber
actor U as ผู้ใช้
participant FE
participant US
participant REC as reconcile
U->>FE: สลับไปบริษัท B (ตอนนั้นสะอาด)
FE->>US: PATCH /v1/companies/default
US->>US: ตรวจตอนสลับ -> B ผ่าน
US-->>FE: 200
REC->>US: รอบใหม่พบว่า B ติด AMLO
US->>US: อัปเดตสถานะ + invalidate
U->>FE: กดทำรายการ
FE->>US: POST ...
US->>US: ตรวจ "บริษัทที่ active ตอนนี้" อีกครั้ง ณ เวลาทำรายการ
US-->>FE: 403 COMPANY_AMLO_BLOCKED
กฎ: การตรวจตอนสลับบริษัทไม่ใช่ใบผ่านถาวร — ต้องตรวจซ้ำ ณ เวลาที่ทำรายการเสมอ เพราะ flag มาถึงได้ทุกเมื่อ