CRM & Loyalty — กระบวนการทำงาน (Process flows)
สถาปัตยกรรม & วงจรชีวิตสร้างอัตโนมัติจากโค้ด (dispatch, CHECK status enums, SET status writes) ส่วนลำดับการทำงาน (sequences) & หมายเหตุเขียนกำกับไว้เอง
สถาปัตยกรรม (Architecture)
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
flowchart LR
C["Client"] --> A["Pages Fn crm<br/>bolt_session auth · company_id scope"]
A --> D["dispatch<br/>REST resources"]
D --> K["Kernel<br/>erp_document + journal<br/>atomic GL / stock"]
K --> DB[("D1 tables")]
D --> DB
D --> EV["events<br/>append audit trail"]
EV --> DB
DB -.-> T["crm_loyalty_pipeline<br/>crm_loyalty_pipeline_stage<br/>crm_loyalty_lead<br/>party<br/>crm_loyalty_opportunity<br/>crm_loyalty_consent_ledger<br/>crm_loyalty_retention_policy<br/>crm_loyalty_dsar"]
วงจรชีวิต (Lifecycle)
วงจรชีวิตเอกสารระดับ Kernel (erp_document.docstatus)
เอกสารทุกฉบับในโมดูลนี้ที่มีการเคลื่อนไหวของเงิน/สต็อกจะถูก post ผ่าน kernel ที่ใช้ร่วมกัน โดยเดินตาม docstatus แบบตายตัว:
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","secondaryColor":"#3a3d41","secondaryBorderColor":"#dcdcaa","tertiaryColor":"#252526","tertiaryBorderColor":"#569cd6","clusterBkg":"#252526","clusterBorder":"#569cd6","titleColor":"#569cd6","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa"},"flowchart":{"useMaxWidth":false},"state":{"useMaxWidth":false}}}%%
stateDiagram-v2
[*] --> Draft: createDocument (docstatus 0)
Draft --> Submitted: submitDocument (docstatus 1, journal posted)
Submitted --> Cancelled: cancelDocument (docstatus 2, reversed)
Submitted --> [*]
Cancelled --> [*]
crm_loyalty_lead.status
สถานะ: New (เริ่มต้น) · Working · Qualified · Disqualified · Merged
crm_loyalty_program.status
สถานะ: Draft (เริ่มต้น) · Active · Paused · Ended · Cancelled
crm_loyalty_dsar.status
สถานะ: Open (เริ่มต้น) · InProgress · Completed · Rejected
Flow สำคัญ: การแลกรางวัล (reward redemption) โพสต์หนี้สินสะสมแต้มเข้าสู่ GL
reward/redeem (โดยส่ง reward_id + membership_id มาใน body) เป็น wrapper ครอบ points/redeem ซึ่งจะตัดใช้ point lots แบบ FIFO และ — เฉพาะเมื่อมีมูลค่าที่เป็นตัวเงินเท่านั้น — จึงจะโพสต์ journal ที่สมดุลผ่าน kernel
%%{init: {"theme":"base","themeVariables":{"darkMode":true,"background":"#1e1e1e","primaryColor":"#2d2d30","primaryBorderColor":"#569cd6","primaryTextColor":"#d4d4d4","lineColor":"#6796c6","actorBkg":"#2d2d30","actorBorder":"#569cd6","actorTextColor":"#d4d4d4","signalColor":"#9cdcfe","signalTextColor":"#d4d4d4","noteBkgColor":"#3a3d41","noteTextColor":"#dcdcaa","labelBoxBkgColor":"#3a3d41","labelBoxBorderColor":"#dcdcaa"},"sequence":{"useMaxWidth":false}}}%%
sequenceDiagram
autonumber
participant C as Client
participant API as CRM API (redeemReward)
participant D1 as D1 tables + views
participant K as Kernel / GL
C->>API: POST reward/redeem {membership_id, reward_id}
API->>D1: load membership+program (must be Active), active reward
API->>D1: idempotency check (reward_redemption)
Note over API: else call redeemPoints(reward.points_cost)
API->>D1: availableLots FIFO (point_lot_available)
API->>API: assertCanConsume(lots, points)
alt monetary_value > 0
API->>K: resolveAccount(liability, redemption_offset)
API->>K: createDocument LOYALTY-REDEMPTION
API->>K: submitDocument (dr liability / cr offset)
K-->>API: kernel_document_id
end
API->>D1: insert burn ledger entry (points < 0)
API->>D1: insert burn_allocation per lot (FIFO)
API->>D1: insert reward_redemption row
API-->>C: balance + tier + posting
หมายเหตุ
- วัตถุประสงค์: รวม CRM ก่อนการขาย (leads/opportunities ที่ป้อนเข้าสู่
erp_sales_orderผ่านlink-order) เข้ากับ loyalty หลังการขาย (โปรแกรมสะสมแต้ม, tiers, rewards) และความเป็นส่วนตัวสไตล์ PDPA (consent ledger, retention policy, DSAR) ไว้บน surface เดียวที่ scope ตาม tenant - ความปลอดภัยด้านเงิน (Money-safety): แต้ม (points) เป็นเพียงแถวใน ledger เท่านั้น ผลกระทบต่อ GL จริงเกิดขึ้นเฉพาะใน
postMonetaryRedemptionที่เรียกkernel.createDocument+kernel.submitDocumentเพื่อโพสต์ journalLOYALTY-REDEMPTIONที่สมดุล (drliability_transaction_kind, crredemption_credit_kind) ส่วนearn/expireไม่แตะ GL เลย - Idempotency ทุกจุด:
consent,earn,redeem,reward,expireแต่ละตัวสร้างidempotency_keyแบบ deterministic และจะ short-circuit คืนค่า{ idempotent: true }หากมีแถวเดิมอยู่แล้ว การโพสต์ผ่าน kernel ก็ reusekernel:${idem}ดังนั้นการ retry จึงไม่มีทางโพสต์ซ้ำ (double-post) - การบัญชี lot แบบ FIFO:
availableLotsเรียงลำดับตามวันหมดอายุ (expiry) แล้วตามด้วยoccurred_at;insertConsumptionเขียนแถว ledger ที่เป็นค่าลบพร้อมกับpoint_burn_allocationหนึ่งแถวต่อ lot ที่ถูกตัดใช้ และจะ throwpoint allocation failedหากไม่สามารถครอบคลุมการ burn ได้ครบ — ยอดคงเหลือ (balance) มาจาก view เสมอ ไม่เคยเป็น counter ที่แก้ไขได้ - Audit แบบ append-only:
point_ledgerมี CHECK constraints บังคับเครื่องหมายตามชนิด (sign-by-kind) โดยearn/adjust_credit/migrate_inเป็นบวก ส่วนburn/expire/adjust_debit/reverseเป็นลบ; การเคลื่อนไหวของ opportunity และการให้/ถอน consent ถูกบันทึกเป็น events ที่แก้ไขไม่ได้ (immutable) โดยเปิดสถานะปัจจุบันผ่าน views (consent_current,membership_tier_projection,weighted_forecast) - Gotchas ที่เจอในโค้ด:
lead/qualifyเป็น fan-out — อาจสร้างerp_party, upsert party facets และ spawn opportunity ได้ในการเรียกครั้งเดียว;opportunity/link-orderรันALTER TABLE erp_sales_order ADD COLUMN opportunity_idแบบมี guard (เพราะ D1CREATE IF NOT EXISTSจะข้าม column ใหม่) และจะ fail closed (409) หาก SO นั้นถูก link ไว้ที่อื่นแล้ว