package financial import ( "context" "database/sql" "encoding/json" "fmt" "time" ) // Receipt represents a financial transaction receipt. type Receipt struct { ID string `json:"id"` // Same as transaction ID TournamentID string `json:"tournament_id"` TournamentName string `json:"tournament_name"` VenueName string `json:"venue_name"` CurrencyCode string `json:"currency_code"` CurrencySymbol string `json:"currency_symbol"` PlayerID string `json:"player_id"` PlayerName string `json:"player_name"` TransactionType string `json:"transaction_type"` Amount int64 `json:"amount"` // cents ChipsReceived int64 `json:"chips_received"` ReceiptNumber int `json:"receipt_number"` // Sequential per tournament Timestamp int64 `json:"timestamp"` // Unix seconds OperatorID string `json:"operator_id"` OperatorName string `json:"operator_name"` // Running totals for this player in this tournament PlayerTotalBuyIns int `json:"player_total_buyins"` PlayerTotalRebuys int `json:"player_total_rebuys"` PlayerTotalAddOns int `json:"player_total_addons"` // Reprint tracking OriginalTimestamp int64 `json:"original_timestamp,omitempty"` // For reprints IsReprint bool `json:"is_reprint"` } // GenerateReceipt creates a receipt for a given transaction. func (e *Engine) GenerateReceipt(ctx context.Context, transaction *Transaction) (*Receipt, error) { // Get tournament name var tournamentName string err := e.db.QueryRowContext(ctx, "SELECT name FROM tournaments WHERE id = ?", transaction.TournamentID, ).Scan(&tournamentName) if err != nil { return nil, fmt.Errorf("financial: load tournament name: %w", err) } // Get venue info venueName, currencyCode, currencySymbol := e.loadVenueInfo(ctx) // Get player name var playerName string err = e.db.QueryRowContext(ctx, "SELECT name FROM players WHERE id = ?", transaction.PlayerID, ).Scan(&playerName) if err != nil { playerName = "Unknown" // Defensive } // Get operator name var operatorName string err = e.db.QueryRowContext(ctx, "SELECT name FROM operators WHERE id = ?", transaction.OperatorID, ).Scan(&operatorName) if err != nil { operatorName = transaction.OperatorID // Fallback to ID } // Calculate receipt number (sequential within tournament) receiptNumber, err := e.nextReceiptNumber(ctx, transaction.TournamentID) if err != nil { return nil, err } // Get player running totals buyins, rebuys, addons := e.playerRunningTotals(ctx, transaction.TournamentID, transaction.PlayerID) receipt := &Receipt{ ID: transaction.ID, TournamentID: transaction.TournamentID, TournamentName: tournamentName, VenueName: venueName, CurrencyCode: currencyCode, CurrencySymbol: currencySymbol, PlayerID: transaction.PlayerID, PlayerName: playerName, TransactionType: transaction.Type, Amount: transaction.Amount, ChipsReceived: transaction.Chips, ReceiptNumber: receiptNumber, Timestamp: time.Now().Unix(), OperatorID: transaction.OperatorID, OperatorName: operatorName, PlayerTotalBuyIns: buyins, PlayerTotalRebuys: rebuys, PlayerTotalAddOns: addons, } // Store receipt data on the transaction receiptJSON, err := json.Marshal(receipt) if err != nil { return nil, fmt.Errorf("financial: marshal receipt: %w", err) } _, err = e.db.ExecContext(ctx, "UPDATE transactions SET receipt_data = ? WHERE id = ?", string(receiptJSON), transaction.ID, ) if err != nil { return nil, fmt.Errorf("financial: save receipt: %w", err) } return receipt, nil } // GetReceipt retrieves a saved receipt for a transaction. func (e *Engine) GetReceipt(ctx context.Context, transactionID string) (*Receipt, error) { var receiptData sql.NullString err := e.db.QueryRowContext(ctx, "SELECT receipt_data FROM transactions WHERE id = ?", transactionID, ).Scan(&receiptData) if err == sql.ErrNoRows { return nil, ErrTransactionNotFound } if err != nil { return nil, fmt.Errorf("financial: load receipt: %w", err) } if !receiptData.Valid || receiptData.String == "" { return nil, fmt.Errorf("financial: no receipt for transaction %s", transactionID) } var receipt Receipt if err := json.Unmarshal([]byte(receiptData.String), &receipt); err != nil { return nil, fmt.Errorf("financial: parse receipt: %w", err) } return &receipt, nil } // ReprintReceipt returns the same receipt data with a new print timestamp. func (e *Engine) ReprintReceipt(ctx context.Context, transactionID string) (*Receipt, error) { receipt, err := e.GetReceipt(ctx, transactionID) if err != nil { return nil, err } // Mark as reprint with new timestamp receipt.OriginalTimestamp = receipt.Timestamp receipt.Timestamp = time.Now().Unix() receipt.IsReprint = true return receipt, nil } // loadVenueInfo loads the venue settings. func (e *Engine) loadVenueInfo(ctx context.Context) (name, currencyCode, currencySymbol string) { err := e.db.QueryRowContext(ctx, "SELECT venue_name, currency_code, currency_symbol FROM venue_settings WHERE id = 1", ).Scan(&name, ¤cyCode, ¤cySymbol) if err != nil { return "Venue", "DKK", "kr" // Defaults } return } // nextReceiptNumber returns the next sequential receipt number for a tournament. func (e *Engine) nextReceiptNumber(ctx context.Context, tournamentID string) (int, error) { var count int err := e.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM transactions WHERE tournament_id = ? AND receipt_data IS NOT NULL`, tournamentID, ).Scan(&count) if err != nil { return 0, fmt.Errorf("financial: count receipts: %w", err) } return count + 1, nil } // playerRunningTotals returns the player's current transaction counts. func (e *Engine) playerRunningTotals(ctx context.Context, tournamentID, playerID string) (buyins, rebuys, addons int) { e.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(CASE WHEN type = 'buyin' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN type = 'rebuy' THEN 1 ELSE 0 END), 0), COALESCE(SUM(CASE WHEN type = 'addon' THEN 1 ELSE 0 END), 0) FROM transactions WHERE tournament_id = ? AND player_id = ? AND undone = 0`, tournamentID, playerID, ).Scan(&buyins, &rebuys, &addons) return }