Solution A: Sequence Diagrams — Flow 1–6
Solution A: Sequence Diagrams — Flow 1–6
แสดงลำดับการทำงานของ Flow ที่เกี่ยวข้องกับ Solution A
อ่านคู่กับsolution-a-mapping.mdและ3.legacy-fullsystem-tier-flows.md
Shared prerequisite: sequence นี้สมมติว่า recent sales/refund 48 เดือนถูกเตรียมพร้อมก่อน deploy logic ใหม่แล้ว
ต้องแก้recent-sales-transaction.service.tsให้ retention =accumulateThresholdMonth + 24(จากเดิม+ 12) และรัน one-time backfill ก่อน deploy
Legend
| Symbol | ความหมาย |
|---|---|
| 🟢 | ไม่เปลี่ยนจาก Legacy |
| 🟡 | เพิ่ม logic เล็กน้อย |
| 🔴 | เปลี่ยน logic สำคัญ |
| 🔵 | เพิ่ม method ใหม่ |
Flow 1: Sales Transaction
ไม่เปลี่ยนอะไรเลย — เหมือน Legacy 100%
sequenceDiagram
autonumber
participant ExtSys as External Sales System
participant Kafka as Kafka Broker
participant STCtrl as SalesTransaction Controller<br/>(Kafka Consumer)
participant STSvc as SalesTransactionService
participant MRepo as MemberRepository
participant TRepo as TierRepository
participant Member as Member Entity
participant DB as Database (Prisma)
participant EvBus as Domain Event Bus
participant Listener as MemberTierUpdated Listener
participant KOut as Kafka (Outbound)
ExtSys->>Kafka: Publish SalesTransactionCompletedDomainEvent
Kafka->>STCtrl: @EventPattern consume message
STCtrl->>STSvc: createSalesTransaction(memberId, brandCode, amounts, ...)
STSvc->>MRepo: findById(memberId) + load recent sales txns
MRepo->>DB: SELECT Member + SalesTransactions + RefundSalesTransactions
DB-->>MRepo: member data with transactions
MRepo-->>STSvc: MemberEntity (hydrated)
STSvc->>TRepo: findAllActive()
TRepo->>DB: SELECT Tier WHERE active = true
DB-->>TRepo: tier rows
TRepo-->>STSvc: TierEntity[]
STSvc->>STSvc: Create SalesTransactionEntity
STSvc->>Member: addNewSalesTransaction(salesTxnEntity, tiers)
Note over Member: 🟢 Push txn to _newSalesTransaction list
Note over Member: 🟢 calculateAccumulateSpending()
Note right of Member: sum(totalAccumSpendableAmount)<br/>for txns in [now - 24 months, now]<br/>minus sum(revokeAccumSpendableAmount)<br/>for refunds in same window
Note over Member: 🟢 calculateAccumulateMaintainSpending()
Note right of Member: sum(totalAccumSpendableAmount)<br/>for txns in [tierStartedAt, tierEndedAt]<br/>minus refunds in same window
Note over Member: 🟢 lifeTimeSpending += txn.totalAccumSpendableAmount
Member->>Member: calculateTierToUpgrade(tiers)
Note right of Member: 🟢 Filter NORMAL tiers,<br/>sort by minimumSpending DESC,<br/>find first where accumulateSpending >= minimumSpending,<br/>apply minimumTier floor (co-brand)
alt newTier.id !== currentTierId (UPGRADE)
Member->>Member: updateMemberTier(newTier, newStartDate, newEndDate)
Member->>Member: Create MemberTierHistory record
Member->>EvBus: emit MemberTierUpdatedDomainEvent
Member->>EvBus: emit MemberTierUpgradedDomainEvent
else newTier.id === currentTierId (NO UPGRADE)
Note over Member: 🟢 ไม่เปลี่ยน tier
end
Member-->>STSvc: return (member updated in memory)
STSvc->>MRepo: save(member)
MRepo->>DB: BEGIN TRANSACTION
MRepo->>DB: INSERT SalesTransaction
MRepo->>DB: UPDATE Member (accumulateSpending, accumulateMaintainSpending, lifeTimeSpending, tierId, tierStartedAt, tierEndedAt)
MRepo->>DB: INSERT MemberTierHistory (if tier changed)
MRepo->>DB: COMMIT
alt Tier changed
EvBus->>Listener: handle MemberTierUpgradedDomainEvent
Listener->>KOut: publish to Kafka topic
KOut-->>ExtSys: Downstream services consume (Engagement, Notification)
end
Solution A: ไม่เปลี่ยนอะไรเลย — Flow นี้เหมือน Legacy 100%
Flow 2: Refund / Void
🔴 เปลี่ยน guard condition และ down-tier decision
sequenceDiagram
autonumber
participant ExtSys as External Sales System
participant Kafka as Kafka Broker
participant STCtrl as SalesTransaction Controller<br/>(Kafka Consumer)
participant RefSvc as RefundSalesTransactionService
participant MRepo as MemberRepository
participant TRepo as TierRepository
participant Member as Member Entity
participant DB as Database (Prisma)
participant KOut as Kafka (Outbound)
alt Refund trigger
ExtSys->>Kafka: Publish SalesTransactionRefundedDomainEvent
Kafka->>STCtrl: handleSalesTransactionRefunded()
STCtrl->>STCtrl: Validate status == EARNED and latest refund type<br/>in FULL_REFUND | PARTIAL_REFUND | ADJUSTMENT
STCtrl->>RefSvc: createRefundSalesTransaction(..., type = refundTransaction.type)
else Void trigger
ExtSys->>Kafka: Publish SalesTransactionVoidedDomainEvent
Kafka->>STCtrl: handleSalesTransactionVoided()
STCtrl->>STCtrl: Validate status == VOIDED or latest refund type == VOID
STCtrl->>RefSvc: createRefundSalesTransaction(..., type = VOID)
end
RefSvc->>MRepo: find sales transaction by branchCode + salesTransactionExternalId
MRepo->>DB: SELECT SalesTransaction from recent/all sales tables
DB-->>MRepo: original SalesTransaction
MRepo-->>RefSvc: SalesTransactionEntity
RefSvc->>RefSvc: Check duplicate refund by refundTransactionExternalId
RefSvc->>MRepo: findById(memberId) + load recent txns
MRepo->>DB: SELECT Member + transactions
DB-->>MRepo: member data
MRepo-->>RefSvc: MemberEntity
RefSvc->>TRepo: findAllActive()
TRepo-->>RefSvc: TierEntity[]
RefSvc->>RefSvc: Create RefundSalesTransactionEntity<br/>(revokeAccumSpendableAmount = negative amount)
RefSvc->>Member: addNewRefundSalesTransaction(refundEntity, tiers)
Note over Member: 🟢 Push refund to _newRefundSalesTransaction
Note over Member: 🟢 calculateAccumulateSpending()
Note right of Member: Re-sum 24-month window<br/>(now includes negative refund/void)
Note over Member: 🟢 calculateAccumulateMaintainSpending()
Note right of Member: Re-sum tier period<br/>(now includes negative refund/void)
Note over Member: 🟢 lifeTimeSpending += revokeAccumSpendableAmount (negative)
alt originalSale.completedAt > tierStartedAt
Note over Member: 🔴 จบเลย ไม่ check downgrade (กฎ 2)
else originalSale.completedAt <= tierStartedAt
Note over Member: 🔵 NEW: result = calculateTierAdjustmentWithNewLogic(tiers)
Note right of Member: 🔵 ใช้ calculateAccumTierLogicSpending()<br/>แทน calculateTierAdjustment() (rolling 24mo)<br/>+ capToCurrentTier() ป้องกัน upgrade
alt newTier.id === currentTierId (NO DOWNGRADE)
Note over Member: 🟢 คง tier เดิม
Note over Member: 🔵 NEW: recalculate maintain จาก qualifying txn ใหม่
Note right of Member: findQualifyingTransactionDate(currentTier.minimumSpending)<br/>→ maintain = SUM txns หลัง qualifying txn
else newTier.id !== currentTierId (DOWNGRADE)
Note over Member: 🔴 NEW: หา tier dates (กฎ 5)
Note over Member: findPreviousTierInfo() → มี history valid?
Note over Member: ถ้าไม่มี → findQualifyingTransactionDate()<br/>(scan 24mo ก่อน up-tier + หลัง up-tier จนถึงปัจจุบัน, หักลบ refund)
Note over Member: ถ้าไม่มี → tierStartedAt = now
Note over Member: 🔴 NEW: calculateAccumulateMaintainSpending(newTierStartedAt, newTierEndedAt)
Note over Member: maintain คำนวณจาก txns ในช่วง newTierStartedAt ถึง newTierEndedAt
Note over Member: 🟢 updateMemberTier(newTier, newStart, newEnd)
Note over Member: 🟢 Create MemberTierHistory
Note over Member: 🟢 emit MemberTierUpdatedDomainEvent
Note over Member: 🟢 emit MemberTierDowngradedDueToRefundDomainEvent
end
end
Member-->>RefSvc: return
RefSvc->>MRepo: save(member)
MRepo->>DB: BEGIN TRANSACTION
MRepo->>DB: INSERT RefundSalesTransaction
MRepo->>DB: UPDATE Member (spending fields, tierId if changed)
MRepo->>DB: INSERT MemberTierHistory (if downgraded)
MRepo->>DB: COMMIT
MRepo-->>KOut: publish domain events to Kafka
Solution A: Flow เดียวครอบทั้ง no-down-tier และ down-tier
- Guard ใหม่: ถ้า original sale เกิดหลัง up-tier date → ไม่ down-tier (กฎ 2)
- Tier decision ใหม่: ใช้
calculateTierAdjustmentWithNewLogic()(Accum Tier Logic) แทนcalculateTierAdjustment()(rolling 24mo) - Tier dates ใหม่: หาจาก history หรือ qualifying transaction scan (กฎ 5)
- Maintain ใหม่: ไม่ reset เป็น 0 แต่คำนวณจาก transactions หลัง up-tier (กฎ 4)
- ไม่มี schema change: คำนวณสดจาก transactions ใน memory ทุกครั้ง
ตัวอย่าง: Refund → Down-tier (ตัวเลขจริง)
Timeline ของคุณ A:
ต.ค. 2023 ซื้อ 10,000 ─┐
พ.ย. 2023 ซื้อ 10,000 │ 24 เดือนก่อน up-tier
ธ.ค. 2023 ซื้อ 40,000 │
ม.ค. 2024 ซื้อ 10,000 ─┼── up-tier date (01 Jan 2024) → ขึ้น SCARLET
ก.พ. 2024 ซื้อ 2,000 ─┐
มี.ค. 2024 ซื้อ 2,000 │ transactions หลัง up-tier
เม.ย. 2024 ซื้อ 2,000 │
พ.ค. 2024 ซื้อ 1,000 │
มิ.ย. 2024 ซื้อ 1,000 │
ก.ค. 2024 ซื้อ 2,000 ─┘ ← ปัจจุบัน
Accum Tier Logic = (10K+10K+40K+10K) + (2K+2K+2K+1K+1K+2K)
= 70,000 + 10,000 = 80,000
เกิด Refund: คืนสินค้าที่ซื้อ ธ.ค. 2023 (ก่อน up-tier) มูลค่า 40,000 บาท
ขั้นตอนที่ 1: calculateAccumTierLogicSpending()
= (70,000 - 40,000) + 10,000 = 40,000
ขั้นตอนที่ 2: หา tier ใหม่จาก 40,000
CROWN = 300,000 → ❌
SCARLET = 50,000 → ❌
NAVY = 0 → ✅ → Down เป็น NAVY
ขั้นตอนที่ 3: หา tier dates
NAVY ไม่มี history → findQualifyingTransactionDate() (scan หักลบ refund, window ถึงปัจจุบัน) → ไม่มี
→ tierStartedAt = now
ขั้นตอนที่ 4: calculatePostUpTierMaintainSpending()
= 2K+2K+2K+1K+1K+2K = 10,000
ผลลัพธ์: Down เป็น NAVY, tierStartedAt = now, maintainSpending = 10,000
Flow 6: Manual Adjustment (Admin Backoffice)
Delegate ไป Flow 1 (ADD) / Flow 2 (DEDUCT) — ใช้ new logic อัตโนมัติ
sequenceDiagram
autonumber
participant Admin as Admin User (Backoffice)
participant API as Admin REST API
participant AdjSvc as AdjustSalesTransactionService
participant MRepo as MemberRepository
participant STSvc as SalesTransactionService
participant RefSvc as RefundSalesTransactionService
participant KOut as Kafka (Outbound)
Admin->>API: POST /admin/members/:id/adjust-transaction<br/>{type: ADD|DEDUCT, amount, memoId, reason}
API->>AdjSvc: createWithTransaction(memberId, request, requester)
AdjSvc->>MRepo: findMemberWithProfileAndTierById(memberId)
MRepo-->>AdjSvc: MemberEntity
AdjSvc->>AdjSvc: Create AdjustmentSalesTransactionEntity<br/>{amount, type: ADD|DEDUCT, memoId, createdBy}
alt type === ADD
AdjSvc->>AdjSvc: Validate duplicate externalId + valid sales status
AdjSvc->>STSvc: createSalesTransaction(..., totalAccumSpendableAmount = amount)
Note over STSvc: 🟢 Delegate to Flow 1<br/>addNewSalesTransaction() runs full accum update<br/>and may upgrade tier → ไม่เปลี่ยน
STSvc-->>AdjSvc: salesTransactionId
else type === DEDUCT
AdjSvc->>AdjSvc: Validate remaining deduct balance
AdjSvc->>RefSvc: createRefundSalesTransaction(..., type = FULL_REFUND or PARTIAL_REFUND)
Note over RefSvc: 🔴 Delegate to Flow 2<br/>addNewRefundSalesTransaction() runs full accum update<br/>and may downgrade tier → ใช้ new logic อัตโนมัติ
RefSvc-->>AdjSvc: refundSalesTransactionId
end
AdjSvc->>KOut: emit MemberUpdatedLogDomainEvent<br/>(adjustSalesTransaction payload)
KOut-->>Admin: Downstream audit trail updated
Solution A: ไม่แก้ Flow 6 โดยตรง — ใช้ new logic อัตโนมัติเพราะ delegate ไป Flow 1/2