test: payments round-2 — webhook gate/M2 refund, gift-card cancel re-issue, till lock contention, sweep VAT rescue coverage

- webhooks: booking-status gate rejects cancelled bookings, M2 stranded-charge refund row + alert, gift-card rows left pending, payable-booking side-effects, unknown-event 503, refund-before-row 503, webhook-after-sync no-double-complete
- giftcards: saved_card_id SCA wire, card_id+token rejected, resume re-issue never over-refunds entitlement, pending-Square-refund blocks, diff re-issue only what is owed
- sweep: VAT on split-rescued primary, all-tip rows VAT-free, till status/key-changed-while-locked skip, recordUntrackedTillSalePayment VAT
- till: suffixed-key slot scan lock held across Square round-trip

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
2026-08-22 00:34:51 +01:00
co-authored by Sisyphus
parent 77317e4a45
commit 985b114c8b
8 changed files with 725 additions and 37 deletions
@@ -717,6 +717,10 @@ func TestWebhook_DisputeStateUpdated_Open_KeepsOpen(t *testing.T) {
func TestWebhook_PaymentUpdated_UpdatesPaymentStatus(t *testing.T) {
const squarePaymentID = "sqp_updated_completed"
payID := createWebhookTestPayment(t, squarePaymentID, "pending")
// The webhook's booking gate (round-8) only completes booking-attached
// rows — a booking-less row is a gift-card purchase and is left pending
// (C6). Attach a payable booking so the gate completes the payment.
attachWebhookTestBooking(t, payID, 10.00)
event := SquareWebhookEvent{
Type: "payment.updated",
@@ -872,6 +876,9 @@ func TestWebhook_PaymentUpdated_Completed_RescuesPendingTillSale(t *testing.T) {
func TestWebhook_PaymentUpdated_IdempotentReplay(t *testing.T) {
const squarePaymentID = "sqp_updated_idem"
payID := createWebhookTestPayment(t, squarePaymentID, "pending")
// The webhook's booking gate (round-8) only completes booking-attached
// rows — attach a payable booking so the gate completes the payment.
attachWebhookTestBooking(t, payID, 10.00)
event := SquareWebhookEvent{
Type: "payment.updated",
@@ -1016,10 +1023,15 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_MarksOriginFailed(t *testing.T) {
}
}
// TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Noop verifies the orphan
// detection is a no-op when no pending origin row matches: the event is
// acknowledged 200 without touching any row or raising a notification.
func TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Noop(t *testing.T) {
// TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Retries verifies the
// round-8 fix 3 behavior: a COMPLETED payment whose square_payment_id matches
// NO local row (payments or till_sales) AND NO pending origin row by
// idempotency key/reference_id is a GENUINELY unknown charge. It is no longer
// acked 200 — the handler returns 503 so Square re-delivers (its retry budget
// bounds the retries) and writes NO dedup row, so the event can never be
// dropped permanently. No critical notification is raised (there is no issue to
// attribute, just an unresolved event).
func TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Retries(t *testing.T) {
const orphanSquareID = "sqp_orphan_noorigin"
before := countCriticalNotifications(t)
@@ -1041,14 +1053,14 @@ func TestWebhook_PaymentUpdated_OrphanedReplay_NoOrigin_Noop(t *testing.T) {
}`),
}
w := deliverWebhook(t, event)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("expected 503 for a genuinely unknown COMPLETED payment (unresolved money event must be retried, not acked), got %d. body: %s", w.Code, w.Body.String())
}
if n := countCriticalNotifications(t) - before; n != 0 {
t.Errorf("expected no new critical notification with no origin match, got %d", n)
}
if got := countWebhookEvents(t, event.EventID); got != 1 {
t.Errorf("expected 1 dedup row, got %d", got)
if got := countWebhookEvents(t, event.EventID); got != 0 {
t.Errorf("expected NO dedup row for the unresolved unknown payment (Square must retry), got %d", got)
}
}
@@ -1602,6 +1614,9 @@ func TestWebhook_RefundUpdated_DoesNotDemoteCompleted(t *testing.T) {
func TestWebhook_EventTypeAliases_RouteToUpdatedHandlers(t *testing.T) {
const sqPayID = "sqp_alias_pay"
payID := createWebhookTestPayment(t, sqPayID, "pending")
// The webhook's booking gate (round-8) only completes booking-attached
// rows — attach a payable booking so the gate completes the payment.
attachWebhookTestBooking(t, payID, 10.00)
payEvent := SquareWebhookEvent{
Type: "payment.created",