feat(wallet): reconcile balances, ledger and lots on a schedule

Adds the reconciliation of docs/prd-point-coin.md §7.5 (PC-108). One
aggregate query per check, across every wallet:

- wallet balance = SUM(ledger), per currency, including customers with
  ledger rows but no wallet row
- wallet balance = SUM(lot remaining)
- lot original - SUM(allocations) = remaining
- SUM(allocations) = |amount| for every deduction
- lots created = amount for every addition, which the engine keeps and the
  other checks rely on

The check on payments.points_used waits for that column (PC-305).

WalletReconciliationJob runs the checks at startup and every six hours,
alongside the omset scheduler. It is silent while the data is consistent.
Each discrepancy is logged with its check, customer, object and the
expected and actual values, and the organization's admins, owners and
managers get a high-priority notification. An organization is notified
again only when its set of discrepancies changes. Nothing is corrected
automatically. At most 50 discrepancies per check are reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-30 10:13:46 +07:00
co-authored by Claude Opus 5.5
parent a6d5a8b056
commit 040780cd2d
5 changed files with 637 additions and 0 deletions
+14
View File
@@ -31,6 +31,7 @@ type App struct {
router *router.Router
shutdown chan os.Signal
omsetScheduler *service.OmsetMilestoneScheduler
walletRecon *service.WalletReconciliationJob
}
func NewApp(db *gorm.DB, redisClient *redis.Client) *App {
@@ -53,6 +54,13 @@ func (a *App) Initialize(cfg *config.Config) error {
processors.notificationProcessor,
)
// Checks that wallet balances, ledger and lots agree (docs/prd-point-coin.md §7.5)
a.walletRecon = service.NewWalletReconciliationJob(
repository.NewWalletReconciliationRepository(a.db),
repos.userRepo,
processors.notificationProcessor,
)
services := a.initServices(processors, repos, cfg)
validators := a.initValidators()
middleware := a.initMiddleware(services, cfg)
@@ -155,6 +163,9 @@ func (a *App) Start(port string) error {
if a.omsetScheduler != nil {
a.omsetScheduler.Start(5 * time.Minute)
}
if a.walletRecon != nil {
a.walletRecon.Start(6 * time.Hour)
}
engine := a.router.Init()
@@ -194,6 +205,9 @@ func (a *App) Shutdown() {
if a.omsetScheduler != nil {
a.omsetScheduler.Stop()
}
if a.walletRecon != nil {
a.walletRecon.Stop()
}
close(a.shutdown)
}