Aller au contenu principal

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:

  1. Validates cart items and customer information
  2. Finds the command module
  3. Creates an UNPAID command with:
    • Generated reference number (format: #XXXXXXXX)
    • Cart with product IDs and quantities (reference only)
    • Customer information
    • Appointment date
    • paid: false
  4. Fetches current product prices from database
  5. Creates Stripe tax rates
  6. Creates a Stripe checkout session with:
    • Line items (products with prices and taxes)
    • Success URL including session_id and command_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:

  1. Retrieves and verifies the Stripe session
  2. Checks that payment status is "paid"
  3. 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

  1. Use Stripe test keys (starts with sk_test_)
  2. Use test card numbers from Stripe Testing Documentation:
    • Success: 4242 4242 4242 4242
    • Decline: 4000 0000 0000 0002
  3. 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

  1. Session Verification: The confirm-payment endpoint verifies the Stripe session to ensure payment was actually completed
  2. Command Pre-Creation: Commands are created before payment but remain unpaid until confirmed
  3. Customer Data: Customer information is sent directly from your frontend and stored securely
  4. 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

Troubleshooting

Commands Not Being Marked as Paid

  1. Check that session_id and command_id are in the success URL
  2. Verify the frontend calls /confirm-payment after redirect
  3. Check server logs for errors in confirmPayment method
  4. Ensure Stripe session payment_status is "paid"
  5. 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