Checkout Command Creation
Overview
This document explains how commands are created before payment and then marked as paid when the customer successfully completes checkout via Stripe.
Flow
1. Command Creation & Checkout Session
When a customer initiates checkout, the frontend calls:
POST /api/checkout/create-stripe-checkout-session
Required Parameters:
{
items: [
{
product_id: string,
quantity: number,
type?: string // Optional: for products with multiple price types
}
],
customer: {
firstName: string,
lastName: string,
email: string,
phone: string,
address: string,
postalCode: string,
city: string,
fax?: string // Optional
},
appointment: Date // Appointment/delivery date
}
Process:
- Validates cart items and customer information
- Finds the command module
- Creates an UNPAID command with:
- Generated reference number (format:
#XXXXXXXX) - Cart with product IDs and quantities (reference only)
- Customer information
- Appointment date
paid: false
- Generated reference number (format:
- Fetches current product prices from database
- Creates Stripe tax rates
- Creates a Stripe checkout session with:
- Line items (products with prices and taxes)
- Success URL including
session_idandcommand_id - Cancel URL including
command_id
Response:
{
"url": "https://checkout.stripe.com/...",
"commandId": "507f1f77bcf86cd799439011"
}
2. Customer Payment
The customer is redirected to Stripe's checkout page where they:
- Enter their payment information
- Complete the payment
3. Payment Confirmation
After successful payment, Stripe redirects to:
https://yourdomain.com/commande/terminee?session_id={STRIPE_SESSION_ID}&command_id={COMMAND_ID}
The frontend then calls:
POST /api/checkout/confirm-payment
Required Parameters:
{
sessionId: string, // Stripe checkout session ID from URL
commandId: string // Command ID from URL
}
Process:
- Retrieves and verifies the Stripe session
- Checks that payment status is "paid"
- Calls
commandService.markAsPaid(commandId)which:- Creates an immutable cart snapshot with current prices
- Sets
paid: true - Saves the command
Response:
{
"command": {
"_id": "...",
"ref": "#ABC12345",
"paid": true,
"cartSnapshot": {
"products": [...],
"totalPrice": 99.99
},
...
}
}
Configuration
Environment Variables
Add to your .env file:
STRIPE_SECRET_KEY="sk_test_..."
Command Module
The system automatically finds the command module by looking for:
Module.findOne({ type: "commands" })
Ensure you have a module with type: "commands" in your database.
Cart Snapshot System
When a command is marked as paid, the system creates an immutable snapshot:
Before Payment (Reference Only):
{
cart: {
products: [
{ product_id: ObjectId, quantity: 2 }
]
},
paid: false
}
After Payment (With Snapshot):
{
cart: {
products: [
{ product_id: ObjectId, quantity: 2 }
]
},
cartSnapshot: {
products: [
{
product_id: ObjectId,
title: "Product Name",
price: 29.99,
quantity: 2
}
],
totalPrice: 59.98
},
paid: true
}
This ensures that even if product prices change later, you have the exact pricing at the time of purchase.
Error Handling
Webhook Errors
If command creation fails in the webhook:
- The webhook still returns 200 to acknowledge receipt (prevents Stripe retries)
- Error is logged to console
- Manual intervention may be needed to create the command
Missing Metadata
If metadata is missing from the session:
- Returns 400 Bad Request
- Logs error to console
- Stripe will retry the webhook
Testing
Local Testing
- Use Stripe test keys (starts with
sk_test_) - Use test card numbers from Stripe Testing Documentation:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002
- Success:
- Test the full flow:
- Create checkout session
- Complete payment on Stripe
- Verify redirect with session_id and command_id
- Call confirm-payment endpoint
- Check command is marked as paid with snapshot
Frontend Integration Example
// Step 1: Create checkout session (creates unpaid command)
const response = await fetch('/api/checkout/create-stripe-checkout-session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
items: [
{ product_id: '507f1f77bcf86cd799439011', quantity: 2 }
],
customer: {
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com',
phone: '+33612345678',
address: '123 Main St',
postalCode: '75001',
city: 'Paris'
},
appointment: new Date('2025-12-15T14:00:00')
})
});
const { url, commandId } = await response.json();
// Store commandId for later if needed
localStorage.setItem('pendingCommandId', commandId);
// Step 2: Redirect to Stripe checkout
window.location.href = url;
// Step 3: On success page (after Stripe redirects back)
// URL will be: /commande/terminee?session_id=xxx&command_id=yyy
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('session_id');
const commandId = urlParams.get('command_id');
if (sessionId && commandId) {
// Confirm payment and mark command as paid
const confirmResponse = await fetch('/api/checkout/confirm-payment', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId,
commandId
})
});
const { command } = await confirmResponse.json();
console.log('Command paid:', command.ref);
// Clear stored command ID
localStorage.removeItem('pendingCommandId');
}
Security Notes
- Session Verification: The confirm-payment endpoint verifies the Stripe session to ensure payment was actually completed
- Command Pre-Creation: Commands are created before payment but remain unpaid until confirmed
- Customer Data: Customer information is sent directly from your frontend and stored securely
- HTTPS Required: Stripe checkout requires HTTPS in production
Error Handling
Payment Canceled
If the customer cancels, they are redirected to:
https://yourdomain.com/commande/annulee?command_id={COMMAND_ID}
The unpaid command remains in the database. You can:
- Allow customer to retry payment
- Automatically delete abandoned commands after X hours
- Show the command in an "unpaid orders" admin section
Payment Verification Failed
If confirm-payment receives an invalid session or unpaid status:
- Returns 400 error
- Command remains unpaid
- Customer can retry
Related Documentation
- CART_MANAGEMENT.md - Cart and snapshot system details
- API Documentation - Full API reference
Troubleshooting
Commands Not Being Marked as Paid
- Check that
session_idandcommand_idare in the success URL - Verify the frontend calls
/confirm-paymentafter redirect - Check server logs for errors in
confirmPaymentmethod - Ensure Stripe session payment_status is "paid"
- Verify command exists in database with correct ID
Unpaid Commands Accumulating
Implement a cleanup job to handle abandoned carts:
// Delete unpaid commands older than 24 hours
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
await Command.deleteMany({
paid: false,
createdAt: { $lt: oneDayAgo }
});
Customer Data Not Saved
- Verify all required customer fields are sent in the request
- Check validation errors in server logs
- Ensure customer schema matches the data structure
Missing Cart Snapshot
- Check that
commandService.markAsPaid()is being called - Verify products still exist in database
- Check for errors in command service logs