package service import ( "context" "sync" "time" "apskel-pos-be/internal/logger" ) const ( defaultEarningBackfillInterval = 30 * time.Minute // How far back to look for paid orders that never earned. earningBackfillWindow = 72 * time.Hour // Orders looked at per run at most, so one run cannot run away. earningBackfillMaxOrders = 5000 ) type missingEarner interface { EarnMissing(ctx context.Context, since time.Time, maxOrders int) (checked, earned int, err error) } // EarningBackfillJob is the safety net behind earning at payment time // (docs/prd-point-coin.md F3, PC-203). Every run it earns for orders paid in the last // few days that should have earned and did not, for example because the database was // briefly unreachable right after the payment committed. type EarningBackfillJob struct { earner missingEarner now func() time.Time stopCh chan struct{} stopOnce sync.Once } func NewEarningBackfillJob(earner missingEarner) *EarningBackfillJob { return &EarningBackfillJob{earner: earner, now: time.Now, stopCh: make(chan struct{})} } func (j *EarningBackfillJob) Start(interval time.Duration) { if interval <= 0 { interval = defaultEarningBackfillInterval } 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("Earning backfill job started (interval: %s)", interval) } func (j *EarningBackfillJob) Stop() { j.stopOnce.Do(func() { close(j.stopCh) }) } // RunOnce earns for every missed order in the window and reports how many it fixed. // It is quiet when nothing was missed. func (j *EarningBackfillJob) RunOnce(ctx context.Context) int { checked, earned, err := j.earner.EarnMissing(ctx, j.now().Add(-earningBackfillWindow), earningBackfillMaxOrders) if err != nil { logger.NonContext.Error("Earning backfill failed to run", err) } if earned > 0 { logger.NonContext.WarnWithFields("Earning backfill credited orders that had missed their earning", map[string]interface{}{"checked": checked, "earned": earned}, nil) } return earned }