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 gc GiftCard
var lastUsedAt sql.NullTime 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, ` err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, last_used_at) 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()) VALUES ($1, $1, $2, $3, NOW(), $4)
RETURNING id, total_funds_added, amount_remaining, created_by, created_at, is_inventory, last_used_at 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.ID,
&gc.TotalFundsAdded, &gc.TotalFundsAdded,
&gc.AmountRemaining, &gc.AmountRemaining,
@@ -903,11 +908,16 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
var cardID string var cardID string
if req.RecipientType == "self" { 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, ` err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, redeemed_at, redeemed_by, is_inventory) 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) VALUES ($1, 0, $2, NOW(), $2, FALSE, $3)
RETURNING id RETURNING id
`, amountPounds, userID).Scan(&cardID) `, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
if err != nil { if err != nil {
log.Printf("Failed to insert gift card: %v", err) log.Printf("Failed to insert gift card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -937,11 +947,16 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
return return
} }
} else { } 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, ` err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, voucher_type_at_purchase)
VALUES ($1, $1, $2, FALSE) VALUES ($1, $1, $2, FALSE, $3)
RETURNING id RETURNING id
`, amountPounds, userID).Scan(&cardID) `, amountPounds, userID, purchaseVoucherType).Scan(&cardID)
if err != nil { if err != nil {
log.Printf("Failed to insert gift card: %v", err) log.Printf("Failed to insert gift card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -982,16 +997,25 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
CreatedBy: &userID, 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) 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) 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 { if err != nil {
log.Printf("Failed to insert payment record: %v", err) log.Printf("Failed to insert payment record: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return 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 { if err := tx.Commit(ctx); err != nil {
log.Printf("Failed to commit buy transaction: %v", err) log.Printf("Failed to commit buy transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
+76 -3
View File
@@ -89,6 +89,8 @@ type PaymentSummaryResponse struct {
PaidAmount int64 `json:"paid_amount"` PaidAmount int64 `json:"paid_amount"`
RefundedAmount int64 `json:"refunded_amount"` RefundedAmount int64 `json:"refunded_amount"`
RemainingAmount int64 `json:"remaining_amount"` RemainingAmount int64 `json:"remaining_amount"`
TotalVATAmount int64 `json:"total_vat_amount"`
TotalNetAmount int64 `json:"total_net_amount"`
Payments []PaymentResponse `json:"payments"` Payments []PaymentResponse `json:"payments"`
Refunds []RefundResponse `json:"refunds"` Refunds []RefundResponse `json:"refunds"`
} }
@@ -383,6 +385,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
ApplyVATToBookingPayment(r.Context(), tx, paymentID)
} else { // giftcard } else { // giftcard
var customerID sql.NullString var customerID sql.NullString
err = tx.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&customerID) err = tx.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&customerID)
@@ -416,6 +419,7 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
} }
} }
var cardVoucherType string // voucher_type_at_purchase from the gift card
if !usedBalance { if !usedBalance {
// Try direct card redemption (for guests or users without a redeemed balance) // Try direct card redemption (for guests or users without a redeemed balance)
if req.GiftCardID == nil || *req.GiftCardID == "" { if req.GiftCardID == nil || *req.GiftCardID == "" {
@@ -426,7 +430,8 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
var gcRemaining float64 var gcRemaining float64
var redeemedBy sql.NullString var redeemedBy sql.NullString
err = tx.QueryRow(r.Context(), "SELECT amount_remaining, redeemed_by FROM gift_cards WHERE id = $1 FOR UPDATE", cleanCardID).Scan(&gcRemaining, &redeemedBy) var vtp sql.NullString
err = tx.QueryRow(r.Context(), "SELECT amount_remaining, redeemed_by, voucher_type_at_purchase FROM gift_cards WHERE id = $1 FOR UPDATE", cleanCardID).Scan(&gcRemaining, &redeemedBy, &vtp)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Gift card not found", http.StatusNotFound) http.Error(w, "Gift card not found", http.StatusNotFound)
@@ -447,6 +452,14 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
return return
} }
// Record the voucher_type_at_purchase for later VAT decision.
// Legacy cards (created before this column existed) have NULL → default to SPV.
if vtp.Valid {
cardVoucherType = vtp.String
} else {
cardVoucherType = "SPV"
}
// Deduct directly from card remaining amount // Deduct directly from card remaining amount
_, err = tx.Exec(r.Context(), "UPDATE gift_cards SET amount_remaining = amount_remaining - $1, last_used_at = NOW() WHERE id = $2", amountPounds, cleanCardID) _, err = tx.Exec(r.Context(), "UPDATE gift_cards SET amount_remaining = amount_remaining - $1, last_used_at = NOW() WHERE id = $2", amountPounds, cleanCardID)
if err != nil { if err != nil {
@@ -467,6 +480,21 @@ func CreateTerminalPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
// Apply VAT at redemption only if the gift card was purchased as MPV
// (VAT deferred to redemption). For SPV, VAT was already paid at sale.
// For account balance payments (usedBalance=true), VAT was already paid
// when the original card was purchased.
if usedBalance {
// VAT already paid at purchase time — nothing to do here.
} else if cardVoucherType == "MPV" {
vatCfg, vatErr := GetVATConfig(r.Context(), tx)
if vatErr == nil && vatCfg.IsVATRegistered {
if _, vatExecErr := tx.Exec(r.Context(), "SELECT apply_vat_to_payment($1, $2)", paymentID, vatCfg.DefaultVATRate); vatExecErr != nil {
log.Printf("Failed to apply VAT to giftcard payment %s: %v", paymentID, vatExecErr)
}
}
}
} }
if err := tx.Commit(r.Context()); err != nil { if err := tx.Commit(r.Context()); err != nil {
@@ -574,12 +602,27 @@ func GetCheckoutStatus(w http.ResponseWriter, r *http.Request) {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil) tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to begin transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
paymentID, err := service.CreatePaymentRecordTx(r.Context(), tx, record, nil)
if err != nil { if err != nil {
log.Printf("Failed to create payment record: %v", err) log.Printf("Failed to create payment record: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
ApplyVATToBookingPayment(r.Context(), tx, paymentID)
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(PaymentStatusResponse{ json.NewEncoder(w).Encode(PaymentStatusResponse{
@@ -886,6 +929,7 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
defer tx.Rollback(r.Context()) defer tx.Rollback(r.Context())
var primaryPaymentID string var primaryPaymentID string
var paymentIDs []string
for i, rec := range records { for i, rec := range records {
pid, cErr := service.CreatePaymentRecordTx(r.Context(), tx, rec, nil) pid, cErr := service.CreatePaymentRecordTx(r.Context(), tx, rec, nil)
if cErr != nil { if cErr != nil {
@@ -893,11 +937,23 @@ func CreateBookingPayment(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
paymentIDs = append(paymentIDs, pid)
if i == 0 { if i == 0 {
primaryPaymentID = pid primaryPaymentID = pid
} }
} }
// Apply VAT to all split records if the business is VAT-registered.
// Must be inside the transaction so VAT updates are atomic with inserts.
vatCfg, vatErr := GetVATConfig(r.Context(), tx)
if vatErr == nil && vatCfg.IsVATRegistered {
for _, pid := range paymentIDs {
if _, execErr := tx.Exec(r.Context(), "SELECT apply_vat_to_payment($1, $2)", pid, vatCfg.DefaultVATRate); execErr != nil {
log.Printf("Failed to apply VAT to payment %s: %v", pid, execErr)
}
}
}
// Promote deposit to confirmed if total paid meets the 20% threshold. // Promote deposit to confirmed if total paid meets the 20% threshold.
// Check is inside the transaction so it sees the just-inserted payments. // Check is inside the transaction so it sees the just-inserted payments.
var depositMet bool var depositMet bool
@@ -1623,12 +1679,27 @@ func CreateTipPayment(w http.ResponseWriter, r *http.Request) {
CreatedBy: &userID, CreatedBy: &userID,
} }
paymentID, err := service.CreatePaymentRecord(r.Context(), record, nil) tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to begin transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
paymentID, err := service.CreatePaymentRecordTx(r.Context(), tx, record, nil)
if err != nil { if err != nil {
log.Printf("Failed to create payment record: %v", err) log.Printf("Failed to create payment record: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
ApplyVATToBookingPayment(r.Context(), tx, paymentID)
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(PaymentResponse{ json.NewEncoder(w).Encode(PaymentResponse{
@@ -1712,6 +1783,8 @@ func GetBookingPaymentSummary(w http.ResponseWriter, r *http.Request) {
PaidAmount: int64(summary.PaidAmount * 100), PaidAmount: int64(summary.PaidAmount * 100),
RefundedAmount: int64(summary.RefundedAmount * 100), RefundedAmount: int64(summary.RefundedAmount * 100),
RemainingAmount: int64(summary.RemainingAmount * 100), RemainingAmount: int64(summary.RemainingAmount * 100),
TotalVATAmount: int64(summary.TotalVATAmount * 100),
TotalNetAmount: int64(summary.TotalNetAmount * 100),
Payments: payments, Payments: payments,
Refunds: refunds, Refunds: refunds,
}) })
+25 -6
View File
@@ -71,6 +71,8 @@ type PaymentSummary struct {
PaidAmount float64 PaidAmount float64
RefundedAmount float64 RefundedAmount float64
RemainingAmount float64 RemainingAmount float64
TotalVATAmount float64
TotalNetAmount float64
Payments []PaymentRecord Payments []PaymentRecord
Refunds []RefundRecord Refunds []RefundRecord
} }
@@ -201,7 +203,7 @@ func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID
} }
defer rows.Close() defer rows.Close()
var paidAmount float64 var paidAmount, totalVATAmount, totalNetAmount float64
for rows.Next() { for rows.Next() {
var p PaymentRecord var p PaymentRecord
err := rows.Scan( err := rows.Scan(
@@ -216,9 +218,22 @@ func (s *PaymentService) GetBookingPaymentSummary(ctx context.Context, bookingID
summary.Payments = append(summary.Payments, p) summary.Payments = append(summary.Payments, p)
if p.Status == "completed" { if p.Status == "completed" {
paidAmount += p.Amount paidAmount += p.Amount
if p.VATAmount != nil {
totalVATAmount += *p.VATAmount
}
if p.NetAmount != nil {
totalNetAmount += *p.NetAmount
} else if p.VATAmount == nil {
// Only fallback to gross amount if no VAT was applied at all.
// When VAT is present, net_amount is always set by apply_vat_to_payment,
// so this fallback only applies to non-VAT payments where net == gross.
totalNetAmount += p.Amount
}
} }
} }
summary.PaidAmount = paidAmount summary.PaidAmount = paidAmount
summary.TotalVATAmount = totalVATAmount
summary.TotalNetAmount = totalNetAmount
refundRows, err := db.Conn.Query(ctx, ` refundRows, err := db.Conn.Query(ctx, `
SELECT id, payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at SELECT id, payment_id, booking_id, amount, square_refund_id, status, reason, created_by, created_at
@@ -371,12 +386,9 @@ func (s *PaymentService) GetBookingPaymentInfo(ctx context.Context, bookingID st
var info BookingPaymentInfo var info BookingPaymentInfo
err := db.Conn.QueryRow(ctx, ` err := db.Conn.QueryRow(ctx, `
SELECT b.start_time, b.status, SELECT b.start_time, b.status,
COALESCE(bt.total_amount, 0), COALESCE(b.total_amount, 0),
COALESCE(pt.total_paid, 0) COALESCE(pt.total_paid, 0)
FROM bookings b FROM bookings b
LEFT JOIN (
SELECT id, total_amount FROM bookings WHERE id = $1
) bt ON b.id = bt.id
LEFT JOIN ( LEFT JOIN (
SELECT booking_id, SUM(amount) AS total_paid SELECT booking_id, SUM(amount) AS total_paid
FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') GROUP BY booking_id FROM payments WHERE booking_id = $1 AND status = 'completed' AND payment_method NOT IN ('discount', 'on_the_house') GROUP BY booking_id
@@ -526,8 +538,15 @@ func (s *PaymentService) SaveCardForUser(ctx context.Context, userID, squareCard
} }
func (s *PaymentService) GetCardByID(ctx context.Context, cardID, userID string) (*SavedCard, error) { func (s *PaymentService) GetCardByID(ctx context.Context, cardID, userID string) (*SavedCard, error) {
return s.GetCardByIDQuerier(ctx, db.Conn, cardID, userID)
}
// GetCardByIDQuerier is identical to GetCardByID but accepts a db.Querier
// so the lookup can be performed inside a transaction. Callers inside an
// existing transaction should pass their tx variable instead of db.Conn.
func (s *PaymentService) GetCardByIDQuerier(ctx context.Context, q db.Querier, cardID, userID string) (*SavedCard, error) {
var c SavedCard var c SavedCard
err := db.Conn.QueryRow(ctx, ` err := q.QueryRow(ctx, `
SELECT id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default SELECT id, square_card_id, brand, last_4, exp_month, exp_year, fingerprint, is_default
FROM user_saved_cards FROM user_saved_cards
WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL WHERE id = $1 AND user_id = $2 AND deleted_at IS NULL
+45 -16
View File
@@ -120,11 +120,16 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
var giftCardID string var giftCardID string
if req.Action == "create" { if req.Action == "create" {
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, ` err = tx.QueryRow(ctx, `
INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory) INSERT INTO gift_cards (total_funds_added, amount_remaining, created_by, is_inventory, voucher_type_at_purchase)
VALUES ($1, $1, $2, FALSE) VALUES ($1, $1, $2, FALSE, $3)
RETURNING id RETURNING id
`, req.Amount, adminID).Scan(&giftCardID) `, req.Amount, adminID, purchaseVoucherType).Scan(&giftCardID)
if err != nil { if err != nil {
log.Printf("Failed to create gift card: %v", err) log.Printf("Failed to create gift card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -245,23 +250,23 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
if req.IdempotencyKey == "" { if req.IdempotencyKey == "" {
req.IdempotencyKey = "till-cash-" + giftCardID + "-" + time.Now().Format("20060102150405.000000") req.IdempotencyKey = "till-cash-" + giftCardID + "-" + time.Now().Format("20060102150405.000000")
} }
case "saved_card": case "saved_card":
dbPaymentMethod = "online_square" dbPaymentMethod = "online_square"
if req.UserID != nil && *req.UserID != "" { if req.UserID != nil && *req.UserID != "" {
_, err = service.GetCardByID(ctx, *req.UserSavedCardID, *req.UserID) _, err = service.GetCardByIDQuerier(ctx, tx, *req.UserSavedCardID, *req.UserID)
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
http.Error(w, "Saved card not found", http.StatusNotFound) http.Error(w, "Saved card not found", http.StatusNotFound)
return
}
log.Printf("Failed to verify saved card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
log.Printf("Failed to verify saved card: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
} }
}
var sqCardID string var sqCardID string
err = db.Conn.QueryRow(ctx, ` err = tx.QueryRow(ctx, `
SELECT square_card_id SELECT square_card_id
FROM user_saved_cards FROM user_saved_cards
WHERE id = $1 AND deleted_at IS NULL WHERE id = $1 AND deleted_at IS NULL
@@ -395,6 +400,15 @@ func CreateTillSale(w http.ResponseWriter, r *http.Request) {
return return
} }
if req.PaymentMethod != "on_the_house" && saleStatus == "completed" {
vatCfg, vatErr := GetVATConfig(ctx, tx)
if vatErr == nil && vatCfg.IsVATRegistered && vatCfg.VoucherType == "SPV" {
if _, vatExecErr := tx.Exec(ctx, "SELECT apply_vat_to_till_sale($1, $2)", tillSaleID, vatCfg.DefaultVATRate); vatExecErr != nil {
log.Printf("Failed to apply VAT to till sale %s: %v", tillSaleID, vatExecErr)
}
}
}
if err := tx.Commit(ctx); err != nil { if err := tx.Commit(ctx); err != nil {
log.Printf("Failed to commit till sale transaction: %v", err) log.Printf("Failed to commit till sale transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
@@ -458,7 +472,15 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
} }
if paymentResult.Status == "COMPLETED" { if paymentResult.Status == "COMPLETED" {
_, err = db.Conn.Exec(r.Context(), ` tx, err := db.Conn.Begin(r.Context())
if err != nil {
log.Printf("Failed to begin transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
_, err = tx.Exec(r.Context(), `
UPDATE till_sales UPDATE till_sales
SET status = 'completed', SET status = 'completed',
square_payment_id = $1, square_payment_id = $1,
@@ -470,6 +492,13 @@ func GetTillCheckoutStatus(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal server error", http.StatusInternalServerError) http.Error(w, "internal server error", http.StatusInternalServerError)
return return
} }
ApplyVATToTillSale(r.Context(), tx, tillSaleID)
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit transaction: %v", err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(PaymentStatusResponse{ json.NewEncoder(w).Encode(PaymentStatusResponse{