From f5871e56686def9b3be8330885fbfaffc771d42f Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 23 Apr 2025 15:49:31 +0100 Subject: [PATCH] changed billing portal to post, added org config for include past_due --- example/src/app/demo.tsx | 39 +++++--------- .../src/internal/api/customers/cusRouter.ts | 3 ++ .../handlers/handleCreateBillingPortal.ts | 54 +++++++++++++++++++ .../internal/api/entitled/entitledRouter.ts | 15 ++++-- .../entitlements/CusEntitlementService.ts | 5 +- server/test.sh | 10 ++-- shared/models/orgModels/orgConfigModels.ts | 2 + 7 files changed, 92 insertions(+), 36 deletions(-) create mode 100644 server/src/internal/api/customers/handlers/handleCreateBillingPortal.ts diff --git a/example/src/app/demo.tsx b/example/src/app/demo.tsx index 88676dd3a..4f9d6c5b2 100644 --- a/example/src/app/demo.tsx +++ b/example/src/app/demo.tsx @@ -1,27 +1,16 @@ -//
{customer?.name}
-// +import Application from "@/components/application"; +import CustomerDetailsExample from "@/components/billing"; +import Intro from "@/components/introduction"; -// import Application from "@/components/application"; -// import CustomerDetailsExample from "@/components/billing"; -// import Intro from "@/components/introduction"; +export default function Home() { + return ( +
+ -// export default function Home() { -// return ( -//
-// - -//
-// -// -//
-//
-// ); -// } +
+ + +
+
+ ); +} diff --git a/server/src/internal/api/customers/cusRouter.ts b/server/src/internal/api/customers/cusRouter.ts index d987653dc..3f7bd634e 100644 --- a/server/src/internal/api/customers/cusRouter.ts +++ b/server/src/internal/api/customers/cusRouter.ts @@ -22,6 +22,7 @@ import { handlePostCustomerRequest } from "./handlers/handleCreateCustomer.js"; import { entityRouter } from "../entities/entityRouter.js"; import { getCustomerDetails } from "./getCustomerDetails.js"; import { handleUpdateCustomer } from "./handlers/handleUpdateCustomer.js"; +import { handleCreateBillingPortal } from "./handlers/handleCreateBillingPortal.js"; export const cusRouter = Router(); @@ -187,6 +188,8 @@ cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => { } }); +cusRouter.post("/:customer_id/billing_portal", handleCreateBillingPortal); + cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus); cusRouter.use("/:customer_id/entities", entityRouter); diff --git a/server/src/internal/api/customers/handlers/handleCreateBillingPortal.ts b/server/src/internal/api/customers/handlers/handleCreateBillingPortal.ts new file mode 100644 index 000000000..c5b94ed3f --- /dev/null +++ b/server/src/internal/api/customers/handlers/handleCreateBillingPortal.ts @@ -0,0 +1,54 @@ +import { createStripeCli } from "@/external/stripe/utils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; +import { ErrCode, APIVersion } from "@autumn/shared"; +import { Request, Response } from "express"; +import { StatusCodes } from "http-status-codes"; + +export const handleCreateBillingPortal = async (req: any, res: any) => { + try { + const customerId = req.params.customer_id; + let returnUrl = req.body.return_url; + + const [org, customer] = await Promise.all([ + OrgService.getFullOrg({ sb: req.sb, orgId: req.orgId }), + CusService.getById({ + sb: req.sb, + id: customerId, + orgId: req.orgId, + env: req.env, + logger: req.logtail, + }), + ]); + + if (!customer) { + throw new RecaseError({ + message: `Customer ${customerId} not found`, + code: ErrCode.CustomerNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } + + if (!customer.processor?.id) { + throw new RecaseError({ + message: `Customer ${customerId} not connected to Stripe`, + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } + + const stripeCli = createStripeCli({ org, env: req.env }); + const portal = await stripeCli.billingPortal.sessions.create({ + customer: customer.processor.id, + return_url: returnUrl || org.stripe_config.success_url, + }); + + res.status(200).json({ + customer_id: customer.id, + url: portal.url, + }); + } catch (error) { + handleRequestError({ req, error, res, action: "create billing portal" }); + } +}; diff --git a/server/src/internal/api/entitled/entitledRouter.ts b/server/src/internal/api/entitled/entitledRouter.ts index 811919122..0c0eb44f0 100644 --- a/server/src/internal/api/entitled/entitledRouter.ts +++ b/server/src/internal/api/entitled/entitledRouter.ts @@ -7,6 +7,7 @@ import { APIVersion, CusEntWithEntitlement, CusProduct, + CusProductStatus, Customer, Feature, FeatureType, @@ -365,12 +366,20 @@ const getCusEntsAndFeatures = async ({ // 1. Get customer entitlements & features / credit systems const startParallel = Date.now(); + let org = await OrgService.getFullOrg({ + sb, + orgId, + }); + const batchQuery = [ CustomerEntitlementService.getCustomerAndEnts({ sb, customerId: customer_id, orgId, env, + inStatuses: org.config?.include_past_due + ? [CusProductStatus.Active, CusProductStatus.PastDue] + : [CusProductStatus.Active], }).then((result) => { timings.cusEnts = Date.now() - startParallel; return result; @@ -384,13 +393,9 @@ const getCusEntsAndFeatures = async ({ timings.features = Date.now() - startParallel; return result; }), - OrgService.getFullOrg({ - sb, - orgId, - }), ]; - const [res1, res2, org] = await Promise.all(batchQuery); + const [res1, res2] = await Promise.all(batchQuery); const totalTime = Date.now() - startParallel; console.log("Query timings:", { diff --git a/server/src/internal/customers/entitlements/CusEntitlementService.ts b/server/src/internal/customers/entitlements/CusEntitlementService.ts index 23c515c30..aa31471ce 100644 --- a/server/src/internal/customers/entitlements/CusEntitlementService.ts +++ b/server/src/internal/customers/entitlements/CusEntitlementService.ts @@ -1,5 +1,6 @@ import RecaseError from "@/utils/errorUtils.js"; import { + CusProductStatus, CustomerEntitlement, ErrCode, FullCustomerEntitlement, @@ -33,11 +34,13 @@ export class CustomerEntitlementService { customerId, orgId, env, + inStatuses = [CusProductStatus.Active], }: { sb: SupabaseClient; customerId: string; orgId: string; env: string; + inStatuses?: string[]; }) { const { data, error } = await sb .from("customers") @@ -49,7 +52,7 @@ export class CustomerEntitlementService { .eq("id", customerId) .eq("org_id", orgId) .eq("env", env) - .eq("customer_products.status", "active") + .in("customer_products.status", inStatuses) .single(); if (error) { diff --git a/server/test.sh b/server/test.sh index 0b0429052..7ef60ca34 100755 --- a/server/test.sh +++ b/server/test.sh @@ -5,11 +5,11 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" # TEST PARALLEL if [ "$1" == "basic-parallel" ]; then MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ - tests/basic/referrals/*.ts \ - tests/attach/**/*.ts \ - # tests/basic/*.ts \ - # tests/basic/multi-feature/*.ts \ - # tests/basic/entities/*.ts \ + tests/basic/*.ts \ + tests/basic/multi-feature/*.ts \ + tests/basic/entities/*.ts \ + # tests/basic/referrals/*.ts \ + # tests/attach/**/*.ts \ elif [ "$1" == "advanced-parallel" ]; then diff --git a/shared/models/orgModels/orgConfigModels.ts b/shared/models/orgModels/orgConfigModels.ts index a3db3c201..4bfa76dfa 100644 --- a/shared/models/orgModels/orgConfigModels.ts +++ b/shared/models/orgModels/orgConfigModels.ts @@ -10,6 +10,8 @@ export const OrgConfigSchema = z.object({ api_version: z.number().default(0.2), checkout_on_failed_payment: z.boolean().default(true), reverse_deduction_order: z.boolean().default(false), + + include_past_due: z.boolean().default(false), }); export type OrgConfig = z.infer;