Wolkendama-API / Controller / checkoutController.js
checkoutController.js
Raw
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 });
};