Private Docs

AMLO Screening — Data Tier Handoff (สิ่งที่ลงจริงแล้ว)

สรุปของที่ merge และ migrate ขึ้น DEV/UAT แล้วจริง — 3 ตาราง, กฎที่บังคับอยู่ที่ระดับ DB, หลักฐานการทดสอบ, และงานชิ้นถัดไปที่ยังไม่มีใครเขียน (ตัวสร้าง generation)

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

หน้านี้คือสถานะจริง ณ 21/08/2026 ไม่ใช่แผน — สิ่งที่เขียนในนี้รันอยู่บน DEV_CentralizedDb และ UAT_CentralizedDb แล้ว design เบื้องหลังอยู่ที่ Phase 1 Design & Handoff · ความเสี่ยงที่ยังเปิดอยู่ → Open Concerns repo เดียวที่เกี่ยว: Backend_Centralized


1. สรุปใน 5 บรรทัด

  1. ตารางใหม่ 3 ตัว + migration ลงแล้ว: amlo_screening_snapshot_header, amlo_screening_snapshot_detail, amlo_screened_company (schema los)
  2. domain model + EF configuration + test ครบ — dotnet test ผ่าน 851/851, dotnet build -c Release ผ่าน (analyzer เป็น error)
  3. migrate ขึ้น DEV และ UAT แล้ว พร้อมทดสอบพฤติกรรมจริงบน Postgres 10/10 ข้อ ทั้งสอง env
  4. ยังไม่มีโค้ด production ที่เขียนลงตารางเหล่านี้เลยCreate / Compute / CopyFrom ถูกเรียกจาก tests/ เท่านั้น
  5. งานชิ้นถัดไปคือ ตัวสร้าง generation (อ่าน staging → จัดกลุ่ม → เขียน 3 ตาราง → seal) ซึ่งยังไม่ได้เริ่ม
หัวข้อค่า
branchfeature/sprint-13/migration/amlo-screening-generation (push แล้ว ยังไม่เปิด PR)
commit1281311feat(amlo): add screening snapshot and screened company tables (26 ไฟล์, +3334)
migration20260821134240_AddAmloScreeningSnapshotAndScreenedCompany
DEV / UATอยู่ที่ migration นี้แล้ว (เดิมอยู่ที่ 20260806094117_AddCustCodeLogDatetimeUtcIndexToAmloScreeningMatch)
SITไม่แตะ — ยังค้างที่ 20260608112601_RenameDocumentNumberTables (ช้ากว่า 3 migration) เพราะทีมเลิกใช้ SIT แล้ว

2. ตารางทั้ง 3 ทำหน้าที่อะไร

los.amlo_screening_match            ← staging (ของเดิม, ชั่วคราว, ลบได้)
        │  copy ทั้งแถว

los.amlo_screening_snapshot_header  ← 1 แถว = 1 generation (รอบตรวจ)
        ├── _detail                 ← คลังหลักฐาน (สำเนาเต็มรวมชื่อ)
        └── amlo_screened_company   ← คำตอบ (ปลายทางอ่านตารางนี้ตารางเดียว)
ตารางgrainใครอ่าน
snapshot_header1 แถวต่อ generationระบบเอง + audit
snapshot_detail1 แถวต่อ แถวหลักฐาน ที่ copy มาจาก stagingaudit / การสืบย้อนเท่านั้น — มี PII ชื่อบุคคล ห้ามโผล่ใน API หรือ log
screened_company1 แถวต่อ CustCode ต่อ generationระบบปลายทาง (Phase 2)

3. กฎที่บังคับอยู่ที่ระดับ DB แล้ว (ไม่ต้องเขียนโค้ดคุมซ้ำ)

กฎบังคับด้วย
มี generation ที่เป็น current ได้ ใบเดียวunique index บน IsCurrent แบบ WHERE "IsCurrent"
GenerationNo ห้ามซ้ำ แต่ draft หลายใบถือ NULL พร้อมกันได้unique index แบบ WHERE "GenerationNo" IS NOT NULL
RunId ห้ามซ้ำ (กัน re-entry ของ background job)unique index
1 CustCode มีคำตอบได้ใบเดียวต่อ generationunique (SnapshotHeaderId, CustCodeNormalized)
ลบ generation แล้วคำตอบ+หลักฐานหายตามFK cascade
หลักฐานต้องรอดแม้ staging ถูก purgeSourceBatchId ตั้งใจไม่ผูก FK (ADR 0006)

index ที่เหลือ: detail มี (SnapshotHeaderId, CustCodeNormalized) (ไม่ unique — ค้นหลักฐาน), SourceBatchId, RecordGuid · header มี (GateResult, GenerationNo)

ชนิดข้อมูลที่ verify บน Postgres จริงแล้ว: MatchedRoles = jsonb, Similarity = numeric(9,8), CustCodeNormalized = varchar(50), EvidenceFingerprint = varchar(64)


4. กฎที่อยู่ในโค้ด ไม่ใช่ใน DB

กฎอยู่ที่
Normalize = trim + upper เท่านั้น ห้ามตัดศูนย์นำหน้าAmloCustCode.Normalize — ดู ADR 0007
MatchedRoles ถูก trim → distinct → เรียง ordinal → ถ้าว่างเป็น nullAmloScreenedCompany.NormalizeRoles
fingerprint กับ FingerprintVersion เกิดจาก call เดียวกัน ผู้เรียกยัดเองไม่ได้AmloScreenedCompany.Create — ดู ADR 0008
AmloType ที่มีขึ้นบรรทัดคั่นกลางถูกปฏิเสธ (กัน fingerprint ชนกัน)AmloEvidenceFingerprint.NormalizeAmloType
CustCode ว่างถูกปฏิเสธตั้งแต่ขาเข้า stagingAmloScreeningMatch.Create
หลักฐานชุดว่างสร้างคำตอบไม่ได้AmloScreenedCompany.Create

เหตุผลที่ไม่ใส่ guard ใน CopyFrom: CopyFrom อ่านแถวที่ EF materialize ขึ้นมา ซึ่งเลี่ยง Create ได้ ถ้า throw ตรงนั้นแถวเก่า 1 แถวจะล้ม generation ทั้งรอบ — จึงกันที่ทางเข้า (Create) แทน


5. หลักฐานว่ามันทำงานจริง

ทดสอบพฤติกรรมกับ Postgres จริง 10 ข้อ ผ่านทั้ง DEV และ UAT (รันใน BEGIN … ROLLBACK, ตรวจแล้วว่าไม่มีแถวตกค้าง: headers=0 companies=0)

  1. mark generation เป็น current ได้
  2. current ใบที่สอง ทำไม่ได้
  3. non-current หลายใบได้
  4. draft หลายใบถือ GenerationNo = NULL พร้อมกันได้
  5. GenerationNo ซ้ำ ทำไม่ได้
  6. RunId ซ้ำ ทำไม่ได้
  7. insert คำตอบพร้อม role array (jsonb) ได้
  8. "abc123" กับ " ABC123 " ชนกันจริง ✅ (พิสูจน์ ADR 0007)
  9. jsonb round-trip ภาษาไทยไม่เพี้ยน
  10. ลบ generation แล้ว cascade ลงคำตอบจริง

dotnet ef migrations has-pending-model-changesNo changes have been made to the model since the last migration.

⚠️ has-pending-model-changes = none เป็นแค่ครึ่งเดียวของหลักฐาน — test suite ใช้ EnsureCreated() จึงไม่เคยรัน migration เลย รายละเอียดที่ Open Concerns §2


6. งานชิ้นถัดไป — ตัวสร้าง generation (ยังไม่มีใครเขียน)

ตารางพร้อมแล้ว แต่ไม่มีอะไรเติมข้อมูลลงไป ผู้รับงานต่อต้องเขียน service ที่ทำตามลำดับนี้:

  1. อ่าน amlo_screening_match ของ batch ที่ IsTerminal แล้วเท่านั้น (ADR 0006 — seal ข้อมูลที่ยัง process ไม่จบไม่ได้)
  2. CreateDraft header (GenerationNo ยังเป็น NULL)
  3. CopyFrom ทุกแถวลง snapshot_detail
  4. จัดกลุ่มด้วยคีย์ที่ Compliance ยืนยัน — ข้อเสนอคือ (CustCode, RecordGuid) แต่ยังไม่มีมติ ดู Phase 1 §3
  5. AmloScreenedCompany.Create ต่อ 1 CustCode พร้อมส่งหลักฐานชุดเดียวกับที่เขียนลง detail
  6. ผ่านด่านตรวจ 6 ข้อ (Phase 1 §5.3) → Passed หรือ Held
  7. ปั๊ม GenerationNo แล้วสลับ IsCurrent ใน transaction เดียว

สิ่งที่ต้องตัดสินตอนเขียน (ยังไม่มีคำตอบเพราะยังไม่มีผู้เรียก): Compute โยน ArgumentException ได้แล้ว — จะให้ทั้ง generation Failed หรือส่งแถวนั้นไป reject?

ข้อควรระวังจาก Phase 1 ที่ยังใช้ได้อยู่: advisory lock ต้องซ่อนหลัง repository interface (SQLite ไม่มี pg_advisory_xact_lock) และ test harness ของ AmloIngestionRunService มี class ชื่อซ้ำกัน 2 namespace ต้องแก้ทั้งคู่


7. เดินสาย env เอง

dotnet ef migrations script <FromName> <ToName> --idempotent `
  --project src/Centralized02.Infrastructure `
  --startup-project src/Centralized01.API `
  --configuration Release --no-build `
  --output artifacts/amlo-snapshot.sql
จุดที่พลาดบ่อยทำยังไง
ต้องใช้ ชื่อ migration ไม่ใช่ timestamp เปล่า20260806094117 → error; ต้องใช้ AddCustCodeLogDatetimeUtcIndexToAmloScreeningMatch
--output เขียน UTF-8 BOM ออกมา แล้ว Postgres ตอบ syntax error at or near "START"ตัด BOM ก่อนส่ง (.replace(/^\uFEFF/, ''))
หลัง migrations remove ต้อง build ใหม่ ก่อน migrations addไม่งั้น --no-build อ่าน assembly เก่าแล้วบอกว่าชื่อซ้ำ
ไม่มี pipeline ไหนรัน migration ให้เป็น manual step ทุกครั้ง — ci.yml, ci-sit.yml, ci-orchestrate.yml ไม่มี step นี้

migration นี้ เพิ่มอย่างเดียว ไม่มี ALTER/DROP ของเดิม ⇒ DB ล้ำหน้า code ที่ deploy อยู่ได้อย่างปลอดภัย