Files
Crussell/backend/handlers/payments/vat.go
T
popertotsandSisyphus 7b24f8e484 refactor(payments): integrate VAT into gift card buy flow and wrap in transactions
Refactor BuyGiftCard to insert pending payment before Square call with VAT applied. Add transaction wrapping to gift card handlers. Remove redundant Content-Type header sets. Migrate all time.Now() to clock.Now().

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-06-24 23:43:40 +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, on_the_house, and tip payments must never have VAT applied.
// Read the payment method and type 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, ptype string
if qErr := q.QueryRow(ctx, "SELECT payment_method, payment_type FROM payments WHERE id = $1", paymentID).Scan(&method, &ptype); qErr == nil && (method == "discount" || method == "on_the_house" || ptype == "tip") {
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)
}
}