75 lines
2.1 KiB
Go
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
|
|
}
|