Files
apskel-pos-backend/internal/service/earning_backfill_job.go
T
2026-09-30 15:31:11 +07:00

75 lines
2.1 KiB
Go

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
}