Flow — Global Outlet & Cross-Tenant Sub-Ledger
① ทำไมต้องมี — บริบท ปัญหา และเป้าหมาย
ระบบปัจจุบัน (Baseline)
Flow เป็น Multi-Tenant SaaS สำหรับ FMCG trading (Control Plane + Application Plane; 8 regional pool + 2 premium silo; Keycloak Organizations)
- Tenant boundary = Supplier/Distributor — ข้อมูลทุกชิ้นมีเจ้าของเดียวผ่าน tenant context
- Loyalty Ledger เดิม headless + multi-program + multi-wallet (มี sub-wallet partition, FIFO buckets) แต่ทำ Tenant-Level Isolation
- Unified Wallet (Coin) เดิm — double-entry, FIFO, immutable; Coin เป็น cash-equivalent (deposit/returnable → coin)
- Global Master Data (GPM 3-layer) มีอยู่แล้ว: Global ID + Tenant Mapping; มี Global Business Partner และ Customer Master ระดับ Tenant มีฟิลด์
Global BP Number - Clearing House / Settlement ออกแบบให้ Principal ชดเชย Distributor เมื่อ redeem ข้ามองค์กรอยู่แล้ว
- ของเดิมที่ต่อยอดได้: Global BP, sub-wallet partition logic, Double-entry ledger, Coupon Registry, Promotion Rule Engine, Unified Wallet
ปัญหาเชิงธุรกิจ
ร้านค้าหนึ่งค้าขายใต้หลาย Tenant (Distributor) แต่ มูลค่าสะสม (value) ของร้านแตกเป็นเสี่ยงตาม Tenant:
- แต้ม/coin จาก Tenant A กับ B เป็นคนละก้อน ใช้ข้ามกันไม่ได้
- Principal/Brand ออกมูลค่า (แต้ม/credit) ได้แค่ราย Distributor — ไม่มีบัญชีระดับเครือข่าย
- ตัวตนร้านซ้ำซ้อน (1 ร้าน = หลาย Customer record คนละชุดข้อมูล)
- การชดเชยข้ามองค์กรไม่โปร่งใส เกิดข้อพิพาท
เอกสาร Flow เดิม (§3.6 Multi-Wallet) ตั้งคำถามนี้ไว้เองตรงๆ: "sub-agent อาจซื้อจากหลาย agent... point ที่ earn จาก agent รายนั้น นำไปใช้ที่อื่นได้หรือไม่" — โจทย์นี้คือคำตอบ และคำตอบคือ "สร้าง sub-ledger กลางต่อสมาชิก"
สิ่งที่จะสร้าง — 4 Core Models
| # | Core Model | สรุป |
|---|---|---|
| CM-1 | Global Outlet Identity | ร้านค้าลงทะเบียนรับ GOID เดียว ผูกหลาย Tenant Customer |
| CM-2 | Cross-Tenant Sub-Ledger (XSL) | บัญชีย่อยระดับสมาชิก (ผูก Global BP) รวม value ข้าม Tenant |
| CM-3 | Multi-Value / Multi-Currency Units | หลาย value-type/สกุลตามผู้ออก (Points/Coins/Miles/Deposit) |
| CM-4 | Cross-Tenant Settlement & Control-Account Clearing | Clearing House ชดเชยข้ามคู่ค้า (ผู้ออก ≠ ผู้รับ) roll-up ขึ้น control account |
Loyalty = value-type หลักของ CM-2/CM-3 (เช่น ThaiBev coalition points) — แต่ XSL เป็น primitive กว้างที่รองรับ Coin/Deposit/credit ได้ด้วย
เป้าหมาย & ตัวชี้วัด
| รหัส | เป้าหมาย | KPI |
|---|---|---|
| G1 | เพิ่ม retention ร้านค้าด้วย value พกพา | active member, cross-tenant redemption rate |
| G2 | ให้ Principal ออก value ทั้งเครือข่าย | #sponsor program, brand reach |
| G3 | ตัวตนร้านสะอาด single source | GOID coverage, dup-merge rate |
| G4 | ชดเชยข้ามองค์กรโปร่งใส | settlement accuracy, dispute rate, days-to-settle |
| G5 | สร้างรายได้ ledger-as-a-service | take-rate, liability under management |
ผู้เกี่ยวข้อง (Actors)
| Actor | บทบาท |
|---|---|
| Outlet / Retailer | เจ้าของ Member Account (ผูก GBP), ผู้ลงทะเบียน GOID, earn/redeem |
| Seller Tenant (Distributor) | จุดสัมผัส earn/redeem; ผู้รับ reimbursement |
| Principal / Brand | Sponsor / Data Steward / ผู้ออกทุน / เจ้าของ Control Account |
| Third-party Fund | บัตรเครดิต / App Wallet (co-fund) |
| Platform Operator | กติกา/อนุมัติ/ข้อพิพาท/ออก GOID |
①.x ส่วนเติมเต็ม Business Architecture (ให้ครบ framework)
Business / Revenue Model & Unit Economics
แพลตฟอร์มหารายได้แบบ sub-ledger / ledger-as-a-service take-rate
| รายการรายได้ | ผู้จ่าย | cost driver |
|---|---|---|
| Program subscription / setup ต่อ Principal | Principal | onboarding, config |
| % ของมูลค่าที่ออก (issuance fee) | Principal | ledger compute, liability mgmt |
| Settlement/clearing fee ต่อรอบ | Principal/Tenant | netting compute, ERP posting |
| Breakage share (value หมดอายุ) | — (รายได้ platform/principal) | — |
| FX/conversion fee (ข้าม value-type/สกุล) | ผู้แปลง | exchange compute |
Unit economics ที่ต้องวัด: liability under management, breakage %, cost ต่อ earn/redeem txn, take-rate เฉลี่ย, breakeven volume ต่อ program (ตัวเลขจริงให้ Finance เติม — ดู ⑧)
Enterprise Operating Model (centralized vs federated)
"federated execution + centralized trust" (สอดคล้อง 3-plane)
| centralize (Platform Core / Global Plane) | federate (Application Plane / tenant) |
|---|---|
| Global Outlet/BP Registry, Identity Resolution, Cross-Tenant Sub-Ledger, Clearing House, Central ID Allocator, Centralized AuthZ, Consent/PDPA, Audit, Keycloak | order/POS/CRM/pricing/inventory ของแต่ละ tenant (single-owner เดิม) |
MIT operating-model quadrant: "Coordination" — แชร์ลูกค้า(ร้าน)/value/ตัวตนข้าม unit แต่ process การขายแต่ละ tenant อิสระ
Product / Service Catalog ของแพลตฟอร์ม
| บริการ | กลุ่มเป้าหมาย | สาระ |
|---|---|---|
| Global Outlet Identity | platform/tenant | GOID + resolution (CM-1) |
| Cross-Tenant Sub-Ledger | principal | บัญชี value ข้ามเครือข่าย (CM-2) — flagship: loyalty |
| Multi-Value Units | principal | value-type ของผู้ออก (CM-3) |
| Settlement & Clearing | ทุกฝ่าย | ชดเชย/netting/ภาษี/control-account (CM-4) |
| Reward Catalog / Redemption | outlet/principal | แลกของรางวัล/ส่วนลด/cash-equivalent |
Personas & Customer Journeys
| Persona | Journey หลัก |
|---|---|
| Outlet | ลงทะเบียน GOID → consent → ซื้อ/earn value → ดู member account รวม → redeem ที่ tenant ใดก็ได้ → (คืนสินค้า) |
| Distributor (Tenant) | enroll โปรแกรม → earn หน้างาน → รับ reimbursement → ดู statement |
| Principal | สร้างโปรแกรม+agreement → ออกทุน → ดู liability/breakage (control account) → settle → dispute |
| Operator | ออก GOID/merge → กำกับ → ระงับข้อพิพาท → audit |
Capability Heat Map (ใหม่ vs มีอยู่ + ลำดับลงทุน)
| Capability | สถานะ | ลงทุน |
|---|---|---|
| Global Outlet Registry · Identity Resolution · Cross-Tenant Sub-Ledger · Settlement/Clearing | ใหม่ (P0) | สูง |
| Multi-Value Units · Breakage accounting · Consent/PDPA gating | ใหม่ (P1) | กลาง |
| Centralized AuthZ · Audit · Keycloak | ขยายของเดิม | กลาง |
| Sub-wallet partition · Double-entry ledger · Coupon · Promotion Rule Engine · Global BP · Unified Wallet | เดิม (reuse) | ต่ำ |
KPI Tree (driver → goal → KPI)
| Driver | Goal | KPI | baseline→target |
|---|---|---|---|
| value พกพา | G1 | cross-tenant redemption %, repeat purchase | (เติม) |
| โปรแกรมเครือข่าย | G2 | #program, brand reach, earn coverage | (เติม) |
| ตัวตนสะอาด | G3 | GOID coverage %, dup-merge rate, match precision | (เติม) |
| ชดเชยโปร่งใส | G4 | settlement accuracy, dispute rate, days-to-settle | (เติม) |
| รายได้ | G5 | take-rate, liability under mgmt, breakage % | (เติม) |
Governance Body & Dispute Model
- Ledger Program Committee (Platform + Principal) — กติกา funding, valuation, expiry, การ suspend program
- Dispute resolution — Principal ปฏิเสธ claim → Distributor ส่งหลักฐาน (Collaborator Portal) → กัน
disputedออกจากรอบ settlement (⑥.5) - Onboarding/Offboarding — เกณฑ์ enroll tenant เข้าโปรแกรม, จัดการ liability คงค้าง (control account) เมื่อ program ปิด
Regulatory & Adjacent Domains (ที่ต้อง align)
| โดเมน | ประเด็น | สถานะ |
|---|---|---|
| PDPA | consent ข้าม controller, purpose limitation, data residency (ไทย) | ⑥.4 + ⑥.16 |
| Stored value / e-money (ธปท.) | Coin/value cash-equivalent อาจเข้าข่าย e-money/payment | ต้องตรวจ legal → OI-L7 |
| ภาษี | มูลค่า value/ของรางวัลเป็นรายได้ผู้รับ? VAT ตอน redeem? | OI-L8 |
| Breakage / unclaimed | value หมดอายุ → รายได้ (IFRS 15) + กฎ unclaimed property | ⑥.13 |
| สินค้าควบคุม (สุรา/ยาสูบ) | ThaiBev = แอลกอฮอล์ → ข้อจำกัดโฆษณา/ส่งเสริมการขายสุรา | เงื่อนไขใน eligible_sku/agreement → OI-L9 |
| Anti-fraud/AML | earn-return loop, velocity, ฟอกมูลค่าผ่าน value | ⑥.12 |
② ข้อจำกัดหลัก — single-owner vs cross-tenant member account
นี่คือ แกนของวิธีคิดทั้งหมด
หลักการเดิม "ข้อมูลของ Tenant กั้นเด็ดขาด" เป็นรากฐานความปลอดภัย แต่โจทย์ต้องการ บัญชี value ใบเดียวต่อสมาชิกที่รวมรายการจากหลาย Tenant และ ใช้ value ข้าม Tenant — ขัดกับหลัก single-owner โดยตรง
ทำไม "แก้ตรงๆ" ไม่ได้
| วิธีที่ดูง่ายแต่ผิด | ทำไมถึงพัง |
|---|---|
| ให้บัญชี value อยู่ใน Tenant ใด Tenant หนึ่ง | Tenant อื่น earn/redeem ไม่ได้, ใครเป็นเจ้าของ liability? |
| เพิ่ม column "tenant ที่สอง" ในตาราง wallet | ทำลาย invariant single-owner, leak ข้ามได้ |
| copy value ไปทุก tenant | double-spend, reconcile ไม่ได้, ใครจ่าย? |
| รวมข้อมูลร้านสอง tenant ไว้ที่เดียวดื้อๆ | PDPA — ใครเป็น controller, เพิกถอนยังไง |
ข้อสรุปเชิงคิด: ต้อง "ยกระดับ" ไม่ใช่ "ยัดรวม" — เก็บ invariant single-owner ของข้อมูล Tenant ไว้ครบ แล้ว ยกบัญชี value + ตัวตนกลาง ขึ้นไปเป็น "Sub-Ledger บน Global Plane" ที่ Platform เป็นเจ้าของ โดยมี Principal เป็นผู้ออกทุน/steward (เจ้าของ Control Account) และมี consent ของร้านกำกับ
นี่คือนิยามคลาสสิกของ "subsidiary ledger": บัญชีย่อยที่บันทึกรายละเอียดต่อสมาชิก และ roll-up ขึ้น control account ใน GL — เราเพียงทำให้มัน cross-tenant
นิยามเจ้าของข้อมูลใหม่ (Re-anchoring Ownership)
| ชั้น | เจ้าของ | ตัวอย่าง | Plane |
|---|---|---|---|
| Tenant-Owned (เดิม) | Tenant | Order, Tenant Customer/Product | Application Plane |
| Platform-Owned (Global) | Platform | Global BP, Global Outlet, Global Product | Platform Core |
| Sponsor-Governed Sub-Ledger | Platform (steward = Principal) | Cross-Tenant Sub-Ledger + Member Accounts | Global Sub-Ledger Plane |
ข้อมูล Tenant ยังเป็นของ Tenant — แค่ถือ FK (Global BP Number, Global Outlet ID) ขึ้นชั้นกลาง
③ หลักการที่ยึด (Principles)
หลักสถาปัตยกรรม
| รหัส | หลักการ |
|---|---|
| AP-1 | Isolation by default — ตาราง tenant คง invariant single-owner ไม่เปลี่ยน |
| AP-2 | Global Sub-Ledger Plane เป็น bounded context แยก — เจ้าของ identity/member account/clearing กลาง |
| AP-3 | Consent-gated consolidation — รวม value/ข้อมูลข้าม tenant ต้องมี consent ของร้าน |
| AP-4 | No direct cross-DB reach — tenant ไม่อ่าน DB กันเอง ผ่าน Global API/event |
| AP-5 | Event-driven + Saga + Outbox/CDC — financial-grade, ไม่หาย/ไม่ซ้ำ |
| AP-6 | Reuse before build — ใช้ Global BP, sub-wallet partition, double-entry ledger, Unified Wallet, Clearing House เดิม |
| AP-7 | Fail-safe / Degrade — Sub-Ledger ล่ม → ห้าม commit burn (ไม่มีส่วนลดลอย), tenant ขายต่อได้ |
| AP-8 | Liability follows the bucket — ผู้ตั้งหนี้ตอน earn = ผู้ชดใช้ตอน redeem |
กฎเชิงธุรกิจ (Business Rules)
| รหัส | กฎ |
|---|---|
| BR-01 | Golden Rule of Portability — value เดินทางตามผู้ออกทุน (Principal⇒ข้าม tenant; Distributor⇒ล็อก) |
| BR-02 | Member-Account Anchor = นิติบุคคล (Global BP) — หลาย Outlet ของเจ้าของเดียวรวมยอด |
| BR-03 | Consent-before-Consolidate — claim/consent (PDPA) ก่อนรวมข้าม tenant |
| BR-04 | Liability follows the Bucket — ผู้ตั้งหนี้ = ผู้ชดใช้ |
| BR-05 | Value-Unit Isolation — value-type/สกุลต่างกันไม่รวมเอง ข้ามได้เฉพาะผ่าน Exchange Rate |
| BR-06 | Default Cross-Tenant Funding = Principal 100% — co-fund ข้ามคู่ค้าต้องมี agreement |
| BR-07 | Immutable & Zero-sum — ledger insert-only, settlement Σ=0 |
| BR-08 | Purpose Limitation — ใช้ข้อมูลเพื่อ ledger/settlement เท่านั้น ห้าม leak ราคาข้าม tenant |
| BR-09 | Clawback ย้อนตามสายเดิม — คืนสินค้า → เรียก value คืนตามสัดส่วน + ปรับ liability |
④ ไอเดียหลัก — Global Plane + Cross-Tenant Sub-Ledger
ภาพรวมไอเดีย
ยกตัวตน + บัญชี value + clearing ขึ้น Global Plane (bounded context แยก, Platform เป็นเจ้าของ) — สร้าง Cross-Tenant Sub-Ledger ที่บันทึกรายการ value ต่อสมาชิก (Global BP) ข้าม Tenant และตัดสิน portability ด้วย ผู้ออกทุน
หัวใจ: Golden Rule of Portability (consent + funding primitive)
- portability ตัดสินโดยผู้ออกทุน — Principal-funded ⇒ consolidate + redeem ข้าม tenant; Distributor-funded ⇒ ล็อก
- consolidate ต้องมี consent ของร้าน (PDPA) — เหมือน Collaboration Grant ของ Orchestration Layer
- liability ติดไปกับก้อน value (bucket) — ทำให้เคส "ผู้ออก ≠ ผู้รับ" ปิดบัญชีถูก
โครงสร้าง Member Account (XSL)
Member Account (key = Global BP) ← subsidiary ledger ระดับสมาชิก
└── Program THAIBEV_COALITION (owner = ThaiBev, value-type = Loyalty Points)
Available Balance (รวม) 670 ← redeem ข้ามได้ (roll-up ขึ้น Control Account ของ ThaiBev)
Source Partitions (attribution เพื่อ settlement เท่านั้น ไม่ล็อก):
SRC_TENANT_A 400 / SRC_TENANT_B 270
└── Program FLOW_COIN (value-type = Coin / cash-equivalent) ← primitive เดียวกัน
ตัวประกอบรากฐาน (Foundation Primitives)
| Primitive | บทบาท |
|---|---|
| Global Outlet / BP Registry | ตัวตนกลางของร้าน (GOID) + เจ้าของ account (GBP) |
| Identity Resolution | จับคู่ Tenant Customer → GOID (deterministic + AI) |
| Cross-Tenant Sub-Ledger | Member Account ต่อ (Program × GBP) ถือหลาย value-type |
| Sponsor/Coalition Agreement | สัญญา funding/scope/valuation/sign-off (เทียบ Collaboration Grant) |
| Settlement / Clearing House | attribution + multilateral netting + roll-up control account |
| Event Bus + Outbox + Saga | financial-grade eventing |
| Consent/PDPA + Audit | consent gating + ตรวจสอบ |
การวางบน 3-Plane Model (align Orchestration Layer)
| Plane | เจ้าของ | ของเอกสารนี้ |
|---|---|---|
| Control Plane | SaaS provider | Tenant onboarding, Central ID Allocator, Keycloak, Billing |
| Platform Core / Global Plane (HA ข้าม DC) | Platform | Global Registry, Identity Resolution, Cross-Tenant Sub-Ledger, Clearing House, Agreement, Consent, Centralized AuthZ, Audit |
| Application Plane (8+2 clusters) | Tenant | POS/OMS/CRM/Promotion (single-owner) |
Shared services ที่ reuse จาก Orchestration Layer (ไม่สร้างซ้ำ): Central ID Allocator · Clearing House/Settlement · Keycloak (Token Exchange) · Outbox+CDC/Event Bus · Centralized Authorization
Terminology mapping: "Sponsor/Coalition Agreement + Centralized AuthZ" (เอกสารนี้) = consent-gating แบบเดียวกับ "Collaboration/Grant" ของ Orchestration · ทั้งสองวางบน Platform Core/Global Plane เดียวกัน · Cross-Tenant Sub-Ledger ใช้ engine บัญชีเดียวกับ Settlement Ledger ของ Orchestration
Capability Map
| Capability | CM-1 | CM-2 | CM-3 | CM-4 |
|---|---|---|---|---|
| C1 Global Identity | ● | ○ | ||
| C2 Resolution/MDM | ● | ○ | ||
| C3 Sub-Ledger | ● | ● | ○ | |
| C4 Multi-Value | ○ | ● | ○ | |
| C5 Settlement | ○ | ○ | ● | |
| C6 Consent/PDPA | ● | ● | ||
| C7 Governance | ● | ● | ● | ● |
| C8 Redemption/Reverse | ● | ● | ● |
⑤ Core Models ทั้ง 4
แต่ละตัวเล่าตาม: business need → วิธีทำงาน → key decisions/edge (use case ละเอียดอยู่ใน ⑦, กลไกร่วมอยู่ใน ⑥)
⑤.1 CM-1 Global Outlet Identity
Business need: ร้านเดียวถูกสร้างเป็น Customer คนละ record ในหลาย Tenant → ต้อง resolve เป็นตัวตนเดียวโดยไม่ละเมิด PDPA Value: ตัวตนสะอาด · ฐานของ member account · single source of truth
Key decisions / edge: โมเดล 4 ชั้น Global BP (เจ้าของ account) → Global Outlet (GOID) → Tenant Customer (A,B) · ร้านไม่มี Tax ID/National ID OI-L2 · match กำกวม→Manual Review · false-merge→reversible MERGE (UC-F4) · isolation: ไม่ leak ข้อมูล A↔B; consolidate เฉพาะหลัง consent
⑤.2 CM-2 Cross-Tenant Sub-Ledger (XSL)
Business need: Principal อยากให้ value (แต้ม/credit) ของแบรนด์ตัวเองรวมข้ามเครือข่าย ใช้ที่ไหนก็ได้ และบัญชีต้อง audit ได้ระดับการเงิน Value: retention ร้าน · brand loyalty · liability รวมศูนย์ (control account)
Program Tiers
| ระดับ | Sponsor | Plane | Portability |
|---|---|---|---|
| Tenant Program (เดิม) | Distributor | Tenant-isolated | ล็อก |
| Sponsor/Coalition Program | Principal | Cross-Tenant Sub-Ledger | ข้าม tenant |
| Co-funded | Principal+Distributor | XSL, source partition แยกทุน | ตาม flag |
XSL = subsidiary ledger: Member Account roll-up ขึ้น Control Account ของ Sponsor (Available Balance รวม = ยอดที่ redeem ได้; Source Partitions = attribution เพื่อ settlement เท่านั้น ไม่ล็อก spend)
Key decisions / edge: anchor=GBP (หลาย outlet รวม); ยังไม่ consent → tenant-local pending; tier per-program vs global status OI-L3
⑤.3 CM-3 Multi-Value / Multi-Currency Units
Business need: ผู้ออกแต่ละราย/แต่ละ BU มี value-type ต่างกัน (แต้มเบียร์/สุรา/พันธมิตร/coin/มัดจำ) Value: ยืดหยุ่นต่อ Principal · แยก liability ต่อ value-unit
วิธีทำงาน: 1 Value-Unit = 1 Issuer; 1 Member Account ถือหลาย value-type; แยก ledger ต่อ unit; ข้าม unit = burn/mint double-entry + ย้ายเจ้าของหนี้ผ่าน ExchangeRate
Key decisions / edge: แยก fair_value (ตั้ง liability) vs redemption_value (มูลค่าตอนแลก) รองรับ breakage · lock-in/tier ต่อ unit · mixed-unit cart → แยก burn/claim ต่อ issuer · rounding policy OI-L1 · Coin/Deposit เป็น value-unit ชนิด cash-equivalent (เชื่อม Unified Wallet เดิม)
⑤.4 CM-4 Cross-Tenant Settlement & Control-Account Clearing
Business need: ผู้ออก value (Tenant A) กับผู้รับ redeem (Tenant C) เป็นคนละราย → ต้องชดเชยข้ามคู่ค้าอัตโนมัติ และ roll-up เข้า control account ของ Sponsor Value: โปร่งใส · ลดข้อพิพาท · auto-reconcile
Key decisions / edge: liability ติด bucket → Principal 42 / TenantA 15 / TenantB 3 → ชดเชย Tenant C (worked example ⑥.5) · co-fund ข้ามคู่ค้า = อุดหนุนคู่แข่ง → default Principal 100% OI-L5 · Negative balance ตอน clawback OI-L4
⑤.5 การประกอบกันข้าม Model
⑥ กลไกเบื้องหลัง (Mechanisms)
⑥.1 Data Architecture & Topology
- Global stores อยู่ cluster-independent (Tenant A/B คนละ cluster); Tenant DB ถือ FK ขึ้นกลาง — ไม่ duplicate ownership
- การอ่านข้าม tenant ผ่าน Centralized AuthZ (scope) — ส่งเฉพาะ field จำเป็น
- CQRS read-model: balance อ่านจาก compacted topic in-region; overspend ตัดสินที่ write path เท่านั้น
- Idempotency: ทุก mutating call มี key + processed-event store
- Migration/Backfill: map Customer เดิมทุก tenant → GOID (batch + AI) ก่อนเปิด consolidation (ดู ⑧ Transition)
⚠️ Entity หลัก: GlobalOutlet, OutletTenantMap, SponsorProgram, ValueUnit (currency), MemberAccount, PointBucket, SourcePartition, FundingProfile/FundContribution, LedgerJournal, SettlementClaim, ControlAccount, SponsorAgreement (field-level data dictionary → ดู ⑧ สิ่งที่ BA/Dev ต้องต่อยอด)
⑥.2 Eventing / Kafka (financial-grade)
Event envelope มาตรฐาน
{
"event_id":"uuid","event_type":"ValueEarned","schema_version":"1.0",
"occurred_at":"RFC3339","producer":"sub-ledger",
"tenant_id":"source","partition_key":"global_bp_no",
"correlation_id":"saga","causation_id":"...","idempotency_key":"orderId",
"payload":{ } // PII-free (token/ref เท่านั้น)
}
- Topic naming
subledger.<entity>.<event>.v<n>· partition key =global_bp_no→ per-member ordering (earn ก่อน redeem ไม่สลับ) - No dual-write → Transactional Outbox + CDC (Debezium): journal+outbox commit atomic → Debezium tail WAL → Kafka; DB = source of truth, Kafka = derived (replay ได้)
- Producer:
acks=all,enable.idempotence=true,min.insync.replicas≥2 - Consumer: at-least-once + idempotent (dedup store); settlement exactly-once-effect ด้วย business key (
claim_id/cycle_id) - Compacted state topics + tombstone:
subledger.account.balance.v1,consent.changed.v1,agreement.lifecycle.v1,outlet.lifecycle.v1→ read-model + real-time gating - Schema Registry (Avro, BACKWARD) + consumer-driven contract test ใน CI
- PII-free + right-to-erasure: ใส่ ref (
global_bp_no/goid); PII อยู่ service DB → crypto-shredding - Cross-DC: MirrorMaker2/stretched + partition ownership กัน split-brain + RPO ≤ 5s
- DLQ/Retry tiered (
...retry.5s/1m → ...dlq) + owner + alert + runbook
⑥.3 Saga / Workflow
- Orchestration-based saga (redeem/clawback/settlement); choreography สำหรับ relay เบา
- State persistence:
saga_instancesrecover หลัง crash; timer (hold TTL, agreement window); compensation ทุก step - Redeem saga:
HOLD[comp:release] → DISCOUNT → PAY → BURN+CLAIM[comp:reverse]; fail-safe = ไม่มี discount โดยไม่ตัด value; compensation ล้ม → escalate (saga_dead)
⑥.4 Security & AuthZ
| ด้าน | ข้อกำหนด |
|---|---|
| Scope | claim program_id/tenant_id/gbp_scope → Centralized AuthZ บังคับ slice |
| Cross-tenant token | Keycloak Token Exchange, อายุสั้น, ตรวจ revocation |
| Service-to-service | mTLS |
| Default deny / least privilege | Tenant เห็นเฉพาะ slice ที่ตน earn/redeem |
| Revocation | consent/agreement gate propagate < 1s (compacted topic → KTable) |
| PDPA | consent record, minimization, สิทธิ์ลบ, data residency ไทย |
| Audit | immutable WORM stream, cross-tenant/PII access logged |
Access scope
| บทบาท | เห็น |
|---|---|
| Outlet | member account รวมของตัวเอง |
| Tenant | เฉพาะ source partition ที่ตน earn/redeem |
| Principal | ทั้งโปรแกรม + liability/breakage (control account) |
| Operator | ดูแล + audit (impersonate logged) |
⑥.5 Settlement / Financial (สูตรเต็ม)
Double-entry postings (subsidiary → control account)
| เหตุการณ์ | Debit | Credit |
|---|---|---|
| Earn P value-unit c | Fund Budget (รายทุน) | Value Liability (Control Account) = P×v_c |
Redeem R @ T_r | Value Liability = R×m_c | Payable to T_r |
| Expiry (Breakage) | Value Liability | Breakage Income (รายทุน) |
| Settlement | Payable | Cash / AR-AP Offset |
Funding split (4 แบบ): Fixed % L_f=L×w_f · Fixed/unit min(a×q,L) · Threshold ∫φ · Capped min(L,B−U) overflow→distributor
Bucket attribution & FIFO relief: Reimburse_f = Σ_bucket(consumed_b × m_c × weight_{f,b}) → ผู้ออก≠ผู้รับ ถูกต้อง
Master netting → multilateral:
N(f→T)=Σ Reimburse_f(@T)+Σ C_fixed−Σ Chargeback−Σ D_dispute (แปลง THB)
→ bilateral net → multilateral (min cash-flow) → zero-sum guard → post ERP/B-Plus
Controls: idempotency cycle_id+party+source_txn, zero-sum guard, shadow reconciliation, encumbrance
Worked example: earn A(+500)+B(+300), redeem C(−600), co-fund 70/30 → FIFO burn E1(500)+E2(100) → Reimburse C 60฿ = ThaiBev 42 / TenantA 15 / TenantB 3 (Σ=0) · คงเหลือ 200, liability 20฿
⑥.6 Reverse / Clawback (Returns)
- คืนสินค้าหลัง earn → REVERSE bucket (ถ้ายังไม่ใช้) หรือ Negative Balance / Refund Offset (ถ้าใช้แล้ว)
- Partial clawback prorate; BR-09 ย้อนตามสายเดิม + ปรับ liability/settlement กลับทิศ
⑥.7 Integration กับของเดิม
| ของเดิม | ใช้อย่างไร |
|---|---|
Global BP / Global BP Number | anchor ของ member account + map Customer |
| Sub-wallet partition / double-entry ledger | ยกขึ้น Global plane เป็น XSL |
| Unified Wallet (Coin) เดิม | Coin = value-unit ชนิด cash-equivalent บน primitive เดียวกัน |
| Coupon Registry | redeem → issue coupon |
| Promotion Rule Engine | ตัดสิน eligible + earn |
| Clearing House / B-Plus/VSMS/Certu | settlement → control account → ERP |
| Keycloak / middleware | cross-tenant scope token |
⑥.8 State Machine Catalog
⑥.9 Domain Event Catalog
| Event | Producer | Consumer | partition_key |
|---|---|---|---|
| OutletActivated / Merged | Registry | CRM, Sub-Ledger | goid |
| ConsentChanged | Consent Svc | Sub-Ledger (gate) | global_bp_no |
| AgreementLifecycle | Agreement Svc | Sub-Ledger (gate) | program_id |
| ValueEarned / Redeemed / Expired | Sub-Ledger | Clearing, Notify, Analytics | global_bp_no |
| SettlementClaimCreated | Sub-Ledger | Clearing | program_id |
| SettlementRunPosted | Clearing | ERP webhook, Principal | program_id |
| ValueUnitConverted | Sub-Ledger | Clearing | global_bp_no |
⑥.10 NFR / Observability / Testing
- SLO: redeem hold/burn p95<200ms; earn eventual<5s; balance read lag<1s; gate propagation<1s; settlement<window; Kafka RPO≤5s/RTO≤15m; Sub-Ledger ≥99.95%
- Capacity baseline: ~300k outlets, earn peak ~2k/s, redeem ~500/s, partitions(earned)=24
- Observability: correlation/causation id ไหลทุก event → trace; metrics (liability, breakage, redemption %, consumer lag, settlement accuracy); alert (DLQ, saga_dead, Σ≠0, gate stale)
- Testing: contract testing (Pact) + schema; saga/chaos failure injection; cross-tenant isolation test suite; idempotency/replay; load test earn/redeem
⑥.11 Deployment / Infrastructure View (align Orchestration ⑥.11)
- Global Plane Active-Active ข้าม DC (รูปแบบ Keycloak HA); home-DC ownership ต่อ key range; อ่าน replica in-region, เขียน global
Technology mapping: Keycloak · PostgreSQL (Patroni multi-site) · Kafka+Debezium+Schema Registry · Redis (read-model) · MirrorMaker2 · B-Plus/ERP via API/Webhook · LLM/NLP (resolution) · Superset
⑥.12 Threat Model & Trust Boundaries (STRIDE)
| Threat | ความเสี่ยง | การลด |
|---|---|---|
| Spoofing | ปลอม tenant/ร้าน earn/redeem | Keycloak, mTLS, signed token |
| Tampering | แก้ payload/value | event signing, schema validation, immutable ledger |
| Repudiation | ปฏิเสธรายการ | audit immutable + correlation id |
| Info disclosure | เห็น account/ราคา tenant อื่น | default deny, scope, PII-free events |
| DoS | ถล่ม earn/redeem | rate limit per partner/tenant, backpressure |
| EoP | ยกระดับสิทธิ์/value งอก | enforce 2 ชั้น, zero-sum guard, OCC |
- Key/secret rotation + vault; token อายุสั้น + revocation cache; anti-fraud velocity/earn-return loop (⑥.10)
⑥.13 Master Data Management — Identity & Value-Unit Governance
- Golden record: GOID/GBP single source ข้าม tenant; canonical attribute เจ้าของ = Platform (steward)
- Identity Resolution: deterministic (Tax ID/National ID/phone/GLN) → probabilistic (AI/semantic) →
needs_review - Dedup/Merge: reversible + audit, re-key reference (UC-F4), ไม่ลบของเดิม
- Breakage governance: value หมดอายุ → ปลด liability เป็น Breakage Income ตาม IFRS 15 + กฎ unclaimed property
- Stewardship: คิว review, crowd-sourced suggestion ข้าม tenant โดยไม่ leak รหัสภายใน
⑥.14 Multi-Value & Financial Precision
- 1 value-unit = 1 issuer;
decimal_placesต่อ unit; rounding policy ชัด; minor unit (สตางค์) ทั้งระบบ (OI-L1) fair_valuevsredemption_value; FXExchangeRateeffective-dated +governed_by- ข้าม unit = burn/mint double-entry + ย้ายเจ้าของหนี้ + settlement entry; settle หลาย unit → แปลง THB ก่อน net
⑥.15 Resilience Patterns
| Pattern | ใช้ที่ไหน |
|---|---|
| Fail-safe | Sub-Ledger ไม่ตอบตอน burn → ไม่ commit (ไม่มีส่วนลดลอย); tenant ขายต่อได้ |
| Circuit breaker / bulkhead | call ข้าม service (sub-ledger, ERP) แยก pool |
| Rate limit / backpressure | earn/redeem ต่อ partner |
| Retry + idempotency | ทุก command/event (tiered → DLQ) |
| Timeout + compensation | hold TTL, agreement window |
| Shadow reconciliation | journal vs balance + stream lag/gap |
⑥.16 Data Lifecycle & Retention
| ชั้น | retention | หมายเหตุ |
|---|---|---|
| Ledger / settlement journal | ≥ 1–7 ปี (ภาษี/IFRS) | immutable WORM |
| Event stream (operational) | 30–90 วัน | + compacted state ∞ |
| PII (registry) | ตาม consent + PDPA | crypto-shredding, right-to-erasure |
| Saga instances (เสร็จ) | archive หลัง N วัน |
- Data residency: ข้อมูล+ledger ในไทย (PDPA) → กระทบเลือก DC/region
⑥.17 Interface / API Catalog
| Interface | รูปแบบ | หมายเหตุ |
|---|---|---|
| Outlet Registry / Resolve / Claim | sync REST | Idempotency-Key |
| Earn / Hold / Burn / Release | sync REST | 2PC/Saga, idempotent |
| Member Account / Balance query | sync REST | scope-filtered (CQRS read) |
| Settlement Run / Liability / Export | sync + webhook | HMAC sign, retry |
| Domain events | async Kafka | envelope ⑥.2 |
| Agreement / Consent | sync REST | sign-off / gating |
ทุก mutating call ต้องมี
Idempotency-Key/X-Request-ID
⑥.18 Capacity & Quality-Attribute Scenarios
- Capacity (เติมตัวเลขจริง): earn/redeem peak, #outlet, #program, #partition, throughput ledger
- QA scenarios (ATAM-style):
- Performance: "redeem 500/s peak → hold/burn p95<200ms"
- Availability: "Global Plane DC1 ล่ม → DC2 รับต่อ, redeem/settle ไม่หยุด, RTO<15m"
- Modifiability: "เพิ่ม Principal/value-unit ใหม่ → ใช้ program/unit/agreement เดิมโดยไม่แก้ tenant service"
- Security: "Tenant A ขออ่าน member account ที่ไม่ได้ earn/redeem → ปฏิเสธ + audit"
⑦ Use Cases (ละเอียด)
โครงสร้างทุก UC: Meta → Actors → Pre/Post → Trigger → Main → Alternate → Exception → Rules → Data → Permission → NFR → Sequence → Acceptance(Gherkin) → Open
Index
| UC | ชื่อ | Model | Priority | Complexity |
|---|---|---|---|---|
| F1 | Establish Sponsor Program + Agreement | Foundation | P0 | L |
| F2 | Register Global Outlet | CM-1 | P0 | L |
| F3 | Resolve & Link Tenant Customer | CM-1 | P0 | M |
| F4 | Merge GOID | CM-1 | P1 | M |
| F5 | Settlement Run | CM-4 | P0 | L |
| L1 | Earn Value (cross-tenant) | CM-2 | P0 | XL |
| L2 | Redeem Value (cross-tenant) | CM-2/4 | P0 | XL |
| L3 | View Member Account | CM-2 | P0 | S |
| L4 | Value-Unit Conversion | CM-3 | P1 | M |
| L5 | Clawback (return) | CM-2/4 | P1 | L |
| L6 | Manage Consent / Revoke | CM-1 | P0 | M |
| L7 | Dispute Handling | CM-4 | P1 | M |
Foundation
UC-F1 — Establish Sponsor Program + Agreement
Meta: Foundation · P0 · L · sponsor_program,sponsor_agreement,value_unit,control_account · Actors: Principal, Distributor(ร่วม), Operator · Pre: Principal เป็น Global BP · Post: program ACTIVE + agreement signed + control account เปิด (ถ้าไม่ครบ sign-off → ไม่ ACTIVE) · Trigger: Principal เปิดโปรแกรม
Main: 1) นิยาม program+value-unit+scope+funding+valuation 2) สร้าง agreement DRAFT 3) ส่ง Collaborator Portal 4) ทุกฝ่าย Digital Sign-off 5) ACTIVE+เปิด control account+emit AgreementLifecycle
Alternate: 4a ขอแก้→DRAFT · Exception: 1a funding ไม่สมดุล/ไม่มี value-unit→reject · Rules: BR-06 · NFR: audit, versioned agreement
Scenario: เปิดโปรแกรมสำเร็จ
Given Principal เป็น Global BP และ funding profile สมดุล
When ทุกฝ่าย sign-off
Then program ACTIVE, control account เปิด และ earn ได้
Scenario: sign-off ไม่ครบ
Then program คง PENDING_SIGNOFF ยัง earn ไม่ได้
Open: เกณฑ์ co-fund ที่ต้อง sign-off ของ distributor? (OI-L5)
UC-F2 — Register Global Outlet
Meta: CM-1 · P0 · L · global_outlet,global_bp,consent · Actors: Outlet, Resolution, ID Allocator, Consent Svc · Pre: มี Tax ID/National ID + เบอร์ OTP · Post(success): GOID ACTIVE ผูก GBP + consent · Minimal: ไม่ครบ → PENDING ไม่สูญข้อมูล · Trigger: สมัครผ่าน Flow App
Main: 1) กรอก+GPS+รูป 2) OTP 3) deterministic match 4) ไม่เจอ→mint GOID+upsert GBP(PENDING) 5) consent+claim 6) ACTIVE+OutletActivated
Alternate: 3a เจอ deterministic→link · Exception: 3b AI กลาง→Manual Review; 2e OTP fail→ERR-401-OTP; 5e ปฏิเสธ consent→PENDING ไม่ consolidate
Rules: BR-02,03,08 · NFR: OTP→activate p95<3s, resolution<800ms→fallback manual
Scenario: ลงทะเบียนใหม่สำเร็จ
Given OTP ผ่านและไม่มี outlet ซ้ำ
When ส่งข้อมูล+consent
Then GOID ACTIVE ผูก GBP จาก Tax ID
Scenario: พบซ้ำ deterministic
Then เสนอ link GOID เดิม (ERR-409-MATCH)
Scenario: ปฏิเสธ consent
Then GOID PENDING ไม่เปิด consolidation
Open: ไม่มี Tax ID/National ID ทั้งคู่? (OI-L2)
UC-F3 — Resolve & Link Tenant Customer
Meta: CM-1 · P0 · M · outlet_tenant_map · Actors: Tenant, Resolution · Pre: สร้าง/แก้ Customer · Post: (tenant,customer)→GOID · Main: ส่ง candidate→match→(เจอ)เสนอ link รอ consent→บันทึก map · Alternate: ไม่เจอ→mint GOID PENDING + suggestion ให้ tenant อื่น · Exception: กำกวม→Manual Review · Rules: BR-03 · NFR: batch, deterministic+audit
Scenario: link สำเร็จ
Given Customer ใหม่ตรง GOID เดิม (Tax ID)
Then เสนอ link, หลัง consent บันทึก map
Open: match threshold? canonical attribute เจ้าของ?
UC-F4 — Merge GOID
Meta: CM-1 · P1 · M · global_outlet · Actors: Operator · Pre: พบ 2 GOID = ร้านเดียว · Post: เหลือ GOID เดียว, ยอดโอน, MERGED · Main: ตรวจหลักฐาน→ย้าย map→Journal transfer ยอด bucket (ไม่ลบ, audit)→DEPRECATE · Exception: value-unit/ยอดไม่ตรง→map ตาม unit, log discrepancy · Rules: BR-07
Scenario: merge ร้านซ้ำ
Given 2 GOID พิสูจน์ว่าร้านเดียว
Then ย้าย mapping+โอน bucket (journal transfer), GOID เก่า MERGED
Open: เกณฑ์ auto-merge vs manual?
UC-F5 — Settlement Run
Meta: CM-4 · P0 · L · settlement_claim,settlement_run,control_account · Actors: Operator/scheduler, Clearing House, ERP · Pre: มี claim finalized · Post(success): net transfers Σ=0 + Memo posted + control account reconciled · Minimal: Σ≠0→LOCK ไม่ post · Trigger: cron ตาม cycle
Main: 1) รวม claim finalized 2) เมทริกซ์ N(f→T) แปลง THB 3) validate vs agreement + กัน double-claim 4) multilateral net 5) zero-sum guard 6) post ERP (Credit/Debit Memo) + roll-up control account 7) webhook
Exception: Σ≠0→LOCK+alert; ERP fail→retry→runbook; dispute เปิด→กันออกรอบหน้า · Rules: BR-04,06,07 · NFR: idempotent ต่อ cycle_id, replay ให้ผลเดิม
Scenario: netting zero-sum
Given claims ThaiBev→C 42, A→C 15, B→C 3
Then net transfers รวม 60 ให้ C, Σ=0, post Credit Memo
Scenario: ไม่บาลานซ์
Then LOCK + alert, ไม่ post
Open: Flow เป็นคู่สัญญาการเงินกลาง? ผู้ออกใบกำกับ? (OI-L8)
Ledger Operations
UC-L1 — Earn Value (cross-tenant)
Meta: CM-2 · P0 · XL · value_bucket,ledger_journal · Actors: Tenant POS (for Outlet), Rule Engine, Sub-Ledger, Clearing · Pre: GOID ACTIVE+consent, program ACTIVE, tenant∈scope · Post(success): bucket EARNED + journal(Σ=0) + liability accrued (control account) · Minimal: ledger fail → ไม่ตัดสต็อก/ขายไม่ล้ม (async outbox retry) · Trigger: order finalize (Close Session)
Main: 1) Rule Engine ตรวจ eligible SKU→value+program+funding 2) resolve customer→GBP 3) POST earn(idempotent, source_tenant, source_order) 4) bucket+weights+source partition 5) accrue liability ตาม FundContribution 6) ValueEarned
Alternate: multiplier/temporal; consent ยังไม่มี→tenant-local pending · Exception: SKU ไม่ entitled→ไม่ earn; dup key→คืนผลเดิม; ledger down→outbox retry; void ก่อน finalize→ไม่ earn
Rules: BR-01,04,07 · NFR: async<5s, idempotent ต่อ order, zero-sum
Scenario: earn เข้า member account ระดับนิติบุคคล
Given ร้านมี consent, program ThaiBev ACTIVE
When ซื้อ eligible ที่ Tenant A ได้ 500 (loyalty points)
Then bucket 500 ใน member account ของ GBP, liability 50฿ = ThaiBev 35/TenantA 15
Scenario: idempotent
Then ส่งซ้ำคืนผลเดิม ไม่สร้าง bucket ใหม่
Scenario: ยังไม่ consent
Then value tenant-local pending ไม่ consolidate
Open: expiry ค่ากลาง vs ต่อ agreement? (OI-L6)
UC-L2 — Redeem Value (cross-tenant, ผู้ออก≠ผู้รับ)
Meta: CM-2/4 · P0 · XL · value_bucket,settlement_claim · Actors: Tenant Checkout (for Outlet), Sub-Ledger, Clearing · Pre: available≥R, program portability=CROSS_TENANT · Post(success): REDEEMED + claim ต่อ fund · Minimal: payment fail/timeout→release, ไม่ตัด value · Trigger: checkout ขอใช้ value
Main(2-phase): 1) HOLD R (FIFO ข้าม bucket, TTL) 2) คำนวณ fund splits จาก weight 3) discount=R×m_c 4) จ่ายเงิน 5) BURN(2PC, Close Session) 6) ปลด liability→Payable to redeeming tenant 7) ValueRedeemed
Alternate: mixed-unit→แยก burn/claim ต่อ issuer · Exception: available ไม่พอ→422; TTL หมด→auto-release; sub-ledger ไม่ตอบตอน burn→fail-safe ไม่ commit; cancel หลัง burn→REVERSE (UC-L5)
Rules: BR-01,04,05,07 · Saga: HELD→DISCOUNTED→PAID→BURNED (compensation ถอย) · NFR: p95<200ms sync, idempotent ต่อ checkout
Scenario: redeem ข้าม tenant ผู้ออกคนละราย
Given 800 value (500@A,300@B), redeem ที่ C
When redeem 600 สำเร็จ
Then FIFO 500@A+100@B, Payable to C 60฿ = ThaiBev 42/A 15/B 3, คงเหลือ 200
Scenario: TTL หมด
Then auto-release กลับ EARNED
Scenario: sub-ledger ไม่ตอบตอน burn
Then ไม่ commit (fail-safe), checkout void/retry
Open: redeem ที่ merchant นอกโปรแกรม? (OI-L10)
UC-L3 — View Member Account
Meta: CM-2 · P0 · S · read-model · Actors: Outlet/Tenant/Principal · Pre: auth+scope · Post: แสดงตามสิทธิ์ · Main: Outlet เห็นยอดรวมทุก value-unit · Tenant เห็นเฉพาะ source partition ตน · Principal เห็นทั้งโปรแกรม+liability · Rules: BR-08 · NFR: read-model lag<1s
Scenario: scope filtering
Given Tenant A เปิด member account ของร้าน
Then เห็นเฉพาะ SRC_TENANT_A ไม่เห็นของ B
Open: response shape ต่อ persona?
UC-L4 — Value-Unit Conversion
Meta: CM-3 · P1 · M · value_bucket,exchange_rate · Actors: Outlet/Platform · Pre: ExchangeRate active · Post: ย้ายมูลค่า+เจ้าของหนี้ · Main: ตรวจเรต→BURN ต้นทาง(FIFO)→MINT ปลายทาง(double-entry)→settlement entry issuer→issuer · Rules: BR-05 · NFR: double-entry zero-sum
Scenario: แปลง points→coin
Given เรต 10 PTS=1 COIN active
When แปลง 500 PTS
Then BURN 500 PTS, MINT 50 COIN, settlement ThaiBev→Platform
Open: ใครอนุมัติเรต? rounding?
UC-L5 — Clawback (return)
Meta: CM-2/4 · P1 · L · value_bucket,settlement_claim · Actors: Tenant/Platform · Pre: Return/Refund หลัง earn · Post: liability ปรับถูก · Main: คำนวณ prorate→(ยังไม่ใช้)REVERSE bucket→(ใช้แล้ว)Negative Balance/Refund Offset · Rules: BR-09 · NFR: linked parent_id
Scenario: คืนสินค้าก่อนใช้ value
Then REVERSE, liability ลดตาม weights
Scenario: คืนหลังใช้ value
Then Negative Balance หรือ Refund Offset (ตาม policy OI-L4)
Open: default negative-balance policy? (OI-L4)
UC-L6 — Manage Consent / Revoke
Meta: CM-1 · P0 · M · consent · Actors: Outlet · Pre: GOID · Post: consent state + gate update<1s · Main: แสดง purpose/scope→ยินยอม→เปิด consolidation · ถอน→หยุด consolidate รายการใหม่ · Rules: BR-03,08 · NFR: gate propagate<1s (compacted topic)
Scenario: ถอน consent
When ถอน
Then ภายใน<1s earn ใหม่ไม่ consolidate
Open: consent versioning / re-consent trigger?
UC-L7 — Dispute Handling
Meta: CM-4 · P1 · M · settlement_claim · Actors: Principal/Tenant · Pre: claim BILLED · Post: resolved · Main: Principal reject→Distributor ส่งหลักฐาน(Portal)→review ใน window→ปรับ D_dispute หรือคงรายการ · Rules: BR-07 · NFR: กัน disputed ออกจากรอบ
Scenario: dispute แล้ว resolve
Given Principal reject claim
Then review → VERIFIED หรือหัก D_dispute
Open: dispute SLA, partial-accept?
⑧ Decisions & Open Issues
ลำดับการ build (Rollout)
| Phase | ส่งมอบ | เหตุผล |
|---|---|---|
| 0 Foundation | ID Allocator, Outlet/BP Registry, Resolution, OutletTenantMap, Cross-Tenant Sub-Ledger, Kafka/Outbox (P0), Clearing skeleton | ทุก model พึ่งพา |
| 1 Pilot | 1 Principal (ThaiBev), 2 tenant, Sponsor Program + Member Account (earn+redeem loyalty points) | พิสูจน์ portability |
| 2 Settlement | Clearing cross-party + liability/breakage + control account + ERP webhook | เงินจริง |
| 3 Scale | Co-funded, Multi-value (coin/deposit), ช่องทางกลาง Principal, Cross-DC, GLN | ขยาย |
RACI
| กิจกรรม | Principal | Distributor | Platform | Outlet |
|---|---|---|---|---|
| ออกแบบ program/value-unit/agreement | R/A | C | C | I |
| ออก GOID / Resolve / Merge | I | C | R/A | C |
| Consent | I | I | C | R/A |
| Earn/Redeem หน้างาน | I | R | A | R |
| Settlement/Netting | C | C | R/A | I |
| Dispute | R | R | A | I |
Open Issues
| รหัส | ประเด็น |
|---|---|
| OI-L1 | หน่วยเงิน minor unit (สตางค์) ทั้งระบบ + rounding policy เศษ value |
| OI-L2 | ร้านไม่มี Tax ID/National ID ทั้งคู่ — identity อย่างไร |
| OI-L3 | Tier per-program vs Global Status ข้าม issuer |
| OI-L4 | Negative balance ตอน clawback: ติดลบ vs refund offset (default) |
| OI-L5 | co-fund ข้าม tenant อนุญาต vs Principal 100% เท่านั้น |
| OI-L6 | Expiry policy ค่ากลาง vs ต่อ agreement |
| OI-L7 | Coin/value = stored value เข้าข่าย e-money/ธปท. หรือไม่ |
| OI-L8 | ภาษี/ผู้ออกใบกำกับ ตอน redeem ของรางวัล; Flow เป็นคู่สัญญาการเงินกลาง? |
| OI-L9 | สินค้าควบคุม (สุรา): ข้อจำกัดส่งเสริมการขาย/โฆษณา ใน eligible_sku/agreement |
| OI-L10 | redeem ที่ merchant นอกโปรแกรม (Flow ecosystem) — เฟสไหน |
| OI-L11 | ขอบเขตเชื่อมกับ Orchestration Layer: ใช้ GOID/GBP เป็น customer identity ของ orchestration ด้วยไหม |
Architecture Decision Records (ADR)
| ADR | การตัดสินใจ | สถานะ | เหตุผล |
|---|---|---|---|
| ADR-01 | คง single-owner, ยกตัวตน/บัญชี value ขึ้น Global Plane เป็น Sub-Ledger ("ยกระดับไม่ยัดรวม") | Accepted | รักษา tenant isolation (§②) |
| ADR-02 | Portability ตัดสินด้วยผู้ออกทุน (Golden Rule) | Accepted | ตอบโจทย์ §3.6 เดิม (§④) |
| ADR-03 | Member-Account anchor = นิติบุคคล (Global BP) | Accepted | ยืนยันโดย business |
| ADR-04 | Liability ติด bucket (FIFO relief) → รองรับผู้ออก≠ผู้รับ | Accepted | ความถูกต้อง settlement (§⑥.5) |
| ADR-05 | Event-driven + saga + Outbox/CDC; reuse Clearing House/Keycloak/ID Allocator | Accepted | financial-grade + align Orchestration |
| ADR-06 | Consent-before-consolidate (PDPA gating real-time) | Accepted | กฎหมาย (§⑥.4) |
| ADR-07 | Value-unit isolation; ข้าม unit ผ่าน FX + ย้ายเจ้าของหนี้ | Accepted | บัญชีถูก (§⑥.14) |
| ADR-08 | Sub-Ledger เป็น primitive กว้าง (loyalty/coin/deposit) ไม่ผูกเฉพาะ loyalty | Accepted | reuse Unified Wallet + ขยายได้ (§④) |
| ADR-09 | Default cross-tenant funding = Principal 100% | Proposed | กันอุดหนุนคู่แข่ง (ทบทวน OI-L5) |
Transition / Coexistence Architecture
| ขั้น | สาระ |
|---|---|
| Strangler | เปิด sponsor program ทีละ program/tenant ไม่ big-bang |
| Dual-run | tenant program เดิมทำงานปกติ; sub-ledger earn ค้าง pending ก่อน consent |
| Backfill | resolve Customer เดิม→GOID (batch+AI) + รณรงค์ consent ก่อน consolidate |
| Cutover ต่อราย | เปิดต่อ program/tenant + rollback + fail-safe |
| Coexistence | tenant program (ล็อก) อยู่ร่วมกับ sponsor program (ข้าม) ได้ — portability flag ตัดสิน |
สิ่งที่ BA/Dev ต้องต่อยอด
- Wireframe (Flow App member account, operator console, principal dashboard)
Endpoint spec + data dictionary→ ทำแล้ว ใน ส่วนที่ 2 + §I DB Schema — เหลือ generate ไฟล์ OpenAPI 3.1 (subledger-openapi/openapi.yaml) จาก endpoint blocksตาราง error code→ ทำแล้ว ใน §D — เหลือเติมข้อความผู้ใช้ (ไทย) ต่อ code- ตัวเลข NFR/SLO + capacity จริง
- Event schema จริง (Avro) ต่อ event ใน ⑥.9 + Schema Registry
- B-Plus/VAT/IFRS mapping (settlement+breakage+control account) — เสี่ยงสูง ทำก่อน go-live
- Report/analytics (liability, breakage, redemption %, settlement summary)
AuthZ matrix→ ทำแล้ว ใน §C.3 — เหลือ Saga compensation รูปธรรมต่อ step (โครงอยู่ ⑥.3)
ส่วนที่ 2 — API Specification (มาตรฐานเดียวกับเอกสาร OS)
ส่วนนี้แปลง design ①–⑧ เป็นสเปกระดับ endpoint ตามมาตรฐานเดียวกับ
Flow Orchestration Layer - OS.md(Foundations → endpoint blocks → ภาคผนวก) — narrative ของแต่ละ UC อยู่ ⑦ แล้ว ที่นี่ให้เฉพาะ contract: paths, headers, request/response, error codes โครงสร้าง: A. สิ่งที่สำรวจจาก source จริง → B. Conventions → C. Auth + AuthZ matrix → D. Error catalog → E. Integration mapping → S1–S4 endpoint blocks ครบ 12 UC (F1–F5, L1–L7) — สถาปัตยกรรม (F ของมาตรฐาน OS) ไม่ทำซ้ำ: ดู ⑥.1 / ⑥.2 / ⑥.11
A. ความเข้ากันได้กับ flow-api — สิ่งที่สำรวจจาก source จริง
สเปกนี้ อิงของจริง: loyalty/wallet primitive มีอยู่แล้วใน tenant layer (flow-api + flow-library) — XSL ไม่ได้สร้างจากศูนย์ แต่ "ยกแบบ" ของเดิมขึ้น Global Plane ตาราง mapping ของจริง → ของใหม่:
| ของจริงใน code | path (จริง) | ใช้ต่อยอดเป็น |
|---|---|---|
points — id, name, supplier_id (NULLABLE), is_default, type, principle | flow-library dao/model/points.gen.go | ราก Value Unit (CM-3) — คอลัมน์ principle (ผู้ออกระดับแบรนด์) มีอยู่แล้ว; supplier_id nullable = point ที่ไม่ผูก tenant เดียวมีอยู่แล้ว |
wallets — customer_id, point_id, balance, coin_id, coin_balance | dao/model/wallets.gen.go | wallet ระดับ tenant (ยอดรวมเดียว ไม่มี bucket/FIFO) → ยกขึ้นเป็น Member Account |
point_balance — customer_id, point_id, wallet_id, balance | dao/model/point_balance.gen.go | sub-wallet partition ต่อ point_id ของจริง — ตรรกะเดียวกับ Source Partition ของ XSL |
point_transactions — wallet_id, supplier_id, point, order_id, reward_id, point_status_id, campaign_id, point_id | dao/model/point_transactions.gen.go | journal ระดับ tenant — มี supplier_id ต่อรายการอยู่แล้ว = ราก attribution; XSL เพิ่ม double-entry + bucket |
coin twins — coin, coin_transactions, coin_exchanges, coin_reasons, coin_statuses | dao/model/coin*.gen.go | Coin = value-unit ชนิด cash-equivalent (⑥.7, ADR-08) — primitive เดียวกัน |
point_exchanges — rate, maximum, maximum_type enum('percent','amount'), start_date/end_date, is_default | dao/model/point_exchanges.gen.go | ราก ExchangeRate effective-dated (⑥.14, UC-L4) |
customers — outlet_id varchar(36) มีอยู่แล้ว, personal_id varchar(13), phone_1/2, has_liquor_license + license fields | dao/model/customers.gen.go | จุดเกี่ยว GOID ฝั่ง tenant (UC-F3) + identity attributes (UC-F2) + เงื่อนไขสุรา (OI-L9) |
routes ordering/points (AuthorizationCustomerToken) · backoffice/points (AuthorizationSupplierToken) — GET /wallet/:id, POST /reward, PUT /adjust/:id, GET /transactions/history, PUT /cancel/:id | product-service/startup/prodmodule/routes.go | ช่องทาง earn/redeem/adjust เดิม — spec นี้ mirror วิธี mount + middleware naming |
BevFam bridge — GetBevFamPoint, PointRedeemBevfamRequest (SermsukRoyaltyService) | product-service/handler/ordering/pointhandler/point.go | precedent การ bridge loyalty ภายนอกเข้า wallet — pattern เดียวกับ partner value-unit |
backoffice/reports/promotion-settlement* (8 endpoints) | prodmodule/routes.go | รายงาน settlement เดิม → ฐาน statement/claim report (UC-F5) |
httpserve.Response — {status_code, message, data}, NewResponse/NewResponseWithData | flow-library library/httputil/httpserve/handler.go | envelope status/error (§D) |
Promotion Rule Engine — backoffice/promotions (Put /activate,/deactivate), promotionsvc | prodmodule/routes.go | ผู้ตัดสิน eligible SKU + จำนวน earn (UC-L1 ขั้น 1) |
สิ่งที่ไม่มีในของเดิม → XSL ต้องเพิ่ม (สอดคล้องที่เอกสาร OS §A.1 ระบุ): FIFO bucket + expiry ต่อก้อน · hold/2-phase burn · double-entry journal (ของเดิม insert + PUT /cancel/:id) · Idempotency-Key/X-Request-ID · Outbox/CDC/Kafka · consent gating · cross-tenant scope token · control account/clearing
ข้อสรุป reuse: tenant program เดิม (
wallets/point_transactions) คงเดิมไม่แตะ — sponsor/coalition program เท่านั้นที่วิ่งขึ้น XSL (Transition ⑧: Coexistence — portability flag ตัดสิน)
B. Conventions (native flow-api)
Base URL & Mount point
https://ttmart-gateway.flow-solution.co/api/v1/subledger
- service ใหม่
subledger-serviceบน Global Plane (bounded context เดียวครอบ registry + ledger + clearing ตาม AP-2 — แยก service ภายหลังได้โดย path ไม่เปลี่ยน) - mount แบบเดียวกับ flow-api:
api := app.Group("api/v1")แล้วapi.Group("subledger/...")— pattern เดียวกับ channelordering/backoffice/เดิม
// subledger-service/startup/subledgermodule/routes.go (เสนอ — mirror prodmodule/routes.go จริง)
api.Group("subledger/member-accounts").Use(middleware.AuthorizationSubLedgerToken()).
Get("/:gbp_no", h.GetMemberAccount).
Get("/:gbp_no/transactions", h.GetTransactions)
api.Group("subledger/holds").Use(middleware.AuthorizationSubLedgerToken()).
Post("/", h.CreateHold).
Post("/:id/burn", h.Burn).
Put("/:id/release", h.Release)
Headers (ตามมาตรฐาน OS §B — ของใหม่ที่ tenant layer ไม่มี)
| Header | ใช้เมื่อ |
|---|---|
Authorization: Bearer <jwt> | ทุก protected route (token ตาม §C) |
Idempotency-Key | บังคับ ทุก POST/PUT ที่เปลี่ยนสถานะ — semantics ตาม OS §B (key เดิม+payload เดิม→ผลเดิม · key เดิม+payload ต่าง→409 IDEMPOTENCY_KEY_REUSED · ไม่ส่ง→400 IDEMPOTENCY_KEY_REQUIRED) เก็บใน processed_events TTL 24 ชม. |
X-Request-ID | correlation/audit — ไหลเข้า correlation_id ของ event envelope ⑥.2 |
Path / Query / Pagination
- ตามมาตรฐาน OS §B ทุกข้อ: path kebab-case (
member-accounts,settlement-runs,value-units) · param:id/:gbp_no/:goid· query snake_case · paginationpage/page_size→{ total, page, page_size, data: [] } - status-change ใช้
PUT /.../:id/<verb>(mirrorPut /activate//deactivateของจริง) — ยกเว้นPOST /holds/:id/burnที่เป็นการ สร้างผลทางบัญชี (journal) ไม่ใช่แค่เปลี่ยนสถานะ - ทุก mutating call ข้าม tenant ตอบ
202เมื่อเข้าคิว saga/outbox (earn, merge, clawback, settlement run) และ200/201เมื่อผลจบใน sync path (hold, burn — ต้องการคำตอบทันทีที่ checkout, SLO p95<200ms ⑥.10)
Health check
GET /health
Auth: public — pattern เดียวกับ GET /health ของ flow-api เดิม
Response 200
{ "status": "ok", "service": "subledger-service", "version": "3.2.0", "timestamp": "2026-06-11T08:00:00Z" }
C. Authentication & Authorization
ต่อยอด jwthandler + middleware naming เดิมของ flow-api (Authorization<Actor>Token()) + Keycloak Token Exchange ตาม ⑥.4 — ไม่สร้าง IdP ใหม่
C.1 Token / Middleware
| Token | Middleware (เสนอ ตาม naming เดิม) | Claims |
|---|---|---|
| Outlet (Flow App) | AuthorizationOutletToken() — ต่อยอด AuthorizationCustomerToken() เดิม | เดิม (customer_id, user_id, supplier_ids[]) + ใหม่ goid, gbp_no, consent_ver |
| Tenant → Sub-Ledger (earn/hold/burn) | AuthorizationSubLedgerToken() | supplier_id, program_ids[], scope[] (subledger:earn,subledger:redeem,subledger:read_partition) — ออกผ่าน Keycloak Token Exchange, อายุสั้น 300s, ผูก agreement (เทียบ delegation token ของ OS §C) |
| Principal | AuthorizationPrincipalToken() (actor ใหม่ — เทียบ AuthorizationCarrierToken() ของ OS) | principal_gbp_no, program_ids[], roles |
| Operator | role ใน platform token | roles: ["platform_operator"] |
Enforce 2 ชั้น (defense in depth — แบบเดียวกับ OS §C): (1) Keycloak ตรวจ agreement/consent ก่อนออก token (2) subledger-service ตรวจ scope ซ้ำต่อ request + gate real-time จาก compacted topic (consent.changed.v1, agreement.lifecycle.v1) — revocation propagate < 1s (⑥.4)
C.2 Scope ของข้อมูล (ใครเห็นอะไร — บังคับที่ read path)
ตาม ⑥.4 Access scope: Outlet เห็นยอดรวมของตน · Tenant เห็นเฉพาะ source_partitions ที่ตน earn/redeem · Principal เห็นทั้ง program + control account · Operator ทุกอย่าง (impersonate logged) — resource นอก scope ตอบ 404 NOT_FOUND (ไม่ leak ว่ามีอยู่)
C.3 AuthZ Matrix (role × resource × action)
| Resource × Action | Outlet | Tenant | Principal | Operator |
|---|---|---|---|---|
POST /outlets (register) · PUT /outlets/:goid/claim | ✓ ของตน | — | — | ✓ |
POST /outlets/resolve | — | ✓ | — | ✓ |
POST /outlets/:goid/merge | — | — | — | ✓ |
POST /consents · PUT /consents/:id/revoke | ✓ เจ้าของ | — | — | ✓ (มีหลักฐาน, logged) |
POST /value-units · POST /programs · POST /programs/:id/agreements | — | ✓ ร่วม sign-off | ✓ R/A | ✓ approve |
PUT /programs/:id/suspend·resume | — | — | ✓ | ✓ |
POST /earn | — | ✓ source tenant (subledger:earn) | — | — |
POST /holds · /holds/:id/burn · PUT /holds/:id/release | — | ✓ redeeming tenant (subledger:redeem) | — | — |
GET /member-accounts/:gbp_no | ✓ รวมของตน | ✓ partition ตนเท่านั้น | ✓ ทั้ง program | ✓ |
POST /conversions | ✓ เจ้าของบัญชี | — | — | ✓ |
POST /clawbacks | — | ✓ source tenant | — | ✓ |
PUT /claims/:id/dispute | — | ✓ ส่งหลักฐาน | ✓ reject claim | ✓ ตัดสิน (/resolve) |
POST /settlement-runs | — | — | — | ✓ (+scheduler) |
GET /control-accounts/:program_id | — | — | ✓ ของตน | ✓ |
D. Response & Error Envelope (เพิ่มจาก OS §D)
รูปแบบ response 2 แบบตาม flow-api เป๊ะ (เหมือน OS §D — data GET = payload ดิบ · status/error = httpserve.Response {status_code, message, data}, message = UPPER_SNAKE) — ไม่ทำซ้ำที่นี่
Error code catalog เพิ่มเติมของ Sub-Ledger
code ทั่วไป (VALIDATION_ERROR, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INVALID_STATE, INVALID_TRANSITION, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_KEY_REQUIRED, RATE_LIMITED, GRANT_DENIED, DISPUTED_EXCLUDED, NEEDS_REVIEW) reuse จาก catalog OS §D — ตารางนี้คือ code ใหม่เฉพาะโดเมนนี้:
| Code | HTTP | UC | ความหมาย |
|---|---|---|---|
OTP_FAILED | 401 | F2 | OTP ไม่ผ่าน (⑦ เดิมเขียน ERR-401-OTP) |
DUPLICATE_OUTLET | 409 | F2 | deterministic match เจอ GOID เดิม → เสนอ link (⑦ เดิมเขียน ERR-409-MATCH) |
MERGE_CONFLICT | 409 | F4 | value-unit/ยอดไม่ตรงตอน merge → map ตาม unit + log discrepancy |
CONSENT_REQUIRED | 403 | L1, L3 | ยังไม่มี consent → earn ตกเป็น tenant-local pending (BR-03) |
OUTLET_NOT_ACTIVE | 409 | F2, L1 | GOID ไม่อยู่สถานะ active |
PROGRAM_NOT_ACTIVE | 409 | L1, L2 | program ไม่ active (รวม agreement gate) |
FUNDING_UNBALANCED | 422 | F1 | Σ funding weight ≠ 1 (UC-F1 exception 1a) |
SKU_NOT_ENTITLED | 422 | L1 | SKU นอก eligible_sku ของ agreement |
INSUFFICIENT_BALANCE | 422 | L2 | available < จำนวนที่ขอ hold |
PORTABILITY_DENIED | 422 | L2 | bucket เป็น distributor-funded ล็อก tenant (BR-01) |
HOLD_EXPIRED | 410 | L2 | hold เกิน TTL — auto-release แล้ว |
UNIT_MISMATCH | 422 | L2, L4 | ปน value-unit ใน operation เดียว (BR-05) |
EXCHANGE_RATE_NOT_FOUND | 422 | L4 | ไม่มีเรต active สำหรับคู่ unit |
NEGATIVE_BALANCE_BLOCKED | 422 | L5 | นโยบายไม่อนุญาตติดลบ → ต้องใช้ refund offset (OI-L4) |
CLAWBACK_EXCEEDED | 422 | L5 | จำนวน clawback เกินยอด earn ต้นทาง |
ZERO_SUM_VIOLATION | 409 | F5 | netting Σ≠0 → run locked ไม่ post (BR-07) |
DISPUTE_WINDOW_CLOSED | 422 | L7 | เกิน window ยื่น dispute |
GRANT_DENIEDในโดเมนนี้ = tenant ∉ program scope หรือ agreement ไม่ active — semantics เดียวกับ collaboration gate ของ OS
E. Integration → Tenant Route Mapping
ทิศทางกลับด้านกับ OS §E: OS ให้ orchestrator เรียก tenant route on-behalf — โดเมนนี้ tenant เป็นผู้เรียกขึ้น Global Plane (earn/hold/burn ด้วย scoped token) ส่วน Global Plane ไม่เรียกเขียนกลับเข้า tenant DB เลย (AP-4) — จุดเชื่อมกับของเดิม:
| sub-ledger action | ของเดิม flow-api ที่เกี่ยว (จริง) | ทิศทาง / หมายเหตุ |
|---|---|---|
| ตัดสิน eligible SKU + จำนวน earn | Promotion Rule Engine — backoffice/promotions, promotionsvc | tenant คำนวณจบแล้วค่อยเรียก POST /earn — sub-ledger ไม่ rerun rule (เก็บ basis ไว้ audit) |
| resolve Customer → GOID/GBP | user-service customers (มีคอลัมน์ outlet_id, personal_id แล้ว) | tenant เรียก POST /outlets/resolve ตอน create/update Customer (UC-F3) แล้วเก็บ GOID ลง customers.outlet_id |
| tenant-local wallet เดิม | wallets / point_balance / point_transactions + routes ordering/points, backoffice/points | คงเดิม สำหรับ tenant program (BR-01 ล็อก) — เฉพาะ sponsor program วิ่งขึ้น XSL |
| earn ค้างก่อน consent (tenant-local pending) | point_transactions + point_statuses ของ tenant | สถานะ pending ฝั่ง tenant; consent มา → replay เข้า POST /earn (Transition ⑧ Dual-run) |
| แจกของรางวัล/คูปองหลัง redeem | backoffice/coupons (POST /) · ordering/points POST /reward | redeeming tenant ออก coupon/ของรางวัลหลัง burn สำเร็จ (Coupon Registry ⑥.7) |
| ปรับยอด manual | backoffice/points PUT /adjust/:id / /adjust-coin/:id | tenant-local เท่านั้น — ฝั่ง XSL ห้าม adjust ตรง ต้องผ่าน journal entry (BR-07 immutable) |
| external loyalty bridge | BevFam — SermsukRoyaltyService.GetBevFamPoint, PointRedeemBevfamRequest | precedent ที่มีอยู่จริง → pattern เดียวกันสำหรับ partner value-unit (CM-3) |
| ERP / B-Plus posting | Clearing House → Credit/Debit Memo + webhook (OS §5.1 + ⑥.5) | settlement run posting — reuse Clearing House ของ Orchestration (④) |
S1. Registry & Identity APIs (UC-F2 · UC-F3 · UC-F4 · UC-L6)
narrative + Gherkin อยู่ ⑦ (UC-F2 Register · UC-F3 Resolve & Link · UC-F4 Merge · UC-L6 Consent) — ที่นี่คือ contract
APIs ในหัวข้อนี้
POST /api/v1/subledger/outlets— ลงทะเบียน Global Outlet →pending_verificationPOST /outlets/resolve— tenant ส่ง candidate จับคู่ Customer → GOIDPUT /outlets/:goid/claim— outlet ยืนยันตัว + consent แรก →activeGET /outlets/:goid— รายละเอียด (scope-filtered) ·GET /outlets— รายการ (operator, paged)GET /outlets/:goid/tenant-maps— รายการ mapping ต่อ tenantPOST /outlets/:goid/merge— operator รวม GOID ซ้ำ →202sagaPOST /consents— บันทึก consent →grantedPUT /consents/:id/revoke— ถอน consent →revoked(gate < 1s)
ลงทะเบียน Global Outlet → pending_verification
UC-F2 ขั้น 1–4: ร้านกรอกข้อมูล + GPS + รูป + OTP → deterministic match (Tax ID/National ID/phone — เทียบ
customers.personal_idของจริง) → ไม่เจอ → mint GOID จาก Central ID Allocator + upsert GBP · เจอ →409 DUPLICATE_OUTLETเสนอ link GOID เดิม · AI กลาง →409 NEEDS_REVIEWเข้าคิว Manual Review
POST /api/v1/subledger/outlets
Headers: Authorization: Bearer <outlet-jwt> · Idempotency-Key · X-Request-ID
Request
{
"name": "ร้านโชคดีค้าส่ง",
"tax_id": "0105561234567",
"national_id": null,
"phone": "0812345678",
"otp_ref": "otp_8842",
"geo": { "lat": 13.7563, "lng": 100.5018 },
"photos": ["minio://outlets/reg/8842-1.jpg"],
"shop_type": "wholesale"
}
Response 201
{
"goid": "GOID-7HF3K2",
"global_bp_no": "GBP-000123",
"status": "pending_verification",
"match_result": "no_match",
"created_date": "2026-06-11T08:30:00Z"
}
Errors
{ "status_code": 409, "message": "DUPLICATE_OUTLET", "data": { "matched_goid": "GOID-3XK9P1", "match_method": "deterministic_tax_id", "suggestion": "link" } }
401 OTP_FAILED · 409 NEEDS_REVIEW (AI score กลาง → Manual Review) · 409 IDEMPOTENCY_KEY_REUSED
tenant จับคู่ Customer → GOID
UC-F3: tenant เรียกตอน create/update Customer — deterministic → probabilistic (AI) → ผลสามทาง:
matched/no_match(mint GOIDpending) /needs_review· ผลmatchedยังไม่ consolidate จนกว่ามี consent (BR-03) — tenant เก็บ GOID ที่ได้ลงcustomers.outlet_id(คอลัมน์มีอยู่แล้ว)
POST /outlets/resolve
Headers: Authorization: Bearer <subledger-token> · Idempotency-Key · X-Request-ID
Request
{
"source_tenant_id": "supplier_A",
"customer_id": "cust_A_889",
"attributes": { "tax_id": "0105561234567", "phone": "0812345678", "name": "โชคดีค้าส่ง", "geo": { "lat": 13.7563, "lng": 100.5018 } }
}
Response 200
{
"result": "matched",
"goid": "GOID-7HF3K2",
"global_bp_no": "GBP-000123",
"match_method": "deterministic_tax_id",
"confidence": 1.0,
"map_status": "awaiting_consent"
}
Errors: 409 NEEDS_REVIEW (กำกวม → คิว steward ⑥.13) · 400 VALIDATION_ERROR
outlet ยืนยันตัว + consent แรก → active
UC-F2 ขั้น 5–6: outlet claim GOID + ให้ consent (purpose/scope ตาม PDPA) →
active+ emitOutletActivated· ปฏิเสธ consent → คงpending_verificationไม่เปิด consolidation (ไม่สูญข้อมูล)
PUT /outlets/:goid/claim
Headers: Authorization: Bearer <outlet-jwt> · Idempotency-Key
Request
{ "consent": { "purposes": ["consolidation", "settlement"], "version": "1.2" } }
Response 200
{ "goid": "GOID-7HF3K2", "status": "active", "consent_id": "consent_01J9...", "activated_at": "2026-06-11T08:35:00Z" }
Errors: 409 INVALID_STATE (ไม่อยู่ pending_verification) · 403 FORBIDDEN (ไม่ใช่เจ้าของ)
รายละเอียด / รายการ outlet
GET /outlets/:goid
Response 200 (scope-filtered — tenant เห็นเฉพาะ map ของตน)
{
"goid": "GOID-7HF3K2",
"global_bp_no": "GBP-000123",
"name": "ร้านโชคดีค้าส่ง",
"status": "active",
"tenant_maps": [ { "source_tenant_id": "supplier_A", "customer_id": "cust_A_889", "status": "linked" } ],
"created_date": "2026-06-11T08:30:00Z"
}
Errors: 404 NOT_FOUND (ไม่มี/นอก scope)
GET /outlets
Query: status q page page_size (operator เท่านั้น — 403 FORBIDDEN)
Response 200: { total, page, page_size, data: [ {goid, name, status, global_bp_no} ] }
GET /outlets/:goid/tenant-maps
Response 200: { total, page, page_size, data: [ {source_tenant_id, customer_id, match_method, confidence, status} ] }
operator รวม GOID ซ้ำ → 202 (saga journal transfer)
UC-F4: ตรวจหลักฐาน → ย้าย tenant-map → journal transfer ยอด bucket ทุก value-unit (ไม่ลบของเดิม, reversible, BR-07) → GOID เก่า
merged· ยอด/unit ไม่ตรง →409 MERGE_CONFLICTmap ตาม unit + log discrepancy
POST /outlets/:goid/merge
Headers: Authorization: Bearer <operator-jwt> · Idempotency-Key · X-Request-ID
Request
{ "merge_from_goid": "GOID-3XK9P1", "evidence": ["minio://merge/req-22/doc1.pdf"], "note": "ร้านเดียวกัน ยืนยันจาก Tax ID + ที่อยู่" }
Response 202
{ "merge_id": "merge_01J9...", "status": "processing", "correlation_id": "saga_01J9...", "surviving_goid": "GOID-7HF3K2" }
Errors: 409 MERGE_CONFLICT · 409 INVALID_STATE · 403 FORBIDDEN
บันทึก consent → granted
UC-L6: แสดง purpose/scope → ยินยอม → เปิด consolidation — consent record versioned, gate propagate < 1s ผ่าน compacted topic
consent.changed.v1(⑥.2)
POST /consents
Headers: Authorization: Bearer <outlet-jwt> · Idempotency-Key
Request
{ "global_bp_no": "GBP-000123", "purposes": ["consolidation", "settlement"], "version": "1.2" }
Response 201
{ "consent_id": "consent_01J9...", "status": "granted", "granted_date": "2026-06-11T08:35:00Z" }
ถอน consent → revoked (gate < 1s)
UC-L6: ถอนแล้ว earn รายการใหม่ไม่ consolidate (ตกเป็น tenant-local) ภายใน < 1s — ยอดที่ consolidate ไปแล้วคงอยู่ (ledger immutable) แต่หยุดรวมรายการใหม่ · สิทธิ์ลบ PII → crypto-shredding (⑥.16)
PUT /consents/:id/revoke
Headers: Authorization: Bearer <outlet-jwt> · Idempotency-Key
Request
{ "reason": "ไม่ประสงค์รวมข้อมูลข้ามผู้จัดจำหน่าย" }
Response 200
{ "consent_id": "consent_01J9...", "status": "revoked", "revoked_date": "2026-06-11T10:00:00Z", "gate_propagation_sla": "1s" }
Errors: 409 INVALID_STATE (ถอนไปแล้ว)
Acceptance: ลงทะเบียนใหม่ → GOID pending_verification → claim+consent → active · เจอซ้ำ deterministic → DUPLICATE_OUTLET เสนอ link · ถอน consent → < 1s earn ใหม่ไม่ consolidate (Gherkin เต็มดู ⑦ UC-F2/F3/F4/L6)
เอกสารนี้รวม design + spec + detailed use cases + technical mechanisms ไว้ในไฟล์เดียว เรียงตามเบื้องหลังวิธีคิด · กรอบคิดหลัก = Cross-Tenant Sub-Ledger (financial primitive ที่รองรับ loyalty points/coins/value) · ครบตาม framework Business Architecture + Solution Architecture · align กับเอกสาร Flow Orchestration Layer