From 79b406d07b4bde0a46f95cf1a0c094768cb61a20 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 24 Sep 2025 10:19:38 +0100 Subject: [PATCH] fix: rewards don't apply to specific products (on update) --- server/biome.json | 3 +- .../stripeCouponUtils/stripeCouponUtils.ts | 4 ++ .../handlers/rewards/handleUpdateCoupon.ts | 14 +++---- .../mergeUtils/phaseUtils/phaseUtils.ts | 10 ++--- .../internal/products/prices/PriceService.ts | 32 ++++++++-------- server/tests/attach/checkout/checkout4.ts | 38 +++++++++---------- server/tests/attach/entities/entity1.ts | 32 ++++++++-------- 7 files changed, 66 insertions(+), 67 deletions(-) diff --git a/server/biome.json b/server/biome.json index 76170ffa5..5b4e7f7d5 100644 --- a/server/biome.json +++ b/server/biome.json @@ -9,7 +9,8 @@ "files": { "ignoreUnknown": false, "experimentalScannerIgnores": ["dist/**"], - "includes": ["src/**/*.ts", "tests/**/*.ts"] + "includes": ["src/**/*.ts", "tests/**/*.ts"], + "ignore": ["dist/**"] }, "formatter": { "enabled": true, diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 9e7c9de50..1a6e8e6a6 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -171,6 +171,10 @@ export const createStripeCoupon = async ({ } catch (_) {} } + console.log("Reward type:", reward.type); + console.log("stripeProdIds", stripeProdIds); + console.log("Apply to all", discountConfig!.apply_to_all); + const stripeCoupon = await stripeCli.coupons.create({ // id: reward.internal_id, id: reward.id, diff --git a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts index a5c9e05f3..2c13271b8 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts @@ -44,17 +44,15 @@ export default async (req: any, res: any) => let prices: any[] = []; if (rewardCat === RewardCategory.Discount) { - const stripePriceIds = + const priceIds = rewardBody.discount_config?.price_ids ?? reward.discount_config?.price_ids ?? []; - const byStripeId = await PriceService.getByStripeIds({ + + prices = await PriceService.getInIds({ db, - stripePriceIds, + ids: priceIds, }); - prices = stripePriceIds - .map((id: string) => byStripeId[id]) - .filter(Boolean); } else if (rewardCat === RewardCategory.FreeProduct) { const freeProductId = rewardBody.free_product_id ?? reward.free_product_id; @@ -78,9 +76,7 @@ export default async (req: any, res: any) => try { await stripeCli.coupons.del(reward.id); await stripeCli.coupons.del(reward.internal_id); - } catch (_) { - // console.log(`Failed to delete coupon from stripe: ${error.message}`); - } + } catch (_) {} if ( rewardCat === RewardCategory.Discount || diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts index c99d3349f..f7bd80ed3 100644 --- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts +++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts @@ -1,10 +1,10 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { FullCusProduct } from "@autumn/shared"; +import { differenceInDays, subDays } from "date-fns"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; import { formatPrice } from "@/internal/products/prices/priceUtils.js"; import { formatUnixToDate, notNullish } from "@/utils/genUtils.js"; -import { FullCusProduct } from "@autumn/shared"; -import { differenceInDays, subDays } from "date-fns"; -import Stripe from "stripe"; export const cusProductInPhase = ({ phaseStart, @@ -105,7 +105,7 @@ export const logPhases = async ({ db: DrizzleCli; }) => { for (const phase of phases) { - // @ts-ignore + // @ts-expect-error const timestampInMillis = ensureMilliseconds(phase.start_date); console.log(`Phase ${formatUnixToDate(timestampInMillis)}:`); await logPhaseItems({ items: phase.items, db }); diff --git a/server/src/internal/products/prices/PriceService.ts b/server/src/internal/products/prices/PriceService.ts index 6d03ab336..bcdfd3af5 100644 --- a/server/src/internal/products/prices/PriceService.ts +++ b/server/src/internal/products/prices/PriceService.ts @@ -1,7 +1,7 @@ -import { buildConflictUpdateColumns } from "@/db/dbUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { Price, prices, Product } from "@autumn/shared"; +import { type Price, type Product, prices } from "@autumn/shared"; import { and, eq, inArray, or, sql } from "drizzle-orm"; +import { buildConflictUpdateColumns } from "@/db/dbUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; export class PriceService { static async get({ db, id }: { db: DrizzleCli; id: string }) { @@ -59,7 +59,7 @@ export class PriceService { } static async upsert({ db, data }: { db: DrizzleCli; data: Price | Price[] }) { - if (Array.isArray(data) && data.length == 0) return; + if (Array.isArray(data) && data.length === 0) return; const updateColumns = buildConflictUpdateColumns(prices, ["id"]); @@ -104,20 +104,18 @@ export class PriceService { if (!stripePriceIds || stripePriceIds.length === 0) return {} as Record; - const perIdExprs = stripePriceIds.map((id) => - or( - sql`${prices.config} ->> 'stripe_price_id' = ${id}`, - sql`${prices.config} ->> 'stripe_empty_price_id' = ${id}`, - ), - ); - - let whereExpr = perIdExprs[0]; - for (let i = 1; i < perIdExprs.length; i++) { - whereExpr = or(whereExpr, perIdExprs[i]); - } - + // Build a more efficient query using SQL with proper JSON path operations const rows = (await db.query.prices.findMany({ - where: whereExpr, + where: sql`( + ${prices.config} ->> 'stripe_price_id' = ANY(ARRAY[${sql.join( + stripePriceIds.map((id) => sql`${id}`), + sql`, `, + )}]) + OR ${prices.config} ->> 'stripe_empty_price_id' = ANY(ARRAY[${sql.join( + stripePriceIds.map((id) => sql`${id}`), + sql`, `, + )}]) + )`, with: { product: true }, })) as (Price & { product: Product })[]; diff --git a/server/tests/attach/checkout/checkout4.ts b/server/tests/attach/checkout/checkout4.ts index 0b8e69d5f..1615d97dc 100644 --- a/server/tests/attach/checkout/checkout4.ts +++ b/server/tests/attach/checkout/checkout4.ts @@ -1,25 +1,25 @@ -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { AppEnv, DiscountType, Organization, RewardType } from "@autumn/shared"; +import { type AppEnv, type Organization, RewardType } from "@autumn/shared"; +import { expect } from "chai"; import chalk from "chalk"; -import Stripe from "stripe"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type Stripe from "stripe"; import { setupBefore } from "tests/before.js"; +import { TestFeature } from "tests/setup/v2Features.js"; +import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { createProducts, createReward } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "../utils.js"; +import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { timeout } from "@/utils/genUtils.js"; +import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; import { constructCoupon, constructProduct, } from "@/utils/scriptUtils/createTestProducts.js"; -import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js"; -import { TestFeature } from "tests/setup/v2Features.js"; -import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; -import { timeout } from "@/utils/genUtils.js"; -import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { expect } from "chai"; -import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { addPrefixToProducts } from "../utils.js"; -export let pro = constructProduct({ +const pro = constructProduct({ items: [ constructFeatureItem({ featureId: TestFeature.Messages, @@ -38,12 +38,12 @@ const reward = constructCoupon({ const testCase = "checkout4"; describe(`${chalk.yellowBright(`${testCase}: Testing attach coupon`)}`, () => { - let customerId = testCase; - let autumn: AutumnInt = new AutumnInt(); + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt(); let testClockId: string; let db: DrizzleCli, org: Organization, env: AppEnv; let stripeCli: Stripe; - let curUnix = new Date().getTime(); + const curUnix = new Date().getTime(); before(async function () { await setupBefore(this); @@ -89,7 +89,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach coupon`)}`, () => { testClockId = testClockId1!; }); - it("should attach pro and one off product", async function () { + it("should attach pro and one off product", async () => { const res = await autumn.attach({ customer_id: customerId, product_id: pro.id, @@ -107,7 +107,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing attach coupon`)}`, () => { }); expect(customer.invoices.length).to.equal(1); - let totalPrice = getBasePrice({ product: pro }); + const totalPrice = getBasePrice({ product: pro }); expect(customer.invoices[0].total).to.equal(totalPrice * 0.5); }); }); diff --git a/server/tests/attach/entities/entity1.ts b/server/tests/attach/entities/entity1.ts index bff0cfc11..923b4c8c0 100644 --- a/server/tests/attach/entities/entity1.ts +++ b/server/tests/attach/entities/entity1.ts @@ -1,21 +1,21 @@ -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import { APIVersion, type AppEnv, type Organization } from "@autumn/shared"; import chalk from "chalk"; -import Stripe from "stripe"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type Stripe from "stripe"; import { setupBefore } from "tests/before.js"; -import { createProducts } from "tests/utils/productUtils.js"; -import { addPrefixToProducts } from "../utils.js"; -import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; import { TestFeature } from "tests/setup/v2Features.js"; -import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js"; +import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { addPrefixToProducts } from "../utils.js"; const testCase = "aentity1"; -export let pro = constructProduct({ +const pro = constructProduct({ items: [ constructArrearItem({ featureId: TestFeature.Words, @@ -26,13 +26,13 @@ export let pro = constructProduct({ }); describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via checkout`)}`, () => { - let customerId = testCase; - let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); + const customerId = testCase; + const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 }); let testClockId: string; let db: DrizzleCli, org: Organization, env: AppEnv; let stripeCli: Stripe; - let curUnix = new Date().getTime(); + const curUnix = new Date().getTime(); before(async function () { await setupBefore(this); @@ -76,9 +76,9 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via }, ]; - it("should attach pro product to entity 1", async function () { + it("should attach pro product to entity 1", async () => { await autumn.entities.create(customerId, newEntities); - let entityId = newEntities[0].id; + const entityId = newEntities[0].id; await attachAndExpectCorrect({ autumn, @@ -91,7 +91,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via entityId, }); - let customer = await autumn.customers.get(customerId); + const customer = await autumn.customers.get(customerId); expectProductAttached({ customer, product: pro,