Merge pull request #340 from useautumn/checkout-revamp

Checkout revamp: Move checkout to Hono router
This commit is contained in:
John Yeo
2025-11-26 14:59:36 +00:00
committed by GitHub
40 changed files with 790 additions and 502 deletions

View File

@@ -21,8 +21,8 @@
"recommended": true,
"suspicious": {
"noExportsInTest": "off",
"noExplicitAny": "off",
"noImplicitAnyLet": "off"
"noExplicitAny": "on",
"noImplicitAnyLet": "on"
},
"complexity": {
"noStaticOnlyClass": "off"

View File

@@ -1,25 +1,25 @@
import { checkToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.js";
import { FullCustomer, FullProduct } from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachBody } from "@autumn/shared";
import { attachParamsToPreview } from "@/internal/customers/attach/handleAttachPreview/attachParamsToPreview.js";
import {
type AttachBody,
AttachFunction,
AttachPreview,
CheckProductPreview,
Feature,
Organization,
type AttachPreview,
type CheckProductPreview,
type Feature,
type FullCustomer,
type FullProduct,
type Organization,
} from "@autumn/shared";
import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js";
import { Decimal } from "decimal.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { attachParamsToPreview } from "@/internal/billing/attachPreview/attachParamsToPreview.js";
import { checkToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { Decimal } from "decimal.js";
import { notNullish } from "@/utils/genUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js";
export const attachToCheckPreview = async ({
preview,
@@ -39,26 +39,26 @@ export const attachToCheckPreview = async ({
fullCus: FullCustomer;
}) => {
// 1. If check
let attachFunc = preview.func;
const attachFunc = preview.func;
if (
attachFunc == AttachFunction.AddProduct &&
attachFunc === AttachFunction.AddProduct &&
isFreeProduct(product.prices)
) {
return null;
}
const noOptions = !preview.options || preview.options.length === 0;
if (attachFunc == AttachFunction.CreateCheckout && noOptions) {
if (attachFunc === AttachFunction.CreateCheckout && noOptions) {
return null;
}
let scenario = await getAttachScenario({
const scenario = await getAttachScenario({
preview,
product,
});
let items = preview.due_today?.line_items?.map((item) => {
const items = preview.due_today?.line_items?.map((item) => {
return {
price: notNullish(item.amount)
? formatAmount({
@@ -73,21 +73,26 @@ export const attachToCheckPreview = async ({
};
});
let options = preview.options?.map((option: any) => {
const options = preview.options?.map((option: any) => {
return {
...option,
price: new Decimal(option.price).toDecimalPlaces(2).toNumber(),
};
});
let due_today = preview.due_today
const due_today = preview.due_today
? {
price: preview.due_today.total,
currency: org.default_currency || "usd",
}
: undefined;
let due_next_cycle = undefined;
let due_next_cycle:
| {
price: number;
currency: string;
}
| undefined;
if (preview.due_next_cycle) {
due_next_cycle = {
@@ -101,7 +106,7 @@ export const attachToCheckPreview = async ({
};
}
let checkPreview: CheckProductPreview = {
const checkPreview: CheckProductPreview = {
// title: "Check",
// message: "Check",
scenario,
@@ -158,10 +163,9 @@ export const getProductCheckPreview = async ({
};
const preview = await attachParamsToPreview({
req,
ctx: req as AutumnContext,
attachParams,
attachBody,
logger,
});
const checkPreview = await attachToCheckPreview({

View File

@@ -3,43 +3,42 @@ import {
AttachBranch,
AttachFunction,
cusProductToProduct,
notNullish,
} from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js";
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
import { getDowngradeProductPreview } from "./getDowngradeProductPreview.js";
import { getMultiAttachPreview } from "./getMultiAttachPreview.js";
import { getNewProductPreview } from "./getNewProductPreview.js";
import { getUpgradeProductPreview } from "./getUpgradeProductPreview.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import { getAttachBranch } from "@/internal/customers/attach/attachUtils/getAttachBranch.js";
import { getAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import { getAttachFunction } from "@/internal/customers/attach/attachUtils/getAttachFunction.js";
import { getDowngradeProductPreview } from "@/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.js";
import { getMultiAttachPreview } from "@/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.js";
import { getNewProductPreview } from "@/internal/customers/attach/handleAttachPreview/getNewProductPreview.js";
import { getUpgradeProductPreview } from "@/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv";
export const attachParamsToPreview = async ({
req,
ctx,
attachParams,
attachBody,
logger,
withPrepaid = false,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
attachBody: AttachBody;
logger: any;
withPrepaid?: boolean;
}) => {
const { logger } = ctx;
// Handle existing product
const branch = await getAttachBranch({
req,
ctx,
attachBody,
attachParams,
fromPreview: true,
});
const { flags, config } = await getAttachConfig({
req,
const { config } = await getAttachConfig({
ctx,
attachParams,
attachBody,
branch,
@@ -65,10 +64,9 @@ export const attachParamsToPreview = async ({
notNullish(attachParams.productsList)
) {
preview = await getMultiAttachPreview({
req,
ctx,
attachBody,
attachParams,
logger,
config,
branch,
});
@@ -103,7 +101,7 @@ export const attachParamsToPreview = async ({
func === AttachFunction.UpdatePrepaidQuantity
) {
preview = await getUpgradeProductPreview({
req,
ctx,
attachParams,
branch,
now,

View File

@@ -1,7 +1,9 @@
import { Hono } from "hono";
import type { HonoEnv } from "../../honoUtils/HonoEnv.js";
import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js";
import { handleSetupPayment } from "./handlers/handleSetupPayment.js";
export const billingRouter = new Hono<HonoEnv>();
billingRouter.post("/setup_payment", ...handleSetupPayment);
billingRouter.post("/checkout", ...handleCheckoutV2);

View File

@@ -0,0 +1,27 @@
import { AttachBranch, cusProductToPrices } from "@autumn/shared";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const getHasProrations = async ({
branch,
attachParams,
}: {
branch: AttachBranch;
attachParams: AttachParams;
}) => {
const { curMainProduct } = attachParamToCusProducts({ attachParams });
if (branch === AttachBranch.Upgrade) {
const curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
if (!isFreeProduct(curPrices)) {
return true;
}
}
if (branch === AttachBranch.UpdatePrepaidQuantity) {
return true;
}
return false;
};

View File

@@ -0,0 +1,205 @@
// import {
// type AttachBody,
// AttachBodySchema,
// AttachFunction,
// type FeatureOptions,
// } from "@autumn/shared";
// import { attachParamsToPreview } from "@/internal/billing/attachPreview/attachParamsToPreview.js";
// import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
// import { handleCreateInvoiceCheckout } from "@/internal/customers/add-product/handleCreateInvoiceCheckout.js";
// import {
// checkStripeConnections,
// handlePrepaidErrors,
// } from "@/internal/customers/attach/attachRouter.js";
// import { getAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/getAttachParams.js";
// import { attachParamsToProduct } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
// import { getAttachBranch } from "@/internal/customers/attach/attachUtils/getAttachBranch.js";
// import { getAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
// import { getAttachFunction } from "@/internal/customers/attach/attachUtils/getAttachFunction.js";
// import { handleCheckoutErrors } from "@/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.js";
// import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
// import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
// import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
// import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
// import type {
// ExtendedRequest,
// ExtendedResponse,
// } from "@/utils/models/Request.js";
// import { routeHandler } from "@/utils/routerUtils.js";
// import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
// import { getHasProrations } from "./getHasProrations.js";
// import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
// const getAttachVars = async ({
// req,
// attachBody,
// }: {
// req: ExtendedRequest;
// attachBody: AttachBody;
// }) => {
// const { attachParams } = await getAttachParams({
// req,
// attachBody,
// });
// const branch = await getAttachBranch({
// req,
// attachBody,
// attachParams,
// fromPreview: true,
// });
// const { flags, config } = await getAttachConfig({
// req,
// attachParams,
// attachBody,
// branch,
// });
// const func = await getAttachFunction({
// branch,
// attachParams,
// attachBody,
// config,
// });
// return {
// attachParams,
// flags,
// branch,
// config,
// func,
// };
// };
// const getCheckoutOptions = async ({
// req,
// attachParams,
// }: {
// req: ExtendedRequest;
// attachParams: AttachParams;
// }) => {
// const product = attachParamsToProduct({ attachParams });
// const prepaidPrices = product.prices.filter((p) =>
// isPrepaidPrice({ price: p }),
// );
// const newOptions: FeatureOptions[] = structuredClone(
// attachParams.optionsList,
// );
// for (const prepaidPrice of prepaidPrices) {
// const feature = priceToFeature({
// price: prepaidPrice,
// features: req.features,
// });
// const option = getPriceOptions(prepaidPrice, attachParams.optionsList);
// if (!option) {
// newOptions.push({
// feature_id: feature?.id ?? "",
// internal_feature_id: feature?.internal_id,
// quantity: 1,
// });
// }
// }
// attachParams.optionsList = newOptions;
// return newOptions;
// };
// export const handleCheckout = (req: any, res: any) =>
// routeHandler({
// req,
// res,
// action: "attach-preview",
// handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
// const { logger } = req;
// const attachBody = AttachBodySchema.parse(req.body);
// const { attachParams, branch, func, config } = await getAttachVars({
// req,
// attachBody,
// });
// let checkoutUrl = null;
// handleCheckoutErrors({
// attachParams,
// branch,
// });
// if (func === AttachFunction.CreateCheckout) {
// await checkStripeConnections({
// ctx: req as AutumnContext,
// attachParams,
// createCus: true,
// useCheckout: true,
// });
// await handlePrepaidErrors({
// attachParams,
// config,
// useCheckout: config.onlyCheckout,
// });
// if (config.invoiceCheckout) {
// const result = await handleCreateInvoiceCheckout({
// req,
// attachParams,
// attachBody,
// branch,
// config,
// });
// checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
// } else {
// const checkout = await handleCreateCheckout({
// req,
// res,
// attachParams,
// config,
// returnCheckout: true,
// });
// checkoutUrl = checkout?.url;
// }
// }
// console.log(`Branch: ${branch}, Func: ${func}`);
// await getCheckoutOptions({
// req,
// attachParams,
// });
// const preview = await attachParamsToPreview({
// req,
// attachParams,
// logger,
// attachBody,
// withPrepaid: true,
// });
// const checkoutRes = await previewToCheckoutRes({
// req,
// attachParams,
// preview,
// branch,
// });
// // Get has prorations
// const hasProrations = await getHasProrations({
// req,
// branch,
// attachParams,
// });
// res.status(200).json({
// ...checkoutRes,
// url: checkoutUrl,
// has_prorations: hasProrations,
// });
// return;
// },
// });

View File

@@ -0,0 +1,112 @@
import {
AffectedResource,
ApiVersion,
AttachFunction,
CheckoutParamsV0Schema,
} from "@autumn/shared";
import { createRoute } from "../../../honoMiddlewares/routeHandler";
import type { ExtendedRequest } from "../../../utils/models/Request";
import { handleCreateCheckout } from "../../customers/add-product/handleCreateCheckout";
import { handleCreateInvoiceCheckout } from "../../customers/add-product/handleCreateInvoiceCheckout";
import {
checkStripeConnections,
handlePrepaidErrors,
} from "../../customers/attach/attachRouter";
import { handleCheckoutErrors } from "../../customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors";
import { attachParamsToPreview } from "../attachPreview/attachParamsToPreview";
import { getHasProrations } from "./getHasProrations";
import { previewToCheckoutRes } from "./previewToCheckoutRes";
import { checkoutToAttachContext } from "./utils/checkoutToAttachContext";
import { getCheckoutOptions } from "./utils/getCheckoutOptions";
export const handleCheckoutV2 = createRoute({
versionedBody: {
latest: CheckoutParamsV0Schema,
[ApiVersion.V1_Beta]: CheckoutParamsV0Schema,
},
resource: AffectedResource.Checkout,
handler: async (c) => {
const ctx = c.get("ctx");
const body = c.req.valid("json");
const { attachParams, branch, func, config } =
await checkoutToAttachContext({
ctx,
checkoutParams: body,
});
let checkoutUrl = null;
handleCheckoutErrors({
attachParams,
branch,
});
if (func === AttachFunction.CreateCheckout) {
await checkStripeConnections({
ctx,
attachParams,
createCus: true,
useCheckout: true,
});
await handlePrepaidErrors({
attachParams,
config,
useCheckout: config.onlyCheckout,
});
if (config.invoiceCheckout) {
const result = await handleCreateInvoiceCheckout({
req: ctx as ExtendedRequest,
attachParams,
attachBody: body,
branch,
config,
});
checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
} else {
const checkout = await handleCreateCheckout({
req: ctx as ExtendedRequest,
attachParams,
config,
returnCheckout: true,
});
checkoutUrl = checkout?.url;
}
}
await getCheckoutOptions({
ctx,
attachParams,
});
const preview = await attachParamsToPreview({
ctx,
attachParams,
attachBody: body,
withPrepaid: true,
});
const checkoutRes = await previewToCheckoutRes({
req: ctx as ExtendedRequest,
attachParams,
preview,
branch,
});
// Get has prorations
const hasProrations = await getHasProrations({
branch,
attachParams,
});
return c.json({
...checkoutRes,
url: checkoutUrl,
has_prorations: hasProrations,
});
},
});

View File

@@ -1,8 +1,8 @@
import {
type AttachBranch,
type AttachPreview,
type CheckoutLine,
CheckoutResponseSchema,
type CheckoutLineV0,
CheckoutResponseV0Schema,
cusProductToEnts,
cusProductToPrices,
cusProductToProduct,
@@ -12,6 +12,11 @@ import {
UsageModel,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import {
attachParamsToProduct,
attachParamToCusProducts,
} from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
import {
@@ -20,11 +25,6 @@ import {
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToProduct,
attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
export const previewToCheckoutRes = async ({
req,
@@ -52,7 +52,7 @@ export const previewToCheckoutRes = async ({
const newEnts = attachParams.entitlements;
const allPrices = [...curPrices, ...newPrices];
const allEnts = [...curEnts, ...newEnts];
let lines: CheckoutLine[] = [];
let lines: CheckoutLineV0[] = [];
if (preview.due_today && preview.due_today.line_items.length > 0) {
lines = preview.due_today.line_items
@@ -77,7 +77,7 @@ export const previewToCheckoutRes = async ({
}),
};
})
.filter(notNullish) as CheckoutLine[];
.filter(notNullish) as CheckoutLineV0[];
}
const curProduct = curCusProduct
@@ -172,7 +172,7 @@ export const previewToCheckoutRes = async ({
})
.filter(notNullish);
return CheckoutResponseSchema.parse({
return CheckoutResponseV0Schema.parse({
customer_id: attachParams.customer.id,
lines,
product: newProduct,

View File

@@ -0,0 +1,61 @@
import type {
AttachBranch,
AttachConfig,
AttachFunction,
CheckoutParamsV0,
} from "@autumn/shared";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
import { getAttachParams } from "../../../customers/attach/attachUtils/attachParams/getAttachParams";
import { getAttachBranch } from "../../../customers/attach/attachUtils/getAttachBranch";
import { getAttachConfig } from "../../../customers/attach/attachUtils/getAttachConfig";
import { getAttachFunction } from "../../../customers/attach/attachUtils/getAttachFunction";
import type { AttachFlags } from "../../../customers/attach/models/AttachFlags";
import type { AttachParams } from "../../../customers/cusProducts/AttachParams";
export const checkoutToAttachContext = async ({
ctx,
checkoutParams,
}: {
ctx: AutumnContext;
checkoutParams: CheckoutParamsV0;
}): Promise<{
attachParams: AttachParams;
flags: AttachFlags;
branch: AttachBranch;
config: AttachConfig;
func: AttachFunction;
}> => {
const { attachParams } = await getAttachParams({
ctx,
attachBody: checkoutParams,
});
const branch = await getAttachBranch({
ctx,
attachBody: checkoutParams,
attachParams,
fromPreview: true,
});
const { flags, config } = await getAttachConfig({
ctx,
attachParams,
attachBody: checkoutParams,
branch,
});
const func = await getAttachFunction({
branch,
attachParams,
attachBody: checkoutParams,
config,
});
return {
attachParams,
flags,
branch,
config,
func,
};
};

View File

@@ -0,0 +1,40 @@
import { type FeatureOptions, isPrepaidPrice } from "@autumn/shared";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
import { attachParamsToProduct } from "../../../customers/attach/attachUtils/convertAttachParams";
import type { AttachParams } from "../../../customers/cusProducts/AttachParams";
import { getPriceOptions } from "../../../products/prices/priceUtils";
import { priceToFeature } from "../../../products/prices/priceUtils/convertPrice";
export const getCheckoutOptions = async ({
ctx,
attachParams,
}: {
ctx: AutumnContext;
attachParams: AttachParams;
}) => {
const product = attachParamsToProduct({ attachParams });
const prepaidPrices = product.prices.filter((p) =>
isPrepaidPrice({ price: p }),
);
const newOptions: FeatureOptions[] = structuredClone(
attachParams.optionsList,
);
for (const prepaidPrice of prepaidPrices) {
const feature = priceToFeature({
price: prepaidPrice,
features: ctx.features,
});
const option = getPriceOptions(prepaidPrice, attachParams.optionsList);
if (!option) {
newOptions.push({
feature_id: feature?.id ?? "",
internal_feature_id: feature?.internal_id,
quantity: 1,
});
}
}
attachParams.optionsList = newOptions;
return newOptions;
};

View File

@@ -27,7 +27,7 @@ export const handleCreateCheckout = async ({
returnCheckout = false,
}: {
req: any;
res: any;
res?: any;
attachParams: AttachParams;
config: AttachConfig;
returnCheckout?: boolean;
@@ -182,7 +182,7 @@ export const handleCreateCheckout = async ({
}
}
if (returnCheckout) {
if (returnCheckout || !res) {
return checkout;
}

View File

@@ -1,22 +1,19 @@
import {
AttachParams,
AttachResultSchema,
} from "../cusProducts/AttachParams.js";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
import {
AttachBody,
AttachBranch,
AttachConfig,
type AttachBody,
type AttachBranch,
type AttachConfig,
SuccessCode,
} from "@autumn/shared";
import Stripe from "stripe";
import type Stripe from "stripe";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
import { handleMultiAttachFlow } from "../attach/attachFunctions/multiAttach/handleMultiAttachFlow.js";
import {
type AttachParams,
AttachResultSchema,
} from "../cusProducts/AttachParams.js";
export const handleCreateInvoiceCheckout = async ({
req,

View File

@@ -26,7 +26,7 @@ import {
} from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish, nullOrUndefined } from "@/utils/genUtils.js";
import { handleCheckout } from "./checkout/handleCheckout.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { handleAttach } from "./handleAttach.js";
import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js";
@@ -125,18 +125,18 @@ export const handlePublicAttachErrors = async ({
};
export const checkStripeConnections = async ({
req,
ctx,
attachParams,
createCus = true,
useCheckout = false,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
createCus?: boolean;
useCheckout?: boolean;
}) => {
const { org, customer, products, stripeCus, stripeCli } = attachParams;
const logger = req.logger;
const { logger, db } = ctx;
const env = customer.env;
// 2. If invoice only and no email, save email
@@ -144,7 +144,7 @@ export const checkStripeConnections = async ({
customer.email = `${customer.id}-${org.id}@invoices.useautumn.com`;
await Promise.all([
CusService.update({
db: req.db,
db,
idOrInternalId: customer.internal_id,
orgId: org.id,
env,
@@ -164,7 +164,7 @@ export const checkStripeConnections = async ({
if (createCus) {
batchProductUpdates.push(
createStripeCusIfNotExists({
db: req.db,
db,
org,
env,
customer,
@@ -176,7 +176,7 @@ export const checkStripeConnections = async ({
for (const product of products) {
batchProductUpdates.push(
checkStripeProductExists({
db: req.db,
db,
org,
env,
product,
@@ -189,23 +189,24 @@ export const checkStripeConnections = async ({
await createStripePrices({
attachParams,
useCheckout,
req,
ctx,
logger,
});
};
export const createStripePrices = async ({
ctx,
attachParams,
useCheckout,
req,
logger,
}: {
ctx: AutumnContext;
attachParams: AttachParams;
useCheckout: boolean;
req: any;
logger: any;
}) => {
const { prices, entitlements, products, org, stripeCli } = attachParams;
const { db } = ctx;
const batchPriceUpdates = [];
@@ -216,7 +217,7 @@ export const createStripePrices = async ({
batchPriceUpdates.push(
createStripePriceIFNotExist({
db: req.db,
db,
stripeCli,
price,
entitlements,
@@ -250,4 +251,4 @@ export const customerHasPm = async ({
attachRouter.post("/attach", handleAttach);
attachRouter.post("/attach/preview", handleAttachPreview);
attachRouter.post("/checkout", handleCheckout);
// attachRouter.post("/checkout", handleCheckout);

View File

@@ -11,16 +11,15 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../../honoUtils/HonoEnv";
import { getExistingCusProducts } from "../../../../cusProducts/cusProductUtils/getExistingCusProducts";
const getProductsForAttach = async ({
req,
ctx,
attachBody,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
}) => {
const {
@@ -31,9 +30,9 @@ const getProductsForAttach = async ({
} = attachBody;
const products = await ProductService.listFull({
db: req.db,
orgId: req.orgId,
env: req.env,
db: ctx.db,
orgId: ctx.org.id,
env: ctx.env,
inIds: inputProducts
? inputProducts.map((p) => p.product_id)
: product_ids || [product_id!],
@@ -74,15 +73,15 @@ const getProductsForAttach = async ({
};
export const getCustomerAndProducts = async ({
req,
ctx,
attachBody,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
}) => {
const [customer, products] = await Promise.all([
getOrCreateCustomer({
ctx: req as unknown as AutumnContext,
ctx,
customerId: attachBody.customer_id,
customerData: {
...attachBody.customer_data,
@@ -96,7 +95,7 @@ export const getCustomerAndProducts = async ({
entityId: attachBody.entity_id || undefined,
entityData: attachBody.entity_data || undefined,
}),
getProductsForAttach({ req, attachBody }),
getProductsForAttach({ ctx, attachBody }),
]);
// if customer is on product v3, products[0] should just be the customer's product if version isn't explicitly passed in.
@@ -109,13 +108,11 @@ export const getCustomerAndProducts = async ({
internalEntityId: customer.entity?.internal_id,
});
// console.log(
// `Product ${product.id} (${product.version}), curSameProduct: ${curSameProduct?.product.id} (version: ${curSameProduct?.product.version})`,
// );
if (curSameProduct) {
products[i] = cusProductToProduct({ cusProduct: curSameProduct });
}
}
}
return { customer, products };
};

View File

@@ -16,22 +16,22 @@ import {
} from "@/internal/products/free-trials/freeTrialUtils.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../../honoUtils/HonoEnv.js";
import { mapOptionsList } from "../../mapOptionsList.js";
export const getPricesAndEnts = async ({
req,
ctx,
attachBody,
customer,
products,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
customer: FullCustomer;
products: FullProduct[];
}) => {
const { options: optionsInput, is_custom, free_trial } = attachBody;
const { features, db, org, logger } = req;
const { features, db, org, logger } = ctx;
const { curMainProduct, curSameProduct } = getExistingCusProducts({
product: products[0],

View File

@@ -1,14 +1,15 @@
import type { AttachBody } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import type { ExtendedRequest } from "../../../../../utils/models/Request.js";
import type { AttachParams } from "../../../cusProducts/AttachParams.js";
import { processAttachBody } from "./processAttachBody.js";
export const getAttachParams = async ({
req,
ctx,
attachBody,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
}) => {
const {
@@ -23,7 +24,7 @@ export const getAttachParams = async ({
stripeVars,
rewards,
} = await processAttachBody({
req,
ctx,
attachBody,
});
@@ -51,10 +52,11 @@ export const getAttachParams = async ({
replaceables: [],
rewards,
// From req
req,
org: req.org,
req: ctx as ExtendedRequest,
org: ctx.org,
entities: customer.entities,
features: req.features,
features: ctx.features,
internalEntityId,
entityId: entityId || undefined,
cusProducts: customer.customer_products,
@@ -72,7 +74,7 @@ export const getAttachParams = async ({
setupPayment: attachBody.setup_payment,
// Others
apiVersion: req.apiVersion.value,
apiVersion: ctx.apiVersion.value,
};
return {

View File

@@ -3,17 +3,17 @@ import type Stripe from "stripe";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
import { getCustomerAndProducts } from "./attachParamsUtils/getCusAndProducts.js";
import { getPricesAndEnts } from "./attachParamsUtils/getPricesAndEnts.js";
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
export const getRewards = async ({
req,
ctx,
attachBody,
stripeCli,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
stripeCli: Stripe;
}) => {
@@ -31,10 +31,10 @@ export const getRewards = async ({
// 1. Get reward by id or promo code
const rewards = await RewardService.getByIdOrCode({
db: req.db,
db: ctx.db,
codes: rewardArray,
orgId: req.org.id,
env: req.env,
orgId: ctx.org.id,
env: ctx.env,
});
for (const reward of rewardArray) {
@@ -62,33 +62,33 @@ export const getRewards = async ({
};
export const processAttachBody = async ({
req,
ctx,
attachBody,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
}) => {
// 1. Get customer and products
const { org, env, logger } = req;
const { org, env, logger } = ctx;
const stripeCli = createStripeCli({ org, env });
const { customer, products } = await getCustomerAndProducts({
req,
ctx,
attachBody,
});
const [stripeCusData, rewardData] = await Promise.all([
getStripeCusData({
stripeCli,
db: req.db,
db: ctx.db,
org,
env,
customer,
logger,
}),
getRewards({
req,
ctx,
attachBody,
stripeCli,
}),
@@ -104,7 +104,7 @@ export const processAttachBody = async ({
customPrices,
customEnts,
} = await getPricesAndEnts({
req,
ctx,
attachBody,
customer,
products,

View File

@@ -19,7 +19,7 @@ import {
isProductUpgrade,
} from "@/internal/products/productUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { isMainTrialBranch } from "./attachUtils.js";
@@ -291,12 +291,13 @@ const getChangeProductBranch = async ({
};
export const getAttachBranch = async ({
req,
// biome-ignore lint/correctness/noUnusedFunctionParameters: might be used in the future
ctx,
attachBody,
attachParams,
fromPreview,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
attachParams: AttachParams;
fromPreview?: boolean;

View File

@@ -10,6 +10,7 @@ import {
import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { willMergeSub } from "../mergeUtils/mergeUtils.js";
import type { AttachFlags } from "../models/AttachFlags.js";
@@ -55,12 +56,12 @@ export const intervalsAreSame = ({
};
export const getAttachConfig = async ({
req,
ctx,
attachParams,
attachBody,
branch,
}: {
req: any;
ctx: AutumnContext;
attachParams: AttachParams;
attachBody: AttachBody;
branch: AttachBranch;
@@ -68,7 +69,7 @@ export const getAttachConfig = async ({
const { org, prices, paymentMethod } = attachParams;
const flags: AttachFlags = {
isPublic: req.isPublic,
isPublic: ctx.isPublic,
forceCheckout: attachBody.force_checkout || false,
invoiceOnly: attachParams.invoiceOnly || false,
isFree: isFreeProduct(prices),

View File

@@ -1,36 +0,0 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachBranch } from "@autumn/shared";
import {
attachParamsToCurCusProduct,
attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const getHasProrations = async ({
req,
branch,
attachParams,
}: {
req: ExtendedRequest;
branch: AttachBranch;
attachParams: AttachParams;
}) => {
let hasProrations = false;
let { curMainProduct } = attachParamToCusProducts({ attachParams });
if (branch == AttachBranch.Upgrade) {
let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
if (!isFreeProduct(curPrices)) {
return true;
}
}
if (branch == AttachBranch.UpdatePrepaidQuantity) {
return true;
}
return false;
};

View File

@@ -1,204 +0,0 @@
import {
type AttachBody,
AttachBodySchema,
AttachFunction,
type FeatureOptions,
} from "@autumn/shared";
import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js";
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { getPriceOptions } from "@/internal/products/prices/priceUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import {
checkStripeConnections,
handlePrepaidErrors,
} from "../attachRouter.js";
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
import { attachParamsToProduct } from "../attachUtils/convertAttachParams.js";
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
import { handleCheckoutErrors } from "../attachUtils/handleAttachErrors/handleCheckoutErrors.js";
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
import { getHasProrations } from "./getHasProrations.js";
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
const getAttachVars = async ({
req,
attachBody,
}: {
req: ExtendedRequest;
attachBody: AttachBody;
}) => {
const { attachParams } = await getAttachParams({
req,
attachBody,
});
const branch = await getAttachBranch({
req,
attachBody,
attachParams,
fromPreview: true,
});
const { flags, config } = await getAttachConfig({
req,
attachParams,
attachBody,
branch,
});
const func = await getAttachFunction({
branch,
attachParams,
attachBody,
config,
});
return {
attachParams,
flags,
branch,
config,
func,
};
};
const getCheckoutOptions = async ({
req,
attachParams,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
}) => {
const product = attachParamsToProduct({ attachParams });
const prepaidPrices = product.prices.filter((p) =>
isPrepaidPrice({ price: p }),
);
const newOptions: FeatureOptions[] = structuredClone(
attachParams.optionsList,
);
for (const prepaidPrice of prepaidPrices) {
const feature = priceToFeature({
price: prepaidPrice,
features: req.features,
});
const option = getPriceOptions(prepaidPrice, attachParams.optionsList);
if (!option) {
newOptions.push({
feature_id: feature?.id ?? "",
internal_feature_id: feature?.internal_id,
quantity: 1,
});
}
}
attachParams.optionsList = newOptions;
return newOptions;
};
export const handleCheckout = (req: any, res: any) =>
routeHandler({
req,
res,
action: "attach-preview",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { logger, features } = req;
const attachBody = AttachBodySchema.parse(req.body);
const { attachParams, branch, func, config } = await getAttachVars({
req,
attachBody,
});
let checkoutUrl = null;
handleCheckoutErrors({
attachParams,
branch,
});
if (func === AttachFunction.CreateCheckout) {
await checkStripeConnections({
req,
attachParams,
createCus: true,
useCheckout: true,
});
await handlePrepaidErrors({
attachParams,
config,
useCheckout: config.onlyCheckout,
});
if (config.invoiceCheckout) {
const result = await handleCreateInvoiceCheckout({
req,
attachParams,
attachBody,
branch,
config,
});
checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
} else {
const checkout = await handleCreateCheckout({
req,
res,
attachParams,
config,
returnCheckout: true,
});
checkoutUrl = checkout?.url;
}
}
console.log(`Branch: ${branch}, Func: ${func}`);
await getCheckoutOptions({
req,
attachParams,
});
const preview = await attachParamsToPreview({
req,
attachParams,
logger,
attachBody,
withPrepaid: true,
});
const checkoutRes = await previewToCheckoutRes({
req,
attachParams,
preview,
branch,
});
// Get has prorations
const hasProrations = await getHasProrations({
req,
branch,
attachParams,
});
res.status(200).json({
...checkoutRes,
url: checkoutUrl,
has_prorations: hasProrations,
});
return;
},
});

View File

@@ -5,6 +5,7 @@ import type {
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
import { checkStripeConnections } from "./attachRouter.js";
import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js";
import { getAttachBranch } from "./attachUtils/getAttachBranch.js";
@@ -23,20 +24,22 @@ export const handleAttach = async (req: any, res: any) =>
const attachBody = AttachBodySchema.parse(req.body);
const ctx = req as AutumnContext;
const { attachParams, customPrices, customEnts } = await getAttachParams({
req,
ctx,
attachBody,
});
// Handle existing product
const branch = await getAttachBranch({
req,
ctx,
attachBody,
attachParams,
});
const { flags, config } = await getAttachConfig({
req,
ctx,
attachParams,
attachBody,
branch,
@@ -51,7 +54,7 @@ export const handleAttach = async (req: any, res: any) =>
});
await checkStripeConnections({
req,
ctx,
attachParams,
useCheckout: config.onlyCheckout,
});
@@ -82,7 +85,7 @@ export const handleAttach = async (req: any, res: any) =>
freeTrial: attachParams.freeTrial,
},
});
} catch (error) {}
} catch (_error) {}
await runAttachFunction({
req,

View File

@@ -1,6 +1,7 @@
import {
type AttachBody,
type AttachBranch,
type AttachConfig,
cusProductsToPrices,
type PreviewLineItem,
} from "@autumn/shared";
@@ -9,7 +10,7 @@ import type Stripe from "stripe";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import { getAddAndRemoveProducts } from "../attachFunctions/multiAttach/getAddAndRemoveProducts.js";
import { priceToNewPreviewItem } from "../attachPreviewUtils/priceToNewPreviewItem.js";
@@ -18,18 +19,17 @@ import { getCustomerSub } from "../attachUtils/convertAttachParams.js";
import { handleMultiAttachErrors } from "../attachUtils/handleAttachErrors/handleMultiAttachErrors.js";
export const getMultiAttachPreview = async ({
req,
// biome-ignore lint/correctness/noUnusedFunctionParameters: might be used in the future
ctx,
attachBody,
attachParams,
logger,
config,
branch,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachBody: AttachBody;
attachParams: AttachParams;
logger: any;
config: any;
config: AttachConfig;
branch: AttachBranch;
}) => {
await handleMultiAttachErrors({ attachParams, attachBody, branch });

View File

@@ -12,6 +12,7 @@ import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingInter
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import type { Logger } from "../../../../external/logtail/logtailUtils.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToProduct,
@@ -86,7 +87,7 @@ export const getNewProductPreview = async ({
}: {
branch: AttachBranch;
attachParams: AttachParams;
logger: any;
logger: Logger;
config: AttachConfig;
withPrepaid?: boolean;
}) => {

View File

@@ -25,7 +25,7 @@ import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePrice
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { nullish } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
import {
attachParamsToProduct,
@@ -132,21 +132,21 @@ const filterNoProratePrepaidItems = ({
};
export const getUpgradeProductPreview = async ({
req,
ctx,
attachParams,
branch,
now,
withPrepaid = false,
config,
}: {
req: ExtendedRequest;
ctx: AutumnContext;
attachParams: AttachParams;
branch: AttachBranch;
now: number;
withPrepaid?: boolean;
config: AttachConfig;
}) => {
const { logger } = req;
const { logger } = ctx;
const { curMainProduct, curSameProduct } = attachParamToCusProducts({
attachParams,

View File

@@ -4,30 +4,29 @@ import type {
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { attachParamsToPreview } from "../../../billing/attachPreview/attachParamsToPreview.js";
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
import { attachParamsToPreview } from "./attachParamsToPreview.js";
export const handleAttachPreview = (req: any, res: any) =>
routeHandler({
req,
res,
action: "attach-preview",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { logger } = req;
const attachBody = AttachBodySchema.parse(req.body);
// console.log("attachBody", attachBody);
const ctx = req as AutumnContext;
const { attachParams } = await getAttachParams({
req,
ctx,
attachBody,
});
const attachPreview = await attachParamsToPreview({
req,
ctx,
attachParams,
attachBody,
logger,
});
res.status(200).json(attachPreview);

View File

@@ -54,7 +54,13 @@ export const handleMigrateProductV2 = createRoute({
env,
});
if (currentMigrations.length > 0) {
if (
currentMigrations.find(
(m) =>
m.from_internal_product_id === fromProduct.internal_id &&
m.to_internal_product_id === toProduct.internal_id,
)
) {
throw new RecaseError({
message: "Another migration is ongoing, cannot create a new migration",
});

View File

@@ -6,13 +6,15 @@ import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCo
import { expectInvoiceAfterUsage } from "@tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
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 { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils";
import { hoursToFinalizeInvoice } from "../../utils/constants";
import { advanceTestClock } from "../../utils/stripeUtils";
const testCase = "aentity2";
@@ -115,9 +117,20 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to
});
test("should have correct invoice after cycle", async () => {
curUnix = await advanceToNextInvoice({
// curUnix = await advanceToNextInvoice({
// stripeCli: ctx.stripeCli,
// testClockId,
// });
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
});
curUnix = await advanceTestClock({
stripeCli: ctx.stripeCli,
testClockId,
advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
});
await expectInvoiceAfterUsage({

View File

@@ -149,7 +149,6 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro diff enti
product: pro,
});
});
return;
const entity2Usage = Math.random() * 1000000;
test("should track usage on entity 2", async () => {

View File

@@ -1,20 +1,24 @@
import { type Customer, LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import {
type Customer,
LegacyVersion,
OnDecrease,
OnIncrease,
} from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const testCase = "prepaid1";
@@ -154,9 +158,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
});
const autumnCus = await autumn.customers.get(customerId);
expect(autumnCus.features[TestFeature.Messages].balance).toBe(
newQuantity,
);
expect(autumnCus.features[TestFeature.Messages].balance).toBe(newQuantity);
expect(autumnCus.invoices.length).toBe(3);
expect(autumnCus.invoices[0].total).toBe((newQuantity / 100) * 12.5);

View File

@@ -0,0 +1,19 @@
// import { z } from "zod/v4";
// import {
// AttachBodySchema,
// ExtAttachBodySchema,
// } from "../../core/attachModels.js";
// export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({
// setup_payment: z.boolean().optional(),
// }).meta({
// description:
// "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
// });
// export const CheckoutParamsSchema = AttachBodySchema.extend({
// // skip_checkout: z.boolean().optional(),
// setup_payment: z.boolean().optional(),
// });
// export type CheckoutParams = z.infer<typeof CheckoutParamsSchema>;

View File

@@ -0,0 +1,32 @@
// import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
// import { z } from "zod/v4";
// import { ApiPlanSchema } from "../../products/apiPlan.js";
// // RESULT
// 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),
// plan: ApiPlanSchema.nullish(),
// current_plan: ApiPlanSchema.nullish(),
// options: z.array(FeatureOptionsSchema).nullish(),
// total: z.number().nullish(),
// currency: z.string().nullish(),
// has_prorations: z.boolean().nullish(),
// next_cycle: z
// .object({
// starts_at: z.number(),
// total: z.number(),
// })
// .nullish(),
// });
// export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;

View File

@@ -0,0 +1,43 @@
import {
AttachBodySchema,
ExtAttachBodySchema,
} from "@api/core/attachModels.js";
import { z } from "zod/v4";
export const ExtCheckoutParamsV0Schema = ExtAttachBodySchema.extend({
setup_payment: z.boolean().optional(),
}).meta({
description:
"Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
});
export const CheckoutParamsV0Schema = AttachBodySchema.extend({
setup_payment: z.boolean().optional(),
});
// // RESULT
// export const CheckoutLineV0Schema = z.object({
// description: z.string(),
// amount: z.number(),
// item: ApiProductItemSchema.nullish(),
// });
// export const CheckoutResponseV0Schema = z.object({
// url: z.string().nullish(),
// customer_id: z.string(),
// lines: z.array(CheckoutLineV0Schema),
// 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: z
// .object({
// starts_at: z.number(),
// total: z.number(),
// })
// .nullish(),
// });
export type CheckoutParamsV0 = z.infer<typeof CheckoutParamsV0Schema>;

View File

@@ -4,16 +4,16 @@ import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.
import { z } from "zod/v4";
// RESULT
export const CheckoutLineSchema = z.object({
export const CheckoutLineV0Schema = z.object({
description: z.string(),
amount: z.number(),
item: ApiProductItemSchema.nullish(),
});
export const CheckoutResponseSchema = z.object({
export const CheckoutResponseV0Schema = z.object({
url: z.string().nullish(),
customer_id: z.string(),
lines: z.array(CheckoutLineSchema),
lines: z.array(CheckoutLineV0Schema),
product: ApiProductSchema.nullish(),
current_product: ApiProductSchema.nullish(),
@@ -30,4 +30,5 @@ export const CheckoutResponseSchema = z.object({
.nullish(),
});
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
export type CheckoutLineV0 = z.infer<typeof CheckoutLineV0Schema>;
export type CheckoutResponseV0 = z.infer<typeof CheckoutResponseV0Schema>;

View File

@@ -1,45 +1,45 @@
import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js";
import { ApiProductSchema } from "@api/products/previousVersions/apiProduct.js";
import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
import { z } from "zod/v4";
import { AttachBodySchema, ExtAttachBodySchema } from "./attachModels.js";
// import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js";
// import { ApiProductSchema } from "@api/products/previousVersions/apiProduct.js";
// import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
// import { z } from "zod/v4";
// import { AttachBodySchema, ExtAttachBodySchema } from "./attachModels.js";
export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({
setup_payment: z.boolean().optional(),
}).meta({
description:
"Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
});
// export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({
// setup_payment: z.boolean().optional(),
// }).meta({
// description:
// "Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
// });
export const CheckoutParamsSchema = AttachBodySchema.extend({
// skip_checkout: z.boolean().optional(),
setup_payment: z.boolean().optional(),
});
// export const CheckoutParamsSchema = AttachBodySchema.extend({
// // skip_checkout: z.boolean().optional(),
// setup_payment: z.boolean().optional(),
// });
// RESULT
export const CheckoutLineSchema = z.object({
description: z.string(),
amount: z.number(),
item: ApiProductItemSchema.nullish(),
});
// // RESULT
// 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: z
.object({
starts_at: z.number(),
total: z.number(),
})
.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: z
// .object({
// starts_at: z.number(),
// total: z.number(),
// })
// .nullish(),
// });
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
export type CheckoutParams = z.infer<typeof CheckoutParamsSchema>;
// export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
// export type CheckoutParams = z.infer<typeof CheckoutParamsSchema>;

View File

@@ -19,6 +19,14 @@ export const ApiSubscriptionSchema = z.object({
current_period_start: z.number().nullable(),
current_period_end: z.number().nullable(),
quantity: z.number(),
// feature_quantities: z.array(
// z.object({
// feature_id: z.string(),
// quantity: z.number(),
// upcoming_quantity: z.number().nullable(),
// }),
// ),
});
export type ApiSubscription = z.infer<typeof ApiSubscriptionSchema>;

View File

@@ -1,6 +1,6 @@
// Core
export * from "./core/attachModels.js";
export * from "./core/checkoutModels.js";
// NOTE: coreOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
export * from "./core/coreOpModels.js";
@@ -70,6 +70,10 @@ export * from "./balances/track/trackParams.js";
export * from "./balances/track/trackResponseV2.js";
export * from "./balances/track/trackTypes/pgDeductionUpdate.js";
export * from "./balances/usageModels.js";
export * from "./billing/checkout/prevVersions/checkoutParamsV0.js";
export * from "./billing/checkout/prevVersions/checkoutParamsV0.js";
export * from "./billing/checkout/prevVersions/checkoutResponseV0.js";
export * from "./common/customerData.js";
export * from "./common/entityData.js";
export * from "./customers/cusFeatures/cusFeatureLegacyData.js";

View File

@@ -1,19 +0,0 @@
import { z } from "zod/v4";
import {
AttachBodySchema,
ExtAttachBodySchema,
} from "../../core/attachModels.js";
export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({
setup_payment: z.boolean().optional(),
}).meta({
description:
"Returns a Stripe Checkout URL for the customer to make a payment, or returns payment confirmation information.",
});
export const CheckoutParamsSchema = AttachBodySchema.extend({
// skip_checkout: z.boolean().optional(),
setup_payment: z.boolean().optional(),
});
export type CheckoutParams = z.infer<typeof CheckoutParamsSchema>;

View File

@@ -1,32 +0,0 @@
import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
import { z } from "zod/v4";
import { ApiPlanSchema } from "../../products/apiPlan.js";
// RESULT
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),
plan: ApiPlanSchema.nullish(),
current_plan: ApiPlanSchema.nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
total: z.number().nullish(),
currency: z.string().nullish(),
has_prorations: z.boolean().nullish(),
next_cycle: z
.object({
starts_at: z.number(),
total: z.number(),
})
.nullish(),
});
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;

View File

@@ -15,6 +15,7 @@ export enum AffectedResource {
Feature = "feature",
Check = "check",
Track = "track",
Checkout = "checkout",
// Add more as needed
}