Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +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 eventRoutes from './routes/eventRoutes.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';
Expand Down Expand Up @@ -49,6 +50,7 @@ app.use(
credentials: true,
})
);
app.use('/api/stripe/webhook', stripeWebhookRoutes);

app.use(express.json());

Expand All @@ -74,12 +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/events', eventRoutes);
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;
34 changes: 22 additions & 12 deletions backend/controllers/stripeController.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import e, { Response } from 'express';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { AuthenticatedRequest } from '../types/index.js';
import { StripeService } from '../services/stripeService.js';
// import { stripe } from '../config/stripe.js';
// import { STRIPE_WEBHOOK_SECRET } from '../config/stripe.js';
import { stripe, STRIPE_WEBHOOK_SECRET } from '../config/stripe.js';

export const stripeController = {
/**
* Create subscription
* Create subscription: done
*/
createSubscription: async (req: AuthenticatedRequest, res: Response) => {
try {
Expand All @@ -31,7 +31,7 @@ export const stripeController = {
},

/**
* Get subscription status
* Get subscription status: done
*/
getSubscription: async (req: AuthenticatedRequest, res: Response) => {
try {
Expand Down Expand Up @@ -70,14 +70,24 @@ export const stripeController = {
/**
* Stripe webhook handler
*/
handleWebhook: async (req: AuthenticatedRequest, res: Response) => {
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 });
}
},

Expand Down
17 changes: 17 additions & 0 deletions backend/routes/stripeRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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);

// Public routes
router.post('/customer', StripeService.createCustomer);

export default router;
7 changes: 7 additions & 0 deletions backend/routes/stripeWebhookRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Router } from 'express';
import express from 'express';
import { stripeController } from '../controllers/stripeController.js';

const router = Router();
router.post('/', express.raw({ type: 'application/json' }), stripeController.handleWebhook);
export default router;
120 changes: 117 additions & 3 deletions backend/services/stripeService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { stripe } from '../config/stripe.js';
import { prisma } from '../config/prisma.js';
import Stripe from 'stripe';
import { SubscriptionStatus, PaymentStatus } from '@prisma/client';
Expand Down Expand Up @@ -40,7 +39,7 @@ export class StripeService {
}
}
/**
* Create a Stripe customer for an organization
* Create a Stripe customer for an organization: done
*/
static async createCustomer(organizationId: string) {
try {
Expand Down Expand Up @@ -352,5 +351,120 @@ export class StripeService {
/**
* Handle webhook events
*/
static async handleWebhook(event: Stripe.Event) {}
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: ${JSON.stringify(subscription)}`);
console.log(`Subscription ${subscription.id} updated.`);
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.`);
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.`);
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.`);
break;
case 'invoice.payment_failed':
const failedInvoice = event.data.object as Stripe.Invoice;
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.`);
break;
case 'payment_intent.succeeded':
const paymentIntent = event.data.object as Stripe.PaymentIntent;
await StripeService.updateSubscriptionAfterPaymentConfirmed(paymentIntent.id);
console.log(`PaymentIntent ${paymentIntent.id} succeeded.`);
break;
default:
console.log(`Unhandled event type ${event.type}`);
}
}
}
2 changes: 1 addition & 1 deletion frontend/src/pages/AboutPage/About.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ const AboutPage = ({ previewContent }: AboutPageProps = {}) => {
<div className='flex justify-center'>
<button
onClick={() => setShowAllPartners(!showAllPartners)}
className='text-slate-700 px-6 py-2 rounded-full text-sm font-medium border border-slate-300 hover:bg-slate-50 transition flex items-center gap-2'
className='text-slate-700 px-6 py-2 rounded-full text-sm font-medium border border-slate-300 hover:bg-slate-50 transition flex items-center gap-2 cursor-pointer'
>
{showAllPartners ? 'View less' : 'View more'}
<svg
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/pages/AnnouncementsPage/Announcements.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
<div className='flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4'>
<div className='flex gap-2 items-center overflow-x-auto w-full sm:w-auto'>
<button
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base ${
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base cursor-pointer ${
timeFilter === null
? 'bg-[#EBF3FF] text-[#194B90]'
: 'bg-white text-[#3C3C3C] hover:bg-gray-200'
Expand All @@ -186,7 +186,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
All
</button>
<button
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base ${
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base cursor-pointer ${
timeFilter === '24h'
? 'bg-[#EBF3FF] text-[#194B90]'
: 'bg-white text-[#3C3C3C] hover:bg-gray-200'
Expand All @@ -196,7 +196,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
Last Day
</button>
<button
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base ${
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base cursor-pointer ${
timeFilter === 'week'
? 'bg-[#EBF3FF] text-[#194B90]'
: 'bg-white text-[#3C3C3C] hover:bg-gray-200'
Expand All @@ -206,7 +206,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
Last Week
</button>
<button
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base ${
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base cursor-pointer ${
timeFilter === 'month'
? 'bg-[#EBF3FF] text-[#194B90]'
: 'bg-white text-[#3C3C3C] hover:bg-gray-200'
Expand All @@ -216,7 +216,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
Last Month
</button>
<button
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base ${
className={`px-4 sm:px-6 lg:px-8 py-2 sm:py-3 rounded-lg shadow-sm border border-gray-200 transition-colors whitespace-nowrap text-sm sm:text-base cursor-pointer ${
timeFilter === 'year'
? 'bg-[#EBF3FF] text-[#194B90]'
: 'bg-white text-[#3C3C3C] hover:bg-gray-200'
Expand All @@ -228,7 +228,7 @@ const AnnouncementsPage = ({ previewContent }: AnnouncementsPageProps = {}) => {
<div className='relative tag-dropdown-container' ref={filterRef}>
<button
onClick={() => setIsFilterOpen(!isFilterOpen)}
className={`px-6 py-3 ${isFilterOpen ? 'bg-[#b53a3a]' : 'bg-[#D54242]'} text-white rounded-lg shadow-sm hover:bg-[#b53a3a] transition-colors flex items-center gap-2`}
className={`px-6 py-3 ${isFilterOpen ? 'bg-[#b53a3a]' : 'bg-[#D54242]'} text-white rounded-lg shadow-sm hover:bg-[#b53a3a] transition-colors flex items-center gap-2 cursor-pointer`}
>
Filter
<IoFunnelOutline className='w-5 h-5' />
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/pages/RegisterPage/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,19 +178,19 @@ const RegisterForm = ({ previewContent }: RegisterFormProps = {}) => {
<div className='flex flex-col sm:flex-row gap-4 mt-8'>
<button
onClick={scrollToMembershipSection}
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition'
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition cursor-pointer'
>
{content['hero_join_button']?.value || 'Join the Coalition'}
</button>
<button
onClick={handleEmailSignup}
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition'
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition cursor-pointer'
>
{content['hero_subscribe_button']?.value || 'Subscribe to Emails'}
</button>
<button
onClick={handleContactInquiry}
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition'
className='bg-[#D54242] text-white px-6 py-3 rounded-[18px] text-sm font-semibold shadow-lg hover:bg-[#b53a3a] transition cursor-pointer'
>
{content['hero_contact_button']?.value || 'Contact Us'}
</button>
Expand Down Expand Up @@ -580,7 +580,7 @@ const RegisterForm = ({ previewContent }: RegisterFormProps = {}) => {
<button
type='submit'
disabled={loading}
className='w-[110px] h-[50px] rounded-[15px] bg-[#D54242] text-white hover:bg-[#b53a3a] transition disabled:bg-gray-400 disabled:cursor-not-allowed'
className='w-[110px] h-[50px] rounded-[15px] bg-[#D54242] text-white hover:bg-[#b53a3a] transition disabled:bg-gray-400 disabled:cursor-not-allowed cursor-pointer'
>
{loading ? 'Submitting...' : 'Submit'}
</button>
Expand Down
Loading