Skip to content

Commit d7c85f9

Browse files
committed
feat: Add sponsor program with dedicated pages, API, and payment integration.
1 parent 333d2b8 commit d7c85f9

File tree

17 files changed

+1002
-183
lines changed

17 files changed

+1002
-183
lines changed

apps/api/prisma/schema.prisma

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,15 @@ model Plan {
102102
updatedAt DateTime @updatedAt
103103
subscriptions Subscription[]
104104
}
105+
106+
model Sponsor {
107+
id String @id @default(cuid())
108+
company_name String
109+
description String
110+
website String
111+
image_url String
112+
razorpay_payment_id String?
113+
razorpay_sub_id String?
114+
plan_status String // active, cancelled, pending_payment, pending_submission, failed
115+
created_at DateTime @default(now())
116+
}

apps/api/src/index.ts

Lines changed: 3 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -153,104 +153,10 @@ app.get("/join-community", apiLimiter, async (req: Request, res: Response) => {
153153
}
154154
});
155155

156-
// Razorpay Webhook Handler (Backup Flow)
157-
app.post("/webhook/razorpay", async (req: Request, res: Response) => {
158-
try {
159-
const webhookSecret = process.env.RAZORPAY_WEBHOOK_SECRET;
160-
if (!webhookSecret) {
161-
console.error("RAZORPAY_WEBHOOK_SECRET not configured");
162-
return res.status(500).json({ error: "Webhook not configured" });
163-
}
164-
165-
// Get signature from headers
166-
const signature = req.headers["x-razorpay-signature"] as string;
167-
if (!signature) {
168-
return res.status(400).json({ error: "Missing signature" });
169-
}
170-
171-
// Verify webhook signature
172-
const body = req.body.toString();
173-
const expectedSignature = crypto
174-
.createHmac("sha256", webhookSecret)
175-
.update(body)
176-
.digest("hex");
177-
178-
const isValidSignature = crypto.timingSafeEqual(
179-
Buffer.from(signature),
180-
Buffer.from(expectedSignature)
181-
);
182-
183-
if (!isValidSignature) {
184-
console.error("Invalid webhook signature");
185-
return res.status(400).json({ error: "Invalid signature" });
186-
}
187-
188-
// Parse the event
189-
const event = JSON.parse(body);
190-
const eventType = event.event;
191-
192-
// Handle payment.captured event
193-
if (eventType === "payment.captured") {
194-
const payment = event.payload.payment.entity;
195-
196-
// Extract payment details
197-
const razorpayPaymentId = payment.id;
198-
const razorpayOrderId = payment.order_id;
199-
const amount = payment.amount;
200-
const currency = payment.currency;
156+
import { handleRazorpayWebhook } from "./webhooks.js";
201157

202-
// Get user ID from order notes (should be stored when creating order)
203-
const notes = payment.notes || {};
204-
const userId = notes.user_id;
205-
206-
if (!userId) {
207-
console.error("User ID not found in payment notes");
208-
return res.status(400).json({ error: "User ID not found" });
209-
}
210-
211-
// Get plan ID from notes
212-
const planId = notes.plan_id;
213-
if (!planId) {
214-
console.error("Plan ID not found in payment notes");
215-
return res.status(400).json({ error: "Plan ID not found" });
216-
}
217-
218-
try {
219-
// Create payment record (with idempotency check)
220-
const paymentRecord = await paymentService.createPaymentRecord(userId, {
221-
razorpayPaymentId,
222-
razorpayOrderId,
223-
amount,
224-
currency,
225-
});
226-
227-
// Create subscription (with idempotency check)
228-
await paymentService.createSubscription(
229-
userId,
230-
planId,
231-
paymentRecord.id
232-
);
233-
234-
console.log(
235-
`✅ Webhook: Payment ${razorpayPaymentId} processed successfully`
236-
);
237-
return res.status(200).json({ status: "ok" });
238-
} catch (error: any) {
239-
console.error("Webhook payment processing error:", error);
240-
// Return 200 to prevent Razorpay retries for application errors
241-
return res
242-
.status(200)
243-
.json({ status: "ok", note: "Already processed" });
244-
}
245-
}
246-
247-
// Acknowledge other events
248-
return res.status(200).json({ status: "ok" });
249-
} catch (error: any) {
250-
console.error("Webhook error:", error);
251-
return res.status(500).json({ error: "Internal server error" });
252-
}
253-
});
158+
// Razorpay Webhook Handler
159+
app.post("/webhook/razorpay", handleRazorpayWebhook);
254160

255161
// Connect to database
256162
prismaModule.connectDB();

apps/api/src/routers/_app.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ const testRouter = router({
1414
}),
1515
});
1616

17+
import { sponsorRouter } from "./sponsor.js";
18+
1719
export const appRouter = router({
1820
hello: testRouter,
1921
query: queryRouter,
2022
user: userRouter,
2123
project: projectRouter,
2224
auth: authRouter,
2325
payment: paymentRouter,
26+
sponsor: sponsorRouter,
2427
});
2528

2629
export type AppRouter = typeof appRouter;

apps/api/src/routers/sponsor.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { router, publicProcedure, protectedProcedure } from "../trpc.js";
2+
import { z } from "zod";
3+
import prismaModule from "../prisma.js";
4+
import { paymentService } from "../services/payment.service.js";
5+
6+
const { prisma } = prismaModule;
7+
8+
export const sponsorRouter = router({
9+
// Create a subscription for sponsorship
10+
createSubscription: protectedProcedure
11+
.input(
12+
z.object({
13+
planId: z.string(),
14+
})
15+
)
16+
.mutation(async ({ ctx, input }: { ctx: any, input: any }) => {
17+
const user = ctx.user;
18+
19+
// Create Razorpay order
20+
// Note: In a real scenario, we might want to fetch the plan price from DB
21+
// For now, we'll assume a fixed price or fetch from plan
22+
const plan = await prisma.plan.findUnique({
23+
where: { id: input.planId },
24+
});
25+
26+
if (!plan) {
27+
throw new Error("Plan not found");
28+
}
29+
30+
const order = await paymentService.createOrder({
31+
amount: plan.price,
32+
currency: plan.currency,
33+
receipt: `sponsor_${user.id}_${Date.now()}`,
34+
notes: {
35+
user_id: user.id,
36+
plan_id: input.planId,
37+
type: "sponsor",
38+
},
39+
});
40+
41+
if ("error" in order) {
42+
throw new Error(order.error.description);
43+
}
44+
45+
return {
46+
orderId: order.id,
47+
amount: order.amount,
48+
currency: order.currency,
49+
key: process.env.RAZORPAY_KEY_ID,
50+
};
51+
}),
52+
53+
// Submit sponsor assets
54+
// Submit sponsor assets
55+
submitAssets: protectedProcedure
56+
.input(
57+
z.object({
58+
companyName: z.string(),
59+
description: z.string(),
60+
website: z.string().url(),
61+
imageUrl: z.string().url(),
62+
razorpayPaymentId: z.string(),
63+
})
64+
)
65+
.mutation(async ({ ctx, input }: { ctx: any, input: any }) => {
66+
// Verify payment exists and is successful
67+
const payment = await prisma.payment.findUnique({
68+
where: { razorpayPaymentId: input.razorpayPaymentId },
69+
});
70+
71+
if (!payment || payment.status !== "captured") {
72+
throw new Error("Valid payment not found");
73+
}
74+
75+
// Check if this payment belongs to the user
76+
if (payment.userId !== ctx.user.id) {
77+
throw new Error("Unauthorized");
78+
}
79+
80+
// Upsert sponsor record
81+
const existingSponsor = await prisma.sponsor.findFirst({
82+
where: { razorpay_payment_id: input.razorpayPaymentId },
83+
});
84+
85+
if (existingSponsor) {
86+
return await prisma.sponsor.update({
87+
where: { id: existingSponsor.id },
88+
data: {
89+
company_name: input.companyName,
90+
description: input.description,
91+
website: input.website,
92+
image_url: input.imageUrl,
93+
plan_status: "active",
94+
},
95+
});
96+
} else {
97+
return await prisma.sponsor.create({
98+
data: {
99+
company_name: input.companyName,
100+
description: input.description,
101+
website: input.website,
102+
image_url: input.imageUrl,
103+
razorpay_payment_id: input.razorpayPaymentId,
104+
plan_status: "active",
105+
},
106+
});
107+
}
108+
}),
109+
110+
// Get pending sponsorships for the current user
111+
getPendingSponsorship: protectedProcedure.query(async ({ ctx }: { ctx: any }) => {
112+
const userPayments = await prisma.payment.findMany({
113+
where: {
114+
userId: ctx.user.id,
115+
status: "captured",
116+
},
117+
orderBy: { createdAt: "desc" },
118+
take: 5,
119+
});
120+
121+
for (const payment of userPayments) {
122+
const sponsor = await prisma.sponsor.findFirst({
123+
where: { razorpay_payment_id: payment.razorpayPaymentId },
124+
});
125+
126+
if (!sponsor || sponsor.plan_status === "pending_submission") {
127+
// Check if this payment is likely for sponsorship (e.g. has subscriptionId)
128+
if (payment.subscriptionId) {
129+
return {
130+
paymentId: payment.razorpayPaymentId,
131+
amount: payment.amount,
132+
date: payment.createdAt,
133+
};
134+
}
135+
}
136+
}
137+
return null;
138+
}),
139+
140+
// Get active sponsors
141+
getActiveSponsors: publicProcedure.query(async () => {
142+
return await prisma.sponsor.findMany({
143+
where: {
144+
plan_status: "active",
145+
},
146+
orderBy: {
147+
created_at: "desc",
148+
},
149+
});
150+
}),
151+
});

0 commit comments

Comments
 (0)