From f894ea70424d64f0db0f4cbb64f230a016f57dee Mon Sep 17 00:00:00 2001 From: Rajat Saxena Date: Fri, 27 Dec 2024 10:18:54 +0530 Subject: [PATCH] New payment process --- .../web/app/api/payment/initiate-new/route.ts | 109 ++++++---- apps/web/app/api/payment/webhook-new/route.ts | 201 ++++++++++++++---- 2 files changed, 231 insertions(+), 79 deletions(-) diff --git a/apps/web/app/api/payment/initiate-new/route.ts b/apps/web/app/api/payment/initiate-new/route.ts index 1f2b75e4..62f08a9c 100644 --- a/apps/web/app/api/payment/initiate-new/route.ts +++ b/apps/web/app/api/payment/initiate-new/route.ts @@ -12,10 +12,15 @@ import { } from "@courselit/common-models"; import CommunityModel from "@models/Community"; import CourseModel from "@models/Course"; -import MembershipModel from "@models/Membership"; +import MembershipModel, { InternalMembership } from "@models/Membership"; import constants from "@config/constants"; import PaymentPlanModel from "@models/PaymentPlan"; import { getPaymentMethodFromSettings } from "@/payments-new"; +import { generateUniqueId } from "@courselit/utils"; +import Invoice from "@models/Invoice"; +import { error } from "@/services/logger"; +import { finalizePurchase } from "../webhook-new/route"; +import { responses } from "@config/strings"; const { transactionSuccess, transactionFailed, transactionInitiated } = constants; @@ -70,17 +75,11 @@ export async function POST(req: NextRequest) { return Response.json({ message: "Invalid type" }, { status: 400 }); } - const existingActiveMembership = - await MembershipModel.findOne({ - domain: domain._id, - userId: user.userId, - entityType: type, - entityId: id, - status: Constants.MembershipStatus.ACTIVE, - }); - - if (existingActiveMembership) { - return Response.json({ status: transactionSuccess }); + if (!entity) { + return Response.json( + { message: responses.item_not_found }, + { status: 400 }, + ); } const paymentPlan = await PaymentPlanModel.findOne({ @@ -96,47 +95,63 @@ export async function POST(req: NextRequest) { ); } - if (paymentPlan.type === Constants.PaymentPlanType.FREE) { - await MembershipModel.create({ - domain: domain._id, - userId: user.userId, - paymentPlanId: planId, - entityId: id, - entityType: type, - status: Constants.MembershipStatus.ACTIVE, - }); - // TODO: implement finalizePurchase - return Response.json({ status: transactionSuccess }); - } - const siteinfo = domain.settings; const paymentMethod = await getPaymentMethodFromSettings(siteinfo); - const existingPendingMembership = - await MembershipModel.findOne({ + const existingMembership = + await MembershipModel.findOne({ domain: domain._id, userId: user.userId, - entityId: id, entityType: type, - status: Constants.MembershipStatus.PENDING, + entityId: id, }); - - let membership: Membership; - - if (existingPendingMembership) { - membership = existingPendingMembership; - } else { - membership = await MembershipModel.create({ + let membership: InternalMembership = + existingMembership || + (await MembershipModel.create({ domain: domain._id, userId: user.userId, paymentPlanId: planId, entityId: id, entityType: type, status: Constants.MembershipStatus.PENDING, + })); + + if (membership.status === Constants.MembershipStatus.ACTIVE) { + if (paymentPlan.type === Constants.PaymentPlanType.FREE) { + return Response.json({ status: transactionSuccess }); + } + if ( + membership.subscriptionId && + (paymentPlan.type === Constants.PaymentPlanType.EMI || + paymentPlan.type === Constants.PaymentPlanType.SUBSCRIPTION) + ) { + if ( + await paymentMethod.validateSubscription( + membership.subscriptionId, + ) + ) { + return Response.json({ status: transactionSuccess }); + } else { + membership.status = Constants.MembershipStatus.FAILED; + await membership.save(); + } + } + } + + if (paymentPlan.type === Constants.PaymentPlanType.FREE) { + membership.status = Constants.MembershipStatus.ACTIVE; + await (membership as any).save(); + await finalizePurchase({ + domain, + membership, + paymentPlan, }); + return Response.json({ status: transactionSuccess }); } + const invoiceId = generateUniqueId(); metadata["membershipId"] = membership.membershipId; + metadata["invoiceId"] = invoiceId; const paymentTracker = await paymentMethod.initiate({ metadata, @@ -150,11 +165,33 @@ export async function POST(req: NextRequest) { }, }); + await Invoice.create({ + domain: domain._id, + invoiceId, + membershipId: membership.membershipId, + amount: + paymentPlan.oneTimeAmount || + paymentPlan.subscriptionMonthlyAmount || + paymentPlan.subscriptionYearlyAmount || + paymentPlan.emiAmount || + 0, + status: Constants.InvoiceStatus.PENDING, + paymentProcessor: paymentMethod.name, + paymentProcessorTransactionId: paymentTracker, + }); + + await (membership as any).save(); + return Response.json({ status: transactionInitiated, paymentTracker, }); } catch (err: any) { + error(`Error initiating payment: ${err.message}`, { + domain: domainName, + body, + stack: err.stack, + }); return Response.json( { status: transactionFailed, error: err.message }, { status: 500 }, diff --git a/apps/web/app/api/payment/webhook-new/route.ts b/apps/web/app/api/payment/webhook-new/route.ts index c891cb0b..44f239b9 100644 --- a/apps/web/app/api/payment/webhook-new/route.ts +++ b/apps/web/app/api/payment/webhook-new/route.ts @@ -6,13 +6,22 @@ import { Community, Constants, Course, + Invoice, Membership, + MembershipEntityType, PaymentPlan, + Progress, User, } from "@courselit/common-models"; import { triggerSequences } from "@/lib/trigger-sequences"; import { recordActivity } from "@/lib/record-activity"; import constants from "@config/constants"; +import PaymentPlanModel from "@models/PaymentPlan"; +import InvoiceModel from "@models/Invoice"; +import CourseModel, { Course as InternalCourse } from "@models/Course"; +import { getPlanPrice } from "@ui-lib/utils"; +import UserModel from "@models/User"; +import CommunityModel from "@models/Community"; export async function POST(req: NextRequest) { try { @@ -35,8 +44,11 @@ export async function POST(req: NextRequest) { if (!paymentVerified) { return Response.json({ message: "Payment not verified" }); } + console.log(body); - const { purchaseId: membershipId } = paymentMethod.getMetadata(body); + const metadata = paymentMethod.getMetadata(body); + const { purchaseId: membershipId, invoiceId } = metadata; + console.log("metadata", metadata); const membership = await MembershipModel.findOne({ domain: domain._id, @@ -47,77 +59,180 @@ export async function POST(req: NextRequest) { return Response.json({ message: "Membership not found" }); } - membership.status = Constants.MembershipStatus.ACTIVE; - const currentDate = new Date(); - membership.paymentHistory.push({ - installmentNumber: 1, - amount: 100, - status: Constants.MembershipPaymentStatus.PAID, - paymentProcessor: paymentMethod.name, - paymentProcessorTransactionId: - paymentMethod.getPaymentIdentifier(body), - createdAt: currentDate, - updatedAt: currentDate, + const paymentPlan = await PaymentPlanModel.findOne({ + domain: domain._id, + planId: membership.paymentPlanId, }); - await (membership as any).save(); + let subscriptionId; + console.log("paymentPlan", paymentPlan); + if ( + paymentPlan?.type === Constants.PaymentPlanType.SUBSCRIPTION || + paymentPlan?.type === Constants.PaymentPlanType.EMI + ) { + subscriptionId = paymentMethod.getSubscriptionId(body); + console.log("subscriptionId", subscriptionId); + if (!membership.subscriptionId) { + membership.subscriptionId = subscriptionId; + } + console.log( + "subscriptionId", + subscriptionId, + membership.subscriptionId, + ); + } + const invoice = await InvoiceModel.findOne({ invoiceId }); + if (invoice) { + invoice.status = Constants.InvoiceStatus.PAID; + await (invoice as any).save(); + } else { + await InvoiceModel.create({ + domain: domain._id, + invoiceId, + membershipId, + amount: + paymentPlan?.oneTimeAmount || + paymentPlan?.subscriptionYearlyAmount || + paymentPlan?.subscriptionMonthlyAmount || + paymentPlan?.emiAmount || + 0, + status: Constants.InvoiceStatus.PAID, + paymentProcessor: paymentMethod.name, + paymentProcessorTransactionId: + paymentMethod.getPaymentIdentifier(body), + }); + } + + console.log("PaymentPlan", paymentPlan); + if (paymentPlan?.type === Constants.PaymentPlanType.EMI) { + const paidInvoicesCount = await InvoiceModel.countDocuments({ + domain: domain._id, + membershipId, + status: Constants.InvoiceStatus.PAID, + }); + console.log( + "Paid invoices count", + paidInvoicesCount, + paymentPlan.emiTotalInstallments, + ); + if (paidInvoicesCount >= paymentPlan.emiTotalInstallments!) { + await paymentMethod.cancel(subscriptionId); + } + } + + if (membership.status !== Constants.MembershipStatus.ACTIVE) { + membership.status = Constants.MembershipStatus.ACTIVE; + await (membership as any).save(); + + + await finalizePurchase({ + domain, + membership, + paymentPlan!, + }); + + } return Response.json({ message: "success" }); } catch (e) { + console.error(e.message); return Response.json({ message: e.message }, { status: 400 }); } } -export async function GET(req: NextRequest) { - return Response.json({ message: "success" }); -} -async function finalizePurchase({ +export async function finalizePurchase({ + domain, membership, paymentPlan, - user, - entity, - domain, }: { + domain: Domain; membership: Membership; paymentPlan: PaymentPlan; - user: User; - entity: Course | Community; - domain: Domain; }) { + const user = await UserModel.findOne({ + userId: membership.userId, + }); + if (!user) { + return; + } + let event: (typeof Constants.eventTypes)[number] | undefined = undefined; - let data: string | undefined = undefined; + if (paymentPlan.type !== Constants.PaymentPlanType.FREE) { + await recordActivity({ + domain: domain._id, + userId: user.userId, + type: constants.activityTypes[1], + entityId: membership.entityId, + }); + } if (membership.entityType === Constants.MembershipEntityType.COMMUNITY) { - // Add user to community + await recordActivity({ + domain: domain._id, + userId: user.userId, + type: constants.activityTypes[15], + entityId: membership.entityId, + }); + event = Constants.eventTypes[5]; - data = (entity as Community).communityId; } if (membership.entityType === Constants.MembershipEntityType.COURSE) { - // Add user to course + const product = await CourseModel.findOne({ + courseId: membership.entityId, + }); + if (product) { + await addProductToUser({ + user, + product, + cost: getPlanPrice(paymentPlan).amount, + }); + } + await recordActivity({ + domain: domain._id, + userId: user.userId, + type: constants.activityTypes[0], + entityId: membership.entityId, + }); + event = Constants.eventTypes[2]; - data = (entity as Course).courseId; } - if (event && data) { + if (event) { await triggerSequences({ user, event, - data, + data: membership.entityId, }); } +} - await recordActivity({ - domain: domain._id, - userId: user.userId, - type: constants.activityTypes[0], - entityId: data, - }); - - if (paymentPlan.type !== Constants.PaymentPlanType.FREE) { - await recordActivity({ - domain: domain._id, - userId: user.userId, - type: constants.activityTypes[1], - entityId: data, +async function addProductToUser({ + user, + product, + cost, +}: { + user: User; + product: InternalCourse; + cost: number; +}) { + if ( + !user.purchases.some( + (purchase: Progress) => purchase.courseId === product.courseId, + ) + ) { + user.purchases.push({ + courseId: product.courseId, + completedLessons: [], + accessibleGroups: [], }); + await (user as any).save(); + if (!product.customers.some((customer) => customer === user.userId)) { + product.customers.push(user.userId); + product.sales += product.cost; + await (product as any).save(); + } } } + +export async function GET(req: NextRequest) { + return Response.json({ message: "success" }); +} \ No newline at end of file