fix: payments money-safety round-2 — webhook booking gate + M2 stranded-charge refund, sync-path status guards, gift-card cancel re-issue reconcile, till-sweep clawback lock, sweep VAT rescue, refund credit routing
- webhook payment.updated now re-reads the booking FOR UPDATE: non-payable booking -> payment failed + stranded-charge refund row + flood-capped alert; payable booking -> full completion side-effects; gift-card rows stay pending (C6 same-key retry); unknown events -> 503 so Square retries - sync-path completion flips (saved-card, tip, online) guarded AND status='pending' + post-flip re-read; postChargeRecheck failed-mark guarded — no webhook-first double-processing, no phantom split rows - CancelGiftCard resume reconciles ALL Square refunds (pending blocks re-issue; COMPLETED sum >= entitlement resolves+neutralizes; else re-issues only the difference under a fresh key) — closes double-refund - sweep till_sales fail/clawback takes crussell:till:<key> lock + post-lock status re-read; recordUntrackedTillSalePayment applies VAT; rescue align clears VAT fields before re-apply (split-accurate VAT, tip rows stay VAT-free) - refunds: chargeAggKey widened, redeemed-card refunds route to user_giftcard_balances, guest cash refunds recorded failed + notification - handlers: tip split-records excluded from VAT loop, splitIdempotencyKey hashed, cross-booking key reuse 409, refunded payments excluded from existingCount, SCA-mint exemption for save_card, non-COMPLETED results routed Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
@@ -927,6 +927,18 @@ CREATE TABLE admin_notifications (
|
||||
reason admin_notification_reason NOT NULL,
|
||||
booking_id CHAR(12) REFERENCES bookings(id),
|
||||
user_id CHAR(12) REFERENCES users(id),
|
||||
-- Money-critical event detail (FIX 3b): populated by the
|
||||
-- 'critical_payment_log' / 'refund_failed' insert sites (Square webhooks,
|
||||
-- sweep, account-erasure, refunds) so the admin notifications page can
|
||||
-- render WHAT happened without opening the CRITICAL logs. All three are
|
||||
-- nullable so the routine insert sites (new_booking, pending_booking, ...)
|
||||
-- keep working unchanged. Column contract: amount = the money amount
|
||||
-- involved, square_id = the Square payment/dispute/checkout identifier,
|
||||
-- description = a short human-readable detail line. See the adminnotify
|
||||
-- package doc for the exact per-site mapping.
|
||||
amount NUMERIC(10,2),
|
||||
square_id TEXT,
|
||||
description TEXT,
|
||||
acknowledged_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
@@ -934,6 +946,21 @@ CREATE TABLE admin_notifications (
|
||||
CREATE INDEX idx_admin_notifications_reason ON admin_notifications(reason);
|
||||
CREATE INDEX idx_admin_notifications_acknowledged_at ON admin_notifications(acknowledged_at);
|
||||
|
||||
-- Flood-cap suppression counter (adminnotify.MaxUnacknowledgedCriticalLogs):
|
||||
-- one row per capped reason, counting how many alerts were dropped while that
|
||||
-- reason's unacknowledged admin_notifications queue sat at the cap. Written by
|
||||
-- CriticalLogsCapExceeded at every insert site (the single choke point, so no
|
||||
-- insert site needs to change) and read by GET /api/admin/notifications as the
|
||||
-- operator-facing "suppressed this cycle" count — only reasons whose queue is
|
||||
-- STILL at the cap are reported, so the count naturally resets once the
|
||||
-- operator acknowledges the queue down.
|
||||
CREATE TABLE admin_notification_suppressions (
|
||||
reason admin_notification_reason PRIMARY KEY,
|
||||
suppressed_count INT NOT NULL DEFAULT 0,
|
||||
first_suppressed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_suppressed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- User notification preferences for future user notification system
|
||||
CREATE TABLE user_notification_preferences (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_user_notification_preferences_id(),
|
||||
@@ -1965,7 +1992,8 @@ BEGIN
|
||||
is_vat_applicable = TRUE,
|
||||
updated_at = NOW()
|
||||
WHERE id = payment_id
|
||||
AND vat_amount IS NULL;
|
||||
AND vat_amount IS NULL
|
||||
AND payment_type != 'tip';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
@@ -2757,3 +2785,28 @@ CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_family_id ON refresh_tokens (family_id);
|
||||
|
||||
-- ============================================================================
|
||||
-- Durable S3/R2 profile-picture deletion outbox (GDPR erasure completeness)
|
||||
-- ============================================================================
|
||||
--
|
||||
-- DeleteAccountHandler persists one row here INSIDE its anonymization
|
||||
-- transaction (before it commits), mirroring the Square erasure outbox (Fault
|
||||
-- A1 → A2): the object-store deletion of the user's profile photo (PII) must
|
||||
-- survive a process crash, or the photo would be retained indefinitely. The
|
||||
-- async cleanup goroutine is the primary drain (deleting the row once the
|
||||
-- object is confirmed gone); the retry-s3-deletions job
|
||||
-- (internal/jobs/cleanup.go) is the crash safety net that completes any
|
||||
-- deletion the goroutine could not finish. Rows are deleted on success; a
|
||||
-- failed deletion stays in place and is retried on every job run.
|
||||
CREATE TABLE IF NOT EXISTS pending_s3_deletions (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('pending_s3_deletions'),
|
||||
user_id CHAR(12), -- erased/anonymized user (no FK: guest users are deleted)
|
||||
bucket TEXT NOT NULL, -- S3/R2 bucket holding the object
|
||||
object_key TEXT NOT NULL, -- object key (e.g. profiles/<userID>.jpg)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
attempts INT NOT NULL DEFAULT 0, -- retry attempts so far (operator visibility)
|
||||
last_error TEXT -- most recent deletion error (operator visibility)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_s3_deletions_created_at ON pending_s3_deletions (created_at);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user