payment on hold
This commit is contained in:
148
RACE_CONDITION_FIX.md
Normal file
148
RACE_CONDITION_FIX.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user