package card import ( "fmt" "log" "sync" "time" ) // CardPool manages a pool of cards with TTL expiration and multi-bind support. type CardPool struct { mu sync.Mutex cards []*poolEntry index int ttl time.Duration // card validity duration (0 = no expiry) multiBind bool // allow reusing cards multiple times maxBinds int // max binds per card (0 = unlimited when multiBind=true) } type poolEntry struct { card *CardInfo addedAt time.Time bindCount int rejected bool // upstream rejected this card } // PoolConfig configures the card pool behavior. type PoolConfig struct { TTL time.Duration // card validity period (default: 1h) MultiBind bool // allow one card to be used multiple times MaxBinds int // max uses per card (0 = unlimited) } // NewCardPool creates a card pool with the given config. func NewCardPool(cfg PoolConfig) *CardPool { if cfg.TTL == 0 { cfg.TTL = time.Hour } return &CardPool{ ttl: cfg.TTL, multiBind: cfg.MultiBind, maxBinds: cfg.MaxBinds, } } // AddCards adds cards to the pool. func (p *CardPool) AddCards(cards []*CardInfo) { p.mu.Lock() defer p.mu.Unlock() now := time.Now() for _, c := range cards { p.cards = append(p.cards, &poolEntry{ card: c, addedAt: now, }) } } // GetCard returns the next available card from the pool. func (p *CardPool) GetCard() (*CardInfo, error) { p.mu.Lock() defer p.mu.Unlock() now := time.Now() checked := 0 for checked < len(p.cards) { entry := p.cards[p.index] p.index = (p.index + 1) % len(p.cards) checked++ // Skip expired cards if p.ttl > 0 && now.Sub(entry.addedAt) > p.ttl { continue } // Skip rejected cards if entry.rejected { continue } // Check bind limits if !p.multiBind && entry.bindCount > 0 { continue } if p.multiBind && p.maxBinds > 0 && entry.bindCount >= p.maxBinds { continue } // Don't increment bindCount here — caller must call IncrementBind() after // a real payment attempt reaches a terminal state (success or upstream decline). log.Printf("[card-pool] dispensing card %s...%s (current binds=%d)", entry.card.Number[:4], entry.card.Number[len(entry.card.Number)-4:], entry.bindCount) return entry.card, nil } return nil, fmt.Errorf("no available cards in pool (total=%d)", len(p.cards)) } // ReportResult handles payment terminal states: // - success=true: bindCount++ (card used successfully) // - success=false: bindCount++ AND rejected=true (upstream declined, card is dead) func (p *CardPool) ReportResult(card *CardInfo, success bool) { p.mu.Lock() defer p.mu.Unlock() for _, entry := range p.cards { if entry.card.Number == card.Number { entry.bindCount++ if !success { entry.rejected = true log.Printf("[card-pool] card %s...%s marked as rejected (bind #%d)", card.Number[:4], card.Number[len(card.Number)-4:], entry.bindCount) } else { log.Printf("[card-pool] card %s...%s bind #%d recorded (success)", card.Number[:4], card.Number[len(card.Number)-4:], entry.bindCount) } return } } } // Stats returns pool statistics. func (p *CardPool) Stats() (total, available, expired, rejected int) { p.mu.Lock() defer p.mu.Unlock() now := time.Now() total = len(p.cards) for _, e := range p.cards { switch { case e.rejected: rejected++ case p.ttl > 0 && now.Sub(e.addedAt) > p.ttl: expired++ case !p.multiBind && e.bindCount > 0: // used up in single-bind mode case p.multiBind && p.maxBinds > 0 && e.bindCount >= p.maxBinds: // used up max binds default: available++ } } return }