-- ============================================================ -- 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' -- ============================================================