diff --git a/bun.lock b/bun.lock index 596a18ca9..ee1c7cde6 100644 --- a/bun.lock +++ b/bun.lock @@ -85,7 +85,7 @@ "react": "^18.2.0", "recaseai": "^0.0.37", "resend": "^4.1.1", - "stripe": "18.4.0", + "stripe": "^18.4.0", "svix": "^1.45.1", "tsc-alias": "^1.8.16", "ws": "^8.18.0", @@ -3025,8 +3025,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], diff --git a/server/shell/g1.sh b/server/shell/g1.sh index 6dacc8f47..8d533cc5b 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -16,6 +16,7 @@ fi # 'tests/attach/addOn/*.ts' $MOCHA_CMD \ -'tests/attach/checkout/*.ts' \ -'tests/attach/entities/*.ts' \ -'tests/attach/free/*.ts'\ +'tests/attach/checkout/*.ts' +# \ +# 'tests/attach/entities/*.ts' \ +# 'tests/attach/free/*.ts'\ diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index eb2510e2d..9cf4a618a 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -340,7 +340,7 @@ export class AutumnInt { rewards = { create: async (reward: any) => { - const data = await this.post(`/rewards`, reward); + const data = await this.post(`/rewards?legacyStripe=true`, reward); return data; }, diff --git a/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts b/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts index 70ccc5b94..df80a9157 100644 --- a/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts +++ b/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts @@ -57,7 +57,7 @@ export const deleteCouponFromCus = async ({ try { let stripeCus = (await stripeCli.customers.retrieve( - stripeCusId, + stripeCusId )) as Stripe.Customer; if (stripeCus.discount?.id === discountId) { await stripeCli.customers.deleteDiscount(stripeCusId, discountId); diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 9fed28077..54b08de17 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -9,8 +9,10 @@ import { Product, UsagePriceConfig, RewardType, + AppEnv, } from "@autumn/shared"; import { Stripe } from "stripe"; +import { createStripeCli } from "../utils.js"; const couponToStripeDuration = (coupon: Reward) => { let discountConfig = coupon.discount_config; @@ -62,19 +64,27 @@ const couponToStripeValue = ({ export const createStripeCoupon = async ({ reward, - stripeCli, org, + env, prices, logger, + legacyVersion, }: { reward: Reward; - stripeCli: Stripe; org: Organization; + env: AppEnv; prices: (Price & { product: Product })[]; logger: any; + legacyVersion?: boolean; }) => { let discountConfig = reward.discount_config; + const stripeCli = createStripeCli({ + org, + env, + legacyVersion, + }); + try { await stripeCli.coupons.del(reward.id); } catch (error) {} diff --git a/server/src/external/stripe/utils.ts b/server/src/external/stripe/utils.ts index 1b8ea3c48..990526770 100644 --- a/server/src/external/stripe/utils.ts +++ b/server/src/external/stripe/utils.ts @@ -16,9 +16,13 @@ import Stripe from "stripe"; export const createStripeCli = ({ org, env, + // apiVersion, + legacyVersion, }: { org: Organization; env: AppEnv; + // apiVersion?: string; + legacyVersion?: boolean; }) => { let encrypted = env == AppEnv.Sandbox @@ -34,7 +38,11 @@ export const createStripeCli = ({ } let decrypted = decryptData(encrypted); - return new Stripe(decrypted); + return new Stripe(decrypted, { + apiVersion: legacyVersion + ? ("2025-02-24.acacia" as any) + : "2025-07-30.basil", + }); }; export const calculateMetered1Price = ({ diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts index 455e3c4c8..c1543cd34 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts @@ -18,6 +18,7 @@ import { Decimal } from "decimal.js"; import { generateId } from "@/utils/genUtils.js"; import { addMonths } from "date-fns"; import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; +import { invoiceToSubId } from "../stripeInvoiceUtils.js"; export const handleInvoicePaidDiscount = async ({ db, @@ -39,7 +40,7 @@ export const handleInvoicePaidDiscount = async ({ } let stripeCus = await stripeCli.customers.retrieve( - expandedInvoice.customer as string, + expandedInvoice.customer as string ); try { @@ -86,7 +87,7 @@ export const handleInvoicePaidDiscount = async ({ const curAmount = discount.coupon.amount_off; const amountUsed = totalDiscountAmounts?.find( - (item) => item.discount === discount.id, + (item) => item.discount === discount.id )?.amount; const newAmount = new Decimal(curAmount!).sub(amountUsed!).toNumber(); @@ -103,16 +104,17 @@ export const handleInvoicePaidDiscount = async ({ }); let expired = curExpiresAt && curExpiresAt < now; + const subId = invoiceToSubId({ invoice: expandedInvoice }); if (discountFinished || expired) { logger.info( - `Coupon ${couponId}, stripeCus: ${stripeCus.id}: credits used up or expired. discountFinished: ${discountFinished}, expired: ${expired}`, + `Coupon ${couponId}, stripeCus: ${stripeCus.id}: credits used up or expired. discountFinished: ${discountFinished}, expired: ${expired}` ); - if (expandedInvoice.subscription) { + if (subId) { await deleteCouponFromCus({ stripeCli, - stripeSubId: expandedInvoice.subscription as string, + stripeSubId: subId, stripeCusId: expandedInvoice.customer as string, discountId: discount.id, logger, @@ -123,7 +125,7 @@ export const handleInvoicePaidDiscount = async ({ } logger.info( - `Coupon ${couponId}, stripeCus: ${stripeCus.id}, updating amount from ${curAmount} to ${newAmount}`, + `Coupon ${couponId}, stripeCus: ${stripeCus.id}, updating amount from ${curAmount} to ${newAmount}` ); // Set expiry date @@ -147,18 +149,30 @@ export const handleInvoicePaidDiscount = async ({ }, }); - await stripeCli.customers.update(expandedInvoice.customer as string, { - coupon: newCoupon.id, + const legacyStripeCli = createStripeCli({ + org, + env, + legacyVersion: true, }); + await legacyStripeCli.rawRequest( + "POST", + `/v1/customers/${expandedInvoice.customer}/discounts`, + { + coupon: newCoupon.id, + } + ); + await stripeCli.coupons.del(newCoupon.id); - await deleteCouponFromSub({ - stripeCli, - stripeSubId: expandedInvoice.subscription as string, - discountId: discount.id, - logger, - }); + if (subId) { + await deleteCouponFromSub({ + stripeCli, + stripeSubId: subId, + discountId: discount.id, + logger, + }); + } } } catch (error) { logger.error("invoice.paid: error updating coupon"); diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts index 4a48679bc..e0cb61243 100644 --- a/server/src/internal/api/rewards/rewardRouter.ts +++ b/server/src/internal/api/rewards/rewardRouter.ts @@ -8,7 +8,6 @@ import { OrgService } from "@/internal/orgs/OrgService.js"; import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; -import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; import { constructReward, @@ -54,30 +53,6 @@ rewardRouter.post("", async (req: any, res: any) => { }), ]); - // Initialize prices - - // if (!discountConfig!.apply_to_all) { - // // Create stripe prices if not exists - - // const batchSize = 5; - - // // for (let i = 0; i < prices.length; i += batchSize) { - // // const batch = prices.slice(i, i + batchSize); - // // const batchPriceCreate = batch.map((price) => - // // createStripePriceIFNotExist({ - // // stripeCli, - // // price, - // // entitlements, - // // org, - // // logger, - // // db, - // // product: price.product, - // // }) - // // ); - // // await Promise.all(batchPriceCreate); - // // } - // } - await initRewardStripePrices({ db, prices, @@ -88,10 +63,11 @@ rewardRouter.post("", async (req: any, res: any) => { await createStripeCoupon({ reward: newReward, - stripeCli, org, + env, prices, logger, + legacyVersion: req.query.legacyStripe === "true", }); } @@ -206,10 +182,11 @@ rewardRouter.post("/:internalId", async (req: any, res: any) => { if (rewardCat == RewardCategory.Discount) { await createStripeCoupon({ reward: rewardBody, - stripeCli, org, + env, prices, logger, + legacyVersion: req.query.legacyStripe === "true", }); } diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts index e621b1e07..d92892406 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts @@ -1,24 +1,14 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; import RecaseError from "@/utils/errorUtils.js"; -import { - Customer, - FreeTrial, - Organization, - ErrCode, - BillingInterval, - Reward, - IntervalConfig, -} from "@autumn/shared"; +import { ErrCode, Reward, IntervalConfig } from "@autumn/shared"; import Stripe from "stripe"; import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; -import { formatUnixToDateTime, generateId } from "@/utils/genUtils.js"; -import { ItemSet } from "@/utils/models/ItemSet.js"; +import { generateId } from "@/utils/genUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { getAlignedIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js"; import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; // Get payment method @@ -122,6 +112,15 @@ export const createStripeSub2 = async ({ // coupon: reward ? reward.id : undefined, discounts: reward ? [{ coupon: reward.id }] : undefined, expand: ["latest_invoice"], + + trial_settings: + freeTrial && !freeTrial.card_required + ? { + end_behavior: { + missing_payment_method: "cancel", + }, + } + : undefined, }); // console.log("Latest invoice:", subscription.latest_invoice); diff --git a/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts b/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts index 7e57c7c8d..2e2d190a4 100644 --- a/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts +++ b/server/src/internal/customers/attach/attachUtils/updateStripeSub/updateStripeSub.ts @@ -135,7 +135,7 @@ export const updateStripeSub = async ({ let { replaceables } = await createAndFilterContUseItems({ attachParams, curMainProduct: curMainProduct!, - stripeSubs, + sub: curSub, interval, intervalCount, logger, diff --git a/server/src/internal/customers/cusRouter.ts b/server/src/internal/customers/cusRouter.ts index be0702118..835a93ec0 100644 --- a/server/src/internal/customers/cusRouter.ts +++ b/server/src/internal/customers/cusRouter.ts @@ -12,7 +12,6 @@ import { createStripeCli } from "@/external/stripe/utils.js"; import { handleDeleteCustomer } from "./handlers/cusDeleteHandlers.js"; import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js"; import { handleUpdateEntitlement } from "./handlers/handleUpdateEntitlement.js"; -import { handleCusProductExpired } from "./handlers/handleCusProductExpired.js"; import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js"; import { handlePostCustomerRequest } from "./handlers/handlePostCustomer.js"; import { entityRouter } from "../api/entities/entityRouter.js"; diff --git a/server/src/internal/customers/handlers/handleAddCouponToCus.ts b/server/src/internal/customers/handlers/handleAddCouponToCus.ts index 19cc99235..467cc6925 100644 --- a/server/src/internal/customers/handlers/handleAddCouponToCus.ts +++ b/server/src/internal/customers/handlers/handleAddCouponToCus.ts @@ -44,7 +44,12 @@ export const handleAddCouponToCus = async (req: any, res: any) => { }); } - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ + org, + env, + // apiVersion: "2025-02-24.acacia", + legacyVersion: true, + }); await createStripeCusIfNotExists({ db, @@ -55,9 +60,22 @@ export const handleAddCouponToCus = async (req: any, res: any) => { }); // Attach coupon to customer - await stripeCli.customers.update(customer.processor.id, { - coupon: coupon.id, - }); + // curl https://api.stripe.com/v1/customers/cus_123456/discounts \ + // -u sk_test_your_key: \ + // -d coupon=COUPON_ID + + await stripeCli.rawRequest( + "POST", + `/v1/customers/${customer.processor.id}`, + { + coupon: coupon.id, + } + ); + // await stripeCli.customers.update(customer.processor.id, { + // coupon: coupon.id, + + // discounts: [{ coupon: coupon.id }], + // }); res.status(200).json({ customer, coupon }); } catch (error) { diff --git a/server/tests/attach/checkout/checkout4.ts b/server/tests/attach/checkout/checkout4.ts index 5968026ee..70df32f3e 100644 --- a/server/tests/attach/checkout/checkout4.ts +++ b/server/tests/attach/checkout/checkout4.ts @@ -1,6 +1,6 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { APIVersion, AppEnv, Organization } from "@autumn/shared"; +import { AppEnv, Organization } from "@autumn/shared"; import chalk from "chalk"; import Stripe from "stripe"; import { DrizzleCli } from "@/db/initDrizzle.js"; @@ -16,7 +16,6 @@ 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 { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js"; import { expect } from "chai"; import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";