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;