feat: added api / openapi models to shared folder

This commit is contained in:
John Yeo
2025-10-03 11:57:40 +01:00
parent fc2243e472
commit 9ab609d870
44 changed files with 996 additions and 20728 deletions

View File

@@ -88,6 +88,7 @@ const handleSpecialErrorCases = (
logger.warn(
`ATTACH ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
);
return c.json(
{
message: formattedError,
@@ -172,18 +173,35 @@ export const errorMiddleware = (err: Error, c: Context<HonoEnv>) => {
if (err instanceof ZodError) {
const formattedError = formatZodError(err);
logger.error(
`ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
);
// 1. If it's validation error
if (c.get("validated")) {
logger.error(
`INTERNAL ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
);
logger.error(err);
return c.json(
{
message: formattedError,
code: ErrCode.InvalidInputs,
env: ctx.env,
},
400,
);
return c.json(
{
message: formattedError,
code: ErrCode.InvalidInputs,
env: ctx.env,
},
500,
);
} else {
logger.warn(
`ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
);
return c.json(
{
message: formattedError,
code: ErrCode.InvalidInputs,
env: ctx.env,
},
400,
);
}
}
// 4. Handle unknown errors

View File

@@ -75,6 +75,8 @@ export function createRoute<
}
const wrappedHandler = async (c: ValidatedContext<HonoEnv, Body, Query>) => {
c.set("validated", true);
if (opts.withTx) {
const db = c.get("ctx").db;

View File

@@ -26,5 +26,5 @@ export type RequestContext = {
export type AutumnContext = RequestContext;
export type HonoEnv = {
Variables: { ctx: AutumnContext };
Variables: { ctx: AutumnContext; validated: boolean };
};

View File

@@ -177,7 +177,7 @@ if (process.env.NODE_ENV === "development") {
console.log(`Master ${process.pid} is running`);
console.log("Number of CPUs", numCPUs);
const numWorkers = 7;
const numWorkers = 5;
for (let i = 0; i < numWorkers; i++) {
cluster.fork();

View File

@@ -8,6 +8,7 @@ import { attachRouter } from "../customers/attach/attachRouter.js";
import { handleSetupPayment } from "../customers/attach/handleSetupPayment.js";
import cancelRouter from "../customers/cancel/cancelRouter.js";
import { cusRouter } from "../customers/cusRouter.js";
import { handleCreateBillingPortal } from "../customers/handlers/handleCreateBillingPortal.js";
import { featureRouter } from "../features/featureRouter.js";
import { internalFeatureRouter } from "../features/internalFeatureRouter.js";
import { migrationRouter } from "../migrations/migrationRouter.js";
@@ -59,6 +60,7 @@ apiRouter.use("/check", checkRouter);
apiRouter.use("/events", eventsRouter);
apiRouter.use("/track", eventsRouter);
apiRouter.post("/setup_payment", handleSetupPayment);
apiRouter.post("/billing_portal", handleCreateBillingPortal);
// Analytics
apiRouter.use("/query", analyticsRouter);

View File

@@ -1,13 +1,13 @@
import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type Stripe from "stripe";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import Stripe from "stripe";
const createDefaultBillingPortalConfiguration = async (stripeCli: Stripe) => {
try {
@@ -48,8 +48,8 @@ export const handleCreateBillingPortal = async (req: any, res: any) =>
res,
action: "create_billing_portal",
handler: async (req: any, res: any) => {
const customerId = req.params.customer_id;
let returnUrl = req.body.return_url;
const customerId = req.params.customer_id || req.body.customer_id;
const returnUrl = req.body.return_url;
const [org, customer] = await Promise.all([
OrgService.getFromReq(req),
@@ -116,8 +116,7 @@ export const handleCreateBillingPortal = async (req: any, res: any) =>
// Check if the error is due to missing default configuration
if (
error.message &&
error.message.includes("default configuration has not been created")
error.message?.includes("default configuration has not been created")
) {
try {
// Create a default billing portal configuration

View File

@@ -1,26 +1,25 @@
import "dotenv/config";
import { orgRouter } from "./orgs/orgRouter.js";
import { Router } from "express";
import { userRouter } from "./users/userRouter.js";
import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js";
import { internalFeatureRouter } from "./features/internalFeatureRouter.js";
import { productRouter } from "./products/internalProductRouter.js";
import { devRouter } from "./dev/devRouter.js";
import { cusRouter } from "./customers/internalCusRouter.js";
import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
import { withAdminAuth } from "./admin/withAdminAuth.js";
import { adminRouter } from "./admin/adminRouter.js";
import { autumnHandler } from "autumn-js/express";
import { Autumn } from "autumn-js";
import { analyticsRouter } from "./analytics/internalAnalyticsRouter.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { InvoiceService } from "./invoices/InvoiceService.js";
import { autumnHandler } from "autumn-js/express";
import { Router } from "express";
import rateLimit from "express-rate-limit";
import { createStripeCli } from "@/external/stripe/utils.js";
import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js";
import { adminRouter } from "./admin/adminRouter.js";
import { withAdminAuth } from "./admin/withAdminAuth.js";
import { analyticsRouter } from "./analytics/internalAnalyticsRouter.js";
import { trmnlRouter } from "./api/trmnl/trmnlRouter.js";
import { trmnlAuthMiddleware } from "@/middleware/trmnlAuthMiddleware.js";
import { cusRouter } from "./customers/internalCusRouter.js";
import { devRouter } from "./dev/devRouter.js";
import { internalFeatureRouter } from "./features/internalFeatureRouter.js";
import { InvoiceService } from "./invoices/InvoiceService.js";
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
import { orgRouter } from "./orgs/orgRouter.js";
import { productRouter } from "./products/internalProductRouter.js";
import { viewsRouter } from "./saved-views/savedViewsRouter.js";
import { userRouter } from "./users/userRouter.js";
const mainRouter: Router = Router();
@@ -53,8 +52,8 @@ mainRouter.use(
"/invoices/hosted_invoice_url/:invoiceId",
limiter,
async (req: any, res: any) => {
let invoiceId = req.params.invoiceId;
let invoice = await InvoiceService.get({
const invoiceId = req.params.invoiceId;
const invoice = await InvoiceService.get({
db: req.db,
id: invoiceId,
});
@@ -62,13 +61,15 @@ mainRouter.use(
if (!invoice) return res.status(404).json({ error: "Invoice not found" });
try {
let org = invoice.customer.org;
let env = invoice.customer.env;
let stripeCli = createStripeCli({
const org = invoice.customer.org;
const env = invoice.customer.env;
const stripeCli = createStripeCli({
org,
env,
});
let stripeInvoice = await stripeCli.invoices.retrieve(invoice.stripe_id);
const stripeInvoice = await stripeCli.invoices.retrieve(
invoice.stripe_id,
);
if (stripeInvoice.status == "draft") {
return res
@@ -108,7 +109,7 @@ mainRouter.use(
withOrgAuth,
autumnHandler({
autumn: (req: any) => {
let client = new Autumn({
const client = new Autumn({
url: "http://localhost:8080/v1",
headers: {
cookie: req.headers.cookie,

View File

@@ -6,6 +6,7 @@ import {
type FullProduct,
type Price,
ProductAlreadyExistsError,
type ProductV2,
} from "@autumn/shared";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
@@ -34,7 +35,7 @@ export const disableCurrentDefault = async ({
// freeTrial,
}: {
req: AutumnContext;
newProduct: CreateProductV2Params;
newProduct: CreateProductV2Params | ProductV2;
// items: ProductItem[];
// freeTrial: FreeTrial;
}) => {

View File

@@ -1,9 +1,9 @@
import {
CreateProductV2ParamsSchema,
type FreeTrial,
mapToProductV2,
notNullish,
ProductNotFoundError,
ProductV2Schema,
type ProductV2,
RecaseError,
UpdateProductQuerySchema,
UpdateProductSchema,
@@ -37,8 +37,6 @@ export const handleUpdateProductV2 = createRoute({
const { db, org, env, features, logger } = ctx;
const { version, upsert, disable_version } = c.req.valid("query");
// const { productId } = req.params;
// const { orgId, env, logger, db } = req;
const [fullProduct, rewardPrograms, _defaultProds] = await Promise.all([
ProductService.getFull({
@@ -64,11 +62,6 @@ export const handleUpdateProductV2 = createRoute({
if (!fullProduct) throw new ProductNotFoundError({ productId: productId });
// // How to go into another route handler?
// if (upsert === "true") await handleCreateProduct(c);
// Start a transaction?
const cusProductsCurVersion =
await CusProductService.getByInternalProductId({
db,
@@ -80,14 +73,17 @@ export const handleUpdateProductV2 = createRoute({
features,
});
const cusProductExists = cusProductsCurVersion.length > 0;
const newFreeTrial = body.free_trial as FreeTrial | undefined;
const newProductV2: ProductV2 = {
...curProductV2,
...body,
items: body.items || [],
free_trial: newFreeTrial || curProductV2.free_trial || undefined,
};
await disableCurrentDefault({
req: ctx,
newProduct: CreateProductV2ParamsSchema.parse({
...fullProduct,
...body,
}),
newProduct: newProductV2,
});
await handleUpdateProductDetails({
@@ -103,12 +99,8 @@ export const handleUpdateProductV2 = createRoute({
const itemsExist = notNullish(body.items);
const cusProductExists = cusProductsCurVersion.length > 0;
if (cusProductExists && itemsExist) {
const newProductV2 = ProductV2Schema.parse({
...body,
items: body.items || [],
});
if (disable_version === "true") {
throw new RecaseError({
message: "Cannot auto save product as there are existing customers",
@@ -116,7 +108,7 @@ export const handleUpdateProductV2 = createRoute({
}
const { itemsSame, freeTrialsSame } = productsAreSame({
newProductV2,
newProductV2: newProductV2,
curProductV1: fullProduct,
features,
});
@@ -126,7 +118,7 @@ export const handleUpdateProductV2 = createRoute({
if (!productSame) {
const newProduct = await handleVersionProductV2({
ctx,
newProductV2,
newProductV2: newProductV2,
latestProduct: fullProduct,
org,
env,

View File

@@ -1,27 +1,25 @@
import {
ErrCode,
Feature,
FullProduct,
type Feature,
type FullProduct,
OnDecrease,
OnIncrease,
ProductItem,
ProductV2,
type ProductItem,
type ProductV2,
} from "@autumn/shared";
import { mapToProductItems } from "../productV2Utils.js";
import RecaseError from "@/utils/errorUtils.js";
import { freeTrialsAreSame } from "../free-trials/freeTrialUtils.js";
import {
findSimilarItem,
itemsAreSame,
} from "../product-items/compareItemUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { freeTrialsAreSame } from "../free-trials/freeTrialUtils.js";
import {
isFeaturePriceItem,
isPriceItem,
} from "../product-items/productItemUtils/getItemType.js";
import { StatusCodes } from "http-status-codes";
import { itemToPriceOrTiers } from "../product-items/productItemUtils.js";
import { nullish } from "@/utils/genUtils.js";
import { getResetUsage } from "../product-items/productItemUtils/itemToPriceAndEnt.js";
import { itemToPriceOrTiers } from "../product-items/productItemUtils.js";
import { mapToProductItems } from "../productV2Utils.js";
const sanitizeItems = ({
items,
@@ -31,7 +29,7 @@ const sanitizeItems = ({
features: Feature[];
}) => {
return items.map((item) => {
let priceData = itemToPriceOrTiers({ item });
const priceData = itemToPriceOrTiers({ item });
const newItem = {
...item,
reset_usage_when_enabled: getResetUsage({
@@ -124,7 +122,7 @@ export const productsAreSame = ({
}
for (const item of items1) {
let similarItem = findSimilarItem({
const similarItem = findSimilarItem({
item,
items: items2,
});
@@ -157,7 +155,7 @@ export const productsAreSame = ({
}
for (const item of items2) {
let similarItem = findSimilarItem({
const similarItem = findSimilarItem({
item,
items: items1,
});
@@ -173,10 +171,10 @@ export const productsAreSame = ({
}
// Compare free trial
let freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial;
let freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial;
const freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial;
const freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial;
let freeTrialsSame = freeTrialsAreSame({
const freeTrialsSame = freeTrialsAreSame({
ft1: freeTrial1,
ft2: freeTrial2,
});

View File

@@ -1,36 +1,37 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { AppEnv, Organization } from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { defaultApiVersion } from "tests/constants.js";
import { runMigrationTest } from "./runMigrationTest.js";
import { timeout } from "@/utils/genUtils.js";
import type { AppEnv, Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { defaultApiVersion } from "tests/constants.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let wordsItem = constructArrearItem({
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { addPrefixToProducts } from "../utils.js";
import { runMigrationTest } from "./runMigrationTest.js";
const wordsItem = constructArrearItem({
featureId: TestFeature.Words,
});
export let pro = constructProduct({
export const pro = constructProduct({
items: [wordsItem],
type: "pro",
isDefault: false,
});
let newWordsItem = constructArrearItem({
const newWordsItem = constructArrearItem({
featureId: TestFeature.Words,
includedUsage: 120100,
});
let proWithTrial = constructProduct({
const proWithTrial = constructProduct({
items: [newWordsItem],
type: "pro",
isDefault: false,
@@ -40,13 +41,13 @@ let proWithTrial = constructProduct({
const testCase = "migrations4";
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro with trial (should not start trial)`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
@@ -83,7 +84,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi
testClockId = testClockId1!;
});
it("should attach pro product", async function () {
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -95,7 +96,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi
});
});
it("should update product to new version", async function () {
it("should update product to new version", async () => {
proWithTrial.version = 2;
await autumn.products.update(pro.id, {
items: proWithTrial.items,
@@ -103,8 +104,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro wi
});
});
it("should attach track usage and get correct balance", async function () {
let wordsUsage = 120000;
it("should attach track usage and get correct balance", async () => {
const wordsUsage = 120000;
await autumn.track({
customer_id: customerId,
value: wordsUsage,

View File

@@ -1,28 +1,26 @@
import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
type AppEnv,
BillingInterval,
Organization,
ProductV2,
type Organization,
type ProductV2,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { replaceItems } from "../utils.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js";
import { addPrefixToProducts, replaceItems } from "../utils.js";
export let pro = constructProduct({
export const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
trial: true,
@@ -31,13 +29,13 @@ export let pro = constructProduct({
const testCase = "newVersion2";
describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for trial product`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
@@ -74,7 +72,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for tria
testClockId = testClockId1!;
});
it("should attach pro product", async function () {
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -86,11 +84,11 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for tria
});
});
let usage = 50000;
const usage = 50000;
let newPro: ProductV2;
it("should update product to new version", async function () {
it("should update product to new version", async () => {
newPro = structuredClone(pro);
let newItems = replaceItems({
const newItems = replaceItems({
items: pro.items,
interval: BillingInterval.Month,
newItem: constructPriceItem({
@@ -109,7 +107,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for tria
return;
it("should attach pro v2", async function () {
it("should attach pro v2", async () => {
await runUpdateEntsTest({
autumn,
stripeCli,

View File

@@ -48,7 +48,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
let curUnix = Date.now();
before(async function () {
await setupBefore(this);

View File

@@ -1,30 +1,30 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
type AppEnv,
OnDecrease,
OnIncrease,
Organization,
type Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../../attach/utils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expect } from "chai";
import chalk from "chalk";
import { addHours, addMonths, addWeeks } from "date-fns";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { addPrefixToProducts } from "../../attach/utils.js";
let userItem = constructArrearProratedItem({
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 1,
@@ -34,7 +34,7 @@ let userItem = constructArrearProratedItem({
},
});
export let pro = constructProduct({
export const pro = constructProduct({
items: [userItem],
type: "pro",
});
@@ -42,8 +42,8 @@ export let pro = constructProduct({
const testCase = "entity3";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
@@ -85,7 +85,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables delete
});
let usage = 0;
let firstEntities = [
const firstEntities = [
{
id: "1",
name: "test",
@@ -103,7 +103,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables delete
},
];
it("should create three entities, then attach pro", async function () {
it("should create three entities, then attach pro", async () => {
await autumn.entities.create(customerId, firstEntities);
usage += firstEntities.length;
@@ -124,7 +124,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables delete
});
});
it("should delete 2 entities and have no new invoice", async function () {
it("should delete 2 entities and have no new invoice", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
@@ -148,12 +148,12 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables delete
itemQuantity: usage - numReplaceables,
});
let customer = await autumn.customers.get(customerId);
let invoices = customer.invoices!;
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices!;
expect(invoices.length).to.equal(1);
});
it("should advance clock to next cycle and have correct invoice", async function () {
it("should advance clock to next cycle and have correct invoice", async () => {
await advanceTestClock({
stripeCli,
testClockId,
@@ -168,7 +168,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables delete
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
let basePrice = getBasePrice({ product: pro });
const basePrice = getBasePrice({ product: pro });
expect(invoices.length).to.equal(2);
expect(invoices[0].total).to.equal(basePrice); // 0 entities

View File

@@ -1,32 +1,28 @@
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
AppEnv,
type AppEnv,
AttachBranch,
CreateEntity,
type CreateEntity,
CusProductStatus,
FeatureOptions,
Organization,
ProductV2,
type FeatureOptions,
type Organization,
type ProductV2,
} from "@autumn/shared";
import {
getAttachTotal,
getCurrentOptions,
} from "tests/utils/testAttachUtils/testAttachUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectInvoicesCorrect } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import { notNullish, timeout, toSnakeCase } from "@/utils/genUtils.js";
import { expectSubItemsCorrect } from "tests/utils/expectUtils/expectSubUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type { AttachParams, Customer } from "autumn-js";
import { expect } from "chai";
import { completeCheckoutForm } from "../stripeUtils.js";
import { AttachParams, Customer } from "autumn-js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import {
expectInvoicesCorrect,
expectProductAttached,
} from "tests/utils/expectUtils/expectProductAttached.js";
import { getCurrentOptions } from "tests/utils/testAttachUtils/testAttachUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { timeout, toSnakeCase } from "@/utils/genUtils.js";
import { completeCheckoutForm } from "../stripeUtils.js";
export const attachAndExpectCorrect = async ({
autumn,
@@ -135,9 +131,9 @@ export const attachAndExpectCorrect = async ({
const productCount = customer.products.reduce((acc: number, p: any) => {
if (
product.group == p.group &&
product.group === p.group &&
!p.is_add_on &&
(entityId ? p.entity_id == entityId : true)
(entityId ? p.entity_id === entityId : true)
) {
return acc + 1;
} else return acc;
@@ -145,7 +141,7 @@ export const attachAndExpectCorrect = async ({
const branch = preview.branch;
if (branch == AttachBranch.Downgrade) {
if (branch === AttachBranch.Downgrade) {
expect(
productCount,
`customer should only have 2 products (from this group: ${product.group})`,
@@ -162,15 +158,15 @@ export const attachAndExpectCorrect = async ({
product,
entityId,
status:
preview.branch == AttachBranch.Downgrade
preview.branch === AttachBranch.Downgrade
? CusProductStatus.Scheduled
: undefined,
});
const skipInvoiceCheck =
(preview.branch == AttachBranch.UpdatePrepaidQuantity &&
checkoutRes.total == 0) ||
preview.branch == AttachBranch.Downgrade;
(preview.branch === AttachBranch.UpdatePrepaidQuantity &&
checkoutRes.total === 0) ||
preview.branch === AttachBranch.Downgrade;
const freeProduct = isFreeProductV2({ product });
if (!skipInvoiceCheck && !freeProduct) {
@@ -183,7 +179,7 @@ export const attachAndExpectCorrect = async ({
});
}
if (!skipFeatureCheck && branch !== AttachBranch.Downgrade) {
if (!skipFeatureCheck && branch === AttachBranch.Downgrade) {
expectFeaturesCorrect({
customer,
product,
@@ -195,7 +191,7 @@ export const attachAndExpectCorrect = async ({
});
}
if (branch == AttachBranch.OneOff) {
if (branch === AttachBranch.OneOff) {
return;
}

View File

@@ -1,5 +0,0 @@
# Node environment variables
NODE_ENV="development"
# Development environment variables
DEV_DATABASE_URL="postgres://user:password@localhost:5432/dev_db"

View File

@@ -0,0 +1,35 @@
import { z } from "zod/v4";
export const CustomerDataSchema = z
.object({
name: z.string().nullish().meta({
description: "Customer's name",
example: "John Doe",
}),
email: z.string().nullish().meta({
description: "Customer's email address",
example: "john@example.com",
}),
fingerprint: z.string().nullish().meta({
description:
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
example: "fp_123abc",
}),
metadata: z
.record(z.any(), z.any())
.nullish()
.meta({
description: "Additional metadata for the customer",
example: { company: "Acme Inc" },
}),
stripe_id: z.string().nullish().meta({
description: "Stripe customer ID if you already have one",
example: "cus_stripe123",
}),
})
.meta({
id: "CustomerData",
description: "Customer data for creating or updating a customer",
});
export type CustomerData = z.infer<typeof CustomerDataSchema>;

View File

@@ -0,0 +1,19 @@
import { z } from "zod/v4";
export const EntityDataSchema = z
.object({
feature_id: z.string().meta({
description: "The feature ID that this entity is associated with",
example: "seats",
}),
name: z.string().optional().meta({
description: "Name of the entity",
example: "Team Alpha",
}),
})
.meta({
id: "EntityData",
description: "Entity data for creating an entity",
});
export type EntityData = z.infer<typeof EntityDataSchema>;

View File

@@ -3,6 +3,8 @@ import { CreateFreeTrialSchema } from "@models/productModels/freeTrialModels/fre
import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js";
import { z } from "zod/v4";
import { notNullish } from "../../utils/utils.js";
import { CustomerDataSchema } from "../common/customerData.js";
import { EntityDataSchema } from "../common/entityData.js";
export const ProductOptions = z.object({
product_id: z.string(),
@@ -18,19 +20,18 @@ export const ExtAttachBodySchema = z.object({
.string()
.describe("ID of the customer to attach the product to"),
customer_data: z
.any()
.nullish()
.describe("Customer data if using attach to auto create customer"),
customer_data: CustomerDataSchema.nullish().describe(
"Customer data if using attach to auto create customer",
),
entity_id: z.string().nullish(),
entity_data: z.any().nullish(),
entity_data: EntityDataSchema.nullish(),
// Product Info
product_id: z.string().nullish(),
product_ids: z.array(z.string()).min(1).nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
free_trial: z.boolean(),
free_trial: z.boolean().optional(),
// Others
success_url: z.string().optional(),

View File

@@ -0,0 +1,147 @@
import { EntityDataSchema } from "@api/common/entityData.js";
import { APIProductSchema } from "@api/products/apiProduct.js";
import { z } from "zod/v4";
import { CustomerDataSchema } from "../common/customerData.js";
import { CoreCusFeatureSchema } from "../customers/components/apiCusFeature.js";
// Check Feature Enums
export const CheckFeatureScenarioSchema = z
.enum(["usage_limit", "feature_flag"])
.meta({
id: "CheckFeatureScenario",
description: "Scenario type for feature check",
});
export const ProductScenarioSchema = z
.enum([
"scheduled",
"active",
"new",
"renew",
"upgrade",
"downgrade",
"cancel",
])
.meta({
id: "ProductScenario",
description: "Scenario type for product attachment",
});
// Check Feature Schemas
export const CheckParamsSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer to check",
example: "cus_123",
}),
feature_id: z.string().optional().meta({
description: "The ID of the feature to check access for",
example: "api_calls",
}),
product_id: z.string().optional().meta({
description: "The ID of the product to check",
example: "pro_plan",
}),
entity_id: z.string().optional().meta({
description: "The ID of the entity (optional)",
example: "entity_123",
}),
customer_data: CustomerDataSchema.optional().meta({
description:
"Customer data to create or update the customer if they don't exist",
}),
required_balance: z.number().optional().meta({
description: "The required balance for the check",
example: 1,
}),
send_event: z.boolean().optional().meta({
description: "Whether to send a usage event if allowed",
example: true,
}),
with_preview: z.boolean().optional().meta({
description: "Whether to include preview information in the response",
example: true,
}),
entity_data: EntityDataSchema.optional().meta({
description: "Entity data to create the entity if it doesn't exist",
}),
})
.meta({
id: "CheckParams",
description: "Parameters for checking feature or product access",
});
// Check Feature Preview Schemas
export const CheckFeaturePreviewSchema = z
.object({
scenario: CheckFeatureScenarioSchema.meta({
description: "The scenario type for this feature preview",
example: "usage_limit",
}),
title: z.string().meta({
description: "Title for the preview message",
example: "Usage Limit Reached",
}),
message: z.string().meta({
description: "Detailed message explaining the check result",
example: "You've reached your usage limit. Upgrade to continue.",
}),
feature_id: z.string().meta({
description: "The ID of the feature",
example: "api_calls",
}),
feature_name: z.string().meta({
description: "The name of the feature",
example: "API Calls",
}),
products: z.array(APIProductSchema).meta({
description: "Available products that include this feature",
}),
})
.meta({
id: "CheckFeaturePreview",
description: "Preview information for a feature check",
});
export const CheckResultSchema = z
.object({
allowed: z.boolean().meta({
description: "Whether the customer is allowed to use the feature",
example: true,
}),
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
feature_id: z.string().meta({
description: "The ID of the feature checked",
example: "api_calls",
}),
entity_id: z.string().nullish().meta({
description: "The ID of the entity (if provided)",
example: "entity_123",
}),
required_balance: z.number().meta({
description: "The required balance for this check",
example: 1,
}),
code: z.string().meta({
description: "Response code indicating the result",
example: "allowed",
}),
preview: CheckFeaturePreviewSchema.optional().meta({
description: "Preview information if with_preview was true",
}),
})
.extend(CoreCusFeatureSchema.shape)
.meta({
id: "CheckResult",
description: "Result of a feature check",
});
// Export Types
export type CheckParams = z.infer<typeof CheckParamsSchema>;
export type CheckResponse = z.infer<typeof CheckResultSchema>;
export type CheckFeatureScenario = z.infer<typeof CheckFeatureScenarioSchema>;
// export type CheckFeaturePreview = z.infer<typeof CheckFeaturePreviewSchema>;
export type ProductScenario = z.infer<typeof ProductScenarioSchema>;

View File

@@ -0,0 +1,210 @@
import { z } from "zod/v4";
import { CustomerDataSchema } from "../common/customerData.js";
import { EntityDataSchema } from "../common/entityData.js";
import { ProductScenarioSchema } from "./checkModels.js";
// Check Product Schemas
export const CheckProductParamsSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
product_id: z.string().meta({
description: "The ID of the product to check",
example: "pro_plan",
}),
entity_id: z.string().optional().meta({
description: "The ID of the entity (optional)",
example: "entity_123",
}),
customer_data: CustomerDataSchema.optional().meta({
description:
"Customer data to create or update the customer if they don't exist",
}),
entity_data: EntityDataSchema.optional().meta({
description: "Entity data to create the entity if it doesn't exist",
}),
with_preview: z.boolean().optional().meta({
description: "Whether to include preview information in the response",
example: true,
}),
})
.meta({
id: "CheckProductParams",
description: "Parameters for checking product availability",
});
export const CheckProductPreviewItemSchema = z
.object({
price: z.string().meta({
description: "Formatted price string",
example: "$10.00",
}),
description: z.string().meta({
description: "Description of the item",
example: "Base subscription",
}),
usage_model: z.enum(["prepaid", "pay_per_use"]).optional().meta({
description: "The usage model for this item",
example: "prepaid",
}),
})
.meta({
id: "CheckProductPreviewItem",
description: "Individual item in product preview",
});
export const CheckProductPreviewOptionSchema = z
.object({
feature_id: z.string().meta({
description: "The ID of the feature",
example: "api_calls",
}),
feature_name: z.string().meta({
description: "The name of the feature",
example: "API Calls",
}),
billing_units: z.number().meta({
description: "Number of billing units",
example: 1000,
}),
price: z.number().optional().meta({
description: "Price per billing unit",
example: 0.01,
}),
tiers: z
.array(
z.object({
to: z.union([z.number(), z.string()]).meta({
description: "Upper limit of this tier (can be 'inf' for infinite)",
example: 1000,
}),
amount: z.number().meta({
description: "Price amount for this tier",
example: 10,
}),
}),
)
.optional()
.meta({
description: "Tiered pricing structure",
}),
})
.meta({
id: "CheckProductPreviewOption",
description: "Feature option in product preview",
});
export const CheckProductPreviewSchema = z
.object({
scenario: ProductScenarioSchema.meta({
description: "The scenario type for this product preview",
example: "upgrade",
}),
product_id: z.string().meta({
description: "The ID of the product",
example: "pro_plan",
}),
product_name: z.string().meta({
description: "The name of the product",
example: "Pro Plan",
}),
recurring: z.boolean().meta({
description: "Whether the product is recurring",
example: true,
}),
error_on_attach: z.boolean().optional().meta({
description: "Whether there would be an error attaching this product",
example: false,
}),
next_cycle_at: z.number().optional().meta({
description: "Timestamp of the next billing cycle",
example: 1717000000000,
}),
current_product_name: z.string().optional().meta({
description: "Name of the customer's current product",
example: "Basic Plan",
}),
items: z.array(CheckProductPreviewItemSchema).optional().meta({
description: "Individual items in the product",
}),
options: z.array(CheckProductPreviewOptionSchema).optional().meta({
description: "Feature options available in the product",
}),
due_today: z
.object({
price: z.number().meta({
description: "Amount due today",
example: 10,
}),
currency: z.string().meta({
description: "Currency code",
example: "usd",
}),
})
.optional()
.meta({
description: "Payment due today",
}),
due_next_cycle: z
.object({
price: z.number().meta({
description: "Amount due next cycle",
example: 50,
}),
currency: z.string().meta({
description: "Currency code",
example: "usd",
}),
})
.optional()
.meta({
description: "Payment due in the next cycle",
}),
})
.meta({
id: "CheckProductPreview",
description: "Preview information for a product check",
});
export const CheckProductResultSchema = z
.object({
allowed: z.boolean().meta({
description: "Whether the customer can attach the product",
example: true,
}),
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
product_id: z.string().meta({
description: "The ID of the product",
example: "pro_plan",
}),
entity_id: z.string().optional().meta({
description: "The ID of the entity (if provided)",
example: "entity_123",
}),
status: z.string().optional().meta({
description: "Status code for the check result",
example: "upgrade_available",
}),
preview: CheckProductPreviewSchema.optional().meta({
description: "Preview information if with_preview was true",
}),
})
.meta({
id: "CheckProductResult",
description: "Result of a product check",
});
export type CheckProductParams = z.infer<typeof CheckProductParamsSchema>;
export type CheckProductResult = z.infer<typeof CheckProductResultSchema>;
export type CheckProductPreview = z.infer<typeof CheckProductPreviewSchema>;
export type CheckProductPreviewItem = z.infer<
typeof CheckProductPreviewItemSchema
>;
export type CheckProductPreviewOption = z.infer<
typeof CheckProductPreviewOptionSchema
>;

View File

@@ -1,4 +1,6 @@
import { z } from "zod/v4";
import { CustomerDataSchema } from "../common/customerData.js";
import { EntityDataSchema } from "../common/entityData.js";
// Cancel Schemas
export const CancelBodySchema = z
@@ -57,10 +59,9 @@ export const TrackParamsSchema = z
description: "The ID of the customer",
example: "cus_123",
}),
customer_data: z.any().nullish().meta({
customer_data: CustomerDataSchema.nullish().meta({
description:
"Customer data to create or update the customer if they don't exist",
example: { name: "John Doe", email: "john@example.com" },
}),
event_name: z.string().nonempty().optional().meta({
description: "The name of the event to track",
@@ -71,10 +72,13 @@ export const TrackParamsSchema = z
"The ID of the feature (alternative to event_name for usage events)",
example: "api_calls",
}),
properties: z.record(z.string(), z.any()).nullish().meta({
description: "Additional properties for the event",
example: { endpoint: "/api/users" },
}),
properties: z
.record(z.string(), z.any())
.nullish()
.meta({
description: "Additional properties for the event",
example: { endpoint: "/api/users" },
}),
timestamp: z.number().nullish().meta({
description: "Unix timestamp in milliseconds when the event occurred",
example: 1717000000000,
@@ -88,16 +92,16 @@ export const TrackParamsSchema = z
example: 1,
}),
set_usage: z.boolean().nullish().meta({
description: "Whether to set the usage to this value instead of increment",
description:
"Whether to set the usage to this value instead of increment",
example: false,
}),
entity_id: z.string().nullish().meta({
description: "The ID of the entity this event is associated with",
example: "entity_123",
}),
entity_data: z.any().nullish().meta({
entity_data: EntityDataSchema.nullish().meta({
description: "Data for creating the entity if it doesn't exist",
example: { name: "Team Alpha" },
}),
})
.meta({
@@ -174,9 +178,80 @@ export const QueryResultSchema = z
description: "Result of an analytics query",
});
export const SetupPaymentParamsSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
success_url: z.string().optional().meta({
description: "URL to redirect to after successful payment setup",
example: "https://example.com/success",
}),
checkout_session_params: z.record(z.any(), z.any()).optional().meta({
description: "Additional parameters for the checkout session",
}),
})
.meta({
id: "SetupPaymentParams",
description: "Parameters for setting up a payment method",
});
export const SetupPaymentResultSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
url: z.string().meta({
description: "URL to the payment setup page",
example: "https://checkout.stripe.com/...",
}),
})
.meta({
id: "SetupPaymentResult",
description: "Result of setting up a payment method",
});
export const BillingPortalParamsSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
return_url: z.string().optional().meta({
description:
"URL to return to after exiting the billing portal. Must include http:// or https://",
example: "https://example.com/dashboard",
}),
})
.meta({
id: "BillingPortalParams",
description: "Parameters for accessing the billing portal",
});
export const BillingPortalResultSchema = z
.object({
customer_id: z.string().meta({
description: "The ID of the customer",
example: "cus_123",
}),
url: z.string().meta({
description: "URL to the billing portal",
example: "https://billing.stripe.com/...",
}),
})
.meta({
id: "BillingPortalResult",
description: "Result of creating a billing portal session",
});
export type CancelBody = z.infer<typeof CancelBodySchema>;
export type CancelResult = z.infer<typeof CancelResultSchema>;
export type TrackParams = z.infer<typeof TrackParamsSchema>;
export type TrackResult = z.infer<typeof TrackResultSchema>;
export type QueryParams = z.infer<typeof QueryParamsSchema>;
export type QueryResult = z.infer<typeof QueryResultSchema>;
export type SetupPaymentParams = z.infer<typeof SetupPaymentParamsSchema>;
export type BillingPortalParams = z.infer<typeof BillingPortalParamsSchema>;
export type BillingPortalResult = z.infer<typeof BillingPortalResultSchema>;

View File

@@ -4,96 +4,152 @@ import {
ExtAttachBodySchema,
ExtCheckoutParamsSchema,
} from "@api/models.js";
import { CheckParamsSchema, CheckResultSchema } from "./checkModels.js";
import {
BillingPortalParamsSchema,
BillingPortalResultSchema,
CancelBodySchema,
CancelResultSchema,
QueryParamsSchema,
QueryResultSchema,
SetupPaymentParamsSchema,
SetupPaymentResultSchema,
TrackParamsSchema,
TrackResultSchema,
} from "./coreOpModels.js";
export const coreOps = {
"/core": {
"/attach": {
post: {
summary: "Attach Product",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: ExtAttachBodySchema },
},
"/attach": {
post: {
summary: "Attach Product",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: ExtAttachBodySchema },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: AttachResultSchema } },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: AttachResultSchema } },
},
},
},
"/checkout": {
post: {
summary: "Checkout",
tags: ["core"],
requestBody: {
content: { "application/json": { schema: ExtCheckoutParamsSchema } },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CheckoutResponseSchema } },
},
},
"/checkout": {
post: {
summary: "Checkout",
tags: ["core"],
requestBody: {
content: { "application/json": { schema: ExtCheckoutParamsSchema } },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CheckoutResponseSchema } },
},
},
},
"/cancel": {
post: {
summary: "Cancel Product",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: CancelBodySchema },
},
},
"/cancel": {
post: {
summary: "Cancel Product",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: CancelBodySchema },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CancelResultSchema } },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CancelResultSchema } },
},
},
},
"/track": {
post: {
summary: "Track Event",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: TrackParamsSchema },
},
},
"/track": {
post: {
summary: "Track Event",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: TrackParamsSchema },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: TrackResultSchema } },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: TrackResultSchema } },
},
},
},
"/query": {
post: {
summary: "Query Analytics",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: QueryParamsSchema },
},
},
"/query": {
post: {
summary: "Query Analytics",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: QueryParamsSchema },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: QueryResultSchema } },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: QueryResultSchema } },
},
},
},
},
"/check": {
post: {
summary: "Check Feature Access",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: CheckParamsSchema },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CheckResultSchema } },
},
},
},
},
"/setup_payment": {
post: {
summary: "Setup Payment Method",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: SetupPaymentParamsSchema },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: SetupPaymentResultSchema } },
},
},
},
},
"/billing_portal": {
post: {
summary: "Create Billing Portal Session",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: BillingPortalParamsSchema },
},
},
responses: {
"200": {
description: "200 OK",
content: {
"application/json": { schema: BillingPortalResultSchema },
},
},
},

View File

@@ -14,38 +14,43 @@ export const APITrialsUsedSchema = z.object({
fingerprint: z.string().nullish(),
});
export const APICustomerSchema = z.object({
// Internal fields
autumn_id: z.string().nullish(),
export const APICustomerSchema = z
.object({
// Internal fields
autumn_id: z.string().nullish(),
id: z.string().nullable().meta({
description: "Your internal ID for the customer",
example: "cus_123",
}),
id: z.string().nullable().meta({
description: "Your internal ID for the customer",
example: "cus_123",
}),
created_at: z.number().meta({
description:
"The date and time the customer was created in milliseconds since epoch",
example: 1717000000,
}),
created_at: z.number().meta({
description:
"The date and time the customer was created in milliseconds since epoch",
example: 1717000000,
}),
name: z.string().nullable(),
email: z.string().nullable(),
fingerprint: z.string().nullable(),
stripe_id: z.string().nullable().default(null),
env: z.enum(AppEnv),
name: z.string().nullable(),
email: z.string().nullable(),
fingerprint: z.string().nullable(),
stripe_id: z.string().nullable().default(null),
env: z.enum(AppEnv),
products: z.array(APICusProductSchema),
features: z.record(z.string(), APICusFeatureSchema),
invoices: z.array(APIInvoiceSchema).optional(),
trials_used: z.array(APITrialsUsedSchema).optional(),
products: z.array(APICusProductSchema),
features: z.record(z.string(), APICusFeatureSchema),
invoices: z.array(APIInvoiceSchema).optional(),
trials_used: z.array(APITrialsUsedSchema).optional(),
rewards: APICusRewardsSchema.nullish(),
metadata: z.record(z.any(), z.any()).default({}),
entities: z.array(EntityResponseSchema).optional(),
referrals: z.array(APICusReferralSchema).optional(),
upcoming_invoice: APICusUpcomingInvoiceSchema.nullish(),
payment_method: z.any().nullish(),
});
rewards: APICusRewardsSchema.nullish(),
metadata: z.record(z.any(), z.any()).default({}),
entities: z.array(EntityResponseSchema).optional(),
referrals: z.array(APICusReferralSchema).optional(),
upcoming_invoice: APICusUpcomingInvoiceSchema.nullish(),
payment_method: z.any().nullish(),
})
.meta({
id: "Customer",
description: "Customer object returned by the API",
});
export type APICustomer = z.infer<typeof APICustomerSchema>;

View File

@@ -66,18 +66,7 @@ export const APICusFeatureSchema = z
})
.extend(CoreCusFeatureSchema.shape);
export const CheckResultSchema = z
.object({
allowed: z.boolean(),
customer_id: z.string(),
feature_id: z.string(),
entity_id: z.string().nullish(),
required_balance: z.number(),
code: z.string(),
})
.extend(CoreCusFeatureSchema.shape);
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
export type CusEntResponseV2 = z.infer<typeof APICusFeatureSchema>;
export type CheckResponse = z.infer<typeof CheckResultSchema>;
export type CusRollover = z.infer<typeof CusRolloverSchema>;

View File

@@ -5,7 +5,7 @@ export const APICusProductSchema = z.object({
id: z.string(),
name: z.string().nullable(),
group: z.string().nullable(),
status: z.enum(["active", "expired", "scheduled"]),
status: z.enum(["active", "expired", "scheduled", "trialing"]),
canceled_at: z.number().nullish(),
started_at: z.number(),

View File

@@ -1,4 +1,5 @@
import { z } from "zod/v4";
import { EntityDataSchema } from "../common/entityData.js";
// Create Customer Params (based on handlePostCustomer logic)
export const CreateCustomerParamsSchema = z
@@ -28,7 +29,6 @@ export const CreateCustomerParamsSchema = z
example: "John Doe",
}),
email: z
.string()
.email({ message: "not a valid email address" })
.or(z.literal(""))
.nullish()
@@ -41,10 +41,14 @@ export const CreateCustomerParamsSchema = z
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
example: "fp_123abc",
}),
metadata: z.record(z.any(), z.any()).default({}).nullish().meta({
description: "Additional metadata for the customer",
example: { company: "Acme Inc" },
}),
metadata: z
.record(z.any(), z.any())
.default({})
.nullish()
.meta({
description: "Additional metadata for the customer",
example: { company: "Acme Inc" },
}),
stripe_id: z.string().nullish().meta({
description: "Stripe customer ID if you already have one",
example: "cus_stripe123",
@@ -53,9 +57,8 @@ export const CreateCustomerParamsSchema = z
description: "Entity ID to associate with the customer",
example: "entity_123",
}),
entity_data: z.any().nullish().meta({
entity_data: EntityDataSchema.nullish().meta({
description: "Data for creating an entity",
example: { name: "Team Alpha", feature_id: "seats" },
}),
})
.meta({
@@ -105,11 +108,14 @@ export const UpdateCustomerParamsSchema = z
"Unique identifier (eg, serial number) to detect duplicate customers",
example: "fp_123abc",
}),
metadata: z.record(z.any(), z.any()).nullish().meta({
description:
"Additional metadata for the customer (set individual keys to null to delete them)",
example: { company: "Acme Inc" },
}),
metadata: z
.record(z.any(), z.any())
.nullish()
.meta({
description:
"Additional metadata for the customer (set individual keys to null to delete them)",
example: { company: "Acme Inc" },
}),
stripe_id: z.string().nullish().meta({
description: "Stripe customer ID",
example: "cus_stripe123",

View File

@@ -1,11 +1,12 @@
import { CusExpand } from "@models/cusModels/cusExpand.js";
import { z } from "zod/v4";
import { SuccessResponseSchema } from "../common/commonResponses.js";
import { APICustomerSchema } from "./apiCustomer.js";
import {
CreateCustomerParamsSchema,
ListCustomersResponseSchema,
UpdateCustomerParamsSchema,
} from "./customerOpModels.js";
import { SuccessResponseSchema } from "../common/commonResponses.js";
export const customerOps = {
"/customers": {
@@ -57,7 +58,7 @@ export const customerOps = {
customer_id: z.string(),
}),
query: z.object({
expand: z.string().optional(),
expand: z.array(z.enum(CusExpand)).optional(),
}),
},
responses: {

View File

@@ -1,5 +1,8 @@
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
import type { z } from "zod/v4";
export const APIEntitySchema = EntityResponseSchema;
export const APIEntitySchema = EntityResponseSchema.meta({
id: "Entity",
description: "Entity object returned by the API",
});
export type APIEntity = z.infer<typeof APIEntitySchema>;

View File

@@ -1,7 +1,8 @@
import { EntityExpand } from "@models/cusModels/entityModels/entityExpand.js";
import { z } from "zod/v4";
import { SuccessResponseSchema } from "../common/commonResponses.js";
import { APIEntitySchema } from "./apiEntity.js";
import { CreateEntityParamsSchema } from "./entityOpModels.js";
import { SuccessResponseSchema } from "../common/commonResponses.js";
const EntityListResponseSchema = z
.object({
@@ -13,28 +14,6 @@ const EntityListResponseSchema = z
export const entityOps = {
"/customers/{customer_id}/entities": {
get: {
summary: "List Entities",
tags: ["entities"],
requestParams: {
path: z.object({
customer_id: z.string(),
}),
query: z.object({
expand: z.string().optional(),
}),
},
responses: {
"200": {
description: "200 OK",
content: {
"application/json": {
schema: EntityListResponseSchema,
},
},
},
},
},
post: {
summary: "Create Entity",
tags: ["entities"],
@@ -66,7 +45,7 @@ export const entityOps = {
entity_id: z.string(),
}),
query: z.object({
expand: z.string().optional(),
expand: z.array(z.enum(EntityExpand)).optional(),
}),
},
responses: {

View File

@@ -7,27 +7,32 @@ export enum APIFeatureType {
CreditSystem = "credit_system",
}
export const APIFeatureSchema = z.object({
id: z.string(),
name: z.string().nullish(),
type: z.nativeEnum(APIFeatureType),
display: z
.object({
singular: z.string(),
plural: z.string(),
})
.nullish(),
export const APIFeatureSchema = z
.object({
id: z.string(),
name: z.string().nullish(),
type: z.nativeEnum(APIFeatureType),
display: z
.object({
singular: z.string(),
plural: z.string(),
})
.nullish(),
credit_schema: z
.array(
z.object({
metered_feature_id: z.string(),
credit_cost: z.number(),
}),
)
.nullish(),
credit_schema: z
.array(
z.object({
metered_feature_id: z.string(),
credit_cost: z.number(),
}),
)
.nullish(),
archived: z.boolean().nullish(),
});
archived: z.boolean().nullish(),
})
.meta({
id: "Feature",
description: "Feature object returned by the API",
});
export type APIFeature = z.infer<typeof APIFeatureSchema>;

View File

@@ -1,8 +1,9 @@
// Core
export * from "./core/attachModels.js";
export * from "./core/checkModels.js";
export * from "./core/checkoutModels.js";
export * from "./core/coreOpModels.js";
export * from "./core/coreOpenApi.js";
export * from "./core/coreOpModels.js";
// Customers
@@ -15,8 +16,8 @@ export * from "./customers/customersOpenApi.js";
// Entities
export * from "./entities/apiEntity.js";
export * from "./entities/entityOpModels.js";
export * from "./entities/entitiesOpenApi.js";
export * from "./entities/entityOpModels.js";
// Features
export * from "./features/apiFeature.js";

View File

@@ -1,8 +1,11 @@
import "dotenv/config";
import { writeFileSync } from "node:fs";
import { AppEnv } from "@models/genModels/genEnums.js";
import yaml from "yaml";
import { z } from "zod/v4";
import { createDocument } from "zod-openapi";
import { CustomerDataSchema } from "./common/customerData.js";
import { EntityDataSchema } from "./common/entityData.js";
import { coreOps } from "./core/coreOpenApi.js";
import { customerOps } from "./customers/customersOpenApi.js";
import { entityOps } from "./entities/entitiesOpenApi.js";
@@ -36,12 +39,14 @@ const document = createDocument({
.object({
message: z.string(),
code: z.string(),
env: z.nativeEnum(AppEnv),
env: z.enum(AppEnv),
})
.meta({
id: "AutumnError",
description: "An error that occurred in the API",
}),
customerData: CustomerDataSchema,
entityData: EntityDataSchema,
},
securitySchemes: {
secretKey: {
@@ -64,15 +69,24 @@ const document = createDocument({
// Export to YAML file during build
if (process.env.NODE_ENV !== "production") {
try {
// Export as JSON (YAML export has issues with zod schemas)
// Convert to JSON first to strip out Zod schemas and function references
const jsonStr = JSON.stringify(document, null, 2);
writeFileSync("./openapi.json", jsonStr, "utf8");
console.log("OpenAPI document exported to openapi.json");
// TODO: Fix YAML export - currently fails with "Tag not resolved for Function value"
// const yamlContent = yaml.stringify(document);
// writeFileSync("./openapi.yaml", yamlContent, "utf8");
// console.log("OpenAPI document exported to openapi.yaml");
// Convert JSON to YAML (this avoids function serialization issues)
const jsonObj = JSON.parse(jsonStr);
const yamlContent = yaml.stringify(jsonObj);
if (process.env.STAINLESS_PATH) {
writeFileSync(
`${process.env.STAINLESS_PATH}/openapi.yml`,
yamlContent,
"utf8",
);
}
console.log(
`OpenAPI document exported to ${process.env.STAINLESS_PATH}/openapi.yml`,
);
} catch (error) {
console.error("Failed to export OpenAPI document:", error);
}

View File

@@ -28,27 +28,29 @@ export const CreateProductV2ParamsSchema = z
description: "Create Product",
});
export const UpdateProductV2ParamsSchema = CreateProductV2ParamsSchema.extend({
id: z.string().nonempty().regex(idRegex).optional(),
name: z
.string()
.refine((val) => val.length > 0, {
message: "name must be a non-empty string",
})
.optional(),
export const UpdateProductV2ParamsSchema = z
.object({
id: z.string().nonempty().regex(idRegex).optional(),
name: z
.string()
.refine((val) => val.length > 0, {
message: "name must be a non-empty string",
})
.optional(),
is_add_on: z.boolean().optional(),
is_default: z.boolean().optional(),
version: z.number().optional(),
group: z.string().optional(),
archived: z.boolean().optional(),
is_add_on: z.boolean().optional(),
is_default: z.boolean().optional(),
version: z.number().optional(),
group: z.string().optional(),
archived: z.boolean().optional(),
// items: z.array(CreateProductItemParamsSchema).optional(),
free_trial: CreateFreeTrialSchema.nullish(),
}).meta({
id: "UpdateProductParams",
description: "Update Product",
});
items: z.array(CreateProductItemParamsSchema).optional(),
free_trial: CreateFreeTrialSchema.nullish(),
})
.meta({
id: "UpdateProductParams",
description: "Update Product",
});
export const UpdateProductQuerySchema = z.object({
version: z.string().optional(),

View File

@@ -27,7 +27,6 @@ export * from "./models/attachModels/attachEnums/AttachFunction.js";
// Attach Models
export * from "./models/attachModels/attachPreviewModels.js";
export * from "./models/attachModels/checkoutModels.js";
export * from "./models/authModels/membership.js";
export * from "./models/chatResultModels/chatResultFeature.js";
export * from "./models/chatResultModels/chatResultFeature.js";

View File

@@ -1,32 +0,0 @@
// import { APIProductSchema } from "@api/products/apiProduct.js";
// import { APIProductItemSchema } from "@api/products/apiProductItem.js";
// import { z } from "zod/v4";
// import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
// export const CheckoutLineSchema = z.object({
// description: z.string(),
// amount: z.number(),
// item: APIProductItemSchema.nullish(),
// });
// export const CheckoutResponseSchema = z.object({
// url: z.string().nullish(),
// customer_id: z.string(),
// lines: z.array(CheckoutLineSchema),
// product: APIProductSchema.nullish(),
// current_product: APIProductSchema.nullish(),
// options: z.array(FeatureOptionsSchema).nullish(),
// total: z.number().nullish(),
// currency: z.string().nullish(),
// has_prorations: z.boolean().nullish(),
// // next_cycle_at: z.number().nullish(),
// next_cycle: z
// .object({
// starts_at: z.number().nullish(),
// total: z.number().nullish(),
// })
// .nullish(),
// });
// export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;

View File

@@ -3,7 +3,7 @@ import { FreeTrialDuration } from "./freeTrialEnums.js";
export const FreeTrialSchema = z.object({
id: z.string(),
duration: z.nativeEnum(FreeTrialDuration),
duration: z.enum(FreeTrialDuration),
length: z.number(),
unique_fingerprint: z.boolean(),

View File

@@ -46,23 +46,6 @@ export const UpdateProductSchema = z.object({
archived: z.boolean().optional(),
});
export const FrontendProductSchema = ProductSchema.omit({
org_id: true,
created_at: true,
env: true,
processor: true,
}).extend({
isActive: z.boolean(),
prices: z.array(PriceSchema),
entitlements: z.array(
EntitlementSchema.extend({
feature: FeatureSchema,
}),
),
free_trial: FreeTrialSchema,
options: z.any(),
});
export const FullProductSchema = ProductSchema.extend({
prices: z.array(PriceSchema),
entitlements: z.array(
@@ -84,7 +67,6 @@ export type ProductCounts = {
};
export type Product = z.infer<typeof ProductSchema>;
export type FrontendProduct = z.infer<typeof FrontendProductSchema>;
export type FullProduct = z.infer<typeof FullProductSchema>;
export type CreateProduct = z.infer<typeof CreateProductSchema>;
export type UpdateProduct = z.infer<typeof UpdateProductSchema>;

File diff suppressed because it is too large Load Diff

View File

@@ -1,799 +0,0 @@
openapi: 3.1.0
info:
title: Autumn API
version: 1.2.0
servers:
- url: https://api.useautumn.com
description: Production server
security:
- secretKey: []
paths:
/products:
post:
summary: Create Product
tags:
- products
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/CreateProductParams"
responses:
"200":
description: 200 OK
content:
application/json:
schema:
$ref: "#/components/schemas/Product"
patch:
summary: Update Product
tags:
- products
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/UpdateProductParams"
responses:
"200":
description: 200 OK
content:
application/json:
schema:
$ref: "#/components/schemas/Product"
components:
schemas:
CreateProductParams:
description: Create Product
type: object
properties:
id:
type: string
minLength: 1
pattern: ^[a-zA-Z0-9_-]+$
name:
type: string
is_add_on:
default: false
type: boolean
is_default:
default: false
type: boolean
version:
type: number
group:
default: ""
type: string
items:
type: array
items:
type: object
properties:
feature_id:
anyOf:
- type: string
- type: "null"
feature_type:
anyOf:
- type: string
enum:
- single_use
- continuous_use
- boolean
- static
- type: "null"
included_usage:
anyOf:
- anyOf:
- type: number
- type: string
const: inf
- type: "null"
interval:
anyOf:
- type: string
enum:
- minute
- hour
- day
- week
- month
- quarter
- semi_annual
- year
- type: "null"
interval_count:
anyOf:
- type: number
- type: "null"
entity_feature_id:
anyOf:
- type: string
- type: "null"
usage_model:
anyOf:
- type: string
enum:
- prepaid
- pay_per_use
- type: "null"
price:
anyOf:
- type: number
- type: "null"
tiers:
anyOf:
- type: array
items:
type: object
properties:
to:
anyOf:
- type: number
- type: string
const: inf
amount:
type: number
required:
- to
- amount
- type: "null"
billing_units:
anyOf:
- type: number
- type: "null"
usage_limit:
anyOf:
- type: number
- type: "null"
reset_usage_when_enabled:
anyOf:
- type: boolean
- type: "null"
config:
anyOf:
- type: object
properties:
on_increase:
anyOf:
- type: string
enum:
- bill_immediately
- prorate_immediately
- prorate_next_cycle
- bill_next_cycle
- type: "null"
on_decrease:
anyOf:
- type: string
enum:
- prorate
- prorate_immediately
- prorate_next_cycle
- none
- no_prorations
- type: "null"
rollover:
anyOf:
- type: object
properties:
max:
anyOf:
- type: number
- type: "null"
duration:
default: month
type: string
enum:
- month
- forever
length:
type: number
required:
- max
- length
- type: "null"
- type: "null"
created_at:
anyOf:
- type: number
- type: "null"
entitlement_id:
anyOf:
- type: string
- type: "null"
price_id:
anyOf:
- type: string
- type: "null"
price_config:
anyOf:
- {}
- type: "null"
free_trial:
anyOf:
- type: object
properties:
length:
anyOf:
- type: string
- type: number
unique_fingerprint:
default: false
type: boolean
duration:
default: day
type: string
enum:
- day
- month
- year
card_required:
default: true
type: boolean
required:
- length
- type: "null"
required:
- id
- name
UpdateProductParams:
description: Update Product
type: object
properties:
id:
type: string
minLength: 1
pattern: ^[a-zA-Z0-9_-]+$
name:
type: string
is_add_on:
type: boolean
is_default:
type: boolean
version:
type: number
group:
type: string
items:
type: array
items:
type: object
properties:
feature_id:
anyOf:
- type: string
- type: "null"
feature_type:
anyOf:
- type: string
enum:
- single_use
- continuous_use
- boolean
- static
- type: "null"
included_usage:
anyOf:
- anyOf:
- type: number
- type: string
const: inf
- type: "null"
interval:
anyOf:
- type: string
enum:
- minute
- hour
- day
- week
- month
- quarter
- semi_annual
- year
- type: "null"
interval_count:
anyOf:
- type: number
- type: "null"
entity_feature_id:
anyOf:
- type: string
- type: "null"
usage_model:
anyOf:
- type: string
enum:
- prepaid
- pay_per_use
- type: "null"
price:
anyOf:
- type: number
- type: "null"
tiers:
anyOf:
- type: array
items:
type: object
properties:
to:
anyOf:
- type: number
- type: string
const: inf
amount:
type: number
required:
- to
- amount
- type: "null"
billing_units:
anyOf:
- type: number
- type: "null"
usage_limit:
anyOf:
- type: number
- type: "null"
reset_usage_when_enabled:
anyOf:
- type: boolean
- type: "null"
config:
anyOf:
- type: object
properties:
on_increase:
anyOf:
- type: string
enum:
- bill_immediately
- prorate_immediately
- prorate_next_cycle
- bill_next_cycle
- type: "null"
on_decrease:
anyOf:
- type: string
enum:
- prorate
- prorate_immediately
- prorate_next_cycle
- none
- no_prorations
- type: "null"
rollover:
anyOf:
- type: object
properties:
max:
anyOf:
- type: number
- type: "null"
duration:
default: month
type: string
enum:
- month
- forever
length:
type: number
required:
- max
- length
- type: "null"
- type: "null"
created_at:
anyOf:
- type: number
- type: "null"
entitlement_id:
anyOf:
- type: string
- type: "null"
price_id:
anyOf:
- type: string
- type: "null"
price_config:
anyOf:
- {}
- type: "null"
free_trial:
anyOf:
- type: object
properties:
length:
anyOf:
- type: string
- type: number
unique_fingerprint:
default: false
type: boolean
duration:
default: day
type: string
enum:
- day
- month
- year
card_required:
default: true
type: boolean
required:
- length
- type: "null"
archived:
type: boolean
Product:
description: A product
type: object
properties:
id:
description: The ID of the product you set when creating the product
example: pro_plan
type: string
name:
description: The name of the product
example: Pro Plan
type: string
group:
description: The group of the product
example: product_set_1
anyOf:
- type: string
- type: "null"
env:
description: The environment of the product
example: production
type: string
enum:
- sandbox
- live
is_add_on:
description: Whether the product is an add-on and can be purchased alongside
other products
example: true
type: boolean
is_default:
description: Whether the product is the default product
example: true
type: boolean
archived:
description: Whether this product has been archived and is no longer available
example: false
type: boolean
version:
description: The version of the product
example: 1
type: number
created_at:
description: The timestamp of when the product was created in milliseconds since
epoch
example: 1759247877000
type: number
items:
description: Array of product items that define the features and pricing
example:
- feature_id: <string>
feature_type: single_use
included_usage: 123
interval: <string>
usage_model: prepaid
price: 123
billing_units: 123
entity_feature_id: <string>
reset_usage_when_enabled: true
tiers:
- to: 123
amount: 123
type: array
items:
$ref: "#/components/schemas/ProductItem"
free_trial:
description: Free trial configuration for this product, if available
example:
duration: <string>
length: 123
unique_fingerprint: true
anyOf:
- type: object
properties:
duration:
type: string
enum:
- day
- month
- year
length:
type: number
unique_fingerprint:
type: boolean
card_required:
type: boolean
trial_available:
default: true
anyOf:
- type: boolean
- type: "null"
required:
- duration
- length
- unique_fingerprint
- card_required
- trial_available
additionalProperties: false
- type: "null"
base_variant_id:
description: ID of the base variant this product is derived from
example: var_1234567890abcdef
anyOf:
- type: string
- type: "null"
scenario:
description: Scenario context for when this product is used in attach flows
example: upgrade
type: string
enum:
- scheduled
- active
- new
- renew
- upgrade
- downgrade
- cancel
- expired
properties:
description: Additional properties and metadata for the product
example:
is_free: false
is_one_off: false
interval_group: monthly
has_trial: true
updateable: true
type: object
properties:
is_free:
description: True if the product has no base price or usage prices
example: false
type: boolean
is_one_off:
description: True if the product only contains a one-time price
example: false
type: boolean
interval_group:
description: The billing interval group for recurring products (e.g., 'monthly',
'yearly')
example: monthly
anyOf:
- type: string
- type: "null"
has_trial:
description: True if the product includes a free trial
example: true
anyOf:
- type: boolean
- type: "null"
updateable:
description: True if the product can be updated after creation (only applicable
if there are prepaid recurring prices)
example: true
anyOf:
- type: boolean
- type: "null"
required:
- is_free
- is_one_off
additionalProperties: false
required:
- id
- name
- group
- env
- is_add_on
- is_default
- archived
- version
- created_at
- items
- free_trial
- base_variant_id
additionalProperties: false
ProductItem:
description: A product item that defines a feature
example:
feature_id: feature_1
feature_type: single_use
included_usage: 123
interval: monthly
usage_model: prepaid
type: object
properties:
type:
anyOf:
- type: string
enum:
- feature
- priced_feature
- price
- type: "null"
feature_id:
anyOf:
- type: string
- type: "null"
feature_type:
anyOf:
- type: string
enum:
- single_use
- continuous_use
- boolean
- static
- type: "null"
feature:
anyOf:
- type: object
properties:
id:
type: string
name:
anyOf:
- type: string
- type: "null"
type:
type: string
enum:
- boolean
- single_use
- continuous_use
- credit_system
display:
anyOf:
- type: object
properties:
singular:
type: string
plural:
type: string
required:
- singular
- plural
additionalProperties: false
- type: "null"
credit_schema:
anyOf:
- type: array
items:
type: object
properties:
metered_feature_id:
type: string
credit_cost:
type: number
required:
- metered_feature_id
- credit_cost
additionalProperties: false
- type: "null"
archived:
anyOf:
- type: boolean
- type: "null"
required:
- id
- type
additionalProperties: false
- type: "null"
included_usage:
anyOf:
- anyOf:
- type: number
- type: string
const: inf
- type: "null"
interval:
anyOf:
- type: string
enum:
- minute
- hour
- day
- week
- month
- quarter
- semi_annual
- year
- type: "null"
interval_count:
anyOf:
- type: number
- type: "null"
price:
anyOf:
- type: number
- type: "null"
tiers:
anyOf:
- type: array
items:
type: object
properties:
to:
anyOf:
- type: number
- type: string
const: inf
amount:
type: number
required:
- to
- amount
additionalProperties: false
- type: "null"
usage_model:
anyOf:
- type: string
enum:
- prepaid
- pay_per_use
- type: "null"
billing_units:
anyOf:
- type: number
- type: "null"
reset_usage_when_enabled:
anyOf:
- type: boolean
- type: "null"
quantity:
anyOf:
- type: number
- type: "null"
next_cycle_quantity:
anyOf:
- type: number
- type: "null"
entity_feature_id:
anyOf:
- type: string
- type: "null"
display:
anyOf:
- type: object
properties:
primary_text:
type: string
secondary_text:
anyOf:
- type: string
- type: "null"
required:
- primary_text
additionalProperties: false
- type: "null"
additionalProperties: false
AutumnError:
description: An error that occurred in the API
type: object
properties:
message:
type: string
code:
type: string
env:
type: string
enum:
- sandbox
- live
required:
- message
- code
- env
additionalProperties: false
securitySchemes:
secretKey:
type: http
scheme: bearer
bearerFormat: JWT

View File

@@ -14,6 +14,7 @@
"license": "Apache-2.0",
"scripts": {
"build:tsc": "tsc && bun api/openapi.ts",
"api": "bun api/openapi.ts",
"build": "bun build ./index.ts --outdir dist --format esm --target bun --external zod",
"dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build && bun run dev:dts\"",
"dev:dts": "tsc --emitDeclarationOnly --outDir dist --project tsconfig.json",

View File

@@ -1,31 +1,24 @@
"use client";
import ProductSidebar from "@/views/products/product/ProductSidebar";
import LoadingScreen from "@/views/general/LoadingScreen";
import { useState, useEffect, useRef } from "react";
import {
Customer,
Entity,
Feature,
ProductItem,
ProductV2,
} from "@autumn/shared";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import { CustomToaster } from "@/components/general/CustomToaster";
import { ManageProduct } from "@/views/products/product/ManageProduct";
import { ProductContext } from "@/views/products/product/ProductContext";
import { type ProductItem, type ProductV2 } from "@autumn/shared";
import { useEffect, useRef, useState } from "react";
import { Link, useParams, useSearchParams } from "react-router";
import ErrorScreen from "@/views/general/ErrorScreen";
import { ProductOptions } from "./ProductOptions";
import { useEnv } from "@/utils/envUtils";
import { CustomerProductBreadcrumbs } from "./components/CustomerProductBreadcrumbs";
import { FrontendProduct, useAttachState } from "./hooks/useAttachState";
import { sortProductItems } from "@/utils/productUtils";
import { useCusQuery } from "../hooks/useCusQuery";
import { CustomToaster } from "@/components/general/CustomToaster";
import { useOrg } from "@/hooks/common/useOrg";
import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery";
import { useCusProductQuery } from "./hooks/useCusProductQuery";
import { useEnv } from "@/utils/envUtils";
import { notNullish } from "@/utils/genUtils";
import { sortProductItems } from "@/utils/productUtils";
import ErrorScreen from "@/views/general/ErrorScreen";
import LoadingScreen from "@/views/general/LoadingScreen";
import { ManageProduct } from "@/views/products/product/ManageProduct";
import { ProductContext } from "@/views/products/product/ProductContext";
import ProductSidebar from "@/views/products/product/ProductSidebar";
import { useCusQuery } from "../hooks/useCusQuery";
import { CustomerProductBreadcrumbs } from "./components/CustomerProductBreadcrumbs";
import { useAttachState } from "./hooks/useAttachState";
import { useCusProductQuery } from "./hooks/useCusProductQuery";
import { ProductOptions } from "./ProductOptions";
interface OptionValue {
feature_id: string;

View File

@@ -1,10 +1,10 @@
import { FeatureOptions, ProductV2 } from "@autumn/shared";
import type { FeatureOptions, ProductV2 } from "@autumn/shared";
export type FrontendProduct = ProductV2 & {
isActive: boolean;
options: FeatureOptions[];
isCanceled: boolean;
};
// export type FrontendProduct = ProductV2 & {
// isActive: boolean;
// options: FeatureOptions[];
// isCanceled: boolean;
// };
export const getAttachBody = ({
customerId,

View File

@@ -1,24 +1,23 @@
import {
type AttachPreview,
type FeatureOptions,
type FullCusProduct,
isCanceled,
type ProductItem,
type ProductV2,
UsageModel,
} from "@autumn/shared";
import { useEffect, useState } from "react";
import { notNullish } from "@/utils/genUtils";
import { isFeatureItem } from "@/utils/product/getItemType";
import { isOneOffProduct } from "@/utils/product/priceUtils";
import { sortProductItems } from "@/utils/productUtils";
import {
AttachPreview,
CusProduct,
FeatureOptions,
FullCusProduct,
isCanceled,
ProductItem,
ProductV2,
UsageModel,
} from "@autumn/shared";
import { useEffect, useState } from "react";
export type FrontendProduct = ProductV2 & {
isActive: boolean;
options: FeatureOptions[];
isCanceled: boolean;
};
// export type FrontendProduct = ProductV2 & {
// isActive: boolean;
// options: FeatureOptions[];
// isCanceled: boolean;
// };
export enum AttachCase {
AddOn = "Add On",
@@ -31,7 +30,7 @@ export enum AttachCase {
const productHasPrepaid = (items: ProductItem[]) => {
return items.some(
(item) =>
item.usage_model == UsageModel.Prepaid && notNullish(item.interval),
item.usage_model === UsageModel.Prepaid && notNullish(item.interval),
);
};