149 lines
5.2 KiB
Markdown
149 lines
5.2 KiB
Markdown
# 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
|
|
|