package service import ( "context" "sync" "time" "apskel-pos-be/internal/logger" ) // Lots expire at the end of their day, so running every quarter of an hour keeps any // lot from staying past its expiry for more than about that long (PC-503). const defaultWalletExpiryInterval = 15 * time.Minute type walletExpiryWork interface { ExpireDue(ctx context.Context) (int, error) SendReminders(ctx context.Context) (int, error) } // WalletExpiryJob expires the balances whose time is up and reminds customers of what // is about to (docs/prd-point-coin.md F12, PC-503, PC-504). // Unlike OmsetMilestoneScheduler it keeps no state in memory: several instances can // run it at once, and a restart repeats nothing, because every lot is expired under // its wallet's lock with an idempotency key. type WalletExpiryJob struct { expirer walletExpiryWork stopCh chan struct{} stopOnce sync.Once } func NewWalletExpiryJob(expirer walletExpiryWork) *WalletExpiryJob { return &WalletExpiryJob{expirer: expirer, stopCh: make(chan struct{})} } func (j *WalletExpiryJob) Start(interval time.Duration) { if interval <= 0 { interval = defaultWalletExpiryInterval } go func() { j.RunOnce(context.Background()) ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: j.RunOnce(context.Background()) case <-j.stopCh: return } } }() logger.NonContext.Infof("Wallet expiry job started (interval: %s)", interval) } func (j *WalletExpiryJob) Stop() { j.stopOnce.Do(func() { close(j.stopCh) }) } // RunOnce expires what is due, sends the reminders that are due, and returns how many // lots it expired. func (j *WalletExpiryJob) RunOnce(ctx context.Context) int { expired, err := j.expirer.ExpireDue(ctx) if err != nil { logger.NonContext.Error("Wallet expiry failed to run", err) } if expired > 0 { logger.NonContext.Infof("Wallet expiry expired %d lots", expired) } reminded, err := j.expirer.SendReminders(ctx) if err != nil { logger.NonContext.Error("Wallet expiry reminders failed to run", err) } if reminded > 0 { logger.NonContext.Infof("Wallet expiry sent %d reminders", reminded) } return expired }