85 lines
payment/store.go
PaymentStore interface and in-memory implementation for tests.
// Package payment processes financial transactions for connected accounts.
package payment
 
import (
	"context"
	"fmt"
	"sync"
	"time"
)
 
// Transaction records a completed payment operation for accounting.
type Transaction struct {
	ID        string
	AccountID string
	Amount    int64
	RefID     string
	CreatedAt time.Time
}
 
// PaymentStore manages account balances and transaction records.
// Balance check and deduction must be performed atomically within a single
// database transaction to prevent concurrent overdrafts.
type PaymentStore interface {
	// GetBalance returns the current balance for accountID.
	GetBalance(ctx context.Context, accountID string) (int64, error)
	// DeductBalance subtracts amount from accountID's balance.
	DeductBalance(ctx context.Context, accountID string, amount int64) error
	// RecordTransaction persists a completed transaction for accounting.
	RecordTransaction(ctx context.Context, tx Transaction) error
	// WithTx begins a database transaction and executes fn within it.
	// All store operations within fn are part of the same transaction.
	WithTx(ctx context.Context, fn func(txCtx context.Context) error) error
}
 
// MemStore is a thread-safe in-memory PaymentStore for testing.
type MemStore struct {
	mu           sync.Mutex
	balances     map[string]int64
	Transactions []Transaction
	RecordErr    error
}
 
// NewMemStore returns a MemStore with the given starting balances.
func NewMemStore(balances map[string]int64) *MemStore {
	return &MemStore{balances: balances}
}
 
// GetBalance returns the current in-memory balance for id.
func (s *MemStore) GetBalance(_ context.Context, id string) (int64, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.balances[id], nil
}
 
// DeductBalance subtracts amount from the in-memory balance.
func (s *MemStore) DeductBalance(_ context.Context, id string, amount int64) error {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.balances[id] -= amount
	return nil
}
 
// RecordTransaction appends a transaction unless RecordErr is configured.
func (s *MemStore) RecordTransaction(_ context.Context, tx Transaction) error {
	if s.RecordErr != nil {
		return s.RecordErr
	}
	s.mu.Lock()
	defer s.mu.Unlock()
	s.Transactions = append(s.Transactions, tx)
	return nil
}
 
// WithTx runs fn in the in-memory transaction boundary used by tests.
func (s *MemStore) WithTx(_ context.Context, fn func(context.Context) error) error {
	return fn(context.Background())
}
 
// Balance returns the current balance for assertions in tests.
func (s *MemStore) Balance(id string) (int64, error) {
	return s.GetBalance(context.Background(), id)
}
 
// ErrInsufficientFunds is returned when the account balance is below the requested amount.
var ErrInsufficientFunds = fmt.Errorf("insufficient funds")