const stripe = require("stripe")(process.env.STRIPE_SECRET); const Shop = require("../Model/shopModel"); const Customer = require("../Model/customerModel"); const AppError = require("../utils/appError"); const catchAsync = require("../utils/catchAsync"); const sendInBlue = require("../utils/sendinblue"); exports.createCheckout = catchAsync(async (req, res, next) => { const shopItem = await Shop.find(); let lineItems = []; let ifError = false; for (const el of req.body.items) { const foundItem = shopItem.find( (shopEl) => shopEl._id.toHexString() === el.shopId ); if (!foundItem) { ifError = "No shop item found, please provide correct shop Id"; break; } if (!el.variant || !foundItem.variant[el.variant]) { ifError = "No shop item found, please provide correct variant of the item"; break; } if (el.quantity > foundItem.variant[el.variant].remainingQuantity) { ifError = `Purchase quantity for ${el.variant} is higher than stock remaining quantity.`; break; } // add line items for stripe lineItems.push({ price_data: { currency: "myr", product_data: { name: `${foundItem.title} - ${el.variant}`, description: foundItem.descriptions, images: [foundItem.imgCover], }, unit_amount: foundItem.variant[el.variant].price * 100, }, quantity: el.quantity, }); } if (ifError) { return next(new AppError(ifError, 400)); } const paymentIntentDescription = `${(Math.random() * 100).toFixed( 0 )}_${Date.now()}`; // Create Checkout const session = await stripe.checkout.sessions.create({ // payment_method_types: ["card", "fpx", "grabpay"], payment_method_types: ["card", "grabpay"], mode: "payment", payment_intent_data: { description: paymentIntentDescription, }, cancel_url: `${req.protocol}://${req.get("host")}/cancel.html`, success_url: `${req.protocol}://${req.get("host")}/success.html`, line_items: lineItems, customer_creation: "always", phone_number_collection: { enabled: true, }, shipping_address_collection: { allowed_countries: ["MY"], }, invoice_creation: { enabled: true, }, billing_address_collection: "required", expires_at: Math.round(Date.now() / 1000) + 60 * process.env.STRIPE_SESSION_EXPIRE_AT_MIN, }); // Change remaining item to reserve item for (let i = 0; i < req.body.items.length; i++) { const foundItem = await Shop.findById(req.body.items[i].shopId); // Deduct remaining quantity const toUpdateVariant = JSON.parse(JSON.stringify(foundItem.variant)); toUpdateVariant[req.body.items[i].variant].remainingQuantity = toUpdateVariant[req.body.items[i].variant].remainingQuantity - req.body.items[i].quantity; // Add the quantity to reserve item const toUpdateReserveItem = [...foundItem.reserveItem]; toUpdateReserveItem.push({ checkoutId: session.id, variant: req.body.items[i].variant, quantity: req.body.items[i].quantity, }); await Shop.findByIdAndUpdate( req.body.items[i].shopId, { variant: toUpdateVariant, reserveItem: toUpdateReserveItem }, { new: true, runValidators: true, } ); } // Varaibles for SendinBlue const cartItem = lineItems.map((el) => { return { name: el.price_data.product_data.name, imageCover: el.price_data.product_data.images[0], price: (el.price_data.unit_amount / 100).toLocaleString("en-us", { style: "currency", currency: "MYR", }), subTotal: ( (el.quantity * el.price_data.unit_amount) / 100 ).toLocaleString("en-us", { style: "currency", currency: "MYR", }), quantity: el.quantity, }; }); const params = { totalPrice: (session.amount_total / 100).toLocaleString("en-us", { style: "currency", currency: "MYR", }), items: cartItem, }; // Create Customer Model await Customer.create({ checkoutId: session.id, reserveItem: req.body.items, emailParams: params, status: "pending_payment", paymentIntentDescription: paymentIntentDescription, }); res.status(200).json({ status: "success", data: session, }); }); exports.webhookCheckout = async (req, res) => { const sig = req.headers["stripe-signature"]; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { res.status(400).send(`Webhook Error: ${err.message}`); return; } // IMPORTANT:Handle the event if (event.type === "checkout.session.expired") { // Find customer const foundCustomer = await Customer.findOne({ checkoutId: event.data.object.id, }); // Guard Clause if (!foundCustomer) { res.status(400).send(`No Customer found!`); return; } const itemArr = foundCustomer.reserveItem; // Find customer and delete await Customer.findOneAndDelete({ checkoutId: event.data.object.id, }); // Reverse back the remaining quantity and take out reserve items for (let i = 0; i < itemArr.length; i++) { const foundItem = await Shop.findById(itemArr[i].shopId); // Add back remaining quantity const toUpdateVariant = JSON.parse(JSON.stringify(foundItem.variant)); toUpdateVariant[itemArr[i].variant].remainingQuantity = toUpdateVariant[itemArr[i].variant].remainingQuantity + itemArr[i].quantity; // Filter out reserve item let toUpdateReserveItem = []; if (foundItem.reserveItem?.length > 0) { toUpdateReserveItem = [...foundItem.reserveItem].filter( (el) => el.checkoutId !== foundCustomer.checkoutId ); } await Shop.findByIdAndUpdate( itemArr[i].shopId, { variant: toUpdateVariant, reserveItem: toUpdateReserveItem }, { new: true, runValidators: true, } ); } } else if (event.type === "checkout.session.completed") { // Retrieve session items const session = await stripe.checkout.sessions.retrieve( event.data.object.id, { expand: ["line_items", "invoice"] } ); // Save sessions items to Customer Model let customerData = { amountTotal: session.amount_total / 100, billingName: session.customer_details.name ? session.customer_details.name : session.shipping_details.name, email: session.customer_details.email, phone: session.customer_details.phone, billingAddress: session.customer_details.address, shippingAddress: session.shipping_details.address, shippingName: session.shipping_details.name, paymentIntentId: session.payment_intent, paymentIntentDescription: null, }; customerData.purchasedItem = session.line_items.data.map((el) => { return { subtotal: el.amount_total / 100, item: el.description, quantity: el.quantity, pricePerUnit: el.price.unit_amount / 100, }; }); await Customer.findOneAndUpdate({ checkoutId: session.id }, customerData, { new: true, runValidators: true, }); // Remove reverse item for Shop Model and Customer Model const foundCustomer = await Customer.findOne({ checkoutId: event.data.object.id, }); const itemArr = foundCustomer.reserveItem; // Filter out reserve item for (let i = 0; i < itemArr.length; i++) { const foundItem = await Shop.findById(itemArr[i].shopId); let toUpdateReserveItem = []; if (foundItem.reserveItem?.length > 0) { toUpdateReserveItem = [...foundItem.reserveItem].filter( (el) => el.checkoutId !== foundCustomer.checkoutId ); } await Shop.findByIdAndUpdate( itemArr[i].shopId, { reserveItem: toUpdateReserveItem }, { new: true, runValidators: true, } ); } // Customer reserve item === empty array and status is payment_success await Customer.findOneAndUpdate( { checkoutId: session.id }, { reserveItem: [], status: "payment_success", emailParams: {}, invoiceUrl: session.invoice.hosted_invoice_url, }, { new: true, runValidators: true, } ); let emailParams = { ...foundCustomer.emailParams, billingName: customerData.billingName, shippingName: customerData.shippingName, shippingAddress: customerData.shippingAddress, invoice: session.invoice.hosted_invoice_url, }; // Send email for successful payment sendInBlue.sendPurchaseConfirmationEmail( session.customer_details.name, session.customer_details.email, emailParams ); } else if (event.type === "payment_intent.payment_failed") { // Find customer const customerData = await Customer.findOne({ paymentIntentDescription: event.data.object.description, }); // Expire the checkout session if payment failed await stripe.checkout.sessions.expire(customerData.checkoutId); } else { // ... handle other event types console.log(`Unhandled event type ${event.type}`); } // Return a 200 response to acknowledge receipt of the event res.status(200).json({ received: true }); };