diff --git a/client/src/components/common/HeaderCreate.tsx b/client/src/components/common/HeaderCreate.tsx index 112e2ae4e..f4c8bcc4d 100644 --- a/client/src/components/common/HeaderCreate.tsx +++ b/client/src/components/common/HeaderCreate.tsx @@ -7,7 +7,12 @@ import Tooltip from "@mui/material/Tooltip"; import { useSelector } from "react-redux"; import type { RootState } from "@/store"; import { useMemo } from "react"; -import type { EntitlemenstKey } from "@/types/entitlements"; +type EntitlementsKey = + | "monitorsMax" + | "notificationChannelsMax" + | "statusPagesMax" + | "checksIntervalMsMin" + | "teamsMax"; export const HeaderCreate = ({ label, isLoading, @@ -18,7 +23,7 @@ export const HeaderCreate = ({ label?: string; isLoading: boolean; path: string; - entitlement: EntitlemenstKey | null; + entitlement: EntitlementsKey | null; entitlementCount: number; }) => { const theme = useTheme(); @@ -29,7 +34,7 @@ export const HeaderCreate = ({ if (!entitlement) return false; if (!ent || typeof ent[entitlement] !== "number") return false; return entitlementCount < ent[entitlement] ? false : true; - }, [ent, entitlementCount]); + }, [ent, entitlement, entitlementCount]); return ( ); } diff --git a/client/src/components/design-elements/Fallback.tsx b/client/src/components/design-elements/Fallback.tsx index c32e581c0..b52005a71 100644 --- a/client/src/components/design-elements/Fallback.tsx +++ b/client/src/components/design-elements/Fallback.tsx @@ -123,12 +123,14 @@ export const EmptyMonitorFallback = ({ bullets, actionButtonText, actionLink, + monitorLimitReached, }: { page: string; title: string; bullets: any; actionButtonText: string; actionLink: string; + monitorLimitReached: boolean; }) => { const theme = useTheme(); const navigate = useNavigate(); @@ -158,8 +160,9 @@ export const EmptyMonitorFallback = ({ variant="contained" color="accent" onClick={() => navigate(actionLink)} + disabled={monitorLimitReached} > - {actionButtonText} + {monitorLimitReached ? "Monitor limit reached" : actionButtonText} diff --git a/client/src/pages/billing/Billing.tsx b/client/src/pages/billing/Billing.tsx index 7e88f5c4a..48b8e9717 100644 --- a/client/src/pages/billing/Billing.tsx +++ b/client/src/pages/billing/Billing.tsx @@ -1,6 +1,8 @@ import { BasePage, InfoBox, PlanCard } from "@/components/design-elements"; import Grid from "@mui/material/Grid"; +import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; +import { Button } from "@/components/inputs"; import { useAppSelector } from "@/hooks/AppHooks"; import { useGet, usePost } from "@/hooks/UseApi"; @@ -27,6 +29,14 @@ const BillingPage = () => { } }; + const handleCancel = async () => { + const result = await post("/billing/cancel", {}); + const redirectUrl = result?.data?.redirectUrl; + if (redirectUrl) { + window.location = redirectUrl; + } + }; + return ( { ))} + {selectedPlan !== "free" && ( + + + + )} ); }; diff --git a/client/src/pages/billing/Success.tsx b/client/src/pages/billing/Success.tsx index d689a8081..112f2b933 100644 --- a/client/src/pages/billing/Success.tsx +++ b/client/src/pages/billing/Success.tsx @@ -1,5 +1,37 @@ +import { BasePage } from "@/components/design-elements"; +import Stack from "@mui/material/Stack"; +import CircularProgress from "@mui/material/CircularProgress"; +import Typography from "@mui/material/Typography"; + +import { useTheme } from "@mui/material/styles"; +import { Navigate, useSearchParams } from "react-router-dom"; +import { useGet } from "@/hooks/UseApi"; +import type { ApiResponse } from "@/hooks/UseApi"; + const BillingSuccessPage = () => { - return "Great"; + const theme = useTheme(); + const [searchParams] = useSearchParams(); + const plan = searchParams.get("plan"); + + const { response } = useGet>( + `/billing/confirm?plan=${plan}`, + {}, + {} + ); + + const success = response?.data?.success ?? false; + if (success === true) { + return ; + } else { + return ( + + + + Awaiting confirmation.. + + + ); + } }; export default BillingSuccessPage; diff --git a/client/src/pages/infrastructure/InfraMonitors.tsx b/client/src/pages/infrastructure/InfraMonitors.tsx index 2161c052d..8e0c1d896 100644 --- a/client/src/pages/infrastructure/InfraMonitors.tsx +++ b/client/src/pages/infrastructure/InfraMonitors.tsx @@ -19,12 +19,14 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; import { useDelete } from "@/hooks/UseApi"; import { InitializingStatusBox } from "@/components/design-elements/StatusBox"; +import { useAppSelector } from "@/hooks/AppHooks"; const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; const PageSpeedMonitorsPage = () => { const theme = useTheme(); const { t } = useTranslation(); + const { user } = useAppSelector((state) => state.auth); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const [selectedMonitor, setSelectedMonitor] = useState(null); const isDialogOpen = Boolean(selectedMonitor); @@ -64,6 +66,8 @@ const PageSpeedMonitorsPage = () => { const downCount = response?.data?.downCount || 0; const pausedCount = response?.data?.pausedCount || 0; + const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0); + const handleConfirm = async () => { await deleteFn(`/monitors/${selectedMonitor?._id}`); setSelectedMonitor(null); @@ -79,6 +83,7 @@ const PageSpeedMonitorsPage = () => { loading={isValidating} error={error} items={monitors} + monitorLimitReached={monitorLimitReached} page="infrastructure" actionLink="/infrastructure/create" > diff --git a/client/src/pages/pagespeed/PageSpeedMonitors.tsx b/client/src/pages/pagespeed/PageSpeedMonitors.tsx index 94622f054..045752d52 100644 --- a/client/src/pages/pagespeed/PageSpeedMonitors.tsx +++ b/client/src/pages/pagespeed/PageSpeedMonitors.tsx @@ -19,11 +19,14 @@ import useMediaQuery from "@mui/material/useMediaQuery"; import { useState } from "react"; import { useDelete } from "@/hooks/UseApi"; import { InitializingStatusBox } from "@/components/design-elements/StatusBox"; +import { useAppSelector } from "@/hooks/AppHooks"; const GLOBAL_REFRESH = import.meta.env.VITE_APP_GLOBAL_REFRESH; const PageSpeedMonitorsPage = () => { const theme = useTheme(); + const { user } = useAppSelector((state) => state.auth); + const { t } = useTranslation(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const [selectedMonitor, setSelectedMonitor] = useState(null); @@ -64,6 +67,8 @@ const PageSpeedMonitorsPage = () => { const downCount = response?.data?.downCount || 0; const pausedCount = response?.data?.pausedCount || 0; + const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0); + const handleConfirm = async () => { await deleteFn(`/monitors/${selectedMonitor?._id}`); setSelectedMonitor(null); @@ -81,6 +86,7 @@ const PageSpeedMonitorsPage = () => { items={monitors} page="pageSpeed" actionLink="/pagespeed/create" + monitorLimitReached={monitorLimitReached} > { const theme = useTheme(); + const { user } = useAppSelector((state) => state.auth); const { t } = useTranslation(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); const [selectedMonitor, setSelectedMonitor] = useState(null); @@ -91,6 +93,8 @@ const UptimeMonitors = () => { const downCount = response?.data?.downCount || 0; const pausedCount = response?.data?.pausedCount || 0; + const monitorLimitReached = count >= (user?.entitlements?.monitorsMax || 0); + const handleConfirm = async () => { await deleteFn(`/monitors/${selectedMonitor?._id}`); setSelectedMonitor(null); @@ -120,6 +124,7 @@ const UptimeMonitors = () => { items={monitorItems} page="uptime" actionLink="/uptime/create" + monitorLimitReached={monitorLimitReached} > Promise; + confirmPlan: ( + req: Request, + res: Response, + next: NextFunction + ) => Promise; } class BillingController implements IBillingController { @@ -38,6 +43,7 @@ class BillingController implements IBillingController { if (!orgId) throw new ApiError("Organization not found", 404); const planKey: PlanKey | undefined = req.body.planKey; + const redirectUrl = await this.billingService.subscribePlan( user.email, orgId, @@ -53,6 +59,44 @@ class BillingController implements IBillingController { next(error); } }; + + cancelPlan = async (req: Request, res: Response, next: NextFunction) => { + try { + const orgId = req.user?.orgId; + if (!orgId) throw new ApiError("Organization not found", 404); + + const redirectUrl = await this.billingService.cancelSubscription(orgId); + + res.status(200).json({ + message: "Subscription cancellation successful", + data: { redirectUrl }, + }); + return; + } catch (error) { + next(error); + } + }; + + confirmPlan = async (req: Request, res: Response, next: NextFunction) => { + try { + const orgId = req.user?.orgId; + if (!orgId) throw new ApiError("Organization not found", 404); + + const expectedPlanKey = req.query.plan as PlanKey; + const success = await this.billingService.confirmPlan( + orgId, + expectedPlanKey + ); + + res.status(200).json({ + message: "Plan confirmation endpoint hit", + data: { success }, + }); + return; + } catch (error) { + next(error); + } + }; } export default BillingController; diff --git a/server/src/controllers/StripeController.ts b/server/src/controllers/StripeController.ts index fb3874950..b861aff1d 100644 --- a/server/src/controllers/StripeController.ts +++ b/server/src/controllers/StripeController.ts @@ -1,46 +1,27 @@ import type { Request, Response, NextFunction } from "express"; +import { StripeService } from "@/services/index.js"; +import { config } from "@/config/index.js"; export interface IStripeController { webhook: (req: Request, res: Response, next: NextFunction) => Promise; } class StripeController implements IStripeController { + private stripeService: StripeService; + + constructor(stripeService: StripeService) { + this.stripeService = stripeService; + } webhook = async (req: Request, res: Response, next: NextFunction) => { try { - const sig = req.headers["stripe-signature"] as string | undefined; - const raw = req.body as unknown; // Buffer when express.raw is used - - let event: any = undefined; - if (Buffer.isBuffer(raw)) { - const json = raw.toString("utf8"); - try { - event = JSON.parse(json); - } catch (_e) { - res.status(401).json({ message: "Unauthorized" }); - } - } else if (typeof raw === "string") { - try { - event = JSON.parse(raw); - } catch (_e) { - res.status(400).json({ message: "Invalid JSON payload" }); - } - } else if (raw && typeof raw === "object") { - event = raw; + let event = req.body; + const signature = req.headers["stripe-signature"]; + if (!signature || Array.isArray(signature)) { + throw new Error("Stripe signature is missing or invalid"); } - // Basic routing by event type (no-ops for now) - const type = event?.type; - switch (type) { - case "checkout.session.completed": - case "customer.subscription.updated": - case "customer.subscription.deleted": - case "invoice.payment_succeeded": - case "invoice.payment_failed": - default: - break; - } - - res.status(200); + const result = await this.stripeService.webhook(event, signature); + res.status(200).send({ received: true }); } catch (error) { throw error; } diff --git a/server/src/controllers/TeamMemberController.ts b/server/src/controllers/TeamMemberController.ts index 4e75926fa..08d89bcd3 100644 --- a/server/src/controllers/TeamMemberController.ts +++ b/server/src/controllers/TeamMemberController.ts @@ -147,8 +147,6 @@ class TeamMemberController implements ITeamMemberController { throw new ApiError("No team member ID", 400); } - console.log(teamMemberId); - const deleted = await this.teamMemberService.delete(orgId, teamMemberId); invalidateCachesForUser(teamMemberId); if (!deleted) { diff --git a/server/src/db/models/auth/Org.ts b/server/src/db/models/auth/Org.ts index c5ad73290..71b1e3141 100644 --- a/server/src/db/models/auth/Org.ts +++ b/server/src/db/models/auth/Org.ts @@ -18,12 +18,11 @@ export interface IOrg extends Document { // Billing billingCustomerId?: string; subscriptionId?: string; + priceId: string; priceIds?: string[]; subscriptionStatus?: SubscriptionStatus; currentPeriodStart?: Date; currentPeriodEnd?: Date; - trialEnd?: Date; - cancelAtPeriodEnd?: boolean; // Cached entitlements entitlements: { @@ -61,8 +60,6 @@ const orgSchema = new Schema( }, currentPeriodStart: { type: Date }, currentPeriodEnd: { type: Date }, - trialEnd: { type: Date }, - cancelAtPeriodEnd: { type: Boolean }, entitlements: { plan: { type: String }, monitorsMax: { type: Number }, diff --git a/server/src/init/controllers.ts b/server/src/init/controllers.ts index b007f2833..91fe2af6e 100644 --- a/server/src/init/controllers.ts +++ b/server/src/init/controllers.ts @@ -76,7 +76,7 @@ export const initControllers = (services: any) => { services.notificationService ); - controllers.stripeController = new StripeController(); + controllers.stripeController = new StripeController(services.stripeService); controllers.billingController = new BillingController( services.billingService ); diff --git a/server/src/init/services.ts b/server/src/init/services.ts index 24127cc51..8146050d9 100644 --- a/server/src/init/services.ts +++ b/server/src/init/services.ts @@ -28,6 +28,7 @@ import { RecoveryService, IncidentService, SettingsService, + StripeService, BillingService, } from "@/services/index.js"; @@ -44,6 +45,7 @@ export const initServices = async () => { const networkService = new NetworkService(got); const statusService = new StatusService(); const settingsService = new SettingsService(); + const stripeService = new StripeService(); const billingService = new BillingService(); const emailService = new EmailService(userService, settingsService); settingsService.setEmailService(emailService); @@ -99,6 +101,7 @@ export const initServices = async () => { incidentService, settingsService, billingService, + stripeService, }; Object.values(services).forEach((service) => { diff --git a/server/src/middleware/VerifyPermission.ts b/server/src/middleware/VerifyPermission.ts index fcc93409d..0386fe010 100644 --- a/server/src/middleware/VerifyPermission.ts +++ b/server/src/middleware/VerifyPermission.ts @@ -61,8 +61,6 @@ const verifyOrgPermission = (resourceActions: string[]) => { return next(new ApiError("User not authenticated", 401)); } - console.log(req.user); - const orgPermissions = userContext.roles?.orgRole?.permissions || []; const allowed = hasPermission(orgPermissions, resourceActions); if (!allowed) { diff --git a/server/src/routes/billing.ts b/server/src/routes/billing.ts index 9f05c41fc..baa503f2e 100644 --- a/server/src/routes/billing.ts +++ b/server/src/routes/billing.ts @@ -26,6 +26,18 @@ class BillingRoutes { validateBody(subscribePlanSchema), this.controller.subscribePlan ); + this.router.post( + "/cancel", + verifyToken, + addUserContext, + this.controller.cancelPlan + ); + this.router.get( + "/confirm", + verifyToken, + addUserContext, + this.controller.confirmPlan + ); }; getRouter() { diff --git a/server/src/routes/stripe.ts b/server/src/routes/stripe.ts index 055aab977..56def789e 100644 --- a/server/src/routes/stripe.ts +++ b/server/src/routes/stripe.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +import { Router, raw } from "express"; import StripeController from "@/controllers/StripeController.js"; // Note: webhook route must be mounted with express.raw at app/init level. @@ -12,7 +12,11 @@ class StripeRoutes { } initRoutes = () => { - this.router.post("/webhook", this.controller.webhook); + this.router.post( + "/webhook", + raw({ type: "application/json" }), + this.controller.webhook + ); }; getRouter() { @@ -21,4 +25,3 @@ class StripeRoutes { } export default StripeRoutes; - diff --git a/server/src/services/index.ts b/server/src/services/index.ts index e3f1dacad..d6812d452 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -24,5 +24,6 @@ export { default as JobGenerator } from "@/services/infrastructure/JobGenerator. export { default as ServiceRegistry } from "@/services/system/ServiceRegistry.js"; export { default as SettingsService } from "@/services/system/SettingsService.js"; export { default as BillingService } from "@/services/system/BillingService.js"; +export { default as StripeService } from "@/services/system/StripeService.js"; export * from "@/services/infrastructure/NotificationServices/index.js"; diff --git a/server/src/services/system/BillingService.ts b/server/src/services/system/BillingService.ts index 10cfa95f2..f71ffacd9 100644 --- a/server/src/services/system/BillingService.ts +++ b/server/src/services/system/BillingService.ts @@ -15,6 +15,8 @@ export interface IBillingService { orgId: string, planKey: PlanKey ): Promise; + cancelSubscription(orgId: string): Promise; + confirmPlan(orgId: string, expectedPlanKey: PlanKey): Promise; } const SERVICE_NAME = "BillingService"; @@ -27,10 +29,10 @@ class BillingService implements IBillingService { this.SERVICE_NAME = SERVICE_NAME; this.stripeClient = new Stripe(config.STRIPE_SECRET); } - async listPlans(): Promise { + listPlans = async () => { const order: PlanKey[] = ["free", "pro", "business", "enterprise"]; return order.map((key) => Plans[key]); - } + }; subscribePlan = async (email: string, orgId: string, planKey: PlanKey) => { if (!planKey) { @@ -41,7 +43,6 @@ class BillingService implements IBillingService { throw new ApiError("Invalid planKey", 400); } - // Ensure billing customer exists for this org const org = await Org.findById(orgId); if (!org) throw new ApiError("Organization not found", 404); if (!org.billingCustomerId) { @@ -63,28 +64,110 @@ class BillingService implements IBillingService { throw new ApiError("Price not found for the selected plan", 404); } + let redirectUrl: string; const successUrl = `${config.ORIGIN}/billing/success?plan=${planKey}`; const cancelUrl = `${config.ORIGIN}/billing/`; - const session = await this.stripeClient.checkout.sessions.create({ - billing_address_collection: "auto", - line_items: [ - { - price, - quantity: 1, + + const subscriptionId = org.subscriptionId; + + // Updating an existing subscription + if (subscriptionId) { + const session = await this.stripeClient.billingPortal.sessions.create({ + customer: String(org.billingCustomerId), + return_url: successUrl, + flow_data: { + type: "subscription_update", + subscription_update: { + subscription: subscriptionId, + }, }, - ], - mode: "subscription", - success_url: successUrl, - cancel_url: cancelUrl, - customer: String(org.billingCustomerId), - client_reference_id: String(org._id), - subscription_data: { metadata: { orgId: String(org._id) } }, - metadata: { orgId: String(org._id) }, - }); - if (session.url === null) { - throw new ApiError("Failed to create checkout session", 500); + }); + redirectUrl = session.url; } - return session.url; + + // Creating a new subscription + else { + const session = await this.stripeClient.checkout.sessions.create({ + billing_address_collection: "auto", + line_items: [ + { + price, + quantity: 1, + }, + ], + mode: "subscription", + success_url: successUrl, + cancel_url: cancelUrl, + customer: String(org.billingCustomerId), + client_reference_id: String(org._id), + subscription_data: { metadata: { orgId: String(org._id) } }, + metadata: { orgId: String(org._id) }, + }); + if (!session.url) { + throw new ApiError("Failed to create checkout session", 500); + } + redirectUrl = session.url; + } + + return redirectUrl; + }; + + cancelSubscription = async (orgId: string): Promise => { + const successUrl = `${config.ORIGIN}/billing/success?plan=free`; + + const org = await Org.findById(orgId); + if (!org) throw new ApiError("Organization not found", 404); + + const subscriptionId = org.subscriptionId; + if (!subscriptionId) { + throw new ApiError("No active subscription found", 400); + } + + const session = await this.stripeClient.billingPortal.sessions.create({ + customer: String(org.billingCustomerId), + return_url: successUrl, + flow_data: { + type: "subscription_cancel", + subscription_cancel: { + subscription: subscriptionId, + }, + }, + }); + + const redirect = session.url; + if (!redirect) { + throw new ApiError("Failed to create billing portal session", 500); + } + + return redirect; + }; + + confirmPlan = async (orgId: string, expectedPlanKey: PlanKey) => { + const org = await Org.findOne({ _id: orgId }); + if (!org) throw new ApiError("Organization not found", 404); + + if (!org.subscriptionId) { + return expectedPlanKey === "free"; + } + + const sub = await this.stripeClient.subscriptions.retrieve( + org.subscriptionId + ); + const activePlan = sub.items.data[0]?.price.lookup_key; + + if (activePlan === org.planKey) { + return true; + } + + if (activePlan === expectedPlanKey) { + return true; + } + + if (activePlan !== expectedPlanKey) { + return false; + } + + return true; }; } diff --git a/server/src/services/system/StripeService.ts b/server/src/services/system/StripeService.ts new file mode 100644 index 000000000..f9de3d435 --- /dev/null +++ b/server/src/services/system/StripeService.ts @@ -0,0 +1,170 @@ +import Stripe from "stripe"; +import { config } from "@/config/index.js"; +import { + Org, + Monitor, + StatusPage, + NotificationChannel, +} from "@/db/models/index.js"; +import { PlanKey, Plans } from "@/types/entitlements.js"; + +const SERVICE_NAME = "StripeService"; + +interface IStripeService { + webhook: ( + event: string | Buffer, + signature: string + ) => Promise; +} + +class StripeService implements IStripeService { + public SERVICE_NAME: string; + private stripe: Stripe; + + constructor() { + this.SERVICE_NAME = SERVICE_NAME; + this.stripe = new Stripe(config.STRIPE_SECRET); + } + + private handleSubscription = async (subscription: Stripe.Subscription) => { + const orgId = subscription.metadata.orgId; + const subscriptionId = subscription.id; + const planKey = + (subscription.items.data[0]?.price.lookup_key as PlanKey) || "free"; + const priceId = subscription.items.data[0]?.price.id || "unknown"; + const status = subscription.status; + const currentPeriodStart = + subscription.items.data[0]?.current_period_start || 0; + const currentPeriodEnd = + subscription.items.data[0]?.current_period_end || 0; + + await Org.findOneAndUpdate( + { _id: orgId }, + { + $set: { + planKey, + subscriptionId, + subscriptionStatus: status, + priceId, + currentPeriodStart, + currentPeriodEnd, + entitlements: Plans[planKey], + }, + }, + { new: true } + ); + + await this.pruneResourcesForOrg(String(orgId), Plans[planKey]); + }; + + private handleCancel = async (subscription: Stripe.Subscription) => { + const orgId = subscription.metadata.orgId; + await Org.findOneAndUpdate( + { _id: orgId }, + { + $set: { + planKey: "free", + entitlements: Plans["free"], + }, + $unset: { + subscriptionId: "", + subscriptionStatus: "", + priceId: "", + currentPeriodStart: "", + currentPeriodEnd: "", + }, + }, + { new: true } + ); + await this.pruneResourcesForOrg(String(orgId), Plans["free"]); + }; + + private pruneResourcesForOrg = async ( + orgId: string, + entitlements: { + monitorsMax: number; + notificationChannelsMax: number; + statusPagesMax: number; + } + ) => { + const prune = async ( + model: any, + cap: number, + filter: Record + ) => { + if (!Number.isFinite(cap) || cap === Number.MAX_SAFE_INTEGER) return; + const count = await model.countDocuments(filter); + if (count <= cap) return; + const excess = count - cap; + const docs = await model + .find(filter) + .sort({ createdAt: -1 }) + .limit(excess) + .select("_id"); + const ids = docs.map((d: any) => d._id); + if (ids.length) { + await model.deleteMany({ _id: { $in: ids } }); + } + }; + + const baseFilter = { orgId } as const; + await prune(Monitor, entitlements.monitorsMax, baseFilter); + await prune( + NotificationChannel, + entitlements.notificationChannelsMax, + baseFilter + ); + await prune(StatusPage, entitlements.statusPagesMax, baseFilter); + }; + + webhook = async (event: string | Buffer, signature: string) => { + const whSecret = config.STRIPE_WEBHOOK_SECRET; + if (!whSecret || whSecret === "not_set") { + throw new Error("Stripe webhook secret is not set"); + } + + const stripeEvent = this.stripe.webhooks.constructEvent( + event, + signature, + whSecret + ); + + let subscription: Stripe.Subscription | null; + let status: Stripe.Subscription.Status; + switch (stripeEvent.type) { + case "customer.subscription.deleted": + subscription = stripeEvent.data.object; + status = subscription.status; + this.handleCancel(subscription); + break; + case "customer.subscription.created": + subscription = stripeEvent.data.object; + status = subscription.status; + this.handleSubscription(subscription); + break; + case "customer.subscription.updated": + subscription = stripeEvent.data.object; + status = subscription.status; + this.handleSubscription(subscription); + break; + + case "checkout.session.completed": + subscription = null; + console.log("Checkout session completed."); + break; + + case "invoice.payment_succeeded": + subscription = null; + console.log("Invoice payment succeeded."); + break; + + default: + // Unexpected event type + subscription = null; + console.log(`Unhandled event type ${stripeEvent.type}.`); + } + return subscription; + }; +} + +export default StripeService;