Skip to content

Commit 72c49db

Browse files
author
Rajat Saxena
committed
Razorpay ported to the new payment system
1 parent d658a9c commit 72c49db

File tree

20 files changed

+333
-38
lines changed

20 files changed

+333
-38
lines changed
286 KB
Loading
412 KB
Loading
513 KB
Loading
368 KB
Loading

apps/docs/src/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export const SIDEBAR: Sidebar = {
102102
{ text: "Introduction", link: "en/schools/introduction" },
103103
{ text: "Create a school", link: "en/schools/create" },
104104
{ text: "Use custom domain", link: "en/schools/add-custom-domain" },
105+
{ text: "Set up payments", link: "en/schools/set-up-payments" },
105106
{ text: "Delete a school", link: "en/schools/delete" },
106107
],
107108
Users: [
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
---
2+
title: Set up payments
3+
description: Learn how to get paid via CourseLit
4+
layout: ../../../layouts/MainLayout.astro
5+
---
6+
7+
CourseLit offers the following payment methods:
8+
9+
- [Stripe](https://stripe.com)
10+
- [Razorpay](https://razorpay.com)
11+
12+
Your school can only have a single payment method activated at a time.
13+
14+
### Set up Razorpay
15+
16+
1. Sign up for an account on Razorpay and get your business approved.
17+
2. In the Razorpay dashboard, go to the `Account & Settings` tab and select `API keys` as shown below:
18+
![Razorpay dashboard](/assets/schools/razorpay-dashboard-api-key.png)
19+
3. Generate a new API key and keep this screen open.
20+
4. In your CourseLit school, go to the `Settings > Payment` tab and select `Razorpay` in the `Payment Method` dropdown.
21+
5. Enter your Razorpay key and its secret in the `Razorpay Key` and `Razorpay Secret Key` input boxes as shown below:
22+
![Payment setup for Razorpay](/assets/schools/payment-setup-razorpay.png)
23+
6. Set up the webhooks. Using webhooks, your school receives timely updates about payments from Razorpay.
24+
7. In the Razorpay dashboard, go to the `Accounts & Settings` tab and select `Webhooks`.
25+
8. Create a new webhook using the button as shown below:
26+
![Razorpay new webhook](/assets/schools/razorpay-add-webhook.png)
27+
9. In the webhook dialog, enter the following:
28+
- The webhook URL for your school (listed in the same payment screen in your school).
29+
- Check the following events:
30+
- `order.paid`: For confirming one-time payments
31+
- `subscription.charged`: For confirming subscription payments
32+
![Razorpay webhook configuration](/assets/schools/razorpay-webhook-config.png)
33+
10. That's it! Your Razorpay configuration is complete, and you are ready to receive payments.

apps/web/app/(with-contexts)/(with-layout)/checkout/page.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ export default function CheckoutPage() {
9292
subscriptionMonthlyAmount
9393
subscriptionYearlyAmount
9494
}
95+
featuredImage {
96+
thumbnail
97+
file
98+
}
9599
autoAcceptMembers
96100
joiningReasonText
97101
}
@@ -109,6 +113,7 @@ export default function CheckoutPage() {
109113
id: response.community.communityId,
110114
name: response.community.name,
111115
type: MembershipEntityType.COMMUNITY,
116+
featuredImage: response.community.featuredImage.file,
112117
joiningReasonText: response.community.joiningReasonText,
113118
autoAcceptMembers: response.community.autoAcceptMembers,
114119
});

apps/web/app/api/payment/initiate-new/route.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import constants from "@config/constants";
1616
import PaymentPlanModel from "@models/PaymentPlan";
1717
import { getPaymentMethodFromSettings } from "@/payments-new";
1818
import { generateUniqueId } from "@courselit/utils";
19-
import Invoice from "@models/Invoice";
19+
import InvoiceModel from "@models/Invoice";
2020
import { error } from "@/services/logger";
2121
import { activateMembership } from "../webhook-new/route";
2222
import { responses } from "@config/strings";
@@ -148,6 +148,7 @@ export async function POST(req: NextRequest) {
148148
const metadata = {
149149
membershipId: membership.membershipId,
150150
invoiceId,
151+
currencyISOCode: siteinfo.currencyISOCode,
151152
};
152153

153154
const paymentTracker = await paymentMethod.initiate({
@@ -164,7 +165,7 @@ export async function POST(req: NextRequest) {
164165
origin,
165166
});
166167

167-
await Invoice.create({
168+
await InvoiceModel.create({
168169
domain: domain._id,
169170
invoiceId,
170171
membershipId: membership.membershipId,
@@ -176,14 +177,18 @@ export async function POST(req: NextRequest) {
176177
0,
177178
status: Constants.InvoiceStatus.PENDING,
178179
paymentProcessor: paymentMethod.name,
179-
paymentProcessorTransactionId: paymentTracker,
180+
paymentProcessorEntityId: paymentTracker,
181+
currencyISOCode: siteinfo.currencyISOCode,
180182
});
181183

184+
membership.subscriptionId = undefined;
185+
membership.subscriptionMethod = undefined;
182186
await (membership as any).save();
183187

184188
return Response.json({
185189
status: transactionInitiated,
186190
paymentTracker,
191+
metadata,
187192
});
188193
} catch (err: any) {
189194
error(`Error initiating payment: ${err.message}`, {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { error, info } from "@/services/logger";
2+
import { NextRequest } from "next/server";
3+
import DomainModel, { Domain } from "@models/Domain";
4+
import { validatePaymentVerification } from "razorpay/dist/utils/razorpay-utils";
5+
import { responses } from "@config/strings";
6+
import Invoice from "@models/Invoice";
7+
import { UIConstants } from "@courselit/common-models";
8+
9+
export async function POST(req: NextRequest) {
10+
const body = await req.json();
11+
const domainName = req.headers.get("domain");
12+
const { signature, paymentId, subscriptionId, orderId } = body;
13+
if (!signature || !paymentId) {
14+
return Response.json({ message: "Bad request" }, { status: 400 });
15+
}
16+
info(
17+
`POST /api/payment/vendor/razorpay/verify: domain detected: ${domainName}`,
18+
);
19+
20+
const domain = await DomainModel.findOne<Domain>({
21+
name: domainName,
22+
});
23+
if (!domain) {
24+
return Response.json({ message: "Domain not found" }, { status: 404 });
25+
}
26+
27+
if (!domain.settings.razorpayKey || !domain.settings.razorpaySecret) {
28+
return Response.json(
29+
{ message: "Invalid Razorpay settings" },
30+
{ status: 500 },
31+
);
32+
}
33+
34+
let isVerified = false;
35+
if (subscriptionId) {
36+
isVerified = validatePaymentVerification(
37+
{ subscription_id: subscriptionId, payment_id: paymentId },
38+
signature,
39+
domain.settings.razorpaySecret,
40+
);
41+
} else {
42+
isVerified = validatePaymentVerification(
43+
{ order_id: orderId, payment_id: paymentId },
44+
signature,
45+
domain.settings.razorpaySecret,
46+
);
47+
}
48+
49+
if (isVerified) {
50+
const invoice = await Invoice.findOne({
51+
domain: domain._id,
52+
paymentProcessor: UIConstants.PAYMENT_METHOD_RAZORPAY,
53+
paymentProcessorEntityId: subscriptionId || orderId,
54+
});
55+
return Response.json({
56+
message: responses.success,
57+
purchaseId: invoice?.invoiceId,
58+
});
59+
}
60+
61+
error(`Could not verify signature`, {
62+
domain: domain._id,
63+
domainName: domain.name,
64+
orderId,
65+
paymentId,
66+
signature,
67+
});
68+
69+
return Response.json({ error: "Verification failed" }, { status: 400 });
70+
}

apps/web/app/api/payment/webhook-new/route.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function POST(req: NextRequest) {
4343
}
4444

4545
const metadata = paymentMethod.getMetadata(body);
46-
const { membershipId, invoiceId } = metadata;
46+
const { membershipId, invoiceId, currencyISOCode } = metadata;
4747

4848
const membership = await getMembership(domain._id, membershipId);
4949
if (!membership) {
@@ -62,11 +62,12 @@ export async function POST(req: NextRequest) {
6262
);
6363

6464
await handleInvoice(
65-
domain._id,
65+
domain,
6666
invoiceId,
6767
membershipId,
6868
paymentPlan,
6969
paymentMethod,
70+
currencyISOCode,
7071
body,
7172
);
7273

@@ -141,21 +142,27 @@ async function handleSubscription(
141142
}
142143

143144
async function handleInvoice(
144-
domainId: mongoose.Types.ObjectId,
145+
domain: Domain,
145146
invoiceId: string,
146147
membershipId: string,
147148
paymentPlan: PaymentPlan | null,
148149
paymentMethod: any,
150+
currencyISOCode: string,
149151
body: any,
150152
) {
151-
const invoice = await InvoiceModel.findOne<Invoice>({ invoiceId });
153+
const invoice = await InvoiceModel.findOne<Invoice>({
154+
domain: domain._id,
155+
invoiceId,
156+
status: Constants.InvoiceStatus.PENDING,
157+
});
152158
if (invoice) {
159+
invoice.paymentProcessorTransactionId =
160+
paymentMethod.getPaymentIdentifier(body);
153161
invoice.status = Constants.InvoiceStatus.PAID;
154162
await (invoice as any).save();
155163
} else {
156164
await InvoiceModel.create({
157-
domain: domainId,
158-
invoiceId,
165+
domain: domain._id,
159166
membershipId,
160167
amount:
161168
paymentPlan?.oneTimeAmount ||
@@ -167,6 +174,7 @@ async function handleInvoice(
167174
paymentProcessor: paymentMethod.name,
168175
paymentProcessorTransactionId:
169176
paymentMethod.getPaymentIdentifier(body),
177+
currencyISOCode,
170178
});
171179
}
172180
}

0 commit comments

Comments
 (0)