Private Docs

AMLO — ตารางข้อมูล สถานะ และการลงข้อมูลตามสถานการณ์

ทุกตาราง ทุกค่าสถานะ และลำดับการเขียนข้อมูลของแต่ละสถานการณ์ พร้อม query ตรวจว่าไม่ตกหล่น — เขียนจากฐานข้อมูล dev จริง

อัปเดต: 2026-09-01

คู่กับ สถาปัตยกรรมและ Flow · หน้านี้ตอบว่า “สถานการณ์นี้ข้อมูลลงตารางไหนบ้าง และสถานะเปลี่ยนเป็นอะไร” schema ดึงจาก information_schema ของ dev จริงเมื่อ 01/09/2026


1. ตารางทั้งหมด 11 ตาราง

ฝั่ง Centralized (DEV_CentralizedDb schema los)

ตารางหนึ่งแถวคือเขียนโดย
amlo_ingestion_batchหนึ่งไฟล์ที่รับเข้ามาingest
amlo_ingestion_rejectหนึ่งบรรทัดที่ parse ไม่ผ่านingest
amlo_screening_matchหนึ่งบรรทัดที่จับคู่ได้ (ดิบ)ingest
amlo_screening_snapshot_headerหนึ่ง generationgeneration builder
amlo_screening_snapshot_detailบรรทัดที่ถูกแช่แข็งเข้า generationgeneration builder
amlo_screened_companyสรุประดับบริษัทของ generationgeneration builder

ฝั่ง UserService (AuthDb schema public)

ตารางหนึ่งแถวคือเขียนโดย
Companies (คอลัมน์ Amlo*)สถานะ AMLO ล่าสุดของบริษัทreconcile job
AmloUserUnblocksสิทธิ์ปลดล็อกของ (คน, บริษัท)admin unblock + reconcile job
AmloAuditLogsเหตุการณ์หนึ่งครั้ง (append-only)ทั้งสองทาง
UserCompanyMappingsความเป็นสมาชิกบริษัทระบบ onboarding (ไม่ใช่ AMLO)
Usersผู้ใช้ระบบ user (ไม่ใช่ AMLO)

2. สถานะทั้งหมด — อย่าจำผิดว่ามีแค่ Flagged

2.1 Companies.AmloStatus — ระดับบริษัท มี 4 ค่า

ค่าความหมายใครเขียน
Flaggedติด AMLOreconcile job
Clearตรวจแล้วไม่ติดreconcile job
NotScreenableมีใน enum แต่ ไม่มีโค้ดไหนเขียนค่านี้เลย (ApplyAmloStatusAsync เขียนได้แค่ Flagged/Clear)
nullยังไม่เคยตรวจ · บริษัทที่ไม่มี CustCode ถูกคัดออกตั้งแต่ query จึงค้างที่ค่านี้ ไม่ใช่ NotScreenableค่าเริ่มต้น

2.2 effective status — ระดับ (คน, บริษัท) มี 4 ค่า

คำนวณจาก GetEffectiveStatus(rawStatus, hasActiveGrant) ไม่ได้เก็บในตาราง

rawมีสิทธิ์ Activeeffectiveผลจาก guard
FlaggedFlagged403 COMPANY_AMLO_BLOCKED
FlaggedClearผ่าน
ClearClearผ่าน
NotScreenable✅ หรือ ❌NotScreenable403 COMPANY_AMLO_NOT_SCREENABLE
null✅ หรือ ❌Unknown403 COMPANY_AMLO_STATUS_UNKNOWN

🔴 สิทธิ์ปลดล็อก override ได้เฉพาะ Flagged เท่านั้นNotScreenable และ null ปลดไม่ได้ ต่อให้ให้สิทธิ์ไปแล้วก็ยังโดน 403

🔴 null ก็โดน 403 — บริษัทที่ยังไม่เคยตรวจถูกบล็อกด้วย และไม่โผล่ในหน้า Compliance Cases (หน้านั้นแสดงเฉพาะ Flagged) ⇒ ปลดให้ไม่ได้ผ่านหน้าจอ

snapshot dev 01/09/2026 (ค่าจะเปลี่ยน ให้ query ใหม่ด้วย §5.6): null = 61 บริษัท · Flagged = 4 · NotScreenable = 1 (จาก 66) ⇒ ถ้าเปิด enforcement ตอนนั้นคนเกือบทั้งระบบจะโดน

2.3 AmloUserUnblocks.Status — 3 ค่าใน enum (เขียนจริง 2)

🔴 ค่าใน DB ไม่ใช่ Revoked — enum เก็บเป็น string ตามชื่อ member

ค่าในคอลัมน์ DBความหมาย
Activeสิทธิ์ใช้ได้อยู่
RevokedBySyncjob เพิกถอนเพราะหลักฐานเปลี่ยน
RevokedByAdminมีใน enum แต่ ยังไม่มีโค้ดไหนเขียน

⚠️ "Revoked" เป็นค่าที่ API collapse ให้ตอนตอบ เท่านั้น (GetAmloCompanyMembersHandler) — เขียน WHERE "Status" = 'Revoked' ใน DB จะได้ 0 แถวเสมอ แล้วสรุปผิดว่าไม่เคยมีการเพิกถอน

มี partial unique index:

UNIQUE ("CompanyId","UserId") WHERE "Status" = 'Active'

⇒ หนึ่งคนมีสิทธิ์ Active ได้ใบเดียวต่อบริษัท แต่มีประวัติที่ถูกเพิกถอนได้หลายใบ ⇒ หา “สถานะปัจจุบัน” ต้องหยิบแถวล่าสุดตาม CreatedAt ไม่ใช่แถวไหนก็ได้

2.4 AmloAuditLogs.Action

ค่าเกิดเมื่อมี TargetUserId
UserUnblockGrantedadmin ปลดล็อกให้คนหนึ่ง
RevokedBySyncjob เพิกถอนเพราะหลักฐานเปลี่ยน
CompanyBlocked / AutoUnblocked / EvidenceRotatedเหตุการณ์ระดับบริษัท
GateHeldมีใน enum แต่ ยังไม่มีโค้ดไหนเขียน

หน้าประวัติแสดงเฉพาะ 2 แบบแรก (ที่มี target user)

2.5 snapshot_header.GateResult

ค่าความหมายreconcile ทำอะไร
Passedด่านตรวจผ่านบล็อก + Clear sweep
Heldด่านตรวจไม่ให้ใช้ปลดล็อก (มี 6 เหตุ เช่น generation แรก หรือ FileSemantics=Delta)บล็อกอย่างเดียว ไม่ปลดใคร
Failedรอบนั้น build ไม่สมบูรณ์ (stale run)ข้ามทั้งรอบเงียบ ๆ
nullยังไม่ได้ตัดสินข้ามทั้งรอบเช่นกัน

⚠️ reconcile รับเฉพาะ Passed/Held — เจอค่าอื่นไม่ทำอะไรเลยและไม่ error ⇒ ดูแค่ “job รันแล้ว” ไม่พอ ต้องดู GateResult ด้วย

2.6 amlo_ingestion_batch.Status

มี 5 ค่า ไม่ใช่ 3

ค่าterminal?หมายเหตุ
Processingกำลังทำ
Completedเข้า generation ได้
CompletedWithErrorsเข้า generation ได้เท่ากับ Completed — อย่าลืมใส่ในเงื่อนไข query
Failedไม่ terminal — ถูก retry ทุกรอบ ไม่ใช่จบแล้ว
Rejectedชื่อไฟล์ผิดรูป ต้องแก้ไฟล์บน SFTP ก่อน

⚠️ Completed ไม่ได้แปลว่าถูกนำไปสร้าง generation แล้ว — ดู GenerationBuiltAtUtc ต่างหาก


3. การลงข้อมูลตามสถานการณ์

สถานการณ์ A — ไฟล์ใหม่เข้ามา

ลำดับตารางลงอะไร
1amlo_ingestion_batch1 แถว ProcessingCompleted
2amlo_screening_matchn แถว (บรรทัดที่จับคู่ได้)
3amlo_ingestion_rejectm แถว (บรรทัดที่ parse ไม่ผ่าน)
4snapshot_header1 แถว IsCurrent=true · แถวเดิมถูกปิด
5snapshot_detailสำเนาแช่แข็งของ match รอบนี้
6amlo_screened_company1 แถวต่อบริษัท พร้อม EvidenceFingerprint
7amlo_ingestion_batchupdate GenerationBuiltAtUtc

🔴 ขั้น 4–7 จะไม่เกิดถ้า processed = 0 (ไม่มีไฟล์ใหม่) เพราะ AmloIngestionRunService return ก่อนเรียก builder

แต่ builder มีกลไกกู้ batch ที่ค้างในตัวเองResolveBatchIdsAsync union ทุก batch ที่ GenerationBuiltAtUtc IS NULL เข้ามาด้วยทุกครั้งที่ถูกเรียก ⇒ วางไฟล์ใหม่ 1 ไฟล์ = ลาก batch เก่าที่ค้างทั้งหมดเข้า generation ให้เอง · 🔴 ห้าม insert snapshot_header/screened_company ด้วยมือ

สถานการณ์ B — reconcile job รันทุกชั่วโมง

กรณีCompaniesAmloUserUnblocksAmloAuditLogs3C
อยู่ในรอบ ไม่เคยติดมาก่อนเขียน 6 คอลัมน์ Flaggedอาจมี grant เก่าค้างที่ถูก revoke ในรอบเดียวกันCompanyBlockedส่งให้สมาชิกที่สถานะเปลี่ยน
อยู่ในรอบ ติดอยู่แล้ว fingerprint เท่าเดิมเขียนทับ 6 คอลัมน์ทุกรอบ (AmloAsOfUtc/AmloGenerationNo เปลี่ยน)ไม่แตะไม่มีไม่ส่ง
อยู่ในรอบ fingerprint ต่าง + version ตรงเขียนทับ fingerprint ใหม่เฉพาะใบที่ version ตรง → RevokedBySyncRevokedBySync ต่อคน + EvidenceRotated ระดับบริษัทส่งให้คนที่ถูกเพิกถอน
อยู่ในรอบ fingerprint ต่าง แต่ version ไม่ตรงเขียนทับไม่แตะเลย — grant รอดทุกใบไม่มีไม่ส่ง
หายจากรอบ (Snapshot)AmloStatus=Clear · AmloEvidenceFingerprint=nullไม่แตะเลย — grant ค้างต่อไปAutoUnblockedส่ง
หายจากรอบ (Delta)ไม่แตะเลยไม่แตะไม่มีไม่ส่ง

🔴 แถวที่ 4 กับ 5 คือช่องที่ต้องระวัง — grant ที่รอดจาก version gate บวกกับ grant ที่ค้างบนบริษัทที่ถูก Clear แล้ว ถ้าบริษัทกลับมา Flagged อีกครั้งด้วย version คนละตัว คนนั้นได้ effective Clear ทั้งที่ไม่มีใครอนุมัติรอบใหม่

⚠️ AmloAsOfUtc ถูกเขียนทับทุกรอบ ⇒ ไม่ใช่ “วันที่ติดครั้งแรก” แต่หน้า Cases เรียงด้วยค่านี้

⚠️ แถวสุดท้ายคือค่าที่ใช้อยู่จริงตอนนี้ — บริษัทที่หลุดจากไฟล์จะติดค้างตลอดไปจนกว่าจะมีคนปลดด้วยมือ

สถานการณ์ C — admin ปลดล็อกให้ 1 คน

ทั้งหมดอยู่ใน transaction เดียว ล็อกแถว Companies ด้วย FOR UPDATE

ลำดับตารางลงอะไร
1AmloUserUnblocks1 แถวใหม่ Status=Active + GrantedAgainstFingerprint/GenerationId
2AmloAuditLogsUserUnblockGranted + Reason + EvidenceFileRefs (jsonb array ของ fileId)
3Service BusAmloStateChanged ให้ผู้ใช้คนนั้น

ไม่แตะ Companies.AmloStatus เลย — บริษัทยังเป็น Flagged ตลอด เปลี่ยนแค่ effective status ของคนคนเดียว

สถานการณ์ D — ผู้ใช้เรียก API ที่มี guard

ไม่เขียนอะไรเลย อ่านอย่างเดียว: Companies.AmloStatus + มี AmloUserUnblocks ที่ Active ไหม → คำนวณ effective → ผ่านหรือ 403


4. ไฟล์หลักฐาน

เก็บที่ FileManagementService หมวด amlo-evidence · AmloAuditLogs.EvidenceFileRefs เก็บแค่ array ของ fileId ไม่ได้เก็บชื่อไฟล์

ชนิดที่รับ (ตั้งแต่ 01/09): PDF · JPEG · PNG เท่านั้น — เพื่อให้ preview ผ่านเบราว์เซอร์ได้ทุกไฟล์ (เดิมรับ Office ด้วย)

สิทธิ์เข้าถึง: RequiredRoles = Global Admin / AdminMaker / AdminApprover ⇒ admin ที่ไม่ใช่ผู้อัปโหลดก็โหลดได้ (เดิมเป็น owner-only ได้ 403)

การตรวจไฟล์ — client 8 กฎ · server 10 กฎ (server มี IsDangerousExtension + แยก null-byte เป็นข้อต่างหาก) ชนิดไฟล์ · นามสกุล · นามสกุลตรงกับชนิด · ชื่อไฟล์ห้ามมีจุดเกินหนึ่งจุด · ห้ามอักขระ path · ยาว ≤255 · ขนาด 1 byte–10 MB · ลายเซ็นไบต์แรกตรงกับชนิด

client เป็นแค่ UX ให้รู้เร็ว — server ยังเป็นด่านจริงเสมอ

⚠️ กฎ “จุดเกินหนึ่งจุด” ทำให้ รายงาน ก.ค.2569.pdf ไม่ผ่าน — เป็นกฎเดิมของ FileUploadSecurityHelper ที่ทุก category ใช้ร่วมกัน


5. Query ตรวจว่าไม่ตกหล่น

5.1 [Centralized] batch ที่ ingest แล้วแต่ยังไม่ได้สร้าง generation 🔴

ตัวสำคัญที่สุด — ตรงนี้คือช่องที่ทำให้ข้อมูลค้างเงียบ ๆ

SELECT "FileName", "Status", "RowCountSuccess", "CreatedAt"
FROM los.amlo_ingestion_batch
WHERE "Status" IN ('Completed', 'CompletedWithErrors')
  AND "GenerationBuiltAtUtc" IS NULL;

⚠️ ต้องมี CompletedWithErrors ด้วย — โค้ดถือว่ามีสิทธิ์เข้า generation เท่ากับ Completed (predicate เดียวกับ GetUnbuiltTerminalBatchIdsAsync)

ควรได้ 0 แถว · มีแถว = ยังไม่เคยกลายเป็น generation · วางไฟล์ใหม่ 1 ไฟล์ ระบบจะเก็บให้เองทั้งหมด ไม่ต้องแก้ด้วยมือ

5.2 [Centralized] generation ปัจจุบันมาจากของจริงหรือคนใส่มือ

SELECT "GenerationNo", "GateResult", "IsCurrent", "CreatedBy", "SourceFileNames",
       "RowCountTotal", "DistinctCustCodeCount"
FROM los.amlo_screening_snapshot_header WHERE "IsCurrent" = true;

SELECT count(*) AS detail_rows FROM los.amlo_screening_snapshot_detail;

CreatedBy ต้องเป็นของระบบ ไม่ใช่ manual:* · detail_rows ต้องมากกว่า 0

บน dev ตอนนี้ generation #1 มี CreatedBy = manual:amlo-simulate-29-08 และ detail = 0 แถว ⇒ เป็นข้อมูลที่คนใส่มือ ไม่ใช่ผลจากไฟล์

5.3 [ข้ามระบบ] สองฝั่งตรงกันไหม

รันบน Centralized แล้วเทียบกับผลจาก UserService ด้วยมือ (คนละฐาน join ไม่ได้)

-- [Centralized] บริษัทที่รอบปัจจุบันบอกว่าติด
SELECT sc."CustCodeNormalized"
FROM los.amlo_screened_company sc
JOIN los.amlo_screening_snapshot_header h ON h."Id" = sc."SnapshotHeaderId"
WHERE h."IsCurrent" = true ORDER BY 1;

-- [UserService] บริษัทที่ระบบบล็อกอยู่จริง
SELECT "CustCode" FROM "Companies"
WHERE "AmloStatus" = 'Flagged' AND "IsDeleted" = false ORDER BY 1;
ผลต่างแปลว่า
มีใน Centralized ไม่มีใน UserServicereconcile ยังไม่รัน · CustCode ไม่ match · หรือไม่มีบริษัทนั้นในระบบ
มีใน UserService ไม่มีใน Centralizedปกติเมื่อ FileSemantics=Delta (ไม่มี Clear sweep)

5.4 [UserService] สิทธิ์ที่ควรถูกเพิกถอนแต่ยังค้าง

SELECT c."CustCode", u."Email",
       g."GrantedAgainstFingerprint" AS granted_against,
       c."AmloEvidenceFingerprint"   AS current_fingerprint
FROM "AmloUserUnblocks" g
JOIN "Companies" c ON c."Id" = g."CompanyId"
JOIN "Users" u     ON u."Id" = g."UserId"
WHERE g."Status" = 'Active'
  AND c."AmloStatus" = 'Flagged'
  AND c."AmloFingerprintVersion" = g."GrantedAgainstFingerprintVersion"
  AND c."AmloEvidenceFingerprint" IS DISTINCT FROM g."GrantedAgainstFingerprint";

🔴 2 เงื่อนไขที่เพิ่มมาจำเป็น ไม่ใช่ของแถม — ถ้าไม่ใส่จะได้ false positive 2 ทาง:

  • บริษัทที่ถูก Clear แล้วมี AmloEvidenceFingerprint = null แต่ grant ยังถือค่าเดิม ⇒ ขึ้นเป็น “ค้าง” ทั้งที่ไม่ใช่ defect
  • grant ที่ FingerprintVersion ไม่ตรงถูกตั้งใจปล่อยไว้ (ดู §3 สถานการณ์ B แถว 4)

ควรได้ 0 แถว · มีแถว = reconcile job ยังไม่ทำงานหรือทำงานไม่ครบ

5.5 [UserService] สิทธิ์ Active ซ้ำ

SELECT "CompanyId", "UserId", count(*)
FROM "AmloUserUnblocks" WHERE "Status" = 'Active'
GROUP BY 1,2 HAVING count(*) > 1;

ควรได้ 0 แถว (partial unique index กันอยู่แล้ว — มีแถว = index หาย)

5.6 [UserService] แจกแจงสถานะแบบไม่ตกหล่น

SELECT COALESCE(NULLIF("AmloStatus", ''), '(ยังไม่เคยตรวจ)') AS status, count(*)
FROM "Companies" WHERE "IsDeleted" = false GROUP BY 1 ORDER BY 2 DESC;

ใช้ GROUP BY แทนการไล่ค่าทีละตัว — ถ้าวันหนึ่งมีค่าใหม่เพิ่มจะได้ไม่นับตกหล่น

5.7 [UserService] ใครถูกบล็อกจริง ณ ตอนนี้

SELECT u."Email", c."CustCode", c."NameTH"
FROM "Companies" c
JOIN "UserCompanyMappings" m ON m."CompanyId" = c."Id" AND m."IsStatus" = 'Active'
JOIN "Users" u ON u."Id" = m."UserId" AND u."IsDeleted" = false
WHERE c."AmloStatus" = 'Flagged' AND c."IsDeleted" = false
  AND NOT EXISTS (SELECT 1 FROM "AmloUserUnblocks" g
                  WHERE g."CompanyId" = c."Id" AND g."UserId" = u."Id" AND g."Status" = 'Active')
ORDER BY c."CustCode", u."Email";

⚠️ IsStatus ใน DB เป็น character varying ค่า Active/Inactive (ยืนยันด้วย information_schema + ข้อมูลจริงบน dev) · ต้องกรองด้วย เพราะ guard เช็คความเป็นสมาชิกก่อนแตะ AMLO — คนที่ mapping ไม่ Active ได้ COMPANY_NOT_MEMBER ไม่ใช่ AMLO block


6. กฎการเขียน query กับตารางเหล่านี้

AmloAuditLogs ไม่มี foreign key เลยโดยตั้งใจ — เป็น append-only ที่เก็บค่า denormalize ไว้ในตัว (CompanyCustCode, ActorEmail, SnapshotGenerationNo, SnapshotFileNames) เพื่อให้อ่านย้อนหลังได้แม้ข้อมูลต้นทางถูกแก้หรือลบ ⇒ LEFT JOIN เสมอ ห้าม INNER JOIN ไม่งั้นแถวที่ user ถูกลบไปแล้วจะหายจากรายงาน

EvidenceFileRefs เป็น jsonb ที่อาจเป็น null — แถวจาก background job ไม่เคยเขียน field นี้ · deserialize ต้องรองรับ null และข้อมูลผิดรูปโดยไม่ throw

หา grant ปัจจุบันต้อง DISTINCT ON ... ORDER BY CreatedAt DESC — ไม่ใช่หยิบแถวไหนก็ได้ เพราะหนึ่งคนมีหลายแถวได้


7. เช็คลิสต์ก่อน deploy env ใหม่

#อย่างทำไม
1apply migration 8 ตัว 2 ฐาน — UserService (AuthDb) 4 ตัว และ Centralized (DEV_CentralizedDb) อีก 4 ตัวไม่มี auto-migrate ทั้งสอง repo · image ที่มีโค้ดแต่ DB ไม่มีคอลัมน์ = ทั้ง service ตาย ไม่ใช่แค่ AMLO
2เพิ่ม menu row /amlo, /amlo/operations, /amlo/cases, /amlo/history + ผูก roleเมนูมาจาก DB ไม่ใช่โค้ด · ไม่ทำ = เปิดหน้าไม่ได้
3เพิ่ม APIM operation ของ endpoint ใหม่ (env ที่ไม่ใช้ wildcard)ไม่มี = หน้าโหลดไม่ขึ้นทั้งที่ backend พร้อม · PUT ทีละ operation ห้าม import ทับ
4ตรวจ AmloEnforcement.Enabled ให้ตรงกับที่ตั้งใจค่ามาจาก IaC overlay ตอน build ไม่ใช่ env var
5ฝั่ง Centralized: เปิด AmloIngestion.Enabled + ใส่ SFTP secret 3 ตัวใน KVIaC ตอนนี้ dev = true · sit/uat = false · prod ยังไม่มี config dir เลย
6รัน query §5.1 หลัง deployจับ batch ที่ค้างไม่ได้สร้าง generation