markpadmarkpad
Document·47 min read·9,416 words

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:

  1. แต้ม/coin จาก Tenant A กับ B เป็นคนละก้อน ใช้ข้ามกันไม่ได้
  2. Principal/Brand ออกมูลค่า (แต้ม/credit) ได้แค่ราย Distributor — ไม่มีบัญชีระดับเครือข่าย
  3. ตัวตนร้านซ้ำซ้อน (1 ร้าน = หลาย Customer record คนละชุดข้อมูล)
  4. การชดเชยข้ามองค์กรไม่โปร่งใส เกิดข้อพิพาท

เอกสาร Flow เดิม (§3.6 Multi-Wallet) ตั้งคำถามนี้ไว้เองตรงๆ: "sub-agent อาจซื้อจากหลาย agent... point ที่ earn จาก agent รายนั้น นำไปใช้ที่อื่นได้หรือไม่" — โจทย์นี้คือคำตอบ และคำตอบคือ "สร้าง sub-ledger กลางต่อสมาชิก"

สิ่งที่จะสร้าง — 4 Core Models

#Core Modelสรุป
CM-1Global Outlet Identityร้านค้าลงทะเบียนรับ GOID เดียว ผูกหลาย Tenant Customer
CM-2Cross-Tenant Sub-Ledger (XSL)บัญชีย่อยระดับสมาชิก (ผูก Global BP) รวม value ข้าม Tenant
CM-3Multi-Value / Multi-Currency Unitsหลาย value-type/สกุลตามผู้ออก (Points/Coins/Miles/Deposit)
CM-4Cross-Tenant Settlement & Control-Account ClearingClearing 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 sourceGOID coverage, dup-merge rate
G4ชดเชยข้ามองค์กรโปร่งใสsettlement accuracy, dispute rate, days-to-settle
G5สร้างรายได้ ledger-as-a-servicetake-rate, liability under management

ผู้เกี่ยวข้อง (Actors)

Actorบทบาท
Outlet / Retailerเจ้าของ Member Account (ผูก GBP), ผู้ลงทะเบียน GOID, earn/redeem
Seller Tenant (Distributor)จุดสัมผัส earn/redeem; ผู้รับ reimbursement
Principal / BrandSponsor / 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 ต่อ PrincipalPrincipalonboarding, config
% ของมูลค่าที่ออก (issuance fee)Principalledger compute, liability mgmt
Settlement/clearing fee ต่อรอบPrincipal/Tenantnetting 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, Keycloakorder/POS/CRM/pricing/inventory ของแต่ละ tenant (single-owner เดิม)

MIT operating-model quadrant: "Coordination" — แชร์ลูกค้า(ร้าน)/value/ตัวตนข้าม unit แต่ process การขายแต่ละ tenant อิสระ

Product / Service Catalog ของแพลตฟอร์ม

บริการกลุ่มเป้าหมายสาระ
Global Outlet Identityplatform/tenantGOID + resolution (CM-1)
Cross-Tenant Sub-Ledgerprincipalบัญชี value ข้ามเครือข่าย (CM-2) — flagship: loyalty
Multi-Value Unitsprincipalvalue-type ของผู้ออก (CM-3)
Settlement & Clearingทุกฝ่ายชดเชย/netting/ภาษี/control-account (CM-4)
Reward Catalog / Redemptionoutlet/principalแลกของรางวัล/ส่วนลด/cash-equivalent

Personas & Customer Journeys

PersonaJourney หลัก
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)

DriverGoalKPIbaseline→target
value พกพาG1cross-tenant redemption %, repeat purchase(เติม)
โปรแกรมเครือข่ายG2#program, brand reach, earn coverage(เติม)
ตัวตนสะอาดG3GOID coverage %, dup-merge rate, match precision(เติม)
ชดเชยโปร่งใสG4settlement accuracy, dispute rate, days-to-settle(เติม)
รายได้G5take-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)

โดเมนประเด็นสถานะ
PDPAconsent ข้าม 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 / unclaimedvalue หมดอายุ → รายได้ (IFRS 15) + กฎ unclaimed property⑥.13
สินค้าควบคุม (สุรา/ยาสูบ)ThaiBev = แอลกอฮอล์ → ข้อจำกัดโฆษณา/ส่งเสริมการขายสุราเงื่อนไขใน eligible_sku/agreement → OI-L9
Anti-fraud/AMLearn-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 ไปทุก tenantdouble-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 (เดิม)TenantOrder, Tenant Customer/ProductApplication Plane
Platform-Owned (Global)PlatformGlobal BP, Global Outlet, Global ProductPlatform Core
Sponsor-Governed Sub-LedgerPlatform (steward = Principal)Cross-Tenant Sub-Ledger + Member AccountsGlobal Sub-Ledger Plane

ข้อมูล Tenant ยังเป็นของ Tenant — แค่ถือ FK (Global BP Number, Global Outlet ID) ขึ้นชั้นกลาง


③ หลักการที่ยึด (Principles)

หลักสถาปัตยกรรม

รหัสหลักการ
AP-1Isolation by default — ตาราง tenant คง invariant single-owner ไม่เปลี่ยน
AP-2Global Sub-Ledger Plane เป็น bounded context แยก — เจ้าของ identity/member account/clearing กลาง
AP-3Consent-gated consolidation — รวม value/ข้อมูลข้าม tenant ต้องมี consent ของร้าน
AP-4No direct cross-DB reach — tenant ไม่อ่าน DB กันเอง ผ่าน Global API/event
AP-5Event-driven + Saga + Outbox/CDC — financial-grade, ไม่หาย/ไม่ซ้ำ
AP-6Reuse before build — ใช้ Global BP, sub-wallet partition, double-entry ledger, Unified Wallet, Clearing House เดิม
AP-7Fail-safe / Degrade — Sub-Ledger ล่ม → ห้าม commit burn (ไม่มีส่วนลดลอย), tenant ขายต่อได้
AP-8Liability follows the bucket — ผู้ตั้งหนี้ตอน earn = ผู้ชดใช้ตอน redeem

กฎเชิงธุรกิจ (Business Rules)

รหัสกฎ
BR-01Golden Rule of Portability — value เดินทางตามผู้ออกทุน (Principal⇒ข้าม tenant; Distributor⇒ล็อก)
BR-02Member-Account Anchor = นิติบุคคล (Global BP) — หลาย Outlet ของเจ้าของเดียวรวมยอด
BR-03Consent-before-Consolidate — claim/consent (PDPA) ก่อนรวมข้าม tenant
BR-04Liability follows the Bucket — ผู้ตั้งหนี้ = ผู้ชดใช้
BR-05Value-Unit Isolation — value-type/สกุลต่างกันไม่รวมเอง ข้ามได้เฉพาะผ่าน Exchange Rate
BR-06Default Cross-Tenant Funding = Principal 100% — co-fund ข้ามคู่ค้าต้องมี agreement
BR-07Immutable & Zero-sum — ledger insert-only, settlement Σ=0
BR-08Purpose Limitation — ใช้ข้อมูลเพื่อ ledger/settlement เท่านั้น ห้าม leak ราคาข้าม tenant
BR-09Clawback ย้อนตามสายเดิม — คืนสินค้า → เรียก value คืนตามสัดส่วน + ปรับ liability

④ ไอเดียหลัก — Global Plane + Cross-Tenant Sub-Ledger

ภาพรวมไอเดีย

ยกตัวตน + บัญชี value + clearing ขึ้น Global Plane (bounded context แยก, Platform เป็นเจ้าของ) — สร้าง Cross-Tenant Sub-Ledger ที่บันทึกรายการ value ต่อสมาชิก (Global BP) ข้าม Tenant และตัดสิน portability ด้วย ผู้ออกทุน

  • 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-LedgerMember Account ต่อ (Program × GBP) ถือหลาย value-type
Sponsor/Coalition Agreementสัญญา funding/scope/valuation/sign-off (เทียบ Collaboration Grant)
Settlement / Clearing Houseattribution + multilateral netting + roll-up control account
Event Bus + Outbox + Sagafinancial-grade eventing
Consent/PDPA + Auditconsent gating + ตรวจสอบ

การวางบน 3-Plane Model (align Orchestration Layer)

Planeเจ้าของของเอกสารนี้
Control PlaneSaaS providerTenant onboarding, Central ID Allocator, Keycloak, Billing
Platform Core / Global Plane (HA ข้าม DC)PlatformGlobal Registry, Identity Resolution, Cross-Tenant Sub-Ledger, Clearing House, Agreement, Consent, Centralized AuthZ, Audit
Application Plane (8+2 clusters)TenantPOS/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

CapabilityCM-1CM-2CM-3CM-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

ระดับSponsorPlanePortability
Tenant Program (เดิม)DistributorTenant-isolatedล็อก
Sponsor/Coalition ProgramPrincipalCross-Tenant Sub-Ledgerข้าม tenant
Co-fundedPrincipal+DistributorXSL, 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_instances recover หลัง 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

ด้านข้อกำหนด
Scopeclaim program_id/tenant_id/gbp_scope → Centralized AuthZ บังคับ slice
Cross-tenant tokenKeycloak Token Exchange, อายุสั้น, ตรวจ revocation
Service-to-servicemTLS
Default deny / least privilegeTenant เห็นเฉพาะ slice ที่ตน earn/redeem
Revocationconsent/agreement gate propagate < 1s (compacted topic → KTable)
PDPAconsent record, minimization, สิทธิ์ลบ, data residency ไทย
Auditimmutable WORM stream, cross-tenant/PII access logged

Access scope

บทบาทเห็น
Outletmember account รวมของตัวเอง
Tenantเฉพาะ source partition ที่ตน earn/redeem
Principalทั้งโปรแกรม + liability/breakage (control account)
Operatorดูแล + audit (impersonate logged)

⑥.5 Settlement / Financial (สูตรเต็ม)

Double-entry postings (subsidiary → control account)

เหตุการณ์DebitCredit
Earn P value-unit cFund Budget (รายทุน)Value Liability (Control Account) = P×v_c
Redeem R @ T_rValue Liability = R×m_cPayable to T_r
Expiry (Breakage)Value LiabilityBreakage Income (รายทุน)
SettlementPayableCash / 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 Numberanchor ของ member account + map Customer
Sub-wallet partition / double-entry ledgerยกขึ้น Global plane เป็น XSL
Unified Wallet (Coin) เดิมCoin = value-unit ชนิด cash-equivalent บน primitive เดียวกัน
Coupon Registryredeem → issue coupon
Promotion Rule Engineตัดสิน eligible + earn
Clearing House / B-Plus/VSMS/Certusettlement → control account → ERP
Keycloak / middlewarecross-tenant scope token

⑥.8 State Machine Catalog

⑥.9 Domain Event Catalog

EventProducerConsumerpartition_key
OutletActivated / MergedRegistryCRM, Sub-Ledgergoid
ConsentChangedConsent SvcSub-Ledger (gate)global_bp_no
AgreementLifecycleAgreement SvcSub-Ledger (gate)program_id
ValueEarned / Redeemed / ExpiredSub-LedgerClearing, Notify, Analyticsglobal_bp_no
SettlementClaimCreatedSub-LedgerClearingprogram_id
SettlementRunPostedClearingERP webhook, Principalprogram_id
ValueUnitConvertedSub-LedgerClearingglobal_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/redeemKeycloak, mTLS, signed token
Tamperingแก้ payload/valueevent signing, schema validation, immutable ledger
Repudiationปฏิเสธรายการaudit immutable + correlation id
Info disclosureเห็น account/ราคา tenant อื่นdefault deny, scope, PII-free events
DoSถล่ม earn/redeemrate 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_value vs redemption_value; FX ExchangeRate effective-dated + governed_by
  • ข้าม unit = burn/mint double-entry + ย้ายเจ้าของหนี้ + settlement entry; settle หลาย unit → แปลง THB ก่อน net

⑥.15 Resilience Patterns

Patternใช้ที่ไหน
Fail-safeSub-Ledger ไม่ตอบตอน burn → ไม่ commit (ไม่มีส่วนลดลอย); tenant ขายต่อได้
Circuit breaker / bulkheadcall ข้าม service (sub-ledger, ERP) แยก pool
Rate limit / backpressureearn/redeem ต่อ partner
Retry + idempotencyทุก command/event (tiered → DLQ)
Timeout + compensationhold TTL, agreement window
Shadow reconciliationjournal 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 + PDPAcrypto-shredding, right-to-erasure
Saga instances (เสร็จ)archive หลัง N วัน
  • Data residency: ข้อมูล+ledger ในไทย (PDPA) → กระทบเลือก DC/region

⑥.17 Interface / API Catalog

Interfaceรูปแบบหมายเหตุ
Outlet Registry / Resolve / Claimsync RESTIdempotency-Key
Earn / Hold / Burn / Releasesync REST2PC/Saga, idempotent
Member Account / Balance querysync RESTscope-filtered (CQRS read)
Settlement Run / Liability / Exportsync + webhookHMAC sign, retry
Domain eventsasync Kafkaenvelope ⑥.2
Agreement / Consentsync RESTsign-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ชื่อModelPriorityComplexity
F1Establish Sponsor Program + AgreementFoundationP0L
F2Register Global OutletCM-1P0L
F3Resolve & Link Tenant CustomerCM-1P0M
F4Merge GOIDCM-1P1M
F5Settlement RunCM-4P0L
L1Earn Value (cross-tenant)CM-2P0XL
L2Redeem Value (cross-tenant)CM-2/4P0XL
L3View Member AccountCM-2P0S
L4Value-Unit ConversionCM-3P1M
L5Clawback (return)CM-2/4P1L
L6Manage Consent / RevokeCM-1P0M
L7Dispute HandlingCM-4P1M

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)

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)

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 FoundationID Allocator, Outlet/BP Registry, Resolution, OutletTenantMap, Cross-Tenant Sub-Ledger, Kafka/Outbox (P0), Clearing skeletonทุก model พึ่งพา
1 Pilot1 Principal (ThaiBev), 2 tenant, Sponsor Program + Member Account (earn+redeem loyalty points)พิสูจน์ portability
2 SettlementClearing cross-party + liability/breakage + control account + ERP webhookเงินจริง
3 ScaleCo-funded, Multi-value (coin/deposit), ช่องทางกลาง Principal, Cross-DC, GLNขยาย

RACI

กิจกรรมPrincipalDistributorPlatformOutlet
ออกแบบ program/value-unit/agreementR/ACCI
ออก GOID / Resolve / MergeICR/AC
ConsentIICR/A
Earn/Redeem หน้างานIRAR
Settlement/NettingCCR/AI
DisputeRRAI

Open Issues

รหัสประเด็น
OI-L1หน่วยเงิน minor unit (สตางค์) ทั้งระบบ + rounding policy เศษ value
OI-L2ร้านไม่มี Tax ID/National ID ทั้งคู่ — identity อย่างไร
OI-L3Tier per-program vs Global Status ข้าม issuer
OI-L4Negative balance ตอน clawback: ติดลบ vs refund offset (default)
OI-L5co-fund ข้าม tenant อนุญาต vs Principal 100% เท่านั้น
OI-L6Expiry policy ค่ากลาง vs ต่อ agreement
OI-L7Coin/value = stored value เข้าข่าย e-money/ธปท. หรือไม่
OI-L8ภาษี/ผู้ออกใบกำกับ ตอน redeem ของรางวัล; Flow เป็นคู่สัญญาการเงินกลาง?
OI-L9สินค้าควบคุม (สุรา): ข้อจำกัดส่งเสริมการขาย/โฆษณา ใน eligible_sku/agreement
OI-L10redeem ที่ 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-02Portability ตัดสินด้วยผู้ออกทุน (Golden Rule)Acceptedตอบโจทย์ §3.6 เดิม (§④)
ADR-03Member-Account anchor = นิติบุคคล (Global BP)Acceptedยืนยันโดย business
ADR-04Liability ติด bucket (FIFO relief) → รองรับผู้ออก≠ผู้รับAcceptedความถูกต้อง settlement (§⑥.5)
ADR-05Event-driven + saga + Outbox/CDC; reuse Clearing House/Keycloak/ID AllocatorAcceptedfinancial-grade + align Orchestration
ADR-06Consent-before-consolidate (PDPA gating real-time)Acceptedกฎหมาย (§⑥.4)
ADR-07Value-unit isolation; ข้าม unit ผ่าน FX + ย้ายเจ้าของหนี้Acceptedบัญชีถูก (§⑥.14)
ADR-08Sub-Ledger เป็น primitive กว้าง (loyalty/coin/deposit) ไม่ผูกเฉพาะ loyaltyAcceptedreuse Unified Wallet + ขยายได้ (§④)
ADR-09Default cross-tenant funding = Principal 100%Proposedกันอุดหนุนคู่แข่ง (ทบทวน OI-L5)

Transition / Coexistence Architecture

ขั้นสาระ
Stranglerเปิด sponsor program ทีละ program/tenant ไม่ big-bang
Dual-runtenant program เดิมทำงานปกติ; sub-ledger earn ค้าง pending ก่อน consent
Backfillresolve Customer เดิม→GOID (batch+AI) + รณรงค์ consent ก่อน consolidate
Cutover ต่อรายเปิดต่อ program/tenant + rollback + fail-safe
Coexistencetenant program (ล็อก) อยู่ร่วมกับ sponsor program (ข้าม) ได้ — portability flag ตัดสิน

สิ่งที่ BA/Dev ต้องต่อยอด

  1. Wireframe (Flow App member account, operator console, principal dashboard)
  2. Endpoint spec + data dictionaryทำแล้ว ใน ส่วนที่ 2 + §I DB Schema — เหลือ generate ไฟล์ OpenAPI 3.1 (subledger-openapi/openapi.yaml) จาก endpoint blocks
  3. ตาราง error codeทำแล้ว ใน §D — เหลือเติมข้อความผู้ใช้ (ไทย) ต่อ code
  4. ตัวเลข NFR/SLO + capacity จริง
  5. Event schema จริง (Avro) ต่อ event ใน ⑥.9 + Schema Registry
  6. B-Plus/VAT/IFRS mapping (settlement+breakage+control account) — เสี่ยงสูง ทำก่อน go-live
  7. Report/analytics (liability, breakage, redemption %, settlement summary)
  8. 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 ของจริง → ของใหม่:

ของจริงใน codepath (จริง)ใช้ต่อยอดเป็น
pointsid, name, supplier_id (NULLABLE), is_default, type, principleflow-library dao/model/points.gen.goราก Value Unit (CM-3) — คอลัมน์ principle (ผู้ออกระดับแบรนด์) มีอยู่แล้ว; supplier_id nullable = point ที่ไม่ผูก tenant เดียวมีอยู่แล้ว
walletscustomer_id, point_id, balance, coin_id, coin_balancedao/model/wallets.gen.gowallet ระดับ tenant (ยอดรวมเดียว ไม่มี bucket/FIFO) → ยกขึ้นเป็น Member Account
point_balancecustomer_id, point_id, wallet_id, balancedao/model/point_balance.gen.gosub-wallet partition ต่อ point_id ของจริง — ตรรกะเดียวกับ Source Partition ของ XSL
point_transactionswallet_id, supplier_id, point, order_id, reward_id, point_status_id, campaign_id, point_iddao/model/point_transactions.gen.gojournal ระดับ tenant — มี supplier_id ต่อรายการอยู่แล้ว = ราก attribution; XSL เพิ่ม double-entry + bucket
coin twins — coin, coin_transactions, coin_exchanges, coin_reasons, coin_statusesdao/model/coin*.gen.goCoin = value-unit ชนิด cash-equivalent (⑥.7, ADR-08) — primitive เดียวกัน
point_exchangesrate, maximum, maximum_type enum('percent','amount'), start_date/end_date, is_defaultdao/model/point_exchanges.gen.goราก ExchangeRate effective-dated (⑥.14, UC-L4)
customersoutlet_id varchar(36) มีอยู่แล้ว, personal_id varchar(13), phone_1/2, has_liquor_license + license fieldsdao/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/:idproduct-service/startup/prodmodule/routes.goช่องทาง earn/redeem/adjust เดิม — spec นี้ mirror วิธี mount + middleware naming
BevFam bridge — GetBevFamPoint, PointRedeemBevfamRequest (SermsukRoyaltyService)product-service/handler/ordering/pointhandler/point.goprecedent การ 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/NewResponseWithDataflow-library library/httputil/httpserve/handler.goenvelope status/error (§D)
Promotion Rule Engine — backoffice/promotions (Put /activate,/deactivate), promotionsvcprodmodule/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 เดียวกับ channel ordering/ 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-IDcorrelation/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 · pagination page/page_size{ total, page, page_size, data: [] }
  • status-change ใช้ PUT /.../:id/<verb> (mirror Put /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

TokenMiddleware (เสนอ ตาม 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)
PrincipalAuthorizationPrincipalToken() (actor ใหม่ — เทียบ AuthorizationCarrierToken() ของ OS)principal_gbp_no, program_ids[], roles
Operatorrole ใน platform tokenroles: ["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 × ActionOutletTenantPrincipalOperator
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 ใหม่เฉพาะโดเมนนี้:

CodeHTTPUCความหมาย
OTP_FAILED401F2OTP ไม่ผ่าน (⑦ เดิมเขียน ERR-401-OTP)
DUPLICATE_OUTLET409F2deterministic match เจอ GOID เดิม → เสนอ link (⑦ เดิมเขียน ERR-409-MATCH)
MERGE_CONFLICT409F4value-unit/ยอดไม่ตรงตอน merge → map ตาม unit + log discrepancy
CONSENT_REQUIRED403L1, L3ยังไม่มี consent → earn ตกเป็น tenant-local pending (BR-03)
OUTLET_NOT_ACTIVE409F2, L1GOID ไม่อยู่สถานะ active
PROGRAM_NOT_ACTIVE409L1, L2program ไม่ active (รวม agreement gate)
FUNDING_UNBALANCED422F1Σ funding weight ≠ 1 (UC-F1 exception 1a)
SKU_NOT_ENTITLED422L1SKU นอก eligible_sku ของ agreement
INSUFFICIENT_BALANCE422L2available < จำนวนที่ขอ hold
PORTABILITY_DENIED422L2bucket เป็น distributor-funded ล็อก tenant (BR-01)
HOLD_EXPIRED410L2hold เกิน TTL — auto-release แล้ว
UNIT_MISMATCH422L2, L4ปน value-unit ใน operation เดียว (BR-05)
EXCHANGE_RATE_NOT_FOUND422L4ไม่มีเรต active สำหรับคู่ unit
NEGATIVE_BALANCE_BLOCKED422L5นโยบายไม่อนุญาตติดลบ → ต้องใช้ refund offset (OI-L4)
CLAWBACK_EXCEEDED422L5จำนวน clawback เกินยอด earn ต้นทาง
ZERO_SUM_VIOLATION409F5netting Σ≠0 → run locked ไม่ post (BR-07)
DISPUTE_WINDOW_CLOSED422L7เกิน 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 + จำนวน earnPromotion Rule Engine — backoffice/promotions, promotionsvctenant คำนวณจบแล้วค่อยเรียก POST /earn — sub-ledger ไม่ rerun rule (เก็บ basis ไว้ audit)
resolve Customer → GOID/GBPuser-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)
แจกของรางวัล/คูปองหลัง redeembackoffice/coupons (POST /) · ordering/points POST /rewardredeeming tenant ออก coupon/ของรางวัลหลัง burn สำเร็จ (Coupon Registry ⑥.7)
ปรับยอด manualbackoffice/points PUT /adjust/:id / /adjust-coin/:idtenant-local เท่านั้น — ฝั่ง XSL ห้าม adjust ตรง ต้องผ่าน journal entry (BR-07 immutable)
external loyalty bridgeBevFam — SermsukRoyaltyService.GetBevFamPoint, PointRedeemBevfamRequestprecedent ที่มีอยู่จริง → pattern เดียวกันสำหรับ partner value-unit (CM-3)
ERP / B-Plus postingClearing 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 ในหัวข้อนี้

  1. POST /api/v1/subledger/outlets — ลงทะเบียน Global Outlet → pending_verification
  2. POST /outlets/resolve — tenant ส่ง candidate จับคู่ Customer → GOID
  3. PUT /outlets/:goid/claim — outlet ยืนยันตัว + consent แรก → active
  4. GET /outlets/:goid — รายละเอียด (scope-filtered) · GET /outlets — รายการ (operator, paged)
  5. GET /outlets/:goid/tenant-maps — รายการ mapping ต่อ tenant
  6. POST /outlets/:goid/merge — operator รวม GOID ซ้ำ → 202 saga
  7. POST /consents — บันทึก consent → granted
  8. PUT /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 GOID pending) / 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


UC-F2 ขั้น 5–6: outlet claim GOID + ให้ consent (purpose/scope ตาม PDPA) → active + emit OutletActivated · ปฏิเสธ 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_CONFLICT map ตาม 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


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" }

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