Fix tip retry amount guard false 400 on non-exact pence values

The pending-retry amount guard compared pence via int64(pounds*100),
which truncates instead of rounding. For non-exact pound values (e.g.
£1.14 stored as the float64 1.1399999999999999) the truncation yields
113 != 114, falsely rejecting a legitimate same-amount retry with 400.
Since the frontend reuses the idempotency key on same-amount retries,
every retry was rejected, permanently stranding the pending record
(and any orphaned Square charge) with no recovery path.

Fix: compare in pence via math.Round — the existing pattern already
used in refunds.go — so non-exact values round to the true pence.

Also applied the same correction to the sibling lossy conversions:
- CreateTipPayment / CreateBookingPayment completed-dedup responses
  (would have reported 113p for a 114p payment)
- BuyGiftCard retry amount guard (latent: £10/£20/£50 are float-exact
  so it never bit, but the identical trap is now closed)

Tests:
- TestTipPayment_RetryPending_NonExactAmountSucceeds: 114p pending
  record + same-amount retry completes and charges 114p (failed on the
  old truncation with 400)
- TestTipPayment_RetryPending_AmountMismatchRejected: a same-key retry
  at a different amount is still rejected with 400 and the pending
  record is left untouched

Verified: full backend suite green (25/25 packages, 0 failures),
-race clean on handlers/payments, go build + go vet clean.
This commit is contained in:
2026-08-22 00:34:49 +01:00
parent 4f5dd5c426
commit d54f526b56
3 changed files with 112 additions and 6 deletions
+2 -1
View File
@@ -8,6 +8,7 @@ import (
"fmt"
"log"
"log/slog"
"math"
"net/http"
"strconv"
"strings"
@@ -944,7 +945,7 @@ func BuyGiftCard(w http.ResponseWriter, r *http.Request) {
// Guard the amount: a retry with a different amount must not
// reuse the pending record (gift card would be issued at the
// new amount against the old charge record).
if int64(existing.Amount*100) != req.Amount {
if int64(math.Round(existing.Amount*100)) != req.Amount {
log.Printf("Gift card retry amount mismatch: pending record %s has %.2f, request has %d pence", existing.ID, existing.Amount, req.Amount)
http.Error(w, "Amount does not match the pending gift card payment", http.StatusBadRequest)
return