feat(payments): integrate VAT into gift cards, payment handlers, and till sales

Track voucher_type_at_purchase on gift card creation. Apply VAT at gift card purchase (SPV), at redemption (MPV). Use transaction-aware GetCardByIDQuerier for till saved card lookups. Apply VAT to all booking payments (split records, terminal, tip, checkout). Add TotalVATAmount and TotalNetAmount to PaymentSummary responses.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-22 17:05:39 +01:00
co-authored by Sisyphus
parent 7cd71e4485
commit 6561ceb7a9
4 changed files with 181 additions and 36 deletions
+35 -11
View File
@@ -354,11 +354,16 @@ func CreateGiftCard(w http.ResponseWriter, r *http.Request) {
var gc GiftCard
var lastUsedAt sql.NullTime
var purchaseVoucherType string
_ = tx.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
if purchaseVoucherType == "" {
purchaseVoucherType = "SPV"
}
err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, last_used_at)
VALUES ($1, $1, $2, $3, NOW())
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, last_used_at, voucher_type_at_purchase)
VALUES ($1, $1, $2, $3, NOW(), $4)
RETURNING id, total_funds_added, amount_remaining, created_by, created_at, is_inventory, last_used_at
`, req.Amount, adminID, req.IsInventory).Scan(
`, req.Amount, adminID, req.IsInventory, purchaseVoucherType).Scan(
&gc.ID,
&gc.TotalFundsAdded,
&gc.AmountRemaining,
@@ -903,11 +908,16 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
var cardID string
if req.RecipientType == "self" {
var purchaseVoucherType string
_ = tx.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
if purchaseVoucherType == "" {
purchaseVoucherType = "SPV"
}
err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory)
VALUES ($1, 0, $2, NOW(), $2, FALSE)
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory, voucher_type_at_purchase)
VALUES ($1, 0, $2, NOW(), $2, FALSE, $3)
RETURNING id
`, amountPounds, userID).Scan(&cardID)
`, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
if err != nil {
log.Printf("Failed to insert gift card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -937,11 +947,16 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
return
}
} else {
var purchaseVoucherType string
_ = tx.QueryRow(ctx, `SELECT COALESCE(voucher_type, 'SPV') FROM business_settings LIMIT 1`).Scan(&purchaseVoucherType)
if purchaseVoucherType == "" {
purchaseVoucherType = "SPV"
}
err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory)
VALUES ($1, $1, $2, FALSE)
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, voucher_type_at_purchase)
VALUES ($1, $1, $2, FALSE, $3)
RETURNING id
`, amountPounds, userID).Scan(&cardID)
`, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
if err != nil {
log.Printf("Failed to insert gift card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -982,16 +997,25 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
CreatedBy: &userID,
}
_, err = tx.Exec(ctx, `
var buyPaymentID string
err = tx.QueryRow(ctx, `
INSERT INTO payments (payment_type, payment_method, status, amount, square_payment_id, idempotency_key, fees, user_saved_card_id, created_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, record.PaymentType, record.PaymentMethod, record.Status, record.Amount, record.SquarePaymentID, record.IdempotencyKey, record.Fees, record.UserSavedCardID, record.CreatedBy, record.CreatedAt, record.UpdatedAt)
RETURNING id
`, record.PaymentType, record.PaymentMethod, record.Status, record.Amount, record.SquarePaymentID, record.IdempotencyKey, record.Fees, record.UserSavedCardID, record.CreatedBy, record.CreatedAt, record.UpdatedAt).Scan(&buyPaymentID)
if err != nil {
log.Printf("Failed to insert payment record: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
vatCfg, vatErr := GetVATConfig(ctx, tx)
if vatErr == nil && vatCfg.IsVATRegistered && vatCfg.VoucherType == "SPV" {
if _, vatExecErr := tx.Exec(ctx, "SELECT apply_vat_to_payment($1, $2)", buyPaymentID, vatCfg.DefaultVATRate); vatExecErr != nil {
log.Printf("Failed to apply VAT to buy gift card payment %s: %v", buyPaymentID, vatExecErr)
}
}
if err := tx.Commit(ctx); err != nil {
log.Printf("Failed to commit buy transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)