Skip to main content

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

รายละเอียดคอลัมน์ของ loyalty.tier, tier_history

ภาพรวม

ฟีเจอร์ระดับสมาชิกเพิ่มเข้ามาใน migration 006-009 ของ schema loyalty ประกอบด้วย 2 ตารางใหม่คือ tier (นิยามแต่ละขั้นบันไดพร้อมกติกา) และ tier_history (ประวัติการเลื่อน/ลดขั้นทุกครั้ง) พร้อมคอลัมน์ที่เพิ่มเข้าไปในตารางเดิม 3 ตาราง (account, milestone, program) ทุก migration ในกลุ่มนี้เป็นแบบ additive — สร้างตารางใหม่และเพิ่มคอลัมน์ที่ NULL ได้หรือมี default เท่านั้น binary รุ่นเก่าจึงยังทำงานกับ schema ใหม่ได้ ทำให้ rollback แบบแก้เฉพาะโค้ดปลอดภัย

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

ตาราง loyalty.tier

ColumnTypeNullableDefaultคำอธิบาย
idSERIALNOauto incrementPrimary key
program_idINTEGERNO-FK ไปยัง loyalty.program(id) ลบแบบ CASCADE
line_oa_idINTEGERNO-LINE OA ที่ระดับสมาชิกนี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
nameVARCHAR(60)NO-ชื่อระดับสมาชิก เช่น Silver, Gold
rankINTEGERNO-ลำดับขั้นบันได (ต้องมากกว่า 0) — เป็นอันดับ ไม่ใช่เกณฑ์ รายการจึงไม่สลับที่เองระหว่างที่ร้านกำลังแก้
thresholdNUMERIC(12,2)YES-เกณฑ์ยอดใช้จ่ายแบบเดิม (บาท) — เลิกใช้แล้ว ถูกแทนที่ด้วย conditions คงไว้เพื่อรองรับการ rollback (migration 007)
colorVARCHAR(9)YES-สีประจำระดับ
bg_image_pathTEXTYES-path ของภาพพื้นหลังบัตรระดับนี้
statusVARCHAR(20)NO'active'สถานะ: active หรือ archived
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่สร้าง
updated_dateTIMESTAMPTZ(3)YES-วันเวลาที่แก้ไขล่าสุด
conditionsJSONBNO'{"join":"and","rules":[]}'ชุดกติกาของระดับนี้ (migration 007)
window_monthsINTEGERYES-กรอบเวลาที่ใช้รวมยอดของระดับนี้ (เดือน, 1-120) — กำหนดแยกรายระดับ (migration 007)
text_colorVARCHAR(9)YES-สีตัวอักษรบนบัตรระดับนี้ — NULL = ใช้ค่าของโปรแกรม (migration 008)
is_defaultBOOLEANNOfalseระดับตั้งต้นที่ลูกค้าอยู่เมื่อยังไม่เข้าเกณฑ์ระดับใด — กติกาของระดับนี้ไม่ถูกประเมิน (migration 009)

รูปแบบของ conditions

{
"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
  • window_months ใช้กับทุก metric ยกเว้น member_months ซึ่งเป็นอายุสมาชิก จึงเป็นระยะเวลา ไม่ใช่ยอดรวมภายในกรอบเวลา

ตาราง loyalty.tier_history

ColumnTypeNullableDefaultคำอธิบาย
idBIGSERIALNOauto incrementPrimary key
account_idINTEGERNO-FK ไปยัง loyalty.account(id) ลบแบบ CASCADE
line_oa_idINTEGERNO-LINE OA ที่ประวัตินี้สังกัด
organization_idINTEGERNO-องค์กรเจ้าของข้อมูล
from_tier_idINTEGERYES-FK ไปยัง loyalty.tier(id) — ระดับก่อนเปลี่ยน (NULL เมื่อเป็นการกำหนดครั้งแรก)
to_tier_idINTEGERYES-FK ไปยัง loyalty.tier(id) — ระดับหลังเปลี่ยน
reasonVARCHAR(20)NO-สาเหตุ: promote, demote, initial
window_valueNUMERIC(12,2)NO0ค่ารวมในกรอบเวลาที่ทำให้ตัดสินใจเช่นนั้น เก็บไว้เพื่อให้แถวอธิบายตัวเองได้
created_dateTIMESTAMPTZ(3)NOCURRENT_TIMESTAMPวันเวลาที่เปลี่ยนระดับ

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

loyalty.account (migration 006)

ColumnTypeNullableDefaultคำอธิบาย
tier_idINTEGERYES-FK ไปยัง loyalty.tier(id) — ระดับที่ลูกค้าอยู่ในปัจจุบัน
tier_sinceTIMESTAMPTZ(3)YES-วันเวลาที่เข้าสู่ระดับปัจจุบัน
tier_evaluated_dateTIMESTAMPTZ(3)YES-วันเวลาที่ประเมินระดับครั้งล่าสุด

loyalty.milestone (migration 006)

ColumnTypeNullableDefaultคำอธิบาย
min_tier_rankINTEGERYES-ระดับขั้นต่ำที่รับรางวัลนี้ได้ — NULL = เปิดให้ทุกคน ซึ่งเป็นค่าที่รางวัลเดิมทุกใบต้องคงไว้

loyalty.program (migration 006)

ColumnTypeNullableDefaultคำอธิบาย
tier_enabledBOOLEANNOfalseเปิด/ปิดฟีเจอร์ระดับสมาชิกของโปรแกรมนี้
tier_window_monthsINTEGERNO3กรอบเวลารวมยอดระดับโปรแกรม (เดือน, 1-120)
tier_fallbackVARCHAR(20)NO'step_down'วิธีลดระดับเมื่อไม่เข้าเกณฑ์: step_down (ลดทีละขั้น) หรือ specific (ลงไประดับที่กำหนด)
tier_fallback_tier_idINTEGERYES-FK ไปยัง loyalty.tier(id) — ระดับปลายทางเมื่อ tier_fallback = 'specific'

หมายเหตุ

  • Check constraint ของ tier: chk_loy_tier_rank (rank > 0), chk_loy_tier_threshold (threshold >= 0), chk_loy_tier_status (status IN ('active','archived')) และ chk_loy_tier_window (window_months เป็น NULL หรืออยู่ระหว่าง 1-120)
  • Check constraint อื่น: chk_loy_tier_hist_reason (reason IN ('promote','demote','initial')), chk_loy_milestone_tier (min_tier_rank เป็น NULL หรือมากกว่า 0), chk_loy_program_tier_window (tier_window_months ระหว่าง 1-120), chk_loy_program_tier_fallback (tier_fallback IN ('step_down','specific'))
  • Unique: idx_loy_tier_rank บน (program_id, rank) WHERE status = 'active' — หนึ่งระดับ ต่อหนึ่งขั้น เป็น partial index จึงปลดล็อกให้ rank เดิมกลับมาใช้ใหม่ได้เมื่อ archive ระดับนั้นไป และ idx_loy_tier_one_default บน program_id WHERE is_default AND status = 'active' — หนึ่งโปรแกรมมีระดับตั้งต้นได้ไม่เกินหนึ่งระดับ ถ้ามีสองระดับจะตอบไม่ได้ว่า "ระดับที่ตกลงมา" คือระดับไหน
  • Index อื่น: idx_loy_tier_program บน (program_id, rank); idx_loy_tier_hist_acct บน (account_id, created_date DESC); idx_loy_account_tier บน (line_oa_id, tier_id) WHERE tier_id IS NOT NULL
  • เกณฑ์วัดจากยอดใช้จ่าย ไม่ใช่แต้ม: threshold และ metric spend คิดเป็นบาทของยอดใช้จ่าย ในกรอบเวลา ไม่ใช่แต้ม เพราะแต้มถูกหักเมื่อแลกรางวัล การใช้แต้มเป็นเกณฑ์จะทำให้ลูกค้าถูกลดระดับ เพราะแลกของรางวัล ซึ่งเป็นเหตุผลเดียวกับที่ไม่คำนวณระดับจากยอดคงเหลือ
  • ทำไม tier_history ถึงจำเป็น: กรอบเวลาแบบ rolling ทำให้ลูกค้าหลุดระดับได้เองโดยไม่ได้ทำอะไร คำถาม "ทำไมฉันถึงถูกลดระดับ" จึงเป็นคำถามที่ฟีเจอร์นี้สร้างขึ้นแน่นอน ถ้าไม่มีค่า window_value ที่ทำให้เกิดการตัดสินใจนั้นเก็บไว้ ก็จะไม่มีใครตอบได้
  • ทำไม conditions ถึงเป็น JSONB ไม่ใช่ตารางลูก: กติกาถูกอ่านทั้งชุดพร้อมกับ tier เสมอ ไม่เคย query ข้าม tier การแยกเป็นตารางจะเพิ่ม join และเพิ่มหน้าจอ CRUD อีกชุดสำหรับสิ่งที่ เป็นออบเจ็กต์เดียว — แนวเดียวกับ bonus_rule.conditions และ audience filter
  • การย้ายข้อมูลของ migration 007: แปลง threshold เดิมของทุก tier เป็นกติกา spend หนึ่งข้อที่มีความหมายเท่าเดิม บันไดของร้านที่ตั้งไว้แล้วจึงไม่ถูกรีเซ็ต และคอลัมน์ threshold ถูกเปลี่ยนเป็น nullable แทนการ drop เพื่อให้ binary รุ่นก่อนหน้ายังหาคอลัมน์ที่คาดหวังเจอ (มี COMMENT ON COLUMN ระบุไว้ว่าเป็น legacy และไม่ถูกอ่าน)