fix: attaching reward to checkout and multi product attach

This commit is contained in:
John Yeo
2025-07-23 10:31:01 +01:00
parent d8d343cff7
commit 5547cc2c1f
6 changed files with 37 additions and 49 deletions

View File

@@ -7,9 +7,10 @@ MOCHA_PARALLEL=true $MOCHA_SETUP \
&& $MOCHA_CMD \ && $MOCHA_CMD \
'tests/attach/basic/*.ts' \ 'tests/attach/basic/*.ts' \
'tests/attach/upgrade/*.ts' \ 'tests/attach/upgrade/*.ts' \
'tests/attach/downgrade/*.ts' \ 'tests/attach/downgrade/*.ts'
$MOCHA_CMD \
'tests/attach/entities/*.ts' \
'tests/attach/free/*.ts' 'tests/attach/free/*.ts'
$MOCHA_CMD 'tests/attach/entities/*.ts'
# 'tests/attach/basic/basic2.ts' \ # 'tests/attach/basic/basic2.ts' \

View File

@@ -12,8 +12,8 @@ import { ErrCode } from "@/errors/errCodes.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { APIVersion } from "@autumn/shared"; import { APIVersion } from "@autumn/shared";
import { SuccessCode } from "@autumn/shared"; import { SuccessCode } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js";
import { getEntityInvoiceDescription } from "@/internal/entities/entityUtils/entityInvoiceUtils.js";
import Stripe from "stripe"; import Stripe from "stripe";
export const handleCreateCheckout = async ({ export const handleCreateCheckout = async ({
@@ -29,7 +29,7 @@ export const handleCreateCheckout = async ({
}) => { }) => {
const { db, logtail: logger } = req; const { db, logtail: logger } = req;
const { customer, org, freeTrial, successUrl } = attachParams; const { customer, org, freeTrial, successUrl, reward } = attachParams;
const stripeCli = createStripeCli({ const stripeCli = createStripeCli({
org, org,
@@ -67,7 +67,7 @@ export const handleCreateCheckout = async ({
if (attachParams.billingAnchor) { if (attachParams.billingAnchor) {
billingCycleAnchorUnixSeconds = Math.floor( billingCycleAnchorUnixSeconds = Math.floor(
attachParams.billingAnchor / 1000, attachParams.billingAnchor / 1000
); );
} }
@@ -84,9 +84,17 @@ export const handleCreateCheckout = async ({
: undefined; : undefined;
let checkoutParams = attachParams.checkoutSessionParams || {}; let checkoutParams = attachParams.checkoutSessionParams || {};
let allowPromotionCodes = notNullish(checkoutParams.discounts) let allowPromotionCodes =
? undefined notNullish(checkoutParams.discounts) || notNullish(reward)
: checkoutParams.allow_promotion_codes || true; ? undefined
: checkoutParams.allow_promotion_codes || true;
let rewardData = {};
if (reward) {
rewardData = {
discounts: [{ coupon: reward.id }],
};
}
const checkout = await stripeCli.checkout.sessions.create({ const checkout = await stripeCli.checkout.sessions.create({
customer: customer.processor.id, customer: customer.processor.id,
@@ -100,6 +108,7 @@ export const handleCreateCheckout = async ({
...(attachParams.metadata ? attachParams.metadata : {}), ...(attachParams.metadata ? attachParams.metadata : {}),
}, },
allow_promotion_codes: allowPromotionCodes, allow_promotion_codes: allowPromotionCodes,
...rewardData,
invoice_creation: !isRecurring invoice_creation: !isRecurring
? { ? {
enabled: true, enabled: true,
@@ -130,7 +139,7 @@ export const handleCreateCheckout = async ({
}, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`, }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
product_ids: attachParams.products.map((p) => p.id), product_ids: attachParams.products.map((p) => p.id),
customer_id: customer.id || customer.internal_id, customer_id: customer.id || customer.internal_id,
}), })
); );
} else { } else {
res.status(200).json({ res.status(200).json({

View File

@@ -26,7 +26,7 @@ const getProductsForAttach = async ({
if (notNullish(product_ids)) { if (notNullish(product_ids)) {
let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial)); let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
console.log("freeTrialProds", freeTrialProds);
if (freeTrialProds.length > 1) { if (freeTrialProds.length > 1) {
throw new RecaseError({ throw new RecaseError({
message: message:
@@ -36,6 +36,8 @@ const getProductsForAttach = async ({
} }
for (const prod of products) { for (const prod of products) {
if (prod.is_add_on) continue;
let otherProd = products.find( let otherProd = products.find(
(p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id (p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id
); );

View File

@@ -49,7 +49,7 @@ export default class RecaseError extends Error {
export function formatZodError(error: ZodError): string { export function formatZodError(error: ZodError): string {
return error.errors return error.errors
.map((err) => .map((err) =>
err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message, err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message
) )
.join(", "); .join(", ");
} }
@@ -112,23 +112,9 @@ export const handleRequestError = ({
`RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`, `RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`,
{ {
error: error.data, error: error.data,
}, }
); );
// logReqUrl(logger, req, "warn");
// logger.warn(
// `Request from ${req.org?.slug || req.orgId || "unknown"} for ${action}`,
// );
// error.print(logger);
// if (req.originalUrl.includes("/webhooks/stripe")) {
// logger.warn("request body", {
// body: getJsonBody(req.body),
// });
// } else {
// logRequestBody(logger, req, "warn");
// }
// logger.warn("--------------------------------");
res.status(error.statusCode).json({ res.status(error.statusCode).json({
message: error.message, message: error.message,
code: error.code, code: error.code,
@@ -137,14 +123,6 @@ export const handleRequestError = ({
return; return;
} }
// logger.error("--------------------------------");
// logger.error("ERROR");
// // logger.error(`${req.method} ${req.originalUrl}`);
// logReqUrl(logger, req, "error");
// logger.error(
// `Request from ${req.org?.slug || req.orgId || "unknown"} for ${action}`,
// );
if (error instanceof Stripe.errors.StripeError) { if (error instanceof Stripe.errors.StripeError) {
let curStack; let curStack;
try { try {
@@ -161,7 +139,7 @@ export const handleRequestError = ({
...rest, ...rest,
stack: curStack, stack: curStack,
}, },
}, }
); );
res.status(400).json({ res.status(400).json({
@@ -170,7 +148,7 @@ export const handleRequestError = ({
}); });
} else if (error instanceof ZodError) { } else if (error instanceof ZodError) {
logger.error( logger.error(
`ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`, `ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`
); );
res.status(400).json({ res.status(400).json({
@@ -185,7 +163,7 @@ export const handleRequestError = ({
stack: error.stack, stack: error.stack,
message: error.message, message: error.message,
}, },
}, }
); );
res.status(500).json({ res.status(500).json({
@@ -220,7 +198,7 @@ export const handleFrontendReqError = ({
error.statusCode == StatusCodes.NOT_FOUND error.statusCode == StatusCodes.NOT_FOUND
) { ) {
req.logtail.warn( req.logtail.warn(
`(frontend) ${req.method} ${req.originalUrl}: not found`, `(frontend) ${req.method} ${req.originalUrl}: not found`
); );
res.status(404).json({ res.status(404).json({
message: error.message, message: error.message,

View File

@@ -14,15 +14,11 @@ import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js"; import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js"; import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js"; import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../utils.js"; import { addPrefixToProducts } from "../utils.js";
import { import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js"; import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { addDays } from "date-fns";
import { addDays, addWeeks } from "date-fns";
import { expect } from "chai"; import { expect } from "chai";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";

View File

@@ -13,7 +13,7 @@
"author": "Recase Inc.", "author": "Recase Inc.",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"build": "bun build ./index.ts --outdir dist --target bun", "build": "bun build ./index.ts --outdir dist --target bun --external zod",
"dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build\"", "dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build\"",
"db:push": "cross-env NODE_OPTIONS=\"--import tsx\" pnpm exec drizzle-kit push --config drizzle.config.ts", "db:push": "cross-env NODE_OPTIONS=\"--import tsx\" pnpm exec drizzle-kit push --config drizzle.config.ts",
@@ -26,7 +26,9 @@
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1", "drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1", "drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.8.2", "drizzle-zod": "^0.8.2"
},
"peerDependencies": {
"zod": "^3.25.23" "zod": "^3.25.23"
}, },
"devDependencies": { "devDependencies": {