147 lines
3.6 KiB
Go
147 lines
3.6 KiB
Go
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
|
|
}
|