Initial sanitized code sync
This commit is contained in:
146
pkg/provider/card/pool.go
Normal file
146
pkg/provider/card/pool.go
Normal file
@@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user