ข้ามไปที่เนื้อหา

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 เพื่อโพสต์ journal LOYALTY-REDEMPTION ที่สมดุล (dr liability_transaction_kind, cr redemption_credit_kind) ส่วน earn/expire ไม่แตะ GL เลย
  • Idempotency ทุกจุด: consent, earn, redeem, reward, expire แต่ละตัวสร้าง idempotency_key แบบ deterministic และจะ short-circuit คืนค่า { idempotent: true } หากมีแถวเดิมอยู่แล้ว การโพสต์ผ่าน kernel ก็ reuse kernel:${idem} ดังนั้นการ retry จึงไม่มีทางโพสต์ซ้ำ (double-post)
  • การบัญชี lot แบบ FIFO: availableLots เรียงลำดับตามวันหมดอายุ (expiry) แล้วตามด้วย occurred_at; insertConsumption เขียนแถว ledger ที่เป็นค่าลบพร้อมกับ point_burn_allocation หนึ่งแถวต่อ lot ที่ถูกตัดใช้ และจะ throw point 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 (เพราะ D1 CREATE IF NOT EXISTS จะข้าม column ใหม่) และจะ fail closed (409) หาก SO นั้นถูก link ไว้ที่อื่นแล้ว