Private Docs

AMLO Screening — Scenario + Sequence Diagram

ทุกเส้นทางของการบล็อกบริษัทที่ติด AMLO เป็น Sequence Diagram — รอบอัตโนมัติ, push ขึ้น modal, เลือกบริษัทใหม่, ไม่มีบริษัทสะอาด, login ภายหลัง, sync มือ, การบังคับใช้ที่ชั้น session, เคส push หาย, ปลดบล็อก และ race ตอนสลับบริษัท

อัปเดต: 2026-08-18

อ่านคู่กับ Architecture — หน้านี้คือ “เกิดอะไรขึ้นตามลำดับ” ส่วนหน้าโน้นคือ “ทำไมถึงออกแบบแบบนั้น” 4 scenario แรกคือสิ่งที่ Owner ระบุมา · S5–S10 คือเส้นทางที่ทีมเพิ่มเพราะถ้าไม่ออกแบบไว้ ระบบจะพังเงียบ

ตัวละครในทุกไดอะแกรม

ย่อตัวจริง
FEFrontend HostApp (Angular shell)
APIMAPIM sentinel facade (sentinel-facade-inbound-v6.xml)
SENSentinel Gateway (/sentinel/internal/resolve-session)
USUserService
CENCentralized (เจ้าของข้อมูล AMLO)
NSNotificationService (SignalR AppHub)
SBService 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 มาถึงได้ทุกเมื่อ