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 ม.ค. 202731 ธ.ค. 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 tierStartedAttierEndedAt
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 ทันทีโดยไม่ต้องซื้อเพิ่ม