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
+14 -6
View File
@@ -423,10 +423,11 @@ func TestHandleSquareWebhook_BodyTooLarge(t *testing.T) {
func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
// Well-formed payment.updated event carrying the full nested payment object
// (data.object.payment with id/status) so handlePaymentUpdated can parse
// and dispatch it — a known money event whose payload fails to parse
// returns 503 without a dedup row (Square retries), which is NOT this
// test's intent. The unique event_id and square_payment_id avoid colliding
// with the other tests' dedup rows and payment fixtures.
// and dispatch it. The COMPLETED charge matches NO local row — round-8 fix
// 3: a genuinely unknown completed payment is retried (503, no dedup row)
// rather than acked, so the signature passing is what this test proves (a
// bad signature would 403 before dispatch). The unique event_id avoids
// colliding with the other tests' dedup rows.
body := []byte(fmt.Sprintf(`{"type":"payment.updated","event_id":"evt_envkey_1","created_at":%q,"data":{"object":{"payment":{"id":"sqp_env_key_1","status":"COMPLETED","amount_money":{"amount":5000,"currency":"GBP"},"updated_at":%q}}}}`, nowInRFC3339(0), nowInRFC3339(0)))
key := "env-signing-key"
notificationURL := "http://localhost:8080/webhooks/square"
@@ -439,8 +440,8 @@ func TestHandleSquareWebhook_ValidSignatureWithEnvKey(t *testing.T) {
t.Setenv("SQUARE_WEBHOOK_SIGNATURE_KEY", key)
w := makeWebhookRequest(body, sig, context.Background())
if w.Code != http.StatusOK {
t.Errorf("expected 200 with valid signature, got %d. body: %s", w.Code, w.Body.String())
if w.Code != http.StatusServiceUnavailable {
t.Errorf("expected 503 for a signed COMPLETED payment.updated with no local row (unresolved unknown money event is retried, not acked), got %d. body: %s", w.Code, w.Body.String())
}
}
@@ -714,6 +715,10 @@ func TestHandleSquareWebhook_ConcurrentSameEvent_Serialized(t *testing.T) {
squarePaymentID := fmt.Sprintf("sqp_concurrent_same_%d", seq)
eventID := fmt.Sprintf("evt_concurrent_same_%d", seq)
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",
@@ -791,6 +796,9 @@ func TestHandleSquareWebhook_DedupCacheEviction_Redispatches(t *testing.T) {
const squarePaymentID = "sqp_dedup_eviction"
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",