3. Legacy Tier Spending Rules
Legacy Tier Calculation — Accumulate Spending Rules
Tier Structure
| Tier | minimumSpending | maintainSpending | หมายเหตุ |
|---|---|---|---|
| Navy | 0 | 0 | Default tier |
| Scarlet | 40,000 | 40,000 | |
| Crown | 300,000 | 300,000 | |
| Vega | 2,000,000 | 2,000,000 | |
| CRYSTAL / VVIP | null | null | INVITATION type — ไม่มี spending rule |
Config (env)
| Variable | Value | ความหมาย |
|---|---|---|
ACCUMULATE_THRESHOLD_MONTH |
24 |
ย้อนหลังกี่เดือนสำหรับ accumulateSpending |
YEARS_TO_MAINTAIN |
2 |
tier cycle ยาวกี่ปี |
หลักการคำนวณ
accumulateSpending = SUM(salesTransactions in window)
+ SUM(revokeAccumSpendableAmount in window)
ข้อสำคัญ — Legacy Data Issue:
ในระบบเดิม revokeAccumSpendableAmount ของ refund บางรายการถูกบันทึกเป็น ค่าบวก (ผิดพลาด)
ทำให้ยอดสะสมสูงเกินจริง ระบบจึงแก้ไขโดยสร้าง refund record ใหม่ที่เป็น ค่าลบ เพื่อหักล้าง
กรณีปกติ (ถูกต้อง):
revokeAccumSpendableAmount = -25,000 ← ลด spending
กรณี legacy bug:
revokeAccumSpendableAmount = +25,000 ← เพิ่ม spending (ผิด!)
การแก้ไข (adjust):
สร้าง refund record ใหม่: revokeAccumSpendableAmount = -25,000
→ หักล้างกัน: +25,000 + (-25,000) = 0
โค้ดที่สร้าง correction record (adjust-sales-transaction.service.ts):
revokeAccumSpendableAmount: -Math.abs(Number(props.amount)); // บังคับเป็นค่าลบเสมอ
Spending ที่ใช้คำนวณมี 2 ประเภท:
| Field | Window | ใช้ทำอะไร |
|---|---|---|
accumulateSpending |
rolling 24 เดือน | ตัดสิน Upgrade / Downgrade จาก refund |
accumulateMaintainSpending |
tierStartedAt → tierEndedAt | ตัดสิน Maintain / Downgrade สิ้นรอบ |
1. accumulateSpending — Upgrade / Downgrade
accumulateStartedAt = startOf(now - 24 เดือน, 'month')
= วันที่ 1 ของเดือน ที่ย้อนหลัง 24 เดือน
window = (accumulateStartedAt, now + 10 นาที) ← exclusive boundary
หมายเหตุ: isBetween ของ dayjs ใช้ exclusive boundary — บวก 10 นาที buffer สำหรับ transaction ที่เพิ่งเกิด
ตัวอย่าง: Upgrade Navy → Scarlet
วันนี้: 29 มี.ค. 2026
accumulateStartedAt = 1 มี.ค. 2024
Sales Transactions ใน window:
- 10 ม.ค. 2025 : +15,000 บาท
- 5 มิ.ย. 2025 : +15,000 บาท
- 20 ก.พ. 2026 : +15,000 บาท
──────────────
accumulateSpending = 45,000 บาท
Scarlet.minimumSpending = 40,000
45,000 >= 40,000 → ✅ Upgrade เป็น Scarlet
tierStartedAt = 20 ก.พ. 2026 (วันที่ transaction ที่ทำให้ขึ้น tier)
tierEndedAt = 31 ธ.ค. 2027
ตัวอย่าง: Transaction นอก window ไม่นับ
วันนี้: 29 มี.ค. 2026
accumulateStartedAt = 1 มี.ค. 2024
Sales Transactions:
- 15 ก.พ. 2024 : +35,000 บาท ← ❌ ก่อน 1 มี.ค. 2024 ไม่นับ
- 10 เม.ย. 2024 : +20,000 บาท ← ✅ อยู่ใน window
──────────────
accumulateSpending = 20,000 บาท (ไม่ถึง Scarlet)
ตัวอย่าง: Upgrade Scarlet → Crown ทันที
Member อยู่ Scarlet, มียอดสะสม: 280,000 บาท
ซื้อเพิ่ม: 30,000 บาท
accumulateSpending ใหม่ = 310,000 บาท
Crown.minimumSpending = 300,000
310,000 >= 300,000 → ✅ Upgrade เป็น Crown ทันที
→ transaction 30,000 บาทนี้ถูกนับรวมในยอดสะสมด้วย
2. accumulateMaintainSpending — Maintain / Downgrade สิ้นรอบ
window = (tierStartedAt, tierEndedAt) ← exclusive boundary
นับ spending ในช่วง tier cycle ปัจจุบันเท่านั้น ไม่ใช่ rolling window
ตัวอย่าง: Maintain Scarlet สำเร็จ
Member อยู่ tier Scarlet (maintainSpending=40,000)
tierStartedAt = 15 มิ.ย. 2024
tierEndedAt = 31 ธ.ค. 2026
Sales Transactions ใน [tierStartedAt, tierEndedAt]:
- 20 ก.ค. 2024 : +25,000 บาท
- 5 มี.ค. 2025 : +20,000 บาท
──────────────
accumulateMaintainSpending = 45,000 บาท
Scarlet.maintainSpending = 40,000
45,000 >= 40,000 → ✅ Maintain Scarlet
tier cycle ใหม่: 1 ม.ค. 2027 → 31 ธ.ค. 2027
ตัวอย่าง: Downgrade Scarlet → Navy สิ้นรอบ
tierStartedAt = 15 มิ.ย. 2024
tierEndedAt = 31 ธ.ค. 2026
Sales Transactions ใน [tierStartedAt, tierEndedAt]:
- 20 ก.ค. 2024 : +15,000 บาท
- 5 มี.ค. 2025 : +10,000 บาท
──────────────
accumulateMaintainSpending = 25,000 บาท
Scarlet.maintainSpending = 40,000
25,000 < 40,000 → ❌ ไม่ผ่าน
หา tier ที่เหมาะสม:
Navy.minimumSpending = 0
25,000 >= 0 → ✅ Downgrade เป็น Navy
3. Tier Cycle
tierEndedAt = endOf(tierStartedAt + 2 ปี, 'year')
ตัวอย่าง
upgrade วันที่ 15 มิ.ย. 2024
→ tierEndedAt = 31 ธ.ค. 2026 23:59:59
เมื่อ 1 ม.ค. 2027 ผ่านไป → Temporal Workflow trigger Maintain Tier
tier cycle ใหม่:
tierStartedAt = 1 ม.ค. 2027
tierEndedAt = 31 ธ.ค. 2027 23:59:59
4. การนับ Refund
Refund ไม่ใช้ completedAt ของ refund transaction โดยตรง
→ ใช้ completedAt ของ sales transaction ที่ถูก refund แทน
refundTransaction → หา salesTransaction ที่ match → ใช้ salesTransaction.completedAt
ตัวอย่าง: Downgrade Scarlet → Navy จาก Refund
Member อยู่ tier Scarlet
tierStartedAt = 1 ม.ค. 2025
Sales Transactions:
- 10 ก.พ. 2025 : +25,000 บาท (หลัง tierStartedAt ✅)
- 5 มี.ค. 2025: +20,000 บาท (หลัง tierStartedAt ✅)
accumulateSpending = 45,000 → อยู่ Scarlet
วันที่ 20 มี.ค. 2025: Refund ของ 10 ก.พ. 2025 (-25,000)
salesTransaction.completedAt = 10 ก.พ. 2025 > tierStartedAt ✅
→ refund อยู่ในรอบปัจจุบัน → ตรวจ tier adjustment
accumulateSpending ใหม่ = 45,000 - 25,000 = 20,000 บาท
Scarlet.minimumSpending = 40,000
20,000 < 40,000 → ❌ ต้องลด tier
Navy.minimumSpending = 0
20,000 >= 0 → ✅ Downgrade เป็น Navy
accumulateMaintainSpending = 0 (reset)
ตัวอย่าง: Refund ไม่ Downgrade (transaction ก่อน tierStartedAt)
Member อยู่ tier Scarlet
tierStartedAt = 1 ม.ค. 2025
Sales Transaction เก่า:
- 15 ธ.ค. 2024 : +25,000 บาท (ก่อน tierStartedAt ❌)
วันที่ 20 มี.ค. 2025: Refund ของ 15 ธ.ค. 2024
salesTransaction.completedAt = 15 ธ.ค. 2024 < tierStartedAt (1 ม.ค. 2025)
→ refund เกิดก่อนรอบปัจจุบัน → ❌ ไม่ downgrade tier
5. Co-Brand Minimum Tier Protection
Member มีบัตร Co-Brand ที่กำหนด minimumTierCode = Crown
accumulateSpending: 60,000 บาท
Tier ที่คำนวณได้: Scarlet
แต่เนื่องจากมี minimumTier = Crown:
→ อยู่ Crown Tier ✅ (ไม่สามารถลดต่ำกว่า Crown ได้)
6. INVITATION Tier — กรณีพิเศษ
INVITATION tier (CRYSTAL / VVIP) ไม่เข้าร่วมการคำนวณ spending อัตโนมัติ
INVITATION tier:
- ไม่มี minimumSpending / maintainSpending
- กำหนดให้ member ด้วยตนเองเท่านั้น (manual assign)
- ไม่ถูกนับในการ sort/compare tier ปกติ
- ไม่ downgrade จาก spending ต่ำ
- ไม่ upgrade จาก spending สูง
ผลต่อ maintain tier สิ้นรอบ
Member อยู่ INVITATION tier (CRYSTAL):
calculateNewTierFromAccumulateMaintainSpending()
→ ตรวจ: currentTier.type === INVITATION → return currentTier ทันที
→ ไม่ตรวจ maintainSpending เลย → คง CRYSTAL ไว้เสมอ
ผลต่อ co-brand card cancel
Member อยู่ INVITATION tier:
coBrandCardUpdated()
→ ตรวจ: currentTier.type === INVITATION → return ทันที
→ ไม่ downgrade แม้ card จะถูก cancel
7. สรุปตาราง
| กรณี | Window | นับจาก |
|---|---|---|
| Upgrade (sales txn) | rolling 24 เดือน | startOf(now - 24M, 'month') → now+10min |
| Downgrade (refund) | rolling 24 เดือน | เหมือนกัน (ดู completedAt ของ sales txn ต้นทาง) |
| Maintain tier (สิ้นรอบ) | tier cycle | tierStartedAt → tierEndedAt |
| Reconcile tier | rolling 24 เดือน | เหมือนกัน |
8. ข้อสังเกต (Legacy Behavior)
- Transaction ที่ trigger tier upgrade ถูกนับรวม ใน accumulateSpending ด้วย
accumulateMaintainSpendingเริ่มนับตั้งแต่tierStartedAtซึ่งรวม transaction ที่ทำให้ขึ้น tier- ทำให้ maintain ง่ายกว่าที่ควรในบางกรณี
ตัวอย่าง:
ขึ้น Crown ด้วย transaction 30,000 บาท (ยอดสะสมรวม = 310,000)
Maintain requirement = 300,000 บาท
accumulateMaintainSpending เริ่มต้น = 310,000 บาท (รวม up-tier transaction)
→ ผ่าน maintain ทันทีโดยไม่ต้องซื้อเพิ่ม