This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/migrations/2026-02-15_payment_hardening.sql
John Mizerek 3e936728db Preserve payment hardening files and migrations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 13:26:17 -07:00

109 lines
3.7 KiB
SQL

-- ============================================================
-- PAYMENT FLOW HARDENING MIGRATION
-- Date: 2026-02-15
-- Purpose: Add idempotency, audit, and integrity constraints
-- ============================================================
-- 1. CREATE PAYMENTS AUDIT TABLE
-- Stores full webhook payloads for forensic debugging
CREATE TABLE IF NOT EXISTS PaymentAudit (
ID INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
OrderID INT UNSIGNED NULL,
PaymentIntentID VARCHAR(64) NULL,
StripeChargeID VARCHAR(64) NULL,
StripeEventID VARCHAR(64) NOT NULL,
EventType VARCHAR(64) NOT NULL,
AmountCents INT UNSIGNED NULL,
Currency CHAR(3) NULL DEFAULT 'usd',
RawPayload MEDIUMTEXT NOT NULL,
ProcessedAt DATETIME NULL,
ProcessingResult ENUM('success', 'skipped_duplicate', 'skipped_already_paid', 'error') NOT NULL DEFAULT 'success',
ErrorMessage TEXT NULL,
CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- Prevent duplicate event processing
UNIQUE KEY uk_stripe_event (StripeEventID),
-- Fast lookups
INDEX idx_order (OrderID),
INDEX idx_payment_intent (PaymentIntentID),
INDEX idx_created (CreatedAt)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. ADD MISSING COLUMNS TO ORDERS TABLE
-- (Run these only if columns don't exist)
-- Add StripePaymentIntentID if it doesn't exist
SET @dbname = 'payfrit_dev';
SET @tablename = 'Orders';
SET @columnname = 'StripePaymentIntentID';
SET @preparedStatement = (
SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
'SELECT 1',
'ALTER TABLE Orders ADD COLUMN StripePaymentIntentID VARCHAR(64) NULL AFTER PaymentStatus'
)
);
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3. ADD UNIQUE CONSTRAINT ON StripePaymentIntentID
-- Each PaymentIntent can only pay ONE order
ALTER TABLE Orders
ADD UNIQUE INDEX uk_stripe_payment_intent (StripePaymentIntentID);
-- Note: If this fails with "Duplicate entry", you have data integrity issues to resolve first
-- 4. ADD AMOUNT VERIFICATION COLUMNS (if not present)
-- Store the expected and actual amounts for verification
SET @columnname = 'ExpectedAmountCents';
SET @preparedStatement = (
SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
'SELECT 1',
'ALTER TABLE Orders ADD COLUMN ExpectedAmountCents INT UNSIGNED NULL AFTER StripePaymentIntentID'
)
);
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @columnname = 'ReceivedAmountCents';
SET @preparedStatement = (
SELECT IF(
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = @tablename AND COLUMN_NAME = @columnname) > 0,
'SELECT 1',
'ALTER TABLE Orders ADD COLUMN ReceivedAmountCents INT UNSIGNED NULL AFTER ExpectedAmountCents'
)
);
PREPARE stmt FROM @preparedStatement;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 5. ADD UNIQUE CONSTRAINT ON WorkPayoutLedgers.StripePaymentIntentID
-- Each PaymentIntent should only be linked to ONE ledger entry
ALTER TABLE WorkPayoutLedgers
ADD UNIQUE INDEX uk_ledger_payment_intent (StripePaymentIntentID);
-- 6. ADD INDEX FOR FAST WEBHOOK LOOKUPS
ALTER TABLE Orders
ADD INDEX idx_payment_status (PaymentStatus);
-- ============================================================
-- PRODUCTION (payfrit) - Run the same on production database
-- Replace @dbname = 'payfrit_dev' with @dbname = 'payfrit'
-- ============================================================