From cd2e5ce6eac3c258d297dde5553b9aec8a8a9525 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 24 Jan 2026 12:17:23 -0600 Subject: [PATCH 01/12] Added controllers --- backend/controllers/stripeController.ts | 26 +++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/backend/controllers/stripeController.ts b/backend/controllers/stripeController.ts index a9fa0b2..c64b7ce 100644 --- a/backend/controllers/stripeController.ts +++ b/backend/controllers/stripeController.ts @@ -1,5 +1,6 @@ -import { Request, Response } from 'express'; -// import { StripeService } from '../services/stripeService.js'; +import e, { Response } from 'express'; +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'; @@ -7,13 +8,22 @@ export const stripeController = { /** * Create subscription */ - createSubscription: async (req: Request, res: Response) => { + createSubscription: async (req: AuthenticatedRequest, res: Response) => { try { + // Get priceId from request body // Get organizationId from req.user // Call StripeService.createSubscription // Return subscription details with clientSecret - res.status(501).json({ error: 'Not implemented' }); + if (!req.user?.id) { + return res.status(401).json({ error: 'User is not authenticated or lacks an organization' }); + } + const organizationId = req.user.id; + const {priceId} = req.body; + + const subscription = StripeService.createSubscription(organizationId, priceId); + res.status(201).json(subscription) + } catch (error: any) { console.error('Error creating subscription:', error); res.status(500).json({ error: error.message }); @@ -23,7 +33,7 @@ export const stripeController = { /** * Get subscription status */ - getSubscription: async (req: Request, res: Response) => { + getSubscription: async (req: AuthenticatedRequest, res: Response) => { try { // Get organizationId from req.user // Call StripeService.getSubscription @@ -37,7 +47,7 @@ export const stripeController = { /** * Cancel subscription */ - cancelSubscription: async (req: Request, res: Response) => { + cancelSubscription: async (req: AuthenticatedRequest, res: Response) => { try { // Get immediate flag from request body // Get organizationId from req.user @@ -52,7 +62,7 @@ export const stripeController = { /** * Stripe webhook handler */ - handleWebhook: async (req: Request, res: Response) => { + handleWebhook: async (req: AuthenticatedRequest, res: Response) => { try { // Verify webhook signature // Call StripeService.handleWebhook @@ -66,7 +76,7 @@ export const stripeController = { /** * Get available pricing plans */ - getPrices: async (req: Request, res: Response) => { + getPrices: async (req: AuthenticatedRequest, res: Response) => { try { // Fetch active prices from Stripe res.status(501).json({ error: 'Not implemented' }); From 8f76f5f81b4b1bb3f7e124bf85f43e3800dc37cb Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 24 Jan 2026 12:19:55 -0600 Subject: [PATCH 02/12] Additional Controller --- backend/controllers/stripeController.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/controllers/stripeController.ts b/backend/controllers/stripeController.ts index c64b7ce..c9fc627 100644 --- a/backend/controllers/stripeController.ts +++ b/backend/controllers/stripeController.ts @@ -37,7 +37,13 @@ export const stripeController = { try { // Get organizationId from req.user // Call StripeService.getSubscription - res.status(501).json({ error: 'Not implemented' }); + if (!req.user?.id) { + return res.status(401).json({ error: 'User is not authenticated or lacks an organization' }); + } + const organizationId = req.user?.id; + + const subscription = StripeService.getSubscription(organizationId); + res.status(201).json(subscription); } catch (error: any) { console.error('Error getting subscription:', error); res.status(500).json({ error: error.message }); From 41f11c329914eff6a74c35809507ff0f6d244ee6 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 24 Jan 2026 22:20:27 -0600 Subject: [PATCH 03/12] Service --- backend/controllers/stripeController.ts | 14 +++++++------ backend/services/stripeService.ts | 27 +++++++++++++++++++++++-- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/backend/controllers/stripeController.ts b/backend/controllers/stripeController.ts index c9fc627..b78177f 100644 --- a/backend/controllers/stripeController.ts +++ b/backend/controllers/stripeController.ts @@ -10,20 +10,20 @@ export const stripeController = { */ createSubscription: async (req: AuthenticatedRequest, res: Response) => { try { - // Get priceId from request body // Get organizationId from req.user // Call StripeService.createSubscription // Return subscription details with clientSecret if (!req.user?.id) { - return res.status(401).json({ error: 'User is not authenticated or lacks an organization' }); + return res + .status(401) + .json({ error: 'User is not authenticated or lacks an organization' }); } const organizationId = req.user.id; - const {priceId} = req.body; + const { priceId } = req.body; const subscription = StripeService.createSubscription(organizationId, priceId); - res.status(201).json(subscription) - + res.status(201).json(subscription); } catch (error: any) { console.error('Error creating subscription:', error); res.status(500).json({ error: error.message }); @@ -38,7 +38,9 @@ export const stripeController = { // Get organizationId from req.user // Call StripeService.getSubscription if (!req.user?.id) { - return res.status(401).json({ error: 'User is not authenticated or lacks an organization' }); + return res + .status(401) + .json({ error: 'User is not authenticated or lacks an organization' }); } const organizationId = req.user?.id; diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 86cf642..46b14db 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -1,18 +1,41 @@ import { stripe } from '../config/stripe.js'; import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; +import { env } from 'process'; export class StripeService { + baseUrl = 'https://api.stripe.com/v1/'; + /** * Create a Stripe customer for an organization */ static async createCustomer(organizationId: string) { - // Implement customer creation - throw new Error('Not implemented'); + try { + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId, + }, + select: { + email: true, + name: true, + primaryContactPhone: true, + }, + }); + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const customer = await stripe.customer.create({ + email: organization?.email, + name: organization?.name, + phone: organization?.primaryContactPhone, + }); + + } catch (error: any) { + console.error('Create customer error: ', error.message); + } } /** * Create or update subscription + * If an organization doesn't have a stripeId than guide them to createCustomer */ static async createSubscription(organizationId: string, priceId: string) { // Implement subscription creation From 626f8fabecb8a6762767acf68895f4cdcf5be3e6 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Mon, 26 Jan 2026 14:21:19 -0600 Subject: [PATCH 04/12] Completed Stripe Create Customer --- backend/services/stripeService.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 46b14db..86770ab 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -15,19 +15,14 @@ export class StripeService { where: { id: organizationId, }, - select: { - email: true, - name: true, - primaryContactPhone: true, - }, }); + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const customer = await stripe.customer.create({ email: organization?.email, name: organization?.name, phone: organization?.primaryContactPhone, }); - } catch (error: any) { console.error('Create customer error: ', error.message); } From 470c5632a5d2aa87fc1894920640e8b53601485a Mon Sep 17 00:00:00 2001 From: matt65471 Date: Thu, 29 Jan 2026 00:06:03 -0600 Subject: [PATCH 05/12] Create Subscription edits --- backend/services/stripeService.ts | 35 ++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 86770ab..faefa67 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -17,12 +17,17 @@ export class StripeService { }, }); + if (!organization) { + throw new Error('Organization not found'); + } const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const customer = await stripe.customer.create({ email: organization?.email, name: organization?.name, phone: organization?.primaryContactPhone, }); + + return customer; } catch (error: any) { console.error('Create customer error: ', error.message); } @@ -35,7 +40,35 @@ export class StripeService { static async createSubscription(organizationId: string, priceId: string) { // Implement subscription creation // Handle payment intent and client secret - throw new Error('Not implemented'); + try { + const subscription = await prisma.subscription.findUnique({ + where: { + organizationId: organizationId, + }, + select: { + stripeCustomerId: true, + stripePriceId: true, + }, + }); + if (subscription) { + const stripeID = subscription?.stripeCustomerId; + if (subscription.stripePriceId === priceId) { + return { error: 'AlreadySubscribed', message: 'You already have this plan!' }; + } + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const newSubscription = await stripe.subscriptions.create({ + customer: stripeID, + items: [ + { + price: priceId, + }, + ], + }); + } else { + } + } catch (error: any) { + console.error('Create subscription failed: ', error.message); + } } /** From 04fdd2a0270074e97ff372c99880a3ece4a5f329 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Thu, 29 Jan 2026 16:12:05 -0600 Subject: [PATCH 06/12] addition to creation --- backend/services/stripeService.ts | 121 ++++++++++++++++++------------ 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index faefa67..9af4b3c 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -2,6 +2,8 @@ import { stripe } from '../config/stripe.js'; import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; import { env } from 'process'; +import { connect } from 'http2'; +import { SubscriptionStatus } from '@prisma/client'; export class StripeService { baseUrl = 'https://api.stripe.com/v1/'; @@ -9,67 +11,92 @@ export class StripeService { /** * Create a Stripe customer for an organization */ - static async createCustomer(organizationId: string) { - try { - const organization = await prisma.organization.findUnique({ - where: { - id: organizationId, - }, - }); - - if (!organization) { - throw new Error('Organization not found'); + static async createCustomer(organizationId: string) { + try { + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId, + }, + }); + + if (!organization) { + throw new Error('Organization not found'); + } + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const customer = await stripe.customer.create({ + email: organization?.email, + name: organization?.name, + phone: organization?.primaryContactPhone, + }); + + return customer; + } catch (error: any) { + console.error('Create customer error: ', error.message); } - const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - const customer = await stripe.customer.create({ - email: organization?.email, - name: organization?.name, - phone: organization?.primaryContactPhone, - }); - - return customer; - } catch (error: any) { - console.error('Create customer error: ', error.message); } - } - /** - * Create or update subscription - * If an organization doesn't have a stripeId than guide them to createCustomer - */ - static async createSubscription(organizationId: string, priceId: string) { - // Implement subscription creation - // Handle payment intent and client secret - try { - const subscription = await prisma.subscription.findUnique({ - where: { - organizationId: organizationId, - }, - select: { - stripeCustomerId: true, - stripePriceId: true, - }, - }); - if (subscription) { - const stripeID = subscription?.stripeCustomerId; - if (subscription.stripePriceId === priceId) { - return { error: 'AlreadySubscribed', message: 'You already have this plan!' }; + /** + * Create or update subscription + * If an organization doesn't have a stripeId than guide them to createCustomer + */ + static async createSubscription(organizationId: string, priceId: string) { + // Implement subscription creation + // Handle payment intent and client secret + try { + + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId + }, + include: { + subscription: true, + } + }) + + + if(!organization){ + throw new Error("Organization id not found"); } + + if(!organization.subscription){ + throw new Error("Organization already has a subscription") + } + + const customerInfo = await this.createCustomer(organizationId); + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const newSubscription = await stripe.subscriptions.create({ - customer: stripeID, + customer: customerInfo.id, items: [ { price: priceId, }, ], }); - } else { + + + + const result = await prisma.subscription.create({ + data: { + organizationId : organizationId, + organization: { + connect: {id: organizationId}, + }, + stripeCustomerId: customerInfo.id, + stripeSubscriptionId: newSubscription.id, + stripePriceId: priceId, + status: SubscriptionStatus.ACTIVE, + currentPeriodStart: new Date(newSubscription.currentPeriodStart * 1000), + currentPeriodEnd: new Date(newSubscription.currentPeriodEnd * 1000), + canelAtPeriodEnd: + }, + }) + + } catch (error: any) { + console.error('Create subscription failed: ', error.message); } - } catch (error: any) { - console.error('Create subscription failed: ', error.message); } - } /** * Cancel subscription From c5a4c20254a9f6b5ea64d3af3d536311ac883f36 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Fri, 6 Feb 2026 13:53:55 -0600 Subject: [PATCH 07/12] Finished creation --- backend/services/stripeService.ts | 53 +++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 9af4b3c..dfb338d 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -3,7 +3,7 @@ import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; import { env } from 'process'; import { connect } from 'http2'; -import { SubscriptionStatus } from '@prisma/client'; +import { SubscriptionStatus, PaymentStatus } from '@prisma/client'; export class StripeService { baseUrl = 'https://api.stripe.com/v1/'; @@ -58,7 +58,7 @@ export class StripeService { throw new Error("Organization id not found"); } - if(!organization.subscription){ + if(organization.subscription){ throw new Error("Organization already has a subscription") } @@ -73,23 +73,64 @@ export class StripeService { price: priceId, }, ], + payment_behavior: "default_incomplete", + expand: ["latest_invoice.payment_intent"], }); + const stripeInvoice = newSubscription.latest_invoice; + if (!stripeInvoice || typeof stripeInvoice === "string") { + throw new Error("Stripe latest_invoice was not expanded"); + } + + const paymentIntent = stripeInvoice.payment_intent; + if (!paymentIntent || typeof paymentIntent === "string") { + throw new Error("Stripe payment_intent was not expanded"); + } const result = await prisma.subscription.create({ data: { organizationId : organizationId, - organization: { - connect: {id: organizationId}, - }, stripeCustomerId: customerInfo.id, stripeSubscriptionId: newSubscription.id, stripePriceId: priceId, status: SubscriptionStatus.ACTIVE, currentPeriodStart: new Date(newSubscription.currentPeriodStart * 1000), currentPeriodEnd: new Date(newSubscription.currentPeriodEnd * 1000), - canelAtPeriodEnd: + cancelAtPeriodEnd: newSubscription.cancel_at_period_end, + payments: { + create: { + stripePaymentIntentId: paymentIntent.id, + stripeInvoiceId: stripeInvoice.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + status: PaymentStatus.PENDING, + } + }, + invoices: { + create: { + stripeInvoiceId: stripeInvoice.id, + amount: + stripeInvoice.amount_due ?? + stripeInvoice.amount_paid ?? + stripeInvoice.amount_remaining ?? + 0, + currency: stripeInvoice.currency ?? "usd", + status: stripeInvoice.status ?? "draft", + invoicePdf: stripeInvoice.invoice_pdf ?? null, + hostedInvoiceUrl: stripeInvoice.hosted_invoice_url ?? null, + + periodStart: new Date( + ((stripeInvoice.period_start ?? stripeInvoice.created) as number) * 1000 + ), + periodEnd: new Date( + ((stripeInvoice.period_end ?? stripeInvoice.created) as number) * 1000 + ), + }, + + }, + createdAt: new Date(newSubscription.created * 1000), + updatedAt: new Date(newSubscription.created * 1000), }, }) From 070975f6327c7e0f392f1862e0335ef9b68f1b68 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Fri, 6 Feb 2026 15:10:34 -0600 Subject: [PATCH 08/12] Finished methods --- backend/services/stripeService.ts | 287 +++++++++++++++++------------- 1 file changed, 168 insertions(+), 119 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index dfb338d..40e02a0 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -6,153 +6,202 @@ import { connect } from 'http2'; import { SubscriptionStatus, PaymentStatus } from '@prisma/client'; export class StripeService { - baseUrl = 'https://api.stripe.com/v1/'; /** * Create a Stripe customer for an organization */ - static async createCustomer(organizationId: string) { - try { - const organization = await prisma.organization.findUnique({ - where: { - id: organizationId, - }, - }); - - if (!organization) { - throw new Error('Organization not found'); - } - const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - const customer = await stripe.customer.create({ - email: organization?.email, - name: organization?.name, - phone: organization?.primaryContactPhone, - }); - - return customer; - } catch (error: any) { - console.error('Create customer error: ', error.message); + static async createCustomer(organizationId: string) { + try { + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId, + }, + }); + + if (!organization) { + throw new Error('Organization not found'); } + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const customer = await stripe.customer.create({ + email: organization?.email, + name: organization?.name, + phone: organization?.primaryContactPhone, + }); + + return customer; + } catch (error: any) { + console.error('Create customer error: ', error.message); + throw error; } + } - /** - * Create or update subscription - * If an organization doesn't have a stripeId than guide them to createCustomer - */ - static async createSubscription(organizationId: string, priceId: string) { - // Implement subscription creation - // Handle payment intent and client secret - try { - - const organization = await prisma.organization.findUnique({ - where: { - id: organizationId - }, - include: { - subscription: true, - } - }) - + /** + * Create or update subscription + * If an organization doesn't have a stripeId than guide them to createCustomer + */ + static async createSubscription(organizationId: string, priceId: string) { + // Implement subscription creation + // Handle payment intent and client secret + // ToDo: Handle issues with paymentIntent and the cases that come with that + try { + const organization = await prisma.organization.findUnique({ + where: { + id: organizationId, + }, + include: { + subscription: true, + }, + }); + + if (!organization) { + throw new Error('Organization id not found'); + } - if(!organization){ - throw new Error("Organization id not found"); - } + if (organization.subscription) { + throw new Error('Organization already has a subscription'); + } - if(organization.subscription){ - throw new Error("Organization already has a subscription") - } + const customerInfo = await this.createCustomer(organizationId); - const customerInfo = await this.createCustomer(organizationId); + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); - const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + const newSubscription = await stripe.subscriptions.create({ + customer: customerInfo.id, + items: [ + { + price: priceId, + }, + ], + payment_behavior: 'default_incomplete', + expand: ['latest_invoice.payment_intent'], + }); + + const stripeInvoice = newSubscription.latest_invoice; + if (!stripeInvoice || typeof stripeInvoice === 'string') { + throw new Error('Stripe latest_invoice was not expanded'); + } - const newSubscription = await stripe.subscriptions.create({ - customer: customerInfo.id, - items: [ - { - price: priceId, - }, - ], - payment_behavior: "default_incomplete", - expand: ["latest_invoice.payment_intent"], - }); + const paymentIntent = stripeInvoice.payment_intent; + if (!paymentIntent || typeof paymentIntent === 'string') { + throw new Error('Stripe payment_intent was not expanded'); + } - const stripeInvoice = newSubscription.latest_invoice; - if (!stripeInvoice || typeof stripeInvoice === "string") { - throw new Error("Stripe latest_invoice was not expanded"); - } - - const paymentIntent = stripeInvoice.payment_intent; - if (!paymentIntent || typeof paymentIntent === "string") { - throw new Error("Stripe payment_intent was not expanded"); - } - - - const result = await prisma.subscription.create({ - data: { - organizationId : organizationId, - stripeCustomerId: customerInfo.id, - stripeSubscriptionId: newSubscription.id, - stripePriceId: priceId, - status: SubscriptionStatus.ACTIVE, - currentPeriodStart: new Date(newSubscription.currentPeriodStart * 1000), - currentPeriodEnd: new Date(newSubscription.currentPeriodEnd * 1000), - cancelAtPeriodEnd: newSubscription.cancel_at_period_end, - payments: { - create: { - stripePaymentIntentId: paymentIntent.id, - stripeInvoiceId: stripeInvoice.id, - amount: paymentIntent.amount, - currency: paymentIntent.currency, - status: PaymentStatus.PENDING, - } + const result = await prisma.subscription.create({ + data: { + organizationId: organizationId, + stripeCustomerId: customerInfo.id, + stripeSubscriptionId: newSubscription.id, + stripePriceId: priceId, + status: SubscriptionStatus.ACTIVE, + currentPeriodStart: new Date(newSubscription.current_period_start * 1000), + currentPeriodEnd: new Date(newSubscription.current_period_end * 1000), + cancelAtPeriodEnd: newSubscription.cancel_at_period_end, + payments: { + create: { + stripePaymentIntentId: paymentIntent.id, + stripeInvoiceId: stripeInvoice.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + status: PaymentStatus.PENDING, }, - invoices: { - create: { - stripeInvoiceId: stripeInvoice.id, - amount: - stripeInvoice.amount_due ?? - stripeInvoice.amount_paid ?? - stripeInvoice.amount_remaining ?? - 0, - currency: stripeInvoice.currency ?? "usd", - status: stripeInvoice.status ?? "draft", - invoicePdf: stripeInvoice.invoice_pdf ?? null, - hostedInvoiceUrl: stripeInvoice.hosted_invoice_url ?? null, - - periodStart: new Date( - ((stripeInvoice.period_start ?? stripeInvoice.created) as number) * 1000 - ), - periodEnd: new Date( - ((stripeInvoice.period_end ?? stripeInvoice.created) as number) * 1000 - ), + }, + invoices: { + create: { + stripeInvoiceId: stripeInvoice.id, + amount: + stripeInvoice.amount_due ?? + stripeInvoice.amount_paid ?? + stripeInvoice.amount_remaining ?? + 0, + currency: stripeInvoice.currency ?? 'usd', + status: stripeInvoice.status ?? 'draft', + invoicePdf: stripeInvoice.invoice_pdf ?? null, + hostedInvoiceUrl: stripeInvoice.hosted_invoice_url ?? null, + + periodStart: new Date( + ((stripeInvoice.period_start ?? stripeInvoice.created) as number) * 1000 + ), + periodEnd: new Date( + ((stripeInvoice.period_end ?? stripeInvoice.created) as number) * 1000 + ), }, - - }, - createdAt: new Date(newSubscription.created * 1000), - updatedAt: new Date(newSubscription.created * 1000), }, - }) - - } catch (error: any) { - console.error('Create subscription failed: ', error.message); - } + createdAt: new Date(newSubscription.created * 1000), + updatedAt: new Date(newSubscription.created * 1000), + }, + }); + } catch (error: any) { + console.error('Create subscription failed: ', error.message); + throw error; } + } /** * Cancel subscription */ static async cancelSubscription(organizationId: string, immediate = false) { - // Implement subscription cancellation - throw new Error('Not implemented'); + try { + const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); + + const subscriptionID = await prisma.subscription.findUnique({ + where: { + organizationId: organizationId, + }, + select: { + id: true, + stripeSubscriptionId: true, + }, + }); + + if (!subscriptionID) { + throw new Error(`No subscription found for : ${organizationId}`); + } + + if (immediate) { + await stripe.subscriptions.cancel(subscriptionID.stripeSubscriptionId); + } else { + await stripe.subscriptions.update(subscriptionID.stripeSubscriptionId, { + cancel_at_period_end: true, + }); + } + + await prisma.subscription.update({ + where: { id: subscriptionID.id }, + data: { + cancelAtPeriodEnd: !immediate, + status: immediate ? SubscriptionStatus.CANCELED : SubscriptionStatus.ACTIVE, + }, + }); + } catch (error: any) { + console.error('Cancel subscription failed: ', error); + throw error; + } } /** * Get subscription status */ static async getSubscription(organizationId: string) { - // Implement get subscription with payments and invoices - throw new Error('Not implemented'); + try{ + const subscription = await prisma.subscription.findUnique({ + where: { + organizationId: organizationId, + }, + select: { + status : true, + } + }); + + if(!subscription){ + throw new Error("Organization doesn't have subscription"); + } + + return subscription.status; + + } + catch(error: any){ + console.error(`Error getting subscription status for ${organizationId}: `, error) + } } /** From 08e61d2491c8ac33fdf5ba2617cec2f1350914b1 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 21 Feb 2026 09:50:37 -0600 Subject: [PATCH 09/12] edits --- backend/services/stripeService.ts | 95 +++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 24 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index 40e02a0..b6c715f 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -6,7 +6,41 @@ import { connect } from 'http2'; import { SubscriptionStatus, PaymentStatus } from '@prisma/client'; export class StripeService { - + private static mapStripeSubscriptionStatus(s: string): SubscriptionStatus { + switch (s) { + case 'active': + return SubscriptionStatus.ACTIVE; + case 'trialing': + return SubscriptionStatus.TRIALING; + case 'incomplete': + return SubscriptionStatus.INCOMPLETE; + case 'past_due': + return SubscriptionStatus.PAST_DUE; + case 'unpaid': + return SubscriptionStatus.PAST_DUE; + case 'canceled': + return SubscriptionStatus.CANCELED; + case 'incomplete_expired': + return SubscriptionStatus.CANCELED; // or INCOMPLETE if you prefer + default: + return SubscriptionStatus.INCOMPLETE; + } + } + private static mapStripePaymentIntentStatus(piStatus: string): PaymentStatus { + switch (piStatus) { + case 'succeeded': + return PaymentStatus.SUCCEEDED; + case 'processing': + case 'requires_payment_method': + case 'requires_confirmation': + case 'requires_action': + return PaymentStatus.PENDING; + case 'canceled': + return PaymentStatus.FAILED; + default: + return PaymentStatus.PENDING; + } + } /** * Create a Stripe customer for an organization */ @@ -42,7 +76,7 @@ export class StripeService { static async createSubscription(organizationId: string, priceId: string) { // Implement subscription creation // Handle payment intent and client secret - // ToDo: Handle issues with paymentIntent and the cases that come with that + // ToDo: Handle issues with paymentIntent and the cases that come with that try { const organization = await prisma.organization.findUnique({ where: { @@ -76,15 +110,19 @@ export class StripeService { expand: ['latest_invoice.payment_intent'], }); + const subscriptionStatus = StripeService.mapStripeSubscriptionStatus(newSubscription.status); + const stripeInvoice = newSubscription.latest_invoice; if (!stripeInvoice || typeof stripeInvoice === 'string') { throw new Error('Stripe latest_invoice was not expanded'); } const paymentIntent = stripeInvoice.payment_intent; - if (!paymentIntent || typeof paymentIntent === 'string') { - throw new Error('Stripe payment_intent was not expanded'); - } + + const paymentStatus = + paymentIntent && typeof paymentIntent !== 'string' + ? StripeService.mapStripePaymentIntentStatus(paymentIntent.status) + : PaymentStatus.PENDING; const result = await prisma.subscription.create({ data: { @@ -92,7 +130,7 @@ export class StripeService { stripeCustomerId: customerInfo.id, stripeSubscriptionId: newSubscription.id, stripePriceId: priceId, - status: SubscriptionStatus.ACTIVE, + status: subscriptionStatus, currentPeriodStart: new Date(newSubscription.current_period_start * 1000), currentPeriodEnd: new Date(newSubscription.current_period_end * 1000), cancelAtPeriodEnd: newSubscription.cancel_at_period_end, @@ -102,7 +140,7 @@ export class StripeService { stripeInvoiceId: stripeInvoice.id, amount: paymentIntent.amount, currency: paymentIntent.currency, - status: PaymentStatus.PENDING, + status: paymentStatus, }, }, invoices: { @@ -130,6 +168,13 @@ export class StripeService { updatedAt: new Date(newSubscription.created * 1000), }, }); + return { + subscription: result, + clientSecret: paymentIntent.client_secret, + stripeSubscriptionId: newSubscription.id, + stripeInvoiceId: stripeInvoice.id, + stripePaymentIntentId: paymentIntent.id, + }; } catch (error: any) { console.error('Create subscription failed: ', error.message); throw error; @@ -178,30 +223,32 @@ export class StripeService { } } + /** + * Update subscription status + */ + /** * Get subscription status */ static async getSubscription(organizationId: string) { - try{ - const subscription = await prisma.subscription.findUnique({ - where: { - organizationId: organizationId, - }, - select: { - status : true, + try { + const subscription = await prisma.subscription.findUnique({ + where: { + organizationId: organizationId, + }, + select: { + status: true, + }, + }); + + if (!subscription) { + throw new Error("Organization doesn't have subscription"); } - }); - if(!subscription){ - throw new Error("Organization doesn't have subscription"); + return subscription.status; + } catch (error: any) { + console.error(`Error getting subscription status for ${organizationId}: `, error); } - - return subscription.status; - - } - catch(error: any){ - console.error(`Error getting subscription status for ${organizationId}: `, error) - } } /** From 87363218088077cdfac914008d9941d3d9038b6c Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 21 Feb 2026 10:04:45 -0600 Subject: [PATCH 10/12] Added payment updates --- backend/services/stripeService.ts | 116 ++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 13 deletions(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index b6c715f..dd0edf6 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -1,8 +1,6 @@ import { stripe } from '../config/stripe.js'; import { prisma } from '../config/prisma.js'; import Stripe from 'stripe'; -import { env } from 'process'; -import { connect } from 'http2'; import { SubscriptionStatus, PaymentStatus } from '@prisma/client'; export class StripeService { @@ -31,11 +29,11 @@ export class StripeService { case 'succeeded': return PaymentStatus.SUCCEEDED; case 'processing': - case 'requires_payment_method': case 'requires_confirmation': case 'requires_action': return PaymentStatus.PENDING; case 'canceled': + case 'requires_payment_method': return PaymentStatus.FAILED; default: return PaymentStatus.PENDING; @@ -224,8 +222,108 @@ export class StripeService { } /** - * Update subscription status + * Update subscription and payment status after Stripe confirms payment. + * Call this from webhook handlers for invoice.payment_succeeded or payment_intent.succeeded. */ + static async updateSubscriptionAfterPaymentConfirmed(stripePaymentIntentId: string) { + try { + const payment = await prisma.payment.findUnique({ + where: { stripePaymentIntentId }, + include: { subscription: true }, + }); + + if (!payment) { + // Not a subscription payment – ignore (avoids webhook retries for other payment types) + return; + } + + const stripeInstance = require('stripe')(process.env.STRIPE_SECRET_KEY); + const stripeSubscription = await stripeInstance.subscriptions.retrieve( + payment.subscription.stripeSubscriptionId + ); + + const subscriptionStatus = StripeService.mapStripeSubscriptionStatus( + stripeSubscription.status + ); + + await prisma.$transaction([ + prisma.payment.update({ + where: { stripePaymentIntentId }, + data: { status: PaymentStatus.SUCCEEDED }, + }), + prisma.subscription.update({ + where: { id: payment.subscriptionId }, + data: { + status: subscriptionStatus, + currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000), + currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000), + cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? false, + }, + }), + ]); + } catch (error: any) { + console.error('Update subscription after payment confirmed failed:', error.message); + throw error; + } + } + + /** + * Update subscription and payment status when Stripe reports payment failure. + * Call from webhook handlers for invoice.payment_failed or payment_intent.payment_failed. + */ + static async updateSubscriptionAfterPaymentFailed( + stripeSubscriptionId: string, + stripePaymentIntentId?: string + ) { + try { + const stripeInstance = require('stripe')(process.env.STRIPE_SECRET_KEY); + const stripeSubscription = await stripeInstance.subscriptions.retrieve(stripeSubscriptionId); + + const subscriptionStatus = StripeService.mapStripeSubscriptionStatus( + stripeSubscription.status + ); + + const subscription = await prisma.subscription.findUnique({ + where: { stripeSubscriptionId }, + }); + + if (!subscription) { + return; + } + + let paymentStatus: PaymentStatus | null = null; + if (stripePaymentIntentId) { + const payment = await prisma.payment.findUnique({ + where: { stripePaymentIntentId }, + }); + if (payment) { + const paymentIntent = await stripeInstance.paymentIntents.retrieve(stripePaymentIntentId); + paymentStatus = StripeService.mapStripePaymentIntentStatus(paymentIntent.status); + } + } + + await prisma.$transaction(async tx => { + await tx.subscription.update({ + where: { id: subscription.id }, + data: { + status: subscriptionStatus, + currentPeriodStart: new Date(stripeSubscription.current_period_start * 1000), + currentPeriodEnd: new Date(stripeSubscription.current_period_end * 1000), + cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? false, + }, + }); + if (stripePaymentIntentId && paymentStatus) { + await tx.payment.update({ + where: { stripePaymentIntentId }, + data: { status: paymentStatus }, + }); + } + }); + } catch (error: any) { + console.error('Update subscription after payment failed:', error.message); + throw error; + } + } /** * Get subscription status @@ -254,13 +352,5 @@ export class StripeService { /** * Handle webhook events */ - static async handleWebhook(event: Stripe.Event) { - // 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'); - } + static async handleWebhook(event: Stripe.Event) {} } From 9e9bccfff3de2d58ba34813f2c43086977b3f7a5 Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 21 Feb 2026 10:05:21 -0600 Subject: [PATCH 11/12] Quick update --- backend/services/stripeService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/services/stripeService.ts b/backend/services/stripeService.ts index dd0edf6..c7eef95 100644 --- a/backend/services/stripeService.ts +++ b/backend/services/stripeService.ts @@ -19,7 +19,7 @@ export class StripeService { case 'canceled': return SubscriptionStatus.CANCELED; case 'incomplete_expired': - return SubscriptionStatus.CANCELED; // or INCOMPLETE if you prefer + return SubscriptionStatus.CANCELED; default: return SubscriptionStatus.INCOMPLETE; } From 2ed63d4fed7aae7421b11a67ca915b9e6f58f71c Mon Sep 17 00:00:00 2001 From: matt65471 Date: Sat, 21 Feb 2026 10:34:10 -0600 Subject: [PATCH 12/12] Completed test cases. Still need to test with webhooks --- .../__tests__/services/stripeService.test.ts | 218 ++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 backend/__tests__/services/stripeService.test.ts diff --git a/backend/__tests__/services/stripeService.test.ts b/backend/__tests__/services/stripeService.test.ts new file mode 100644 index 0000000..55e48df --- /dev/null +++ b/backend/__tests__/services/stripeService.test.ts @@ -0,0 +1,218 @@ +import { mockReset } from 'jest-mock-extended'; + +// Preserve Prisma enums (SubscriptionStatus, PaymentStatus) - setup mocks entire @prisma/client +jest.mock('@prisma/client', () => ({ + ...jest.requireActual('@prisma/client'), + PrismaClient: jest.fn().mockImplementation(() => require('../setup/prisma-mock').prismaMock), +})); + +// Mock Stripe - the service uses require('stripe')(key) and calls methods on the result +const mockStripeInstance = { + customers: { + create: jest.fn(), + }, + customer: { + create: jest.fn(), + }, + subscriptions: { + create: jest.fn(), + retrieve: jest.fn(), + update: jest.fn(), + cancel: jest.fn(), + }, + paymentIntents: { + retrieve: jest.fn(), + }, +}; + +jest.mock('stripe', () => { + return jest.fn(() => mockStripeInstance); +}); + +// Use the prisma mock from global setup - don't override config/prisma +import { prismaMock } from '../setup/prisma-mock.js'; +import { StripeService } from '../../services/stripeService.js'; + +beforeEach(() => { + mockReset(prismaMock); + jest.clearAllMocks(); + // Handle both batch [op1, op2] and interactive (tx) => {} transaction forms + (prismaMock.$transaction as jest.Mock).mockImplementation(async (arg: any) => { + if (Array.isArray(arg)) { + return Promise.all(arg); + } + return arg(prismaMock); + }); +}); + +describe('StripeService', () => { + describe('updateSubscriptionAfterPaymentConfirmed', () => { + const mockPayment = { + id: 'pay-1', + stripePaymentIntentId: 'pi_123', + subscriptionId: 'sub-1', + status: 'PENDING' as const, + subscription: { + id: 'sub-1', + stripeSubscriptionId: 'stripe_sub_123', + }, + } as any; + + const mockStripeSubscription = { + status: 'active', + current_period_start: 1609459200, + current_period_end: 1612137600, + cancel_at_period_end: false, + }; + + it('updates payment to SUCCEEDED and syncs subscription from Stripe', async () => { + prismaMock.payment.findUnique.mockResolvedValue(mockPayment); + mockStripeInstance.subscriptions.retrieve.mockResolvedValue(mockStripeSubscription); + + await StripeService.updateSubscriptionAfterPaymentConfirmed('pi_123'); + + expect(prismaMock.payment.findUnique).toHaveBeenCalledWith({ + where: { stripePaymentIntentId: 'pi_123' }, + include: { subscription: true }, + }); + expect(mockStripeInstance.subscriptions.retrieve).toHaveBeenCalledWith('stripe_sub_123'); + expect(prismaMock.payment.update).toHaveBeenCalledWith({ + where: { stripePaymentIntentId: 'pi_123' }, + data: { status: 'SUCCEEDED' }, + }); + expect(prismaMock.subscription.update).toHaveBeenCalledWith({ + where: { id: 'sub-1' }, + data: expect.objectContaining({ + status: 'ACTIVE', + currentPeriodStart: expect.any(Date), + currentPeriodEnd: expect.any(Date), + }), + }); + }); + + it('returns early if payment not found (non-subscription payment)', async () => { + prismaMock.payment.findUnique.mockResolvedValue(null); + + await StripeService.updateSubscriptionAfterPaymentConfirmed('pi_unknown'); + + expect(mockStripeInstance.subscriptions.retrieve).not.toHaveBeenCalled(); + expect(prismaMock.$transaction).not.toHaveBeenCalled(); + }); + }); + + describe('updateSubscriptionAfterPaymentFailed', () => { + const mockSubscription = { + id: 'sub-1', + stripeSubscriptionId: 'stripe_sub_123', + } as any; + + const mockStripeSubscription = { + status: 'past_due', + current_period_start: 1609459200, + current_period_end: 1612137600, + cancel_at_period_end: false, + }; + + const mockPaymentIntent = { + id: 'pi_123', + status: 'requires_payment_method', + }; + + it('updates subscription and payment status when payment found', async () => { + prismaMock.subscription.findUnique.mockResolvedValue(mockSubscription); + mockStripeInstance.subscriptions.retrieve.mockResolvedValue(mockStripeSubscription); + prismaMock.payment.findUnique.mockResolvedValue({ + id: 'pay-1', + stripePaymentIntentId: 'pi_123', + } as any); + mockStripeInstance.paymentIntents.retrieve.mockResolvedValue(mockPaymentIntent); + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(prismaMock)); + + await StripeService.updateSubscriptionAfterPaymentFailed('stripe_sub_123', 'pi_123'); + + expect(mockStripeInstance.subscriptions.retrieve).toHaveBeenCalledWith('stripe_sub_123'); + expect(prismaMock.subscription.update).toHaveBeenCalledWith({ + where: { id: 'sub-1' }, + data: expect.objectContaining({ + status: 'PAST_DUE', + }), + }); + expect(prismaMock.payment.update).toHaveBeenCalledWith({ + where: { stripePaymentIntentId: 'pi_123' }, + data: { status: 'FAILED' }, + }); + }); + + it('updates only subscription when payment intent id not provided', async () => { + prismaMock.subscription.findUnique.mockResolvedValue(mockSubscription); + mockStripeInstance.subscriptions.retrieve.mockResolvedValue(mockStripeSubscription); + prismaMock.$transaction.mockImplementation(async (fn: any) => fn(prismaMock)); + + await StripeService.updateSubscriptionAfterPaymentFailed('stripe_sub_123'); + + expect(prismaMock.subscription.update).toHaveBeenCalled(); + expect(prismaMock.payment.update).not.toHaveBeenCalled(); + }); + + it('returns early if subscription not found in our DB', async () => { + // Service fetches from Stripe first, then checks our DB + mockStripeInstance.subscriptions.retrieve.mockResolvedValue({ + status: 'past_due', + current_period_start: 1609459200, + current_period_end: 1612137600, + cancel_at_period_end: false, + }); + prismaMock.subscription.findUnique.mockResolvedValue(null); + + await StripeService.updateSubscriptionAfterPaymentFailed('stripe_sub_unknown', 'pi_123'); + + // No DB updates when subscription not in our DB + expect(prismaMock.subscription.update).not.toHaveBeenCalled(); + expect(prismaMock.payment.update).not.toHaveBeenCalled(); + }); + }); + + describe('handleWebhook', () => { + it('handles events without throwing', async () => { + await expect( + StripeService.handleWebhook({ + type: 'invoice.payment_succeeded', + data: { object: { payment_intent: 'pi_123' } }, + } as any) + ).resolves.toBeUndefined(); + }); + + it('handles unknown event types', async () => { + await expect( + StripeService.handleWebhook({ + type: 'customer.unknown', + data: { object: {} }, + } as any) + ).resolves.toBeUndefined(); + }); + }); + + describe('getSubscription', () => { + it('returns subscription status for organization', async () => { + prismaMock.subscription.findUnique.mockResolvedValue({ + status: 'ACTIVE' as const, + } as any); + + const result = await StripeService.getSubscription('org-1'); + + expect(result).toBe('ACTIVE'); + expect(prismaMock.subscription.findUnique).toHaveBeenCalledWith({ + where: { organizationId: 'org-1' }, + select: { status: true }, + }); + }); + + it('returns undefined when organization has no subscription (service logs error)', async () => { + prismaMock.subscription.findUnique.mockResolvedValue(null); + + const result = await StripeService.getSubscription('org-unknown'); + + expect(result).toBeUndefined(); + }); + }); +});