feat: adding checkout router

This commit is contained in:
John Yeo
2025-07-10 15:29:32 +01:00
parent 8cfc4405f4
commit b6370a1335
10 changed files with 159 additions and 304 deletions

View File

@@ -126,8 +126,10 @@ export const handleSubscriptionUpdated = async ({
subscription.latest_invoice,
);
logger.info("Latest invoice billing reason", latestInvoice.billing_reason);
logger.info("Latest invoice status", latestInvoice.status);
logger.info(
`Latest invoice billing reason: ${latestInvoice.billing_reason}`,
);
logger.info(`Latest invoice status: ${latestInvoice.status}`);
if (
latestInvoice.status !== "open" ||

View File

@@ -46,7 +46,7 @@ apiRouter.use("/referrals", referralRouter);
apiRouter.use("/redemptions", redemptionRouter);
// Cus Product
apiRouter.use("/attach", attachRouter);
apiRouter.use("", attachRouter);
apiRouter.use("/cancel", expireRouter);
apiRouter.use("/entitled", checkRouter);
apiRouter.use("/check", checkRouter);

View File

@@ -21,14 +21,6 @@ import { notNullish } from "@/utils/genUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
const getNextCycle = (preview: AttachPreview) => {
if (!preview.due_next_cycle && !preview.due_today) {
return undefined;
}
if (!preview.due_today && preview.due_next_cycle) {
}
};
export const attachToCheckPreview = async ({
preview,
params,

View File

@@ -20,10 +20,12 @@ export const handleCreateCheckout = async ({
req,
res,
attachParams,
returnCheckout = false,
}: {
req: any;
res: any;
attachParams: AttachParams;
returnCheckout?: boolean;
}) => {
const { db, logtail: logger } = req;
@@ -113,6 +115,10 @@ export const handleCreateCheckout = async ({
logger.info(`✅ Successfully created checkout for customer ${customer.id}`);
if (returnCheckout) {
return checkout;
}
let apiVersion = attachParams.apiVersion || APIVersion.v1;
if (apiVersion >= APIVersion.v1_1) {
res.status(200).json(

View File

@@ -33,6 +33,8 @@ import { AttachBodySchema } from "./models/AttachBody.js";
import { processAttachBody } from "./attachUtils/attachParams/processAttachBody.js";
import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js";
import { handleAttach } from "./handleAttach.js";
import { handleCheckout } from "./checkout/handleCheckout.js";
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
export const attachRouter: Router = Router();
@@ -128,9 +130,13 @@ export const handlePublicAttachErrors = async ({
export const checkStripeConnections = async ({
req,
attachParams,
createCus = true,
useCheckout = false,
}: {
req: any;
attachParams: AttachParams;
createCus?: boolean;
useCheckout?: boolean;
}) => {
const { org, customer, products, stripeCus, stripeCli } = attachParams;
const logger = req.logtail;
@@ -154,15 +160,18 @@ export const checkStripeConnections = async ({
]);
}
const batchProductUpdates = [
// createStripeCusIfNotExists({
// db: req.db,
// org,
// env,
// customer,
// logger,
// }),
];
const batchProductUpdates = [];
if (createCus) {
batchProductUpdates.push(
createStripeCusIfNotExists({
db: req.db,
org,
env,
customer,
logger,
}),
);
}
for (const product of products) {
batchProductUpdates.push(
checkStripeProductExists({
@@ -175,6 +184,13 @@ export const checkStripeConnections = async ({
);
}
await Promise.all(batchProductUpdates);
await createStripePrices({
attachParams,
useCheckout,
req,
logger,
});
};
export const createStripePrices = async ({
@@ -228,205 +244,6 @@ export const customerHasPm = async ({
return notNullOrUndefined(paymentMethod) ? true : false;
};
const handleAttachOld = async (req: any, res: any) =>
routeHandler({
action: "attach",
req,
res,
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
await handleAttachRaceCondition({ req, res });
const attachBody = AttachBodySchema.parse(req.body);
const logger = req.logtail;
// PUBLIC STUFF
let forceCheckout = req.isPublic || attachBody.force_checkout || false;
let isCustom = attachBody.is_custom || false;
if (req.isPublic) {
isCustom = false;
}
logger.info("--------------------------------");
const {
customer,
products,
optionsList,
prices,
entitlements,
freeTrial,
} = await processAttachBody({
req,
attachBody,
});
const apiVersion =
orgToVersion({
org: req.org,
reqApiVersion: req.apiVersion,
}) || APIVersion.v1;
const internalEntityId = attachBody.entity_id
? customer.entities.find(
(e) =>
e.id === attachBody.entity_id ||
e.internal_id === attachBody.entity_id,
)?.internal_id
: undefined;
const stripeCli = createStripeCli({ org: req.org, env: req.env });
const paymentMethod = await getCusPaymentMethod({
stripeCli,
stripeId: customer.processor?.id,
});
// const attachParams: AttachParams = {
// stripeCli,
// paymentMethod,
// customer,
// products,
// optionsList,
// prices,
// entitlements,
// freeTrial,
// // From req
// req,
// org: req.org,
// entities: customer.entities,
// features: req.features,
// internalEntityId,
// cusProducts: customer.customer_products,
// // Others
// apiVersion,
// successUrl: attachBody.success_url,
// invoiceOnly: attachBody.invoice_only,
// billingAnchor: attachBody.billing_cycle_anchor,
// metadata: attachBody.metadata,
// disableFreeTrial: attachBody.free_trial === false || false,
// checkoutSessionParams: attachBody.checkout_session_params,
// isCustom,
// };
// attachParams.apiVersion =
// orgToVersion({
// org,
// reqApiVersion: req.apiVersion,
// }) || APIVersion.v1;
// attachParams.req = req;
// attachParams.successUrl = attachBody.success_url;
// attachParams.invoiceOnly = attachBody.invoice_only;
// attachParams.billingAnchor = attachBody.billing_cycle_anchor;
// attachParams.metadata = attachBody.metadata;
// attachParams.isCustom = isCustom || false;
// attachParams.disableFreeTrial = attachBody.free_trial === false || false;
// attachParams.checkoutSessionParams = checkout_session_params;
// logger.info(
// `Customer: ${chalk.yellow(
// `${attachParams.customer.id} (${attachParams.customer.name})`,
// )}, Products: ${chalk.yellow(
// attachParams.products.map((p) => p.id).join(", "),
// )}`,
// );
// // 3. Check for stripe connection
// let hasPm = await customerHasPm({ attachParams });
// const useCheckout = !hasPm || forceCheckout;
// await createStripePrices({
// attachParams,
// useCheckout,
// req,
// logger,
// });
// logger.info(
// `Has PM: ${chalk.yellow(hasPm)}, Force Checkout: ${chalk.yellow(
// forceCheckout,
// )}`,
// );
// logger.info(
// `Use Checkout: ${chalk.yellow(useCheckout)}, Is Custom: ${chalk.yellow(
// isCustom,
// )}, Invoice Only: ${chalk.yellow(invoiceOnly)}`,
// {
// details: { hasPm, forceCheckout, useCheckout, isCustom, invoiceOnly },
// },
// );
// -------------------- ERROR CHECKING --------------------
// 1. Check for normal errors (eg. options, different recurring intervals)
// const { curCusProduct, done } = await handleExistingProduct({
// req,
// res,
// attachParams,
// useCheckout,
// invoiceOnly: attachBody.invoice_only,
// isCustom,
// });
// await handlePrepaidErrors({
// attachParams,
// useCheckout,
// });
// await handlePublicAttachErrors({
// curCusProduct,
// isPublic: req.isPublic || false,
// });
// if (done) return;
// // -------------------- ATTACH PRODUCT --------------------
// SCENARIO 1: Free product, no existing product
// const newProductsFree = isFreeProduct(attachParams.prices);
// const allAddOns = attachParams.products.every((p) => p.is_add_on);
// if (
// (!curCusProduct && newProductsFree) ||
// (allAddOns && newProductsFree)
// ) {
// logger.info("SCENARIO 1: FREE PRODUCT");
// if (useCheckout && !newProductsFree && !attachParams.invoiceOnly) {
// logger.info("SCENARIO 2: USING CHECKOUT");
// await handleCreateCheckout({
// req,
// res,
// attachParams,
// });
// return;
// }
// // SCENARIO 4: Switching product
// if (curCusProduct) {
// logger.info("SCENARIO 3: SWITCHING PRODUCT");
// await handleChangeProduct({
// req,
// res,
// attachParams,
// curCusProduct,
// });
// return;
// }
// SCENARIO 5: No existing product, not free product
// logger.info("SCENARIO 4: ADDING PRODUCT");
// await handleAddProduct({
// req,
// res,
// attachParams,
// });
},
});
attachRouter.post("", handleAttach);
attachRouter.post("/preview", handleAttachPreview);
attachRouter.post("/attach", handleAttach);
attachRouter.post("/attach/preview", handleAttachPreview);
attachRouter.post("/checkout", handleCheckout);

View File

@@ -0,0 +1,115 @@
import {
AttachFunction,
AttachScenario,
FreeTrialResponseSchema,
ProductItemResponseSchema,
ProductResponseSchema,
} from "@autumn/shared";
import { routeHandler } from "@/utils/routerUtils.js";
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
import { AttachBody, AttachBodySchema } from "../models/AttachBody.js";
import { ExtendedResponse } from "@/utils/models/Request.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
import { z } from "zod";
import { checkStripeConnections } from "../attachRouter.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 CheckoutResponseSchema = z.object({
url: z.string().nullish(),
customer_id: z.string().nullish(),
scenario: z.nativeEnum(AttachScenario),
lines: z.array(
z.object({
description: z.string(),
amount: z.number(),
item: ProductItemResponseSchema,
}),
),
// next_cycle: {
// lines
// }
product: ProductResponseSchema,
});
export const handleCheckout = (req: any, res: any) =>
routeHandler({
req,
res,
action: "attach-preview",
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { logtail: logger } = req;
const attachBody = AttachBodySchema.parse(req.body);
const { attachParams, flags, branch, config, func } = await getAttachVars(
{ req, attachBody },
);
if (func == AttachFunction.CreateCheckout) {
await checkStripeConnections({
req,
attachParams,
createCus: true,
useCheckout: true,
});
const checkout = await handleCreateCheckout({
req,
res,
attachParams,
returnCheckout: true,
});
res.status(200).json(CheckoutResponseSchema.parse(checkout));
return;
}
res.status(200).json("ok");
return;
},
});

View File

@@ -49,13 +49,12 @@ export const handleAttach = async (req: any, res: any) =>
config,
});
await checkStripeConnections({ req, attachParams });
await createStripePrices({
await checkStripeConnections({
req,
attachParams,
useCheckout: config.onlyCheckout,
req,
logger,
});
await insertCustomItems({
db: req.db,
customPrices: customPrices || [],

View File

@@ -32,83 +32,5 @@ export const handleAttachPreview = (req: any, res: any) =>
res.status(200).json(attachPreview);
return;
// // Handle existing product
// 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,
// });
// logger.info("--------------------------------");
// logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
// logger.info(`Branch: ${branch}, Function: ${func}`);
// let now = attachParams.now || Date.now();
// let preview: any = null;
// if (
// func == AttachFunction.AddProduct ||
// func == AttachFunction.CreateCheckout ||
// func == AttachFunction.OneOff
// ) {
// preview = await getNewProductPreview({
// attachParams,
// now,
// logger,
// });
// }
// if (func == AttachFunction.ScheduleProduct) {
// preview = await getDowngradeProductPreview({
// attachParams,
// now,
// logger,
// });
// }
// if (
// func == AttachFunction.UpgradeDiffInterval ||
// func == AttachFunction.UpdatePrepaidQuantity ||
// func == AttachFunction.UpgradeSameInterval
// ) {
// preview = await getUpgradeProductPreview({
// req,
// attachParams,
// branch,
// now,
// });
// }
// const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
// attachParams,
// });
// res.status(200).json({
// branch,
// ...preview,
// current_product: curMainProduct
// ? cusProductToProduct({
// cusProduct: curMainProduct,
// })
// : null,
// scheduled_product: curScheduledProduct,
// });
},
});

View File

@@ -175,6 +175,7 @@ export const getCusProductResponse = async ({
canceled_at: cusProduct.canceled_at,
is_default: fullProduct.is_default || false,
is_add_on: fullProduct.is_add_on || false,
version: fullProduct.version,
// stripe_subscription_ids: cusProduct.subscription_ids || [],
started_at: cusProduct.starts_at,

View File

@@ -12,6 +12,7 @@ export const CusProductResponseSchema = z.object({
started_at: z.number(),
is_default: z.boolean(),
is_add_on: z.boolean(),
version: z.number().nullish(),
stripe_subscription_ids: z.array(z.string()).nullish(),