bank-emul/internal/bank/bank.go
2026-03-07 19:45:24 +03:00

178 lines
3.8 KiB
Go

package bank
import (
"fmt"
"math/rand"
"sync"
"time"
"bank-emulator/internal/storage"
)
type ThreeDSSession struct {
SessionID string
CardNumber string
Code string
Attempts int
ExpiresAt time.Time
Confirmed bool
}
type Bank struct {
Store *storage.Store
sessions map[string]*ThreeDSSession
mu sync.Mutex
}
func New(store *storage.Store) *Bank {
return &Bank{
Store: store,
sessions: make(map[string]*ThreeDSSession),
}
}
func (b *Bank) ValidateCard(number string) error {
_, card := b.Store.FindCardByNumber(number)
if card == nil {
return fmt.Errorf("card not found")
}
return nil
}
func (b *Bank) InitiateBind(cardNumber, cvv, expiry string) (*ThreeDSSession, error) {
_, card := b.Store.FindCardByNumber(cardNumber)
if card == nil {
return nil, fmt.Errorf("card not found")
}
if card.CVV != cvv {
return nil, fmt.Errorf("invalid CVV")
}
if expiry != "" && card.Expiry != expiry {
return nil, fmt.Errorf("invalid expiry date")
}
b.mu.Lock()
defer b.mu.Unlock()
sessionID := fmt.Sprintf("3ds-%d", rand.Int63())
code := fmt.Sprintf("%06d", rand.Intn(1_000_000))
session := &ThreeDSSession{
SessionID: sessionID,
CardNumber: cardNumber,
Code: code,
Attempts: 0,
ExpiresAt: time.Now().Add(5 * time.Minute),
}
b.sessions[sessionID] = session
fmt.Printf("\n========== 3-DS CODE ==========\n")
fmt.Printf(" Session: %s\n", sessionID)
fmt.Printf(" Card: %s\n", cardNumber)
fmt.Printf(" Code: %s\n", code)
fmt.Printf(" Expires: %s\n", session.ExpiresAt.Format("15:04:05"))
fmt.Printf("===============================\n\n")
return session, nil
}
func (b *Bank) Confirm3DS(sessionID, code string) error {
b.mu.Lock()
defer b.mu.Unlock()
session, ok := b.sessions[sessionID]
if !ok {
return fmt.Errorf("session not found")
}
if session.Confirmed {
return fmt.Errorf("session already confirmed")
}
if time.Now().After(session.ExpiresAt) {
delete(b.sessions, sessionID)
return fmt.Errorf("session expired")
}
session.Attempts++
if session.Code != code {
remaining := 3 - session.Attempts
if remaining <= 0 {
delete(b.sessions, sessionID)
return fmt.Errorf("max attempts reached, session invalidated")
}
return fmt.Errorf("invalid code, %d attempts remaining", remaining)
}
session.Confirmed = true
fmt.Printf("[3DS] Session %s confirmed successfully\n", sessionID)
return nil
}
func (b *Bank) Charge(cardNumber string, amount float64) (*ThreeDSSession, error) {
if err := b.ValidateCard(cardNumber); err != nil {
return nil, err
}
_, card := b.Store.FindCardByNumber(cardNumber)
if card.Balance < amount {
return nil, fmt.Errorf("insufficient funds: %.2f < %.2f", card.Balance, amount)
}
session, err := b.InitiateBind(cardNumber, card.CVV, "")
if err != nil {
return nil, err
}
return session, nil
}
func (b *Bank) CompleteCharge(sessionID string, amount float64) error {
b.mu.Lock()
session, ok := b.sessions[sessionID]
b.mu.Unlock()
if !ok {
return fmt.Errorf("session not found")
}
if !session.Confirmed {
return fmt.Errorf("3DS not confirmed")
}
err := b.Store.Charge(session.CardNumber, amount)
if err != nil {
return err
}
b.mu.Lock()
delete(b.sessions, sessionID)
b.mu.Unlock()
b.Store.Save()
fmt.Printf("[BANK] Charged %.2f from card %s\n", amount, session.CardNumber)
return nil
}
func (b *Bank) DirectCharge(cardNumber string, amount float64) error {
if amount <= 0 {
return fmt.Errorf("amount must be positive")
}
_, card := b.Store.FindCardByNumber(cardNumber)
if card == nil {
return fmt.Errorf("card not found")
}
if card.Balance < amount {
return fmt.Errorf("insufficient funds: %.2f < %.2f", card.Balance, amount)
}
err := b.Store.Charge(cardNumber, amount)
if err != nil {
return err
}
b.Store.Save()
fmt.Printf("[BANK] Direct charged %.2f from card %s\n", amount, cardNumber)
return nil
}