Merge branch 'main' into staging

This commit is contained in:
John Yeo
2025-07-23 14:08:56 +01:00
7 changed files with 60 additions and 64 deletions

View File

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

View File

@@ -26,7 +26,7 @@ const getProductsForAttach = async ({
if (notNullish(product_ids)) {
let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
console.log("freeTrialProds", freeTrialProds);
if (freeTrialProds.length > 1) {
throw new RecaseError({
message:
@@ -36,6 +36,8 @@ const getProductsForAttach = async ({
}
for (const prod of products) {
if (prod.is_add_on) continue;
let otherProd = products.find(
(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 {
return error.errors
.map((err) =>
err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message,
err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message
)
.join(", ");
}
@@ -112,23 +112,9 @@ export const handleRequestError = ({
`RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`,
{
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({
message: error.message,
code: error.code,
@@ -137,14 +123,6 @@ export const handleRequestError = ({
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) {
let curStack;
try {
@@ -161,7 +139,7 @@ export const handleRequestError = ({
...rest,
stack: curStack,
},
},
}
);
res.status(400).json({
@@ -170,7 +148,7 @@ export const handleRequestError = ({
});
} else if (error instanceof ZodError) {
logger.error(
`ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`,
`ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`
);
res.status(400).json({
@@ -185,7 +163,7 @@ export const handleRequestError = ({
stack: error.stack,
message: error.message,
},
},
}
);
res.status(500).json({
@@ -220,7 +198,7 @@ export const handleFrontendReqError = ({
error.statusCode == StatusCodes.NOT_FOUND
) {
req.logtail.warn(
`(frontend) ${req.method} ${req.originalUrl}: not found`,
`(frontend) ${req.method} ${req.originalUrl}: not found`
);
res.status(404).json({
message: error.message,

View File

@@ -14,15 +14,11 @@ 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, runAttachTest } from "../utils.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { addPrefixToProducts } from "../utils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addDays, addWeeks } from "date-fns";
import { addDays } from "date-fns";
import { expect } from "chai";
import { eq } from "drizzle-orm";

View File

@@ -13,7 +13,7 @@
"author": "Recase Inc.",
"license": "Apache-2.0",
"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\"",
"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",
"drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.8.2",
"drizzle-zod": "^0.8.2"
},
"peerDependencies": {
"zod": "^3.25.23"
},
"devDependencies": {

View File

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { ChevronsUpDown, Loader2 } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
@@ -19,9 +19,8 @@ import {
PopoverTrigger,
} from "@/components/ui/popover";
import { useAxiosPostSWR } from "@/services/useAxiosSwr";
import { AppEnv } from "@autumn/shared";
import { debounce } from "lodash";
import { useEffect, useMemo, useCallback } from "react";
import { useEffect, useCallback } from "react";
import { useNavigate } from "react-router";
import { navigateTo } from "@/utils/genUtils";
import { useEnv } from "@/utils/envUtils";
@@ -40,9 +39,8 @@ export function CustomerComboBox({
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState("");
const [isSearching, setIsSearching] = React.useState(false);
const [cusId, setCusId] = React.useState(customer?.id);
const { data, isLoading, error, mutate } = useAxiosPostSWR({
const { data, mutate } = useAxiosPostSWR({
url: `/v1/customers/all/search`,
env,
data: {
@@ -52,21 +50,29 @@ export function CustomerComboBox({
});
const debouncedSearch = useCallback(
debounce(async (searchValue: string) => {
debounce(async () => {
setIsSearching(true);
await mutate();
setIsSearching(false);
try {
await mutate();
} catch (error) {
console.error("Search failed:", error);
} finally {
setIsSearching(false);
}
}, 300),
[mutate],
[mutate]
);
useEffect(() => {
if (value) {
debouncedSearch(value);
debouncedSearch();
} else {
setIsSearching(false);
}
return () => {
debouncedSearch.cancel();
};
}, [value, debouncedSearch]);
return (
@@ -78,7 +84,7 @@ export function CustomerComboBox({
aria-expanded={open}
className={cn(
"w-[150px] justify-between text-xs",
classNames?.trigger,
classNames?.trigger
)}
onClick={() => {
setValue("");
@@ -126,22 +132,24 @@ export function CustomerComboBox({
</CommandEmpty>
<CommandGroup>
{value &&
data?.customers?.map((c: any) => {
data?.customers &&
data?.customers?.map((c: any, idx: number) => {
if (c.name === customer?.name) {
return null;
}
return (
<CommandItem
key={c.id}
value={c.id}
key={idx}
value={c.id || c.internal_id}
onSelect={() => {
navigateTo(
`/analytics?customer_id=${c.id}`,
navigate,
env,
env
);
setOpen(false);
}}
className="w-full"
>
{c.name || c.email}{" "}
<span className="text-xs text-t3">