package payments import ( "sync" "time" "github.com/google/uuid" ) // benefitState is an account's benefit row for one origin: the no-ads term end (nil = none), // the perpetual forever flag, and the hint count. It is the context-independent stored form; // the read paths aggregate it over the origins applicable in a context. type benefitState struct { adsPaidUntil *time.Time adsForever bool hints int } // walletState is an account's full payments state: chip balances by source and benefits by // origin. It is the raw, context-independent data every read path filters per execution // context โ€” the value the cache holds and the store recomputes from the materialized tables. type walletState struct { chips map[Source]int benefits map[Source]benefitState } // chipsOf returns the chip balance for a source, zero when the segment has no row (an absent // segment reads as zero, ยง2). func (w walletState) chipsOf(s Source) int { return w.chips[s] } // benefitOf returns the benefit for an origin, the zero benefit when the origin has no row. func (w walletState) benefitOf(o Source) benefitState { return w.benefits[o] } // walletCache is the payments read model: each account's chip/benefit state, keyed by account // id, read on every ad-eligibility / hint / wallet / gate request and invalidated on every // payments mutation (spend, grant, fund, refund, merge). It is a write-through cache in front // of the materialized balances/benefits tables โ€” the same pattern the account-suspension gate // uses (backend/internal/account/suspension.go) โ€” so the steady-state hot path issues no query // to the payments schema. It is single-instance, matching the deployment (one shared Store); a // multi-instance backend would need a shared cache. type walletCache struct { mu sync.RWMutex m map[uuid.UUID]walletState } // newWalletCache constructs an empty cache. func newWalletCache() *walletCache { return &walletCache{m: make(map[uuid.UUID]walletState)} } // get returns the cached state for an account and whether it was present. func (c *walletCache) get(id uuid.UUID) (walletState, bool) { c.mu.RLock() defer c.mu.RUnlock() s, ok := c.m[id] return s, ok } // put stores an account's state. func (c *walletCache) put(id uuid.UUID, s walletState) { c.mu.Lock() defer c.mu.Unlock() c.m[id] = s } // invalidate drops an account's entry so the next read reloads it from the materialized tables. // Called after every mutation once its transaction has committed. func (c *walletCache) invalidate(id uuid.UUID) { c.mu.Lock() defer c.mu.Unlock() delete(c.m, id) }