Reapply "feat(loyalty): EnakPoint & EnakCoin" (#32)

This reverts commit 4e24f9bbb0.
This commit is contained in:
efrilm
2026-09-30 15:31:44 +07:00
parent 4e24f9bbb0
commit 582dc75543
195 changed files with 20058 additions and 2177 deletions
+77
View File
@@ -0,0 +1,77 @@
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
}