Don Palito Jr integrates with Stripe to provide secure payment processing. The system supports both immediate Stripe payments and bank transfer orders, with automatic order creation via webhooks.
Before checkout, the system creates a Stripe Payment Intent with validated cart items and pricing:
export async function createPaymentIntent(req, res) { const { cartItems, shippingAddress, couponCode } = req.body; const user = req.user; if (!cartItems || cartItems.length === 0) { return res.status(400).json({ error: "Cart is empty" }); } if (!shippingAddress || !shippingAddress.fullName || !shippingAddress.streetAddress) { return res.status(400).json({ error: "Shipping address is required" }); } let subtotal = 0; const validatedItems = []; // Validate products and calculate subtotal for (const item of cartItems) { const product = await Product.findById(item.product._id); if (!product) { return res.status(404).json({ error: `Product ${item.product.name} not found` }); } if (product.stock < item.quantity) { return res.status(400).json({ error: `Insufficient stock for ${product.name}` }); } subtotal += product.price * item.quantity; validatedItems.push({ product: product._id.toString(), price: product.price, quantity: item.quantity }); } // Apply coupon discount if provided let discount = 0; let appliedCoupon = null; if (couponCode) { const coupon = await Coupon.findOne({ code: couponCode.toUpperCase().trim() }); const isValid = coupon && coupon.isActive && (!coupon.expiresAt || new Date() < coupon.expiresAt); if (!isValid) { return res.status(400).json({ error: "El cupón no es válido o ha expirado." }); } const alreadyUsed = coupon.usedBy.some( (id) => id.toString() === user._id.toString() ); if (alreadyUsed) { return res.status(400).json({ error: "Ya usaste este cupón anteriormente." }); } if (coupon.discountType === "percentage") { discount = Math.round((subtotal * coupon.discountValue) / 100); } else { discount = Math.min(coupon.discountValue, subtotal); } appliedCoupon = coupon; } const shipping = 10000; const total = subtotal + shipping - discount; if (total <= 0) { return res.status(400).json({ error: "Invalid order total" }); } const stripeAmount = Math.round(total * 100); // Stripe minimum amount validation const minAmountCOP = 2000; if (total < minAmountCOP) { return res.status(400).json({ error: `The minimum amount to process payments is $${minAmountCOP} COP` }); } // Get or create Stripe customer let customer; if (user.stripeCustomerId) { try { customer = await stripe.customers.retrieve(user.stripeCustomerId); } catch (error) { customer = null; } } if (!customer) { customer = await stripe.customers.create({ email: user.email, name: user.name, metadata: { clerkId: user.clerkId, userId: user._id.toString(), }, }); await User.findByIdAndUpdate(user._id, { stripeCustomerId: customer.id }); } // Create payment intent const paymentIntent = await stripe.paymentIntents.create({ amount: stripeAmount, currency: "cop", customer: customer.id, automatic_payment_methods: { enabled: true, }, metadata: { clerkId: user.clerkId, userId: user._id.toString(), orderItems: JSON.stringify(validatedItems), shippingAddress: JSON.stringify(shippingAddress), couponCode: appliedCoupon ? appliedCoupon.code : "", totalPrice: total.toString(), }, }); res.status(200).json({ clientSecret: paymentIntent.client_secret, paymentIntentId: paymentIntent.id });}
The payment amount is stored in Stripe’s metadata along with order details. This allows the webhook to automatically create the order when payment succeeds.
The webhook handler verifies the Stripe signature using stripe.webhooks.constructEvent() to ensure the request genuinely comes from Stripe. This prevents malicious actors from creating fake orders.
The webhook checks for duplicate orders by looking for existing orders with the same payment intent ID, preventing double-processing.