Skip to main content

ระดับสมาชิก Loyalty (Tier)

ภาพรวม

ส่วนขยายของ บัตรสะสมแต้มและระบบสมาชิก ที่เพิ่ม ระดับสมาชิก (tier) เช่น Silver, Gold, Platinum โดยเลื่อนขั้นตามเงื่อนไขที่ร้านกำหนด ภายในกรอบเวลาแบบ rolling window

ระบบนี้มีข้อจำกัดที่ตั้งใจไว้คือ ใช้ได้กับโหมด point เท่านั้น เพราะเงื่อนไขหลักคือยอดใช้จ่ายสะสม จากคอลัมน์ amount_spent ซึ่งเป็น NULL ทุกแถวในโหมดแสตมป์ เนื่องจากแสตมป์หนึ่งดวงคือหนึ่งดวง ไม่ว่ามูลค่าตะกร้าจะเป็นเท่าใด หากร้านสลับกลับไปใช้โหมดแสตมป์ ระดับสมาชิกจะถูกพักไว้ เช่นเดียวกับยอดคงเหลือ

migration ทั้งชุดเป็นแบบ additive อย่างเดียว คือมีเฉพาะตารางใหม่และคอลัมน์ที่เป็น nullable ทำให้ binary รุ่นเก่ายังทำงานกับ schema ใหม่ได้ นี่คือสิ่งที่ทำให้การ rollback แบบ "ถอยเฉพาะโค้ด" ปลอดภัยโดยไม่ต้องย้อน schema

โครงสร้างข้อมูลหลัก

loyalty.tier — ขั้นบันไดของโปรแกรม

  • program_id เป็น FK แบบ ON DELETE CASCADE พร้อม name

  • rank คือ ลำดับขั้น ไม่ใช่ threshold เพราะร้านอาจแก้บันไดอยู่เรื่อย ๆ ระบบจึงต้องไม่สลับ ลำดับให้เองโดยอัตโนมัติ มี partial unique (program_id, rank) WHERE status='active' ซึ่งทำให้ rank ของขั้นที่ถูก archive ไปแล้วนำกลับมาใช้ซ้ำได้

  • conditions แบบ JSONB เก็บชุดกฎของแต่ละขั้น แทนที่ threshold ตัวเดียวที่ใช้ในรุ่นแรก

    {"join":"and","rules":[
    {"metric":"member_months","op":"gte","value":6},
    {"metric":"spend","op":"gte","value":1000},
    {"metric":"orders","op":"gte","value":5}]}

    ค่า metric รองรับ spend, orders, points และ member_months ส่วน op รองรับ gte, lte และ eq เหตุผลที่เลือก JSONB แทนตารางลูกคือกฎถูกอ่านทั้งก้อนพร้อมกับ tier เสมอ และไม่เคยมีการ query ข้าม tier

  • window_months คือกรอบเวลาของแต่ละขั้น ซึ่งต้องมาคู่กับตัวเลขเสมอ มิฉะนั้นเงื่อนไขอย่าง "5,000 บาท" จะไม่มีความหมาย ยกเว้น metric member_months ที่ไม่ใช้ค่านี้เพราะเป็นอายุสมาชิก ไม่ใช่ยอดสะสมในกรอบเวลา

  • threshold แบบ NUMERIC เป็นคอลัมน์ legacy ที่เก็บไว้เผื่อการ rollback มี COMMENT ON COLUMN กำกับไว้ และไม่ถูกอ่านแล้วในโค้ดปัจจุบัน

  • การแสดงผล: color, text_color (ค่าว่างหมายถึงใช้สีของโปรแกรม) และ bg_image_path

  • is_default ระบุขั้นพื้นฐานที่ทุกคนอยู่เมื่อยังไม่ผ่านเงื่อนไขใด บังคับด้วย partial unique (program_id) WHERE is_default AND status='active' ให้มีได้เพียงขั้นเดียว และกฎของขั้นนี้จะไม่ถูกนำไปประเมิน

loyalty.tier_history — ประวัติการเลื่อนขั้น

บันทึกทุกการเลื่อนขึ้นและลดลง

  • from_tier_id, to_tier_id และ reason (promote, demote หรือ initial)
  • window_value เก็บค่ายอดในกรอบเวลาที่ทำให้เกิดผลนั้น เพื่อให้ตอบคำถาม "ทำไมฉันถูกลดขั้น" ได้ทันทีโดยไม่ต้องคำนวณย้อนจากข้อมูลที่ขยับไปแล้ว

คอลัมน์ที่เพิ่มในตารางเดิม

ตารางคอลัมน์ที่เพิ่ม
loyalty.accounttier_id, tier_since, tier_evaluated_date
loyalty.milestonemin_tier_rank (ค่าว่างหมายถึงเปิดให้ทุกคน)
loyalty.programtier_enabled, tier_window_months (ค่าเริ่มต้น 3 ต้องอยู่ในช่วง 1–120), tier_fallback (step_down หรือ specific), tier_fallback_tier_id

ไฟล์ที่เกี่ยวข้อง

  • apps/loyalty/migrations/006_tiers.sql — ตาราง tier และ tier_history พร้อมคอลัมน์ที่เพิ่ม
  • apps/loyalty/migrations/007_tier_rules.sqlconditions แบบ JSONB และการย้าย threshold เดิมเข้ามาเป็นกฎ
  • apps/loyalty/migrations/008_tier_text_color.sql — สีตัวอักษรแยกรายขั้น
  • apps/loyalty/migrations/009_tier_default.sql — ขั้นพื้นฐาน
  • docs/superpowers/specs/2026-07-28-loyalty-tier-design.md — สเปกการออกแบบ
  • docs/ROLLBACK-tier.md — commit ของทุก repo ณ จุดก่อนเริ่มฟีเจอร์ พร้อมสคริปต์ DROP หากจำเป็น

จุดเชื่อมต่อกับ Service อื่น

  • worker-go และ cms-api-go ทำหน้าที่เป็นตัวประเมินระดับสมาชิก โดยอ่าน transaction ภายในกรอบเวลา เทียบกับ conditions แล้วอัปเดต account.tier_id พร้อมเขียน tier_history
  • client-api-go แสดงขั้นปัจจุบัน สีของบัตร และรางวัลที่ปลดล็อกตาม min_tier_rank บนหน้าบัตรใน LIFF
  • ทั้งหมดต่อยอดจากโครงสร้างของ บัตรสะสมแต้มและระบบสมาชิก