ระดับสมาชิก 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 บาท" จะไม่มีความหมาย ยกเว้น metricmember_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.account | tier_id, tier_since, tier_evaluated_date |
loyalty.milestone | min_tier_rank (ค่าว่างหมายถึงเปิดให้ทุกคน) |
loyalty.program | tier_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.sql—conditionsแบบ 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 - ทั้งหมดต่อยอดจากโครงสร้างของ บัตรสะสมแต้มและระบบสมาชิก