From e0231e2fbb6f8af428497f9289fc26eecc25729f Mon Sep 17 00:00:00 2001 From: JoshJ7792 Date: Thu, 22 Jan 2026 00:39:40 -0600 Subject: [PATCH 1/5] basic structure of webhook routes created --- backend/app.ts | 5 ++++- backend/routes/stripeRoutes.ts | 21 +++++++++++++++++++++ backend/routes/stripeWebhookRoutes.ts | 9 +++++++++ backend/services/stripeService.ts | 27 ++++++++++++++++++++++++++- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 backend/routes/stripeRoutes.ts create mode 100644 backend/routes/stripeWebhookRoutes.ts diff --git a/backend/app.ts b/backend/app.ts index 31cd018..ba5b4a5 100644 --- a/backend/app.ts +++ b/backend/app.ts @@ -17,6 +17,8 @@ import contactRoutes from './routes/contactRoutes.js'; import fileUploadRoutes from './routes/fileUploadRoutes.js'; import pageContentRoutes from './routes/pageContentRoutes.js'; import mapRoutes from './routes/mapRoutes.js'; +import stripeRoutes from './routes/stripeRoutes.js'; +import stripeWebhookRoutes from './routes/stripeWebhookRoutes.js'; import { clerkMiddleware } from '@clerk/express'; import { connectRedis } from './config/redis.js'; import { warmCache } from './utils/cacheWarmer.js'; @@ -48,6 +50,7 @@ app.use( credentials: true, }) ); +app.use('/api/stripe/webhook', stripeWebhookRoutes); app.use(express.json()); @@ -73,11 +76,11 @@ app.use('/api/contact', contactRoutes); app.use('/api/files', fileUploadRoutes); app.use('/api/page-content', pageContentRoutes); app.use('/api/map', mapRoutes); +app.use('/api/stripe', stripeRoutes); // Add new route imports and register new routes // For Stripe webhook (MUST be before express.json() middleware): // Add this line BEFORE app.use(express.json()): -// app.use('/api/stripe/webhook', express.raw({ type: 'application/json' }), stripeRoutes); export default app; diff --git a/backend/routes/stripeRoutes.ts b/backend/routes/stripeRoutes.ts new file mode 100644 index 0000000..269034e --- /dev/null +++ b/backend/routes/stripeRoutes.ts @@ -0,0 +1,21 @@ +import { Router } from 'express'; +import express from 'express'; +import { authenticateToken } from '../middleware/auth.js'; +import { stripeController } from '../controllers/stripeController.js'; +import { StripeService } from '../services/stripeService.js'; + +const router = Router(); + +// Protected routes +router.get('/subscription', authenticateToken, stripeController.getSubscription); +router.post('/subscription', authenticateToken, stripeController.createSubscription); +router.post('/subscription/cancel', authenticateToken, stripeController.cancelSubscription); + +// Webhook route +router.post('/webhook', express.raw({ type: 'application/json' }), stripeController.handleWebhook); + +// Public routes + +router.post('/customer', StripeService.createCustomer); + +export default router; diff --git a/backend/routes/stripeWebhookRoutes.ts b/backend/routes/stripeWebhookRoutes.ts new file mode 100644 index 0000000..9a0c0ea --- /dev/null +++ b/backend/routes/stripeWebhookRoutes.ts @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import express from 'express'; +import { authenticateToken } from '../middleware/auth.js'; +import { stripeController } from '../controllers/stripeController.js'; +import { StripeService } from '../services/stripeService.js'; + +const router = Router(); +router.post('/', express.raw({ type: 'application/json' }), stripeController.handleWebhook); +export default router; diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 86cf642..dafa402 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -8,7 +8,32 @@ export class StripeService { */ static async createCustomer(organizationId: string) { // Implement customer creation - throw new Error('Not implemented'); + const existingSubscription = await prisma.subscription.findFirst({ + where: { organizationId }, + }); + + if (existingSubscription) { + return existingSubscription.stripeCustomerId; + } + + const organization = await prisma.organization.findUnique({ + where: { id: organizationId }, + }); + + if (!organization) { + throw new Error('Organization not found'); + } + + const customer = await stripe.customers.create({ + email: organization.email, + name: organization.name, + metadata: { + organizationId: organization.id, + clerkId: organization.clerkId, + }, + }); + + return customer.id; } /** From 9c5fc2424389cef8776cf50173e7e0632ff2eb5d Mon Sep 17 00:00:00 2001 From: JoshJ7792 Date: Thu, 22 Jan 2026 00:49:35 -0600 Subject: [PATCH 2/5] removed unncessary imports --- backend/routes/stripeWebhookRoutes.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/routes/stripeWebhookRoutes.ts b/backend/routes/stripeWebhookRoutes.ts index 9a0c0ea..6357c1d 100644 --- a/backend/routes/stripeWebhookRoutes.ts +++ b/backend/routes/stripeWebhookRoutes.ts @@ -1,8 +1,6 @@ import { Router } from 'express'; import express from 'express'; -import { authenticateToken } from '../middleware/auth.js'; import { stripeController } from '../controllers/stripeController.js'; -import { StripeService } from '../services/stripeService.js'; const router = Router(); router.post('/', express.raw({ type: 'application/json' }), stripeController.handleWebhook); From 9ef34c1bbb2141afffaff1f3d7305b108b4c38f9 Mon Sep 17 00:00:00 2001 From: JoshJ7792 Date: Wed, 28 Jan 2026 17:44:45 -0600 Subject: [PATCH 3/5] webhook trigger successful --- backend/controllers/stripeController.ts | 32 +++++--- backend/routes/stripeRoutes.ts | 4 - backend/services/stripeService.ts | 76 ++++++++++++------- frontend/src/pages/AboutPage/About.tsx | 2 +- .../pages/AnnouncementsPage/Announcements.tsx | 12 +-- frontend/src/pages/RegisterPage/Register.tsx | 8 +- 6 files changed, 80 insertions(+), 54 deletions(-) diff --git a/backend/controllers/stripeController.ts b/backend/controllers/stripeController.ts index a9fa0b2..874d1a4 100644 --- a/backend/controllers/stripeController.ts +++ b/backend/controllers/stripeController.ts @@ -1,11 +1,11 @@ import { Request, Response } from 'express'; -// import { StripeService } from '../services/stripeService.js'; -// import { stripe } from '../config/stripe.js'; -// import { STRIPE_WEBHOOK_SECRET } from '../config/stripe.js'; +import Stripe from 'stripe'; +import { StripeService } from '../services/stripeService.js'; +import { stripe, STRIPE_WEBHOOK_SECRET } from '../config/stripe.js'; export const stripeController = { /** - * Create subscription + * Create subscription: done */ createSubscription: async (req: Request, res: Response) => { try { @@ -21,7 +21,7 @@ export const stripeController = { }, /** - * Get subscription status + * Get subscription status: done */ getSubscription: async (req: Request, res: Response) => { try { @@ -53,13 +53,23 @@ export const stripeController = { * Stripe webhook handler */ handleWebhook: async (req: Request, res: Response) => { + let event: Stripe.Event; try { - // Verify webhook signature - // Call StripeService.handleWebhook - res.status(501).json({ error: 'Not implemented' }); - } catch (error: any) { - console.error('Webhook error:', error); - res.status(400).json({ error: error.message }); + const sig = req.headers['stripe-signature'] as string; + event = stripe.webhooks.constructEvent(req.body, sig, STRIPE_WEBHOOK_SECRET); + } catch (error) { + console.error('Error handling webhook:', error); + return res + .sendStatus(400) + .send(`Webhook Error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } + + try { + await StripeService.handleWebhook(event); + res.status(200).json({ received: true }); + } catch (error) { + console.error('Error handling webhook event:', error); + return res.json({ received: true }); } }, diff --git a/backend/routes/stripeRoutes.ts b/backend/routes/stripeRoutes.ts index 269034e..6f4db55 100644 --- a/backend/routes/stripeRoutes.ts +++ b/backend/routes/stripeRoutes.ts @@ -11,11 +11,7 @@ router.get('/subscription', authenticateToken, stripeController.getSubscription) router.post('/subscription', authenticateToken, stripeController.createSubscription); router.post('/subscription/cancel', authenticateToken, stripeController.cancelSubscription); -// Webhook route -router.post('/webhook', express.raw({ type: 'application/json' }), stripeController.handleWebhook); - // Public routes - router.post('/customer', StripeService.createCustomer); export default router; diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index dafa402..cb0ca10 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -1,39 +1,15 @@ import { stripe } from '../config/stripe.js'; import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; +import { SubscriptionStatus } from '@prisma/client'; export class StripeService { /** - * Create a Stripe customer for an organization + * Create a Stripe customer for an organization: done */ static async createCustomer(organizationId: string) { // Implement customer creation - const existingSubscription = await prisma.subscription.findFirst({ - where: { organizationId }, - }); - - if (existingSubscription) { - return existingSubscription.stripeCustomerId; - } - - const organization = await prisma.organization.findUnique({ - where: { id: organizationId }, - }); - - if (!organization) { - throw new Error('Organization not found'); - } - - const customer = await stripe.customers.create({ - email: organization.email, - name: organization.name, - metadata: { - organizationId: organization.id, - clerkId: organization.clerkId, - }, - }); - - return customer.id; + throw new Error('Not implemented'); } /** @@ -65,12 +41,56 @@ export class StripeService { * Handle webhook events */ static async handleWebhook(event: Stripe.Event) { + switch (event.type) { + case 'customer.subscription.updated': + const subscription = event.data.object as Stripe.Subscription; + const validStatuses = ['ACTIVE', 'PAST_DUE', 'CANCELED', 'INCOMPLETE', 'TRIALING']; + const status = subscription.status.toUpperCase(); + await prisma.subscription.update({ + where: { stripeSubscriptionId: subscription.id }, + data: { + status: (validStatuses.includes(status) ? status : 'INCOMPLETE') as SubscriptionStatus, + currentPeriodStart: subscription.current_period_start + ? new Date(subscription.current_period_start * 1000) + : new Date(), + currentPeriodEnd: subscription.current_period_end + ? new Date(subscription.current_period_end * 1000) + : new Date(), + cancelAtPeriodEnd: subscription.cancel_at_period_end, + }, + }); + console.log(`Subscription info: ${subscription}`); + console.log(`Subscription ${subscription.id} updated.`); + // Update subscription in your database + break; + case 'customer.subscription.deleted': + const deletedSubscription = event.data.object as Stripe.Subscription; + console.log(`Subscription ${deletedSubscription.id} deleted.`); + // Handle subscription deletion in your database + break; + case 'invoice.payment_succeeded': + const invoice = event.data.object as Stripe.Invoice; + console.log(`Invoice ${invoice.id} payment succeeded.`); + // Handle successful payment in your database + break; + case 'invoice.payment_failed': + const failedInvoice = event.data.object as Stripe.Invoice; + console.log(`Invoice ${failedInvoice.id} payment failed.`); + // Handle failed payment in your database + break; + case 'payment_intent.succeeded': + const paymentIntent = event.data.object as Stripe.PaymentIntent; + console.log(`PaymentIntent ${paymentIntent.id} succeeded.`); + // Handle successful payment intent in your database + break; + default: + console.log(`Unhandled event type ${event.type}`); + } // Handle different webhook event types: // - customer.subscription.updated // - customer.subscription.deleted // - invoice.payment_succeeded // - invoice.payment_failed // - payment_intent.succeeded - throw new Error('Not implemented'); } } diff --git a/frontend/src/pages/AboutPage/About.tsx b/frontend/src/pages/AboutPage/About.tsx index 22abc23..fc4de27 100644 --- a/frontend/src/pages/AboutPage/About.tsx +++ b/frontend/src/pages/AboutPage/About.tsx @@ -389,7 +389,7 @@ const AboutPage = ({ previewContent }: AboutPageProps = {}) => {
@@ -580,7 +580,7 @@ const RegisterForm = ({ previewContent }: RegisterFormProps = {}) => { From 76d672804eb19442e76e9201cd8ff8b485807df2 Mon Sep 17 00:00:00 2001 From: JoshJ7792 Date: Sat, 7 Feb 2026 14:44:35 -0600 Subject: [PATCH 4/5] most services done --- backend/services/stripeService.ts | 41 ++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index cb0ca10..a44b087 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -59,27 +59,66 @@ export class StripeService { cancelAtPeriodEnd: subscription.cancel_at_period_end, }, }); - console.log(`Subscription info: ${subscription}`); + console.log(`Subscription info: ${JSON.stringify(subscription)}`); console.log(`Subscription ${subscription.id} updated.`); // Update subscription in your database break; case 'customer.subscription.deleted': const deletedSubscription = event.data.object as Stripe.Subscription; + await prisma.subscription.update({ + where: { stripeSubscriptionId: deletedSubscription.id }, + data: { + status: 'CANCELED' as SubscriptionStatus, + cancelAtPeriodEnd: false, + }, + }); console.log(`Subscription ${deletedSubscription.id} deleted.`); // Handle subscription deletion in your database break; + case 'invoice.payment_succeeded': const invoice = event.data.object as Stripe.Invoice; console.log(`Invoice ${invoice.id} payment succeeded.`); + + // await prisma.invoice.create({ + // data: { + // stripeInvoiceId: invoice.id, + // amountPaid: invoice.amount_paid, + // currency: invoice.currency, + // status: invoice.status.toUpperCase(), + // subscription: { + // connect: { stripeSubscriptionId: invoice.subscription as string }, + // }, + // }, + // }); // Handle successful payment in your database break; case 'invoice.payment_failed': const failedInvoice = event.data.object as Stripe.Invoice; + // await prisma.invoice.create({ + // data: { + // stripeInvoiceId: failedInvoice.id, + // amountPaid: failedInvoice.amount_paid, + // currency: failedInvoice.currency, + // status: failedInvoice.status.toUpperCase(), + // subscription: { + // connect: { stripeSubscriptionId: failedInvoice.subscription as string }, + // }, + // }, + // }); console.log(`Invoice ${failedInvoice.id} payment failed.`); // Handle failed payment in your database break; case 'payment_intent.succeeded': const paymentIntent = event.data.object as Stripe.PaymentIntent; + // await prisma.paymentIntent.create({ + // data: { + // stripePaymentIntentId: paymentIntent.id, + // amount: paymentIntent.amount, + // currency: paymentIntent.currency, + // status: paymentIntent.status.toUpperCase(), + // }, + // }); console.log(`PaymentIntent ${paymentIntent.id} succeeded.`); // Handle successful payment intent in your database break; From 7d2680a4104d6040a073c8c90a5ec48ab9b4a74e Mon Sep 17 00:00:00 2001 From: JoshJ7792 Date: Tue, 17 Mar 2026 19:11:07 -0500 Subject: [PATCH 5/5] fix invoice webhook handlers: add guards and required fields --- backend/services/stripeService.ts | 109 +++++++++++++++++++----------- 1 file changed, 68 insertions(+), 41 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index a44b087..12c47be 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -1,4 +1,3 @@ -import { stripe } from '../config/stripe.js'; import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; import { SubscriptionStatus } from '@prisma/client'; @@ -76,60 +75,88 @@ export class StripeService { // Handle subscription deletion in your database break; + case 'customer.subscription.created': + const createdSubscription = event.data.object as Stripe.Subscription; + await prisma.subscription.create({ + data: { + stripeSubscriptionId: createdSubscription.id, + stripeCustomerId: createdSubscription.customer as string, + stripePriceId: createdSubscription.items.data[0].price.id, + status: createdSubscription.status.toUpperCase() as SubscriptionStatus, + currentPeriodStart: createdSubscription.current_period_start + ? new Date(createdSubscription.current_period_start * 1000) + : new Date(), + currentPeriodEnd: createdSubscription.current_period_end + ? new Date(createdSubscription.current_period_end * 1000) + : new Date(), + cancelAtPeriodEnd: createdSubscription.cancel_at_period_end, + organization: { + connect: { id: createdSubscription.metadata.organizationId }, + }, + }, + }); + console.log(`Subscription ${createdSubscription.id} created.`); + // Handle subscription creation in your database + break; + case 'invoice.payment_succeeded': const invoice = event.data.object as Stripe.Invoice; + if (!invoice.subscription) { + console.log(`Invoice ${invoice.id} has no subscription, skipping.`); + break; + } + await prisma.invoice.create({ + data: { + stripeInvoiceId: invoice.id, + amount: invoice.amount_paid, + currency: invoice.currency, + status: (invoice.status ?? 'unknown').toUpperCase(), + invoicePdf: invoice.invoice_pdf ?? null, + hostedInvoiceUrl: invoice.hosted_invoice_url ?? null, + periodStart: invoice.period_start ? new Date(invoice.period_start * 1000) : new Date(), + periodEnd: invoice.period_end ? new Date(invoice.period_end * 1000) : new Date(), + subscription: { + connect: { stripeSubscriptionId: invoice.subscription as string }, + }, + }, + }); console.log(`Invoice ${invoice.id} payment succeeded.`); - - // await prisma.invoice.create({ - // data: { - // stripeInvoiceId: invoice.id, - // amountPaid: invoice.amount_paid, - // currency: invoice.currency, - // status: invoice.status.toUpperCase(), - // subscription: { - // connect: { stripeSubscriptionId: invoice.subscription as string }, - // }, - // }, - // }); - // Handle successful payment in your database break; case 'invoice.payment_failed': const failedInvoice = event.data.object as Stripe.Invoice; - // await prisma.invoice.create({ - // data: { - // stripeInvoiceId: failedInvoice.id, - // amountPaid: failedInvoice.amount_paid, - // currency: failedInvoice.currency, - // status: failedInvoice.status.toUpperCase(), - // subscription: { - // connect: { stripeSubscriptionId: failedInvoice.subscription as string }, - // }, - // }, - // }); + if (!failedInvoice.subscription) { + console.log(`Invoice ${failedInvoice.id} has no subscription, skipping.`); + break; + } + await prisma.invoice.create({ + data: { + stripeInvoiceId: failedInvoice.id, + amount: failedInvoice.amount_due, + currency: failedInvoice.currency, + status: (failedInvoice.status ?? 'unknown').toUpperCase(), + invoicePdf: failedInvoice.invoice_pdf ?? null, + hostedInvoiceUrl: failedInvoice.hosted_invoice_url ?? null, + periodStart: failedInvoice.period_start + ? new Date(failedInvoice.period_start * 1000) + : new Date(), + periodEnd: failedInvoice.period_end + ? new Date(failedInvoice.period_end * 1000) + : new Date(), + subscription: { + connect: { stripeSubscriptionId: failedInvoice.subscription as string }, + }, + }, + }); console.log(`Invoice ${failedInvoice.id} payment failed.`); - // Handle failed payment in your database break; case 'payment_intent.succeeded': + // TODO: requires subscriptionId — coordinate with whoever implements createSubscription + // prisma model is Payment (not PaymentIntent), needs subscription relation const paymentIntent = event.data.object as Stripe.PaymentIntent; - // await prisma.paymentIntent.create({ - // data: { - // stripePaymentIntentId: paymentIntent.id, - // amount: paymentIntent.amount, - // currency: paymentIntent.currency, - // status: paymentIntent.status.toUpperCase(), - // }, - // }); console.log(`PaymentIntent ${paymentIntent.id} succeeded.`); - // Handle successful payment intent in your database break; default: console.log(`Unhandled event type ${event.type}`); } - // Handle different webhook event types: - // - customer.subscription.updated - // - customer.subscription.deleted - // - invoice.payment_succeeded - // - invoice.payment_failed - // - payment_intent.succeeded } }