You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
┌─────────────────────────────────────────────────────────────────┐
│ UNTUK SETIAP ISSUE: │
│ │
│ 1. ANALISA ROOT CAUSE │
│ - Cari code yang terkait │
│ - Identifikasi mengapa bug terjadi │
│ - Tampilkan code snippet yang bermasalah │
│ │
│ 2. BUAT PLAN │
│ - Files yang akan dimodifikasi │
│ - Perubahan yang akan dilakukan │
│ │
│ 3. TUNGGU APPROVAL dari user │
│ │
│ 4. EKSEKUSI setelah approved │
└─────────────────────────────────────────────────────────────────┘
Void coupon dilakukan SEQUENTIAL - satu per satu dengan await dalam loop.
// CURRENT - SANGAT LAMBAT!for(constwinnerofvalidWinners){console.log('[CONFIRM] Voiding for winner',winner.id)awaitvoidCouponsForWinner(winner)// 2 detik per operation!}// Total: 50 × 2 detik = 100 detik
SOLUTION: BATCH OPERATION
Option A: Single Transaction dengan Batch Modify (RECOMMENDED)
// useDrawState.ts atau drawService.ts - confirm functionconstconfirm=async()=>{console.log('[CONFIRM] Starting batch confirm...')constvalidWinners=currentBatchWinners.filter(w=>w.status==='valid')console.log('[CONFIRM] Valid winners count:',validWinners.length)if(validWinners.length===0){dispatch({type: 'CONFIRM_SUCCESS'})return}constnow=newDate().toISOString()try{// SINGLE TRANSACTION untuk semua operationsawaitdb.transaction('rw',[db.winners,db.coupons],async()=>{// Step 1: Batch confirm all winnersconsole.log('[CONFIRM] Step 1: Batch confirming winners...')constwinnerIds=validWinners.map(w=>w.id)awaitdb.winners.where('id').anyOf(winnerIds).modify({confirmedAt: now})console.log('[CONFIRM] Step 1: DONE')// Step 2: Batch void all winning couponsconsole.log('[CONFIRM] Step 2: Batch voiding coupons...')constcouponIds=validWinners.map(w=>w.couponId)awaitdb.coupons.where('id').anyOf(couponIds).modify({status: 'void'})console.log('[CONFIRM] Step 2: DONE')// Step 3: Handle win rule (void remaining coupons for participants who reached max)console.log('[CONFIRM] Step 3: Handling win rule...')awaithandleWinRuleBatch(validWinners)console.log('[CONFIRM] Step 3: DONE')})console.log('[CONFIRM] All operations completed!')dispatch({type: 'CONFIRM_SUCCESS'})}catch(error){console.error('[CONFIRM] ERROR:',error)throwerror}}
Option B: Promise.all untuk Parallel Operations
constconfirm=async()=>{constvalidWinners=currentBatchWinners.filter(w=>w.status==='valid')constnow=newDate().toISOString()// Parallel confirm all winnersconsole.log('[CONFIRM] Parallel confirming winners...')awaitPromise.all(validWinners.map(w=>db.winners.update(w.id,{confirmedAt: now})))// Parallel void all couponsconsole.log('[CONFIRM] Parallel voiding coupons...')awaitPromise.all(validWinners.map(w=>db.coupons.update(w.couponId,{status: 'void'})))dispatch({type: 'CONFIRM_SUCCESS'})}
Option C: Bulk Operations dengan Dexie
constconfirm=async()=>{constvalidWinners=currentBatchWinners.filter(w=>w.status==='valid')constnow=newDate().toISOString()// Bulk update winnersconsole.log('[CONFIRM] Bulk updating winners...')constupdatedWinners=validWinners.map(w=>({
...w,confirmedAt: now}))awaitdb.winners.bulkPut(updatedWinners)// Bulk update coupons - need to fetch firstconsole.log('[CONFIRM] Fetching coupons to void...')constcouponIds=validWinners.map(w=>w.couponId)constcoupons=awaitdb.coupons.where('id').anyOf(couponIds).toArray()console.log('[CONFIRM] Bulk voiding coupons...')constvoidedCoupons=coupons.map(c=>({
...c,status: 'void'asconst}))awaitdb.coupons.bulkPut(voidedCoupons)dispatch({type: 'CONFIRM_SUCCESS'})}
Win Rule Batch Handler
// Handle win rule in batch (not one by one)asyncfunctionhandleWinRuleBatch(validWinners: Winner[],event: Event): Promise<void>{const{ winRule }=eventswitch(winRule.type){case'one-time':
// Void ALL coupons for ALL winning participantsconsole.log('[WinRule] one-time: voiding all participant coupons')constparticipantIds=[...newSet(validWinners.map(w=>w.participantId))]awaitdb.coupons.where('participantId').anyOf(participantIds).filter(c=>c.eventId===event.id&&c.status==='active').modify({status: 'void'})breakcase'limited':
// Check each participant's win count and void if reached maxconsole.log('[WinRule] limited: checking participant win counts')constparticipantWinCounts=newMap<string,number>()// Count wins per participantfor(constwinnerofvalidWinners){constcount=participantWinCounts.get(winner.participantId)||0participantWinCounts.set(winner.participantId,count+1)}// Get participants who reached maxconstmaxWins=winRule.maxWins||1constparticipantsAtMax: string[]=[]for(const[participantId,batchWins]ofparticipantWinCounts){// Get previous confirmed winsconstpreviousWins=awaitdb.winners.where('participantId').equals(participantId).filter(w=>w.eventId===event.id&&w.confirmedAt&&w.status==='valid').count()if(previousWins+batchWins>=maxWins){participantsAtMax.push(participantId)}}// Void all coupons for participants at maxif(participantsAtMax.length>0){console.log('[WinRule] Voiding coupons for',participantsAtMax.length,'participants at max')awaitdb.coupons.where('participantId').anyOf(participantsAtMax).filter(c=>c.eventId===event.id&&c.status==='active').modify({status: 'void'})}breakcase'unlimited':
// Do nothing - only void winning coupon (already done)console.log('[WinRule] unlimited: no additional voiding needed')break}}
Di Revision 11 dan 14, ada instruksi yang SALAH untuk delete cancelled winner saat redraw:
// SALAH - INI YANG HARUS DI-REVERT!// Delete old cancelled winner entry firstconsole.log('[RedrawAll] Deleting old cancelled winner:',cancelledWinner.id)awaitwinnerRepository.delete(cancelledWinner.id)
Dampak:
Cancelled winners HILANG dari database
History page table cancelled KOSONG karena datanya sudah di-delete
Tidak ada audit trail
Fix - HAPUS DELETE LOGIC:
Cari dan HAPUS semua code seperti ini di redrawAll():
// HAPUS LINE INI:awaitwinnerRepository.delete(cancelledWinner.id)// HAPUS JUGA LOG-NYA JIKA ADA:console.log('[RedrawAll] Deleting old cancelled winner:',cancelledWinner.id)
Flow yang BENAR:
// redrawAll() - CORRECT VERSIONasyncredrawAll(eventId: string,prizeId: string): Promise<DrawResult[]>{// 1. Get cancelled winnersconstcancelledWinners=awaitwinnerRepository.getCancelledUnconfirmed(prizeId)if(cancelledWinners.length===0){return[]}// 2. Get active couponsconstactiveCoupons=awaitcouponRepository.getActive(eventId)// 3. For each cancelled winner, draw NEW couponconstnewWinners: DrawResult[]=[]for(constcancelledWinnerofcancelledWinners){// Draw new couponconst[newCoupon]=weightedRandomSelect(eligibleCoupons,1)// Validateconstvalidation=awaitvalidateWinner(newCoupon,event,currentBatchParticipantIds)// ❌ JANGAN DELETE cancelled winner!// await winnerRepository.delete(cancelledWinner.id) // HAPUS INI!// ✅ Cancelled winner TETAP ADA di database untuk audit// History page akan tampilkan di table cancelled// Create NEW winner entry (slot baru, bukan replace)constnewWinner=awaitwinnerRepository.create({
eventId,
prizeId,couponId: newCoupon.id,participantId: newCoupon.participantId,participantName: newCoupon.participantName,lineNumber: getNextLineNumber(),// Line number barubatchNumber: cancelledWinner.batchNumber,status: validation.valid ? 'valid' : 'cancelled',cancelReason: validation.valid ? undefined : {type: 'auto',message: validation.reason},drawnAt: newDate()})// Void new couponawaitcouponRepository.void(eventId,newCoupon.id)newWinners.push(newWinner)}returnnewWinners}
Penjelasan:
Cancelled winner TETAP ADA di database
Saat redraw, CREATE winner BARU (tidak replace/delete yang lama)
History page menampilkan:
Table Winners: semua winner dengan status = 'valid' dan confirmedAt != null
Table Cancelled: semua winner dengan status = 'cancelled'
Data di Database Setelah Redraw:
id
slot
couponId
status
Keterangan
winner-1
1
coupon-A
valid
Winner pertama
winner-2
2
coupon-B
cancelled
Cancelled, tidak di-delete
winner-3
3
coupon-C
valid
Hasil redraw untuk slot 2
Files to Modify
File
Issue
Changes
src/hooks/useDrawState.ts
#1
Batch operation untuk confirm
src/services/drawService.ts
#1, #2
Batch operation, HAPUS delete logic
src/repositories/winnerRepository.ts
#1
Add bulkConfirm method
src/repositories/couponRepository.ts
#1
Add bulkVoid method
Testing Checklist (Issue 2)
Redraw TIDAK delete cancelled winner
Setelah redraw, cancelled winner masih ada di database
History page table cancelled TETAP MENAMPILKAN data
New winner dari redraw adalah entry terpisah (bukan replace)