# Race Condition Fix - Implementation Summary ## Problem When multiple buyers attempt to purchase the last available units simultaneously, a race condition occurs where inventory can be oversold. ## Solution Implemented a 10-minute reservation system using the `pending_orders` table to temporarily hold inventory during checkout. ## Changes Made ### 1. Database Migration - **File**: `migrations/add_expires_at_to_pending_orders.sql` - Added `expires_at` column to `pending_orders` table - Added index on `expires_at` for efficient cleanup queries **To apply the migration:** ```sql -- Run the migration file SOURCE migrations/add_expires_at_to_pending_orders.sql; ``` ### 2. Create Invoice Endpoint (`app/api/payments/create-invoice/route.ts`) - **Atomic Inventory Reservation**: Uses database transactions to atomically check and reserve inventory - **Pending Orders Check**: Includes non-expired pending orders when calculating available inventory - **Expiration Time**: Sets `expires_at` to 10 minutes from creation - **NOWPayments Integration**: Sets `invoice_timeout` to 600 seconds (10 minutes) when creating invoice ### 3. Active Drop Endpoint (`app/api/drops/active/route.ts`) - **Fill Calculation**: Includes non-expired pending orders in fill calculation - **UI Display**: Progress bar now shows total reserved inventory (sales + pending orders) ### 4. Cleanup Endpoint (`app/api/payments/cleanup-expired/route.ts`) - **Automatic Cleanup**: Removes expired pending orders - **NOWPayments Status Check**: Verifies payment status before cleanup - **Monitoring**: GET endpoint to check count of expired orders ### 5. UI Updates (`app/components/Drop.tsx`) - **Visual Indicator**: Added note that items are held for 10 minutes during checkout - **Progress Bar**: Automatically reflects reserved inventory (including pending orders) ## Setup Instructions ### 1. Run Database Migration ```bash mysql -u your_user -p your_database < migrations/add_expires_at_to_pending_orders.sql ``` ### 2. Set Up Cleanup Job The cleanup endpoint should be called periodically (recommended: every 1-2 minutes) to remove expired pending orders. #### Option A: Cron Job (Linux/Mac) ```bash # Add to crontab (crontab -e) */2 * * * * curl -X POST https://your-domain.com/api/payments/cleanup-expired -H "Authorization: Bearer YOUR_CLEANUP_TOKEN" ``` #### Option B: Vercel Cron (if using Vercel) Add to `vercel.json`: ```json { "crons": [{ "path": "/api/payments/cleanup-expired", "schedule": "*/2 * * * *" }] } ``` #### Option C: Node.js Cron Library ```javascript const cron = require('node-cron'); const fetch = require('node-fetch'); cron.schedule('*/2 * * * *', async () => { await fetch('http://localhost:3000/api/payments/cleanup-expired', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.CLEANUP_API_TOKEN}` } }); }); ``` ### 3. Environment Variables Add to your `.env` file: ``` # Required: URL for external IPN handler service IPN_CALLBACK_URL=http://your-ipn-service.com/api/payments/ipn-callback # Optional: Token for cleanup endpoint security CLEANUP_API_TOKEN=your-secure-random-token ``` **Note**: IPN callbacks are handled by an external service. The `IPN_CALLBACK_URL` must point to your external IPN handler that processes payment confirmations and creates sales. See `IPN_INTEGRATION_README.md` for integration details. ## How It Works 1. **Purchase Initiation**: When a buyer clicks "Join Drop": - System checks available inventory (sales + non-expired pending orders) - If available, creates pending order with 10-minute expiration - Creates NOWPayments invoice with 10-minute timeout - Inventory is now "on hold" for 10 minutes 2. **During Checkout**: - Progress bar shows reserved inventory (including pending orders) - Other buyers see reduced availability - If buyer completes payment within 10 minutes → sale is created - If buyer doesn't complete payment within 10 minutes → order expires 3. **Expiration**: - Cleanup job runs every 1-2 minutes - Expired pending orders are removed - Inventory becomes available again - NOWPayments invoice expires on their end 4. **Payment Confirmation** (handled by external IPN service): - External IPN handler receives payment notification from NOWPayments - Checks if pending order is expired - Validates final inventory availability - Creates sale record if valid - Deletes pending order ## Testing 1. **Test Race Condition**: - Open two browser windows - Try to purchase the last available unit simultaneously - Only one should succeed - The other should see "Not enough inventory remaining" 2. **Test Expiration**: - Create a pending order - Wait 10+ minutes - Run cleanup endpoint manually - Verify order is removed 3. **Test Payment Flow**: - Create pending order - Complete payment within 10 minutes - Verify sale is created - Verify pending order is deleted ## Notes - Pending orders are automatically included in inventory calculations - The 10-minute hold prevents overselling while giving buyers time to complete payment - NOWPayments invoices also expire after 10 minutes - Cleanup job should run frequently (every 1-2 minutes) to free up inventory quickly