Close refund system and gate raw-PAN card entry
Refund system (Round 3 fixes + follow-up + alignment): - Serialize cancellation refunds against the manual handler via per-payment advisory locks taken before the prior-refunds read (pg_advisory_xact_lock, ascending, same crussell:refund: key space) - Aggregate pending cancellation refunds into ONE Square refund per charge (stable charge-level -square-agg key); atomic group UPDATE keeps crash-retry amounts identical for Square key-dedup - Persist paymentID-square-amount idempotency keys on cancellation refunds; scheduler reads the stored key (legacy fallback for old rows) - Add sweep-pending-square-refunds cron (*/5, concurrency 1) with refund_attempts cap; sweep retries stale manual pending refunds with each row's own stored idempotency key - Reconcile at Square (GET /v2/refunds ListPaymentRefunds) before every terminal failed transition: tri-state result leaves rows pending on reconcile error instead of false-failing; PAYMENT_ALREADY_REFUNDED resolves to completed - Move over-refund guard inside the lock, counting completed + pending (excluding failed); ErrRefundDeclined distinguishes definitive vs ambiguous outcomes - forgiveFees now executes a real full refund (forceFullRefund override) with admin_forgiven_fees reason threaded to Square - Surface failed card refunds in the admin notification centre (refund_failed enum, RETURNING-id pre-pass inserts, NOT EXISTS dedup) - Dedup double-cancel refund inserts via ON CONFLICT (idempotency_key) DO NOTHING without consuming refundRemaining Frontend: - Remove all raw-PAN card entry: zero card_number/card_cvc/new_card_token in request bodies; gate new-card entry behind CardEntryUnavailable notice + newCardDisabled prop across all 8 flows - Delete hand-rolled CardInput.svelte; keep CardSelection saved-card UI and CardEntryUnavailable fallback - Update cancellation-policy page to in-person cash pickup wording Tests: - Rewrite the two amount-blind dedup tests to assert real money movement (single call, aggregated amount, shared refund ID) - Add coverage: manual refund vs cancellation serialization (concurrent goroutines), reconcile error vs no-match branches, stale manual retry, forgive-fees real refund row + reason, double-cancel dedup, mock refund key dedup, ListPaymentRefunds filtering - Fix time-dependent booking flakes with fixtures.NextWorkingDayAt - 25/25 packages pass; -race clean on payments/square/db/jobs/bookings
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"crussell/clock"
|
||||
"crussell/db"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -178,6 +179,15 @@ func CreateTestBookingAtTime(q db.Querier, userID, serviceID string, startTime t
|
||||
return bookingID, nil
|
||||
}
|
||||
|
||||
// NextWorkingDayAt returns the time at `hour` UTC on a day `daysAhead` days from
|
||||
// now. Hours in [8, 18] are guaranteed inside the fixture's Mon-Sun 08:00-20:00
|
||||
// London working hours at any wall-clock time (London is at most UTC+1), unlike
|
||||
// clock.Now().Add(N * time.Hour), which can land after closing and flake tests.
|
||||
func NextWorkingDayAt(daysAhead, hour int) time.Time {
|
||||
day := clock.Now().AddDate(0, 0, daysAhead)
|
||||
return time.Date(day.Year(), day.Month(), day.Day(), hour, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func CreateTestVerifiedUser(q db.Querier) (string, error) {
|
||||
return createTestUser(q, "Verified", "User", "verified@test.com", "verified_email")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user