diff --git a/server/shell/g1.sh b/server/shell/g1.sh index bd131efdc..7f9f2a8eb 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -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' \ \ No newline at end of file diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index 2e14122b9..dab338416 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -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({ diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts index 4fd4c6fd5..d138ab8b7 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts @@ -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 ); diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index 557488256..cfb35edfc 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -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, diff --git a/server/tests/attach/free/free1.ts b/server/tests/attach/free/free1.ts index 9aa0f9919..81ae30fb4 100644 --- a/server/tests/attach/free/free1.ts +++ b/server/tests/attach/free/free1.ts @@ -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"; diff --git a/shared/package.json b/shared/package.json index 2ff3ce839..396cc0ac6 100644 --- a/shared/package.json +++ b/shared/package.json @@ -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": { diff --git a/vite/src/views/customers/customer/analytics/components/CustomerComboBox.tsx b/vite/src/views/customers/customer/analytics/components/CustomerComboBox.tsx index fdafc9305..c608a28b3 100644 --- a/vite/src/views/customers/customer/analytics/components/CustomerComboBox.tsx +++ b/vite/src/views/customers/customer/analytics/components/CustomerComboBox.tsx @@ -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({ {value && - data?.customers?.map((c: any) => { + data?.customers && + data?.customers?.map((c: any, idx: number) => { if (c.name === customer?.name) { return null; } return ( { navigateTo( `/analytics?customer_id=${c.id}`, navigate, - env, + env ); setOpen(false); }} + className="w-full" > {c.name || c.email}{" "}