New Down-Tier Logic

Tier ยอดสะสม

💙 NAVY      →  ยอดสะสม 049,999 บาท
❤️ SCARLET   →  ยอดสะสม 50,000299,999 บาท
👑 CROWN     →  ยอดสะสม 300,0001,999,999 บาท
⭐ VEGA      →  ยอดสะสม 2,000,000 บาท ขึ้นไป
💎 CRYSTAL / VVIP  →  ได้รับเชิญพิเศษเท่านั้น (ไม่ขึ้นกับยอดซื้อ)

ความแตกต่างจาก Legacy

กฎ 6 ข้อนี้เปลี่ยนเฉพาะ ลง Tier (down-tier) จาก void/refund เท่านั้น
การขึ้น Tier ยังใช้ logic เดิมทุกอย่าง ไม่เปลี่ยน

เหตุผล: ทุกข้อพูดถึง “เมื่อเกิด void/refund” — ไม่มีข้อไหนเกี่ยวกับ “เมื่อซื้อของ”
ปัญหาอยู่ที่ “ลง Tier ง่ายเกินไป” ไม่ใช่ “ขึ้น Tier”

ขึ้น Tier (upgrade) ลง Tier (downgrade จาก void/refund)
Logic เดิม — ไม่เปลี่ยน ใหม่ — ตามกฎ 6 ข้อด้านล่าง
ใช้ยอดสะสม accumulateSpending (rolling 24 เดือน) Accum Tier Logic (window พิเศษ)
Function calculateTierToUpgrade() — ไม่แก้ addNewRefundSalesTransaction() — แก้
Trigger ซื้อของ (Flow 1) คืนสินค้า / ยกเลิกรายการ (Flow 2)
Legacy New
Trigger downgrade refund ลด accumulateSpending ต่ำกว่า tier refund เป็น transaction ที่ทำให้ up-tier หรือก่อนหน้า
Window คำนวณ rolling 24 เดือนจากปัจจุบัน 24 เดือนย้อนหลังจาก up-tier date + ทุก transaction หลัง up-tier
Transaction หลัง up-tier นับรวมใน accumulateSpending ไม่ทำให้ down-tier (ข้อ 2)
Maintain spending นับจาก tierStartedAt นับจาก transaction ถัดจาก up-tier transaction

หลักการ

Accum (ปัจจุบัน — Legacy)

Accum = SUM ย้อนหลัง 24 เดือนจากวันปัจจุบัน

Accum Tier Logic (New — ใช้ตัดสิน down-tier)

Accum Tier Logic = SUM 24 เดือนย้อนหลังจาก up-tier date
                 + SUM ทุก transaction หลัง up-tier จนถึงปัจจุบัน

ตัวอย่าง Timeline:

ต.ค. 2023  ซื้อ 10,000  ─┐
พ.ย. 2023  ซื้อ 10,00024 เดือนย้อนหลังจาก up-tier date
ธ.ค. 2023  ซื้อ 10,000   │
ม.ค. 2024  ซื้อ 10,000  ─┤
           ซื้อ 10,000  ──┼── up-tier date (01 Jan 2024) → ขึ้น SCARLET (50,000)
ก.พ. 2024  ซื้อ 10,000  ─┐
มี.ค. 2024  ซื้อ 10,000   │  transactions หลัง up-tier
เม.ย. 2024  ซื้อ 10,000   │
พ.ค. 2024  ซื้อ 10,000   │
มิ.ย. 2024  ซื้อ 10,000   │
ก.ค. 2024  ซื้อ 10,000  ─┘  ← ปัจจุบัน

Accum (Legacy)     = rolling 24 เดือนจากปัจจุบัน = 80,000 บาท
Accum Tier Logic   = (10K×4 ก่อน up-tier) + (10K×1 up-tier) + (10K×6 หลัง up-tier)
                   = 40,000 + 10,000 + 60,000 = 110,000 บาท

กฎ 6 ข้อ

ข้อ 1 — Transaction Expired ไม่ทำให้ Down-tier

Transaction ที่หลุดออกจาก rolling 24 เดือน (expired) ไม่ทำให้ down-tier

ตัวอย่าง:

up-tier เมื่อ 01 Jan 2024 ด้วย accum = 50,000 บาท (SCARLET)
วันนี้ 01 Feb 2026 → transaction ก่อน 01 Feb 2024 หลุด rolling window ปกติ

Legacy: accum ลดลง → อาจ down-tier
New:    ไม่ down-tier เพราะ transaction expired ไม่ใช่ void/refund ✅

ข้อ 2 — Void/Refund หลัง Up-tier ไม่ทำให้ Down-tier

Void หรือ Refund ที่เกิดหลัง up-tier date ไม่ทำให้ down-tier

ตัวอย่าง:

up-tier วันที่ 01 Jan 2024 (ขึ้น SCARLET)
ซื้อเพิ่ม 10,000 บาท เมื่อ 15 Mar 2024  ← หลัง up-tier
void/refund 10,000 บาท นั้นเมื่อ 20 Apr 2024

Legacy: accum ลดลง → อาจ down-tier
New:    void/refund เกิดหลัง up-tier date (Mar 2024 > Jan 2024)
        → ไม่ down-tier ✅

ข้อ 3 — Void/Refund ก่อนหรือตอน Up-tier → ดูผลลัพธ์ Accum Tier Logic

Void หรือ Refund ที่เกิดก่อนหรือตอน up-tier date → คำนวณยอดใหม่ด้วย:

Accum Tier Logic ใหม่ = Accum Tier Logic ก่อน void/refund - void/refund amount

แล้วดูว่ายังถึง tier ไหน:

ผลลัพธ์ การดำเนินการ
ยังถึง tier ปัจจุบัน คง tier เดิม (ข้อ 3.1)
ข้าม tier (accum อยู่ระหว่าง tier ปัจจุบันกับ tier ก่อนหน้า) Down เป็น tier ก่อนหน้า (ข้อ 3.2)
ต่ำกว่า tier ก่อนหน้า คำนวณใหม่ทั้งหมด + หา tier dates (ข้อ 3.3)

3.1 — ยังถึง tier ปัจจุบัน → คง tier

หมายเหตุ (จาก code): เมื่อ tier ไม่เปลี่ยน (newTier.id === currentTierId) code จะ ไม่เข้า block down-tier เลย
ดังนั้น maintain spending จะถูกคำนวณจาก calculateAccumulateMaintainSpending(tierStartedAt, tierEndedAt) ตามปกติ (ไม่ใช่กฎ 4)
แต่ถ้า refund ทำให้ qualifying transaction เปลี่ยน (เช่น up-tier txn ถูก refund แต่ txn อื่นรวมกันยังถึง tier) → maintain จะถูก recalculate จาก txns ที่ completedAt อยู่ในช่วง tierStartedAt-tierEndedAt ซึ่งอาจเป็น 0 ถ้า up-tier txn ถูก refund หมด

ตัวอย่าง:

สถานะ: CROWN (minimumSpending = 300,000)
Accum Tier Logic ก่อน void/refund = 350,000 บาท

void/refund 50,000 บาท (ก่อน up-tier)
→ Accum Tier Logic ใหม่ = 350,000 - 50,000 = 300,000 บาท

300,000 >= 300,000 (CROWN) ✅
→ คง CROWN

3.2 — ต่ำกว่า tier ปัจจุบัน แต่ถึง tier ก่อนหน้า → ดู validity ของ tier ก่อนหน้า

ดูว่า tier ก่อนหน้านั้น valid ไหม (tierEndedAt ยังไม่หมดอายุ):

ผลลัพธ์ การดำเนินการ
Valid (ยังไม่หมดอายุ) Down เป็น tier ก่อนหน้า + ใช้ tier dates จากประวัติ
ไม่ valid (หมดอายุแล้ว) Fallback → ไปใช้ logic เดียวกับ ข้อ 3.3

ตัวอย่าง (valid):

สถานะ: CROWN (minimumSpending = 300,000)
ประวัติ: SCARLET มี tierEndedAt = Dec 2026 (ยัง valid)
Accum Tier Logic ก่อน void/refund = 320,000 บาท

void/refund 100,000 บาท (ก่อน up-tier)
→ Accum Tier Logic ใหม่ = 320,000 - 100,000 = 220,000 บาท

220,000 < 300,000 (CROWN) ❌
220,000 >= 50,000 (SCARLET) ✅
SCARLET ยัง valid (tierEndedAt ยังไม่หมดอายุ) ✅
→ Down เป็น SCARLET + tierStartedAt/tierEndedAt จากประวัติ

ตัวอย่าง (ไม่ valid):

สถานะ: CROWN (minimumSpending = 300,000)
ประวัติ: SCARLET มี tierEndedAt = Dec 2023 (หมดอายุแล้ว → ไม่ valid)
Accum Tier Logic ก่อน void/refund = 320,000 บาท

void/refund 100,000 บาท (ก่อน up-tier)
→ Accum Tier Logic ใหม่ = 320,000 - 100,000 = 220,000 บาท

220,000 >= 50,000 (SCARLET) ✅ แต่ SCARLET ไม่ valid แล้ว
→ Fallback → scan หา qualifying transaction ภายใน 24 เดือนก่อน up-tier date + หลัง up-tier จนถึงปัจจุบัน (หักลบ refund แล้ว)
   หรือถ้าไม่มี → NAVY มี tierStartedAt = วันนี้

3.3 — ต่ำกว่า tier ก่อนหน้า → คำนวณใหม่ทั้งหมด + หา tier dates

Fall back ไปใช้ logic เดียวกัน คือ: scan หา qualifying transaction ภายใน 24 เดือนก่อน up-tier date เพื่อหา tier ปลายทาง

กรณี ผลลัพธ์
มี qualifying transaction ภายใน 24 เดือนก่อน up-tier date + หลัง up-tier จนถึงปัจจุบัน ที่ทำให้ถึง tier ปลายทาง (หักลบ refund แล้ว) tierStartedAt = completedAt ของ transaction นั้น (3.3.1)
ไม่มี qualifying transaction ใน window นั้น Down เป็น NAVY (3.3.2)

Maintain คำนวณใหม่ตาม ข้อ 4

3.3.1 — มี qualifying transaction ใน 24 เดือนก่อน up-tier + หลัง up-tier จนถึงปัจจุบัน → tierStartedAt = completedAt ของ transaction นั้น

scan หา transaction แรกที่ทำให้ accum สะสม (หักลบ refund แล้ว) ถึง threshold
window = 24 เดือนก่อน up-tier date + หลัง up-tier จนถึงปัจจุบัน

ตัวอย่าง:

ประวัติ tier:
  SCARLET → Jan 2023
  CROWN   → Jul 2023
  VEGA    → Jan 2024  ← current

สถานะ: VEGA (minimumSpending = 2,000,000)
Accum Tier Logic ก่อน void/refund = 2,100,000 บาท

void/refund 1,800,000 บาท (ก่อน up-tier)
→ Accum Tier Logic หลัง void/refund = 300,000 บาท

300,000 < 2,000,000 (VEGA) ❌
300,000 < 300,000 (CROWN) ❌
300,000 >= 50,000 (SCARLET) ✅
→ Down จาก VEGA → SCARLET (ข้าม CROWN)

หา qualifying transaction ภายใน 24 เดือนก่อน up-tier date + หลัง up-tier จนถึงปัจจุบัน (หักลบ refund แล้ว)
→ สมมติ transaction ที่ทำให้ accum ถึง SCARLET (50,000) อยู่ใน window เกิดเมื่อ Jan 2023
→ tierStartedAt = Jan 2023  ← ใช้ completedAt ของ transaction นั้น
→ tierEndedAt = endOf(Jan 2023 + 2 ปี) = Dec 2025
→ maintain คำนวณใหม่ตาม ข้อ 4
3.3.2 — ไม่มี qualifying transaction ใน 24 เดือนก่อน up-tier → Down เป็น NAVY

ตัวอย่าง:

สถานะ: VEGA (minimumSpending = 2,000,000)
Accum Tier Logic ก่อน void/refund = 2,100,000 บาท (มาจาก transaction ที่ทำให้ up-tier เพียงตัวเดียว)

void/refund transaction นั้น 2,000,000 บาท
→ Accum Tier Logic หลัง void/refund = 100,000 บาท

100,000 < 2,000,000 (VEGA) ❌
100,000 < 300,000 (CROWN) ❌
100,000 < 50,000 (SCARLET) ❌
→ Fallback → scan หา qualifying transaction ภายใน 24 เดือนก่อน up-tier date + หลัง up-tier จนถึงปัจจุบัน (หักลบ refund แล้ว)
→ ไม่มี qualifying transaction ใน window นั้น
→ Down เป็น NAVY มี tierStartedAt = วันนี้

ข้อ 4 — Maintain Spending คำนวณใหม่เมื่อ Down-tier

เมื่อเกิด down-tier ยอด maintain ไม่ reset เป็น 0 แต่คำนวณใหม่จาก:

maintainSpending = SUM transactions ถัดจาก up-tier transaction จนถึงปัจจุบัน
                   (ไม่รวม up-tier transaction เอง)

ตัวอย่าง:

up-tier transaction: 10,000 บาท (01 Jan 2024)  ← ไม่นับ
transactions หลัง up-tier:
  ก.พ. 2024  10,000
  มี.ค. 2024  10,000
  เม.ย. 2024  10,000
  พ.ค. 2024  10,000
  มิ.ย. 2024  10,000
  ก.ค. 2024  10,000
  รวม = 60,000 บาท

Legacy: maintainSpending = 0 (reset)
New:    maintainSpending = 60,000 บาท ✅

ข้อ 5 — tierStartedAt / tierEndedAt เมื่อ Down-tier

5.1 — มี transaction ทำให้เกิด tier ก่อนหน้า

เมื่อ down-tier แล้ว tier ที่ได้เป็น tier ที่มี transaction ทำให้เกิด (มีประวัติ)

→ ดูว่า tier dates จากประวัติยัง valid ไหม (tierEndedAt ยังไม่หมดอายุ):

ผลลัพธ์ การดำเนินการ
Valid (ยังไม่หมดอายุ) Down เป็น tier นั้น + tierStartedAt/tierEndedAt จากประวัติ
ไม่ valid (หมดอายุแล้ว) Fallback → ไปใช้ logic เดียวกับ ข้อ 5.2

ตัวอย่าง (valid):

ประวัติ tier:
  NAVY     → Jan 2022
  SCARLET  → Jun 2023  ← มี transaction ทำให้เกิด SCARLET
  CROWN    → Jan 2024

void/refund ทำให้ down จาก CROWN → SCARLET

→ SCARLET มี transaction ทำให้เกิด → ใช้ข้อ 5.1
→ ตรวจ validity: tierEndedAt = Dec 2025 (ยัง valid)
→ ผลลัพธ์: SCARLET มี tierStartedAt = Jun 2023, tierEndedAt = Dec 2025

ตัวอย่าง (ไม่ valid):

ประวัติ tier:
  NAVY     → Jan 2022
  SCARLET  → Jun 2021  ← มี transaction ทำให้เกิด SCARLET
  CROWN    → Jan 2024

void/refund ทำให้ down จาก CROWN → SCARLET

→ SCARLET มี transaction ทำให้เกิด → ใช้ข้อ 5.1
→ ตรวจ validity: tierEndedAt = Dec 2023 (หมดอายุแล้ว → ไม่ valid)
→ Fallback → ไปใช้ logic เดียวกับ ข้อ 5.2
→ คำนวณใหม่จาก scratch → ผลลัพธ์: SCARLET มี tierStartedAt = วันนี้

5.2 — transaction เดียวถึง tier ปัจจุบันเลย (ไม่มี transaction ทำให้ tier ก่อนหน้า)

เมื่อ down-tier แล้ว tier ที่ได้เป็น tier ที่ไม่เคยมี transaction ทำให้เกิด (transaction เดียวถึงเลย)

→ ต้องคำนวณใหม่จาก scratch แล้วดูว่า Accum ที่เหลือไปลงที่ tier อะไร:

ผลลัพธ์ tierStartedAt
ไปลง NAVY วันนี้
ไปลง SCARLET/CROWN/VEGA วันนี้

MT คำนวณใหม่ตาม ข้อ 4

ตัวอย่าง:

ประวัติ tier:
  NAVY    → Jan 2023
  CROWN   → Jul 2023  ← ข้าม SCARLET เพราะ transaction เดียวถึง CROWN
  CROWN   → Jan 2024

ก่อน refund:
  CROWN (ACC: 2.1M, MT: 0, START: 1 JAN 24, END: 31 DEC 26)

Refund 1.8M (ก่อน up-tier จริง Jul 2023)
→ Accum ที่เหลือ = 300K = CROWN (ไม่ถึง VEGA แต่ถึง CROWN)
→ CROWN ไม่เคยมี transaction ทำให้เกิด → ใช้ข้อ 5.2
→ คำนวณใหม่จาก scratch → ผลลัพธ์: CROWN มี tierStartedAt = วันนี้

5.2.3 — tier ไม่เปลี่ยน

ตัวอย่าง:

ประวัติ tier:
  NAVY  → Jan 2023 (START: 1 JAN 23, END: 31 DEC 25)
  SCARLET → Jan 2024 (START: 1 JAN 24, END: 31 DEC 26) ← up-tier + current

ก่อน refund:
  SCARLET (ACC: 65K, MT: 20K, START: 1 JAN 24, END: 31 DEC 26)

Refund 10K (ก่อน up-tier)
→ Accum ที่เหลือ = 55K = SCARLET (ยังถึง tier ปัจจุบัน)
→ tier ไม่เปลี่ยน → ไม่ต้องหา tierStartedAt ใหม่
→ MT คำนวณใหม่ตามข้อ 4

ตัวอย่างเต็ม: Down-tier จาก Refund

Timeline ของคุณ A:
  ต.ค. 2023  ซื้อ 10,000  → accum = 10,000
  พ.ย. 2023  ซื้อ 10,000  → accum = 20,000
  ธ.ค. 2023  ซื้อ 10,000  → accum = 30,000
  ม.ค. 2024  ซื้อ 10,000  → accum = 40,000
             ซื้อ 10,000  → accum = 50,000  ✅ ขึ้น SCARLET  ← up-tier txn
  ก.พ.–ก.ค. 2024  ซื้อ 10,000 × 6 = 60,000  (หลัง up-tier)

Accum Tier Logic = 40,000 + 10,000 + 60,000 = 110,000 บาท

เกิด Refund: คืนสินค้าที่ซื้อ ธ.ค. 2023 (ก่อน up-tier) มูลค่า 70,000 บาท

Accum Tier Logic ใหม่ = 110,000 - 70,000 = 40,000 บาท

CROWN.minimumSpending   = 300,00040,000 < 300,000 ❌
SCARLET.minimumSpending =  50,00040,000 <  50,000 ❌
NAVY.minimumSpending    =       040,000 >=      0 ✅ → Down เป็น NAVY

maintainSpending ใหม่ = SUM txns หลัง up-tier = 60,000 บาท  (ไม่ reset เป็น 0)

NAVY ไม่เคยมี transaction ทำให้เกิด → ใช้ข้อ 5.2
→ scan หา qualifying transaction ภายใน 24 เดือนก่อน up-tier + หลัง up-tier จนถึงปัจจุบัน (หักลบ refund แล้ว)
→ ไม่มี qualifying transaction ใน window
→ ผลลัพธ์: NAVY มี tierStartedAt = วันนี้

ข้อ 6 — Adjust จาก Admin

Adjust คือ Admin หักยอดสมาชิกด้วยมือ (DEDUCT) — ใช้ logic เดียวกับ Void/Refund ทุกประการ คือ:

  • ถ้า adjust เกิดหลัง up-tier date → ไม่ down-tier (ข้อ 2)
  • ถ้า adjust เกิดก่อนหรือตอน up-tier date → ใช้ Accum Tier Logic (ข้อ 3)
  • Maintain spending คำนวณใหม่ (ข้อ 4)
  • tierStartedAt/tierEndedAt ตาม 5.1 / 5.2

สิ่งที่ต่างจาก Refund: sequence การบวกยอดใช้ลำดับตาม purchase date ไม่ใช่วันที่ปรับ

ตัวอย่าง:

ประวัติ tier:
  NAVY  → Jan 2023
  SCARLET → May 2024

Adjust ณ วันที่ 1 Apr ปรับ transaction ที่ซื้อ 1 Feb จำนวน 20K
Sequence การบวกคือ 1+2+4+3 ตาม purchase date
= 60K = SCARLET → tier ไม่เปลี่ยน (ยังถึง tier ปัจจุบัน)
Maintain = 30K

ตัวอย่าง (กรณี adjust ทำให้ down-tier):

ประวัติ tier:
  NAVY  → Jan 2023
  SCARLET → May 2024

Adjust ณ วันที่ 1 Apr ปรับยอดลง 30K (ก่อน up-tier)
→ Accum ที่เหลือ = 40K → SCARLET (50K) → คง SCARLET
Maintain = 30K

สรุปเปรียบเทียบ Legacy vs New

Scenario Legacy New
Void/Refund/Adjust ก่อนหรือตอน up-tier อาจ down-tier down-tier ตามข้อ 3 (ดูผลลัพธ์ Accum Tier Logic)
Void/Refund/Adjust หลัง up-tier อาจ down-tier ไม่ down-tier (ข้อ 2)
Transaction expired (หลุด 24 เดือน) อาจ down-tier ไม่ down-tier (ข้อ 1)
Maintain หลัง down-tier reset = 0 คำนวณจาก transactions หลัง up-tier (ข้อ 4)
tierStartedAt หลัง down-tier now ขึ้นกับ 5.1 / 5.2

Implementation Details (จาก Codebase)

Section นี้ document behavior จริงของ code ที่ implement แล้ว

Guard Condition (กฎ 2)

// member.entity.ts → addNewRefundSalesTransaction()
const isRefundBeforeOrAtUpTier = !DateTimeHelper.isAfter(
  salesTransaction?.completedAt ?? now,
  this.tierStartedAt,
);
  • isAfter(a, b) = true ถ้า a > b (strict)
  • ดังนั้น !isAfter(completedAt, tierStartedAt) = true ถ้า completedAt <= tierStartedAt
  • ผลลัพธ์: refund ของ txn ที่ completedAt > tierStartedAt → ไม่เข้า down-tier logic

Accum Tier Logic (กฎ 3)

// member.entity.ts → calculateAccumTierLogicSpending()
const windowStart = startOf(subtractTime(upTierDate, 24, "month"), "month");
const bufferedUpTierDate = addTime(upTierDate, 10, "minute"); // inclusive up-tier txn
const bufferedNow = addTime(now, 10, "minute");

// window 1: [windowStart, bufferedUpTierDate) — รวม up-tier txn เอง
const preUpTierAccum = calculateAccumulate(windowStart, bufferedUpTierDate);
// window 2: (bufferedUpTierDate, bufferedNow) — ไม่รวม up-tier txn
const postUpTierAccum = calculateAccumulate(bufferedUpTierDate, bufferedNow);

return preUpTierAccum + postUpTierAccum;

หมายเหตุ: ใช้ isBetween (exclusive ทั้งสองด้าน) ดังนั้น buffer +10 นาทีเพื่อให้ txn ที่ตรงขอบถูกรวม

Tier Decision + Cap

// member.entity.ts → calculateTierAdjustmentWithNewLogic()
// 1. หา tier สูงสุดที่ accumTierLogic >= minimumSpending
// 2. apply co-brand floor (minimumTier) ถ้ามี
// 3. cap ไม่ให้สูงกว่า tier ปัจจุบัน (capToCurrentTier)
// member.entity.ts → capToCurrentTier()
private capToCurrentTier(tier, current): TierEntity | undefined {
  if (tier && tier.minimumSpending > current.minimumSpending) {
    return current;  // ไม่ให้สูงกว่า current
  }
  return tier ?? this.tier;
}

Tier Dates (กฎ 5)

// กฎ 5.1: findPreviousTierInfo(newTierId)
// - หา MemberTierHistory ล่าสุดที่ toTierId === newTierId
// - ตรวจว่า tierEndedAt ยังไม่หมดอายุ (now < tierEndedAt)
// - ถ้า valid → return { tierStartedAt, tierEndedAt } จาก history

// กฎ 5.2: findQualifyingTransactionDate(threshold)
// - ถ้า threshold <= 0 (เช่น NAVY) → return null ทันที (base tier ไม่ต้องหา qualifying)
// - window = [24mo ก่อน up-tier, now+10min]
// - scan txns จากเก่า→ใหม่ สะสมยอด (หักลบ refund)
// - หยุดเมื่อ running >= threshold → return completedAt ของ txn นั้น
// - ถ้าไม่มี → return null → tierStartedAt = now

Maintain Spending (กฎ 4)

เมื่อ Down-tier (newTier.id !== currentTierId)

// member.entity.ts → addNewRefundSalesTransaction()
// หลัง updateMemberTier แล้ว:
this.setAccumulateMaintainSpending(
  this.calculateAccumulateMaintainSpending(newTierStartedAt, newTierEndedAt),
);

Logic: maintain = SUM txns ที่ completedAt อยู่ในช่วง (newTierStartedAt, newTierEndedAt) (exclusive ทั้งสองด้านจาก isBetween)

เมื่อ Tier ไม่เปลี่ยน (newTier.id === currentTierId)

// member.entity.ts → addNewRefundSalesTransaction()
const qualifyingDate = this.findQualifyingTransactionDate(
  memberCurrentTier.minimumSpending ?? 0,
);
if (qualifyingDate) {
  // maintain = SUM txns หลัง qualifying txn (strict >)
  this.setAccumulateMaintainSpending(
    this.calculatePostUpTierMaintainSpending(qualifyingDate),
  );
} else {
  this.setAccumulateMaintainSpending(0);
}

Logic: หา qualifying txn ใหม่ (txn ที่ทำให้ accum สะสมถึง tier threshold หลังหักลบ refund) แล้ว maintain = SUM txns ที่ completedAt > qualifyingDate

calculatePostUpTierMaintainSpending

// member.entity.ts → calculatePostUpTierMaintainSpending(upTierDate)
// - SUM sales ที่ completedAt > upTierDate (strict >)
// - SUM refunds ที่ original sale completedAt > upTierDate
// - return max(0, salesSum + refundSum)

สำคัญ: ใช้ isAfter (strict >) ดังนั้น txn ที่ completedAt === upTierDate ไม่ถูกนับ

กรณี tier ไม่เปลี่ยน (newTier.id === currentTierId)

เมื่อ Accum Tier Logic ยังถึง tier ปัจจุบัน:

  • เข้า else block → หา qualifying txn ใหม่
  • maintain = SUM txns หลัง qualifying txn
  • ถ้าไม่มี qualifying txn → maintain = 0
  • Guard: ทำเฉพาะ tier type = NORMAL (invitation/import ไม่เข้า)

Non-NORMAL Tier (Invitation / Import)

// calculateTierAdjustmentWithNewLogic()
if (!memberCurrentTier || memberCurrentTier.type !== TierType.NORMAL) {
  return this.tier; // ไม่ down-tier
}

VVIP, VEGA (invitation), Co-brand (import) ที่ type !== NORMAL → return tier เดิมเสมอ → ไม่ down-tier

QA Test Case Discrepancy (TC 10, 11, 26)

Test case 10, 11, 26 ยังมีตัวเลข maintain ที่ต้อง verify:

TC10: Accum Tier Logic = 40,000 — ถ้า SCARLET threshold > 40,000 → code จะ down tier แต่ expected ไม่ down
TC11: qualifying txn logic อาจให้ maintain ไม่ตรง expected (7,000)
TC26: expected maintain = 2,000 แต่ txn ที่ให้ค่านี้ไม่อยู่ในช่วง newTierStartedAt

สาเหตุที่เป็นไปได้:

  1. SCARLET threshold ใน test env ≠ 50,000 (อาจเป็น 40,000 หรือ 45,000)
  2. Test case มี txn/data ที่ไม่ได้แสดงใน pre-condition
  3. Business rule เพิ่มเติมที่ยังไม่ได้ document

Status: ต้อง verify กับ QA/BA