Files
Crussell/backend/handlers/payments/vat.go
T
popertotsandSisyphus 7cd71e4485 feat(payments): add VAT config helper and apply functions
Add GetVATConfig for reading VAT settings via any db.Querier, ApplyVATToBookingPayment for applying VAT to payment records, and ApplyVATToTillSale for SPV gift card till sales. Errors are logged without blocking payment flow.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-22 17:05:31 +01:00

79 lines
2.7 KiB
Go

package payments
import (
"context"
"log"
"crussell/db"
)
type VATConfig struct {
IsVATRegistered bool
DefaultVATRate float64
VoucherType string
}
// GetVATConfig reads VAT config using the given querier, which can be either
// a *db.PoolProxy (for non-transactional reads), a pgx.Tx (to read inside a
// transaction), or any other type that implements db.Querier.
func GetVATConfig(ctx context.Context, q db.Querier) (*VATConfig, error) {
var cfg VATConfig
err := q.QueryRow(ctx, `
SELECT COALESCE(is_vat_registered, FALSE),
COALESCE(default_vat_rate, 20.0),
COALESCE(voucher_type, 'SPV')
FROM business_settings
LIMIT 1
`).Scan(&cfg.IsVATRegistered, &cfg.DefaultVATRate, &cfg.VoucherType)
if err != nil {
return nil, err
}
return &cfg, nil
}
// ApplyVATToBookingPayment reads VAT config and calls apply_vat_to_payment
// on a booking payment record. The q parameter is used for both reading the
// VAT config and for the defensive payment-method check, ensuring all reads
// are consistent with the caller's transactional context. Errors are logged
// — VAT failure should not block the payment flow.
func ApplyVATToBookingPayment(ctx context.Context, q db.Querier, paymentID string) {
vatCfg, err := GetVATConfig(ctx, q)
if err != nil {
log.Printf("Failed to read VAT config for payment %s: %v", paymentID, err)
return
}
if !vatCfg.IsVATRegistered {
return
}
// Discount and on_the_house payments must never have VAT applied.
// Read the payment method inside the same transactional context so that
// the just-inserted row is visible (defence against READ COMMITTED
// isolation when q is a pgx.Tx).
var method string
if qErr := q.QueryRow(ctx, "SELECT payment_method FROM payments WHERE id = $1", paymentID).Scan(&method); qErr == nil && (method == "discount" || method == "on_the_house") {
return
}
if _, execErr := q.Exec(ctx, "SELECT apply_vat_to_payment($1, $2)", paymentID, vatCfg.DefaultVATRate); execErr != nil {
log.Printf("Failed to apply VAT to payment %s: %v", paymentID, execErr)
}
}
// ApplyVATToTillSale reads VAT config and calls apply_vat_to_till_sale for
// SPV gift card till sales. Errors are logged — VAT failure should not
// block the sale flow.
func ApplyVATToTillSale(ctx context.Context, q db.Querier, saleID string) {
vatCfg, err := GetVATConfig(ctx, q)
if err != nil {
log.Printf("Failed to read VAT config for till sale %s: %v", saleID, err)
return
}
if !vatCfg.IsVATRegistered || vatCfg.VoucherType != "SPV" {
return
}
if _, execErr := q.Exec(ctx, "SELECT apply_vat_to_till_sale($1, $2)", saleID, vatCfg.DefaultVATRate); execErr != nil {
log.Printf("Failed to apply VAT to till sale %s: %v", saleID, execErr)
}
}