From 6d5008f4873da645b921e8beb043697512646ea1 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 17 Aug 2025 13:43:25 -0700 Subject: [PATCH] fix: ts types --- server/shell/g5.sh | 4 +- .../stripeSubUtils/stripeSubItemUtils.ts | 2 +- .../stripe/stripeSubUtils/updateStripeSub.ts | 180 ------------------ .../removePrepaidSingleUseItems.ts | 37 ---- .../createContUseInvoiceItems.ts | 4 +- .../features/utils/constructFeatureUtils.ts | 4 + .../src/internal/invoices/InvoiceService.ts | 6 +- server/src/internal/invoices/invoiceUtils.ts | 6 +- .../invoiceUtils/createAndFinalizeInvoice.ts | 6 +- .../src/internal/products/ProductService.ts | 2 +- .../utils/importUtils/addProductFromSubs.ts | 71 ++++--- server/tests/alex/00_setup.ts | 2 - server/tests/alex/04_topups.ts | 14 +- server/tests/alex/utils.ts | 2 +- 14 files changed, 63 insertions(+), 277 deletions(-) delete mode 100644 server/src/external/stripe/stripeSubUtils/updateStripeSub.ts delete mode 100644 server/src/external/stripe/stripeSubUtils/updateStripeSub/removePrepaidSingleUseItems.ts diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 52d062577..85746dd66 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -1,4 +1,6 @@ -MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && npx mocha --parallel --timeout 10000000 \ +# npx mocha 'tests/alex/00_setup.ts' --timeout 10000000 + +MOCHA_PARALLEL=true npx mocha --parallel --timeout 10000000 \ 'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \ 'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \ --ignore 'tests/alex/00_setup.ts' \ No newline at end of file diff --git a/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts b/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts index 7fd03f10e..7c5e889e1 100644 --- a/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts +++ b/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts @@ -62,7 +62,7 @@ export const findPriceInStripeItems = ({ }: { prices: Price[]; subItem?: Stripe.SubscriptionItem; - lineItem?: Stripe.InvoiceItem; + lineItem?: Stripe.InvoiceItem | Stripe.InvoiceLineItem; billingType?: BillingType; }) => { return prices.find((p: Price) => { diff --git a/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts b/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts deleted file mode 100644 index 2f5d5962b..000000000 --- a/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts +++ /dev/null @@ -1,180 +0,0 @@ -import Stripe from "stripe"; -import { - BillingInterval, - AttachConfig, - ProrationBehavior, -} from "@autumn/shared"; - -import { SubService } from "@/internal/subscriptions/SubService.js"; -import { ItemSet } from "@/utils/models/ItemSet.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { createProrationInvoice } from "./updateStripeSub/createProrationinvoice.js"; -import { - createUsageInvoiceItems, - resetUsageBalances, -} from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.js"; -import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; -import { createAndFilterContUseItems } from "@/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.js"; - -export const updateStripeSubscription = async ({ - db, - attachParams, - config, - trialEnd, - stripeSubs, - itemSet, - logger, - interval, -}: { - db: DrizzleCli; - attachParams: AttachParams; - config: AttachConfig; - stripeSubs: Stripe.Subscription[]; - trialEnd?: number; - itemSet: ItemSet; - shouldPreview?: boolean; - logger: any; - interval?: BillingInterval; -}) => { - const { curMainProduct } = attachParamToCusProducts({ attachParams }); - const { stripeCli, customer, org, paymentMethod } = attachParams; - const { invoiceOnly, proration } = config; - const curSub = stripeSubs[0]; - - let { items, prices } = itemSet; - - let subItems = items.filter( - (i: any, index: number) => - i.deleted || prices[index].config!.interval !== BillingInterval.OneOff - ); - - let subInvoiceItems = items.filter((i: any, index: number) => { - if (index < prices.length) { - return prices[index].config!.interval === BillingInterval.OneOff; - } - - return false; - }); - - // 1. Update subscription - let sub: Stripe.Subscription | null = null; - let stripeProration = - proration == ProrationBehavior.None ? "none" : "create_prorations"; - - try { - sub = await stripeCli.subscriptions.update(curSub.id, { - items: subItems, - proration_behavior: stripeProration, - trial_end: trialEnd, - default_payment_method: paymentMethod?.id, - add_invoice_items: subInvoiceItems, - ...((invoiceOnly && { - collection_method: "send_invoice", - days_until_due: 30, - }) as any), - payment_behavior: "error_if_incomplete", - expand: ["latest_invoice"], - }); - } catch (error: any) { - throw error; - } - - const latestInvoice = sub.latest_invoice as Stripe.Invoice; - - if (proration == ProrationBehavior.None) { - return { - preview: null, - sub: { - ...sub, - latest_invoice: latestInvoice.id, - }, - invoice: null, - }; - } - - // 3. Create invoice items for remaining usages - let invoice: Stripe.Invoice | null = null; - let { cusEntIds } = await createUsageInvoiceItems({ - db, - attachParams, - cusProduct: curMainProduct!, - stripeSubs, - logger, - }); - - await createAndFilterContUseItems({ - attachParams, - curMainProduct: curMainProduct!, - stripeSubs, - interval, - logger, - }); - - if (proration === ProrationBehavior.Immediately) { - if (latestInvoice.id != curSub.latest_invoice) { - sub.latest_invoice = latestInvoice.id; - return { - preview: null, - sub, - invoice: latestInvoice, - }; - } - - invoice = await createProrationInvoice({ - attachParams, - invoiceOnly, - curSub, - updatedSub: sub, - logger, - }); - } - - await resetUsageBalances({ - db, - cusEntIds, - cusProduct: curMainProduct!, - }); - - // Upsert sub - await SubService.addUsageFeatures({ - db, - stripeId: curSub.id, - usageFeatures: itemSet.usageFeatures, - orgId: org.id, - env: customer.env, - }); - - if (invoice) { - sub.latest_invoice = invoice.id; - } else { - sub.latest_invoice = null; - } - - return { - preview: null, - sub, - invoice, - }; -}; - -// if (shouldPreview) { -// let preview = await stripeCli.invoices.createPreview({ -// subscription_details: { -// items: subItems, -// proration_behavior: getStripeProrationBehavior({ -// org, -// prorationBehavior: proration, -// }) as any, -// trial_end: trialEnd as any, -// }, -// subscription: curSub.id, -// invoice_items: subInvoiceItems, -// customer: customer.processor.id, -// }); -// return { -// preview, -// sub: null, -// invoice: null, -// }; -// } diff --git a/server/src/external/stripe/stripeSubUtils/updateStripeSub/removePrepaidSingleUseItems.ts b/server/src/external/stripe/stripeSubUtils/updateStripeSub/removePrepaidSingleUseItems.ts deleted file mode 100644 index 8c83d5bbe..000000000 --- a/server/src/external/stripe/stripeSubUtils/updateStripeSub/removePrepaidSingleUseItems.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Stripe from "stripe"; -import { BillingType, Feature, FeatureUsageType } from "@autumn/shared"; -import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import { FullCusProduct } from "@autumn/shared"; -import { findPriceInStripeItems } from "../stripeSubItemUtils.js"; -import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPrice.js"; - -// For prepaid single use prices... -export const removePrepaidSingleUseItems = async ({ - stripeCli, - curCusProduct, - features, - invoice, -}: { - stripeCli: Stripe; - curCusProduct: FullCusProduct; - features: Feature[]; - invoice: Stripe.Invoice; -}) => { - const curPrices = cusProductToPrices({ cusProduct: curCusProduct }); - - for (const item of invoice.lines.data) { - let price = findPriceInStripeItems({ - prices: curPrices, - subItem: item, - billingType: BillingType.UsageInAdvance, - }); - - if (!price) continue; - - let feature = priceToFeature({ price, features })!; - if (feature.config.usage_type == FeatureUsageType.Single) { - console.log("Deleting item:", item.id, "Feature:", feature.id); - await stripeCli.invoiceItems.del(item.id); - } - } -}; diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts index 84618391e..afed93eb9 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts @@ -95,7 +95,7 @@ export const createAndFilterContUseItems = async ({ // return { newItems: [], oldItems: [], replaceables: [] }; // } - let { newItems, oldItems } = await getContUseInvoiceItems({ + let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({ attachParams, cusProduct: curMainProduct!, sub, @@ -154,5 +154,5 @@ export const createAndFilterContUseItems = async ({ }); } - return { newItems, oldItems }; + return { newItems, oldItems, replaceables }; }; diff --git a/server/src/internal/features/utils/constructFeatureUtils.ts b/server/src/internal/features/utils/constructFeatureUtils.ts index 91a9c1539..87437b962 100644 --- a/server/src/internal/features/utils/constructFeatureUtils.ts +++ b/server/src/internal/features/utils/constructFeatureUtils.ts @@ -32,6 +32,7 @@ export const constructFeature = ({ type, config, display, + archived: false, }; return newFeature; @@ -58,6 +59,7 @@ export const constructBooleanFeature = ({ name: name || keyToTitle(featureId), type: FeatureType.Boolean, config: null, + archived: false, }; return newFeature; @@ -99,6 +101,7 @@ export const constructMeteredFeature = ({ }, usage_type: usageType, }, + archived: false, }; return newFeature; @@ -139,6 +142,7 @@ export const constructCreditSystem = ({ name: name || keyToTitle(featureId), type: FeatureType.CreditSystem, config, + archived: false, }; return newFeature; diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts index fe308bc89..3357edeff 100644 --- a/server/src/internal/invoices/InvoiceService.ts +++ b/server/src/internal/invoices/InvoiceService.ts @@ -39,7 +39,7 @@ export const processInvoice = ({ items: withItems ? (invoice.items || []).map((i) => { let feature = features?.find( - (f) => f.internal_id === i.internal_feature_id, + (f) => f.internal_id === i.internal_feature_id ); return InvoiceItemResponseSchema.parse({ @@ -84,7 +84,7 @@ export class InvoiceService { eq(invoices.internal_customer_id, internalCustomerId), internalEntityId ? eq(invoices.internal_entity_id, internalEntityId) - : undefined, + : undefined ), orderBy: [desc(invoices.created_at)], limit, @@ -146,7 +146,7 @@ export class InvoiceService { internal_customer_id: internalCustomerId, product_ids: uniqueProductIds, created_at: stripeInvoice.created * 1000, - stripe_id: stripeInvoice.id, + stripe_id: stripeInvoice.id!, hosted_invoice_url: stripeInvoice.hosted_invoice_url || null, status: status || (stripeInvoice.status as InvoiceStatus | null), internal_product_ids: uniqueInternalProductIds, diff --git a/server/src/internal/invoices/invoiceUtils.ts b/server/src/internal/invoices/invoiceUtils.ts index 22ce2f3b9..9a2fbd76b 100644 --- a/server/src/internal/invoices/invoiceUtils.ts +++ b/server/src/internal/invoices/invoiceUtils.ts @@ -31,7 +31,7 @@ export const insertInvoiceFromAttach = async ({ // Create or update let invoice = await InvoiceService.getByStripeId({ db, - stripeId: stripeInvoice.id, + stripeId: stripeInvoice.id!, }); let autumnInvoiceItems = await getInvoiceItems({ @@ -43,7 +43,7 @@ export const insertInvoiceFromAttach = async ({ if (invoice) { await InvoiceService.updateByStripeId({ db, - stripeId: stripeInvoice.id, + stripeId: stripeInvoice.id!, updates: { product_ids: attachParams.products.map((p) => p.id), internal_product_ids: attachParams.products.map((p) => p.internal_id), @@ -99,7 +99,7 @@ export const getInvoiceItems = async ({ for (const line of stripeInvoice.lines.data) { let price = findPriceInStripeItems({ prices, - subItem: line, + lineItem: line, }); if (!price) { diff --git a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts index 6065ae56f..743333f77 100644 --- a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts +++ b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts @@ -30,13 +30,13 @@ export const createAndFinalizeInvoice = async ({ for (const item of invoiceItems) { await stripeCli.invoiceItems.create({ ...item, - invoice: invoice.id, + invoice: invoice.id!, customer: stripeCusId, }); } } - let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id, { + let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, { auto_advance: false, }); @@ -47,7 +47,7 @@ export const createAndFinalizeInvoice = async ({ invoice: paidInvoice, } = await payForInvoice({ stripeCli, - invoiceId: finalInvoice.id, + invoiceId: finalInvoice.id!, paymentMethod, logger, errorOnFail: errorOnPaymentFail, diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts index 5942e1d1b..8c5fd4877 100644 --- a/server/src/internal/products/ProductService.ts +++ b/server/src/internal/products/ProductService.ts @@ -303,7 +303,7 @@ export class ProductService { parseFreeTrials({ product: data }); if (!data) { - if (allowNotFound) return null; + if (allowNotFound) return null as unknown as FullProduct; throw new RecaseError({ message: `Product ${idOrInternalId} not found`, code: ErrCode.ProductNotFound, diff --git a/server/src/utils/importUtils/addProductFromSubs.ts b/server/src/utils/importUtils/addProductFromSubs.ts index 958726ab5..a0dba033d 100644 --- a/server/src/utils/importUtils/addProductFromSubs.ts +++ b/server/src/utils/importUtils/addProductFromSubs.ts @@ -20,12 +20,13 @@ import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUt import { subToAutumnInterval } from "@/external/stripe/utils.js"; import { prices as priceTable } from "@autumn/shared"; import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; export const addProductFromSubs = async ({ req, autumnCus, autumnProduct, - stripeSubs, + sub, prices, entitlements, force = false, @@ -35,7 +36,7 @@ export const addProductFromSubs = async ({ req: ExtendedRequest; autumnCus: FullCustomer; autumnProduct: FullProduct; - stripeSubs: Stripe.Subscription[]; + sub: Stripe.Subscription; prices?: Price[]; entitlements?: EntitlementWithFeature[]; force?: boolean; @@ -71,9 +72,7 @@ export const addProductFromSubs = async ({ } // Handle if trialing - let trialEndsAt = stripeSubs?.[0]?.trial_end - ? stripeSubs[0].trial_end * 1000 - : null; + let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null; // throw new Error("test"); @@ -86,6 +85,10 @@ export const addProductFromSubs = async ({ }); } + const { start, end } = subToPeriodStartEnd({ + sub, + }); + let newCusProduct = await createFullCusProduct({ db, attachParams: { @@ -108,18 +111,16 @@ export const addProductFromSubs = async ({ }, logger, trialEndsAt: trialEndsAt || undefined, - subscriptionIds: stripeSubs.map((s) => s.id), - anchorToUnix: anchorToUnix || stripeSubs?.[0]?.current_period_end * 1000, + subscriptionIds: sub ? [sub.id] : [], + anchorToUnix: anchorToUnix || end, - subscriptionStatus: stripeSubs?.[0]?.status - ? (stripeToAutumnSubStatus(stripeSubs[0].status) as CusProductStatus) + subscriptionStatus: sub?.status + ? (stripeToAutumnSubStatus(sub?.status) as CusProductStatus) : undefined, - canceledAt: stripeSubs?.[0]?.canceled_at - ? stripeSubs[0].canceled_at * 1000 - : null, + canceledAt: sub?.canceled_at ? sub.canceled_at * 1000 : null, - createdAt: stripeSubs?.[0]?.created ? stripeSubs[0].created * 1000 : null, + createdAt: sub?.created ? sub.created * 1000 : null, sendWebhook: false, }); @@ -132,31 +133,29 @@ export const addProductFromSubs = async ({ .filter((p) => isUsagePrice({ price: p })) .map((p) => (p.config as UsagePriceConfig).internal_feature_id); - for (const sub of stripeSubs) { - let subFromDb = await SubService.getInStripeIds({ + let subFromDb = await SubService.getInStripeIds({ + db, + ids: [sub.id], + }); + + let subInterval = subToAutumnInterval(sub); + + if (subFromDb.length === 0) { + await SubService.createSub({ db, - ids: [sub.id], + sub: constructSub({ + stripeId: sub.id, + usageFeatures: + subInterval.interval == BillingInterval.Month ? usageFeatures : [], + orgId: org.id, + env, + currentPeriodStart: start, + currentPeriodEnd: end, + }), }); - - let subInterval = subToAutumnInterval(sub); - - if (subFromDb.length === 0) { - await SubService.createSub({ - db, - sub: constructSub({ - stripeId: sub.id, - usageFeatures: - subInterval.interval == BillingInterval.Month ? usageFeatures : [], - orgId: org.id, - env, - currentPeriodStart: sub.current_period_start, - currentPeriodEnd: sub.current_period_end, - }), - }); - logger.info(`Created sub ${sub.id} in DB`); - } else { - logger.info(`Sub ${sub.id} already exists in DB`); - } + logger.info(`Created sub ${sub.id} in DB`); + } else { + logger.info(`Sub ${sub.id} already exists in DB`); } autumnCus.customer_products = [ diff --git a/server/tests/alex/00_setup.ts b/server/tests/alex/00_setup.ts index be8e718eb..fb9839322 100644 --- a/server/tests/alex/00_setup.ts +++ b/server/tests/alex/00_setup.ts @@ -8,8 +8,6 @@ const DEFAULT_ENV = AppEnv.Sandbox; describe("Initialize org for tests", () => { it("should initialize org", async function () { - this.timeout(20000); - this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV }); this.env = DEFAULT_ENV; await setupOrg({ diff --git a/server/tests/alex/04_topups.ts b/server/tests/alex/04_topups.ts index 5d4940cd3..47e0bd257 100644 --- a/server/tests/alex/04_topups.ts +++ b/server/tests/alex/04_topups.ts @@ -65,14 +65,14 @@ describe(chalk.yellowBright("Top ups"), () => { const cusRes = await AutumnCli.getCustomer(customerId); // Get product const product = cusRes.add_ons.find( - (p: any) => p.id === alexProducts.topUpMessages.id, + (p: any) => p.id === alexProducts.topUpMessages.id ); expect(product).to.exist; expect(product.status).to.equal(CusProductStatus.Active); // Check quantity is correct let cusEnt = cusRes.entitlements.find( - (e: any) => e.feature_id === alexFeatures.topUpMessage.id, + (e: any) => e.feature_id === alexFeatures.topUpMessage.id ); expect(cusEnt).to.exist; @@ -86,7 +86,7 @@ describe(chalk.yellowBright("Top ups"), () => { const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, alexFeatures.topUpMessage.id, - true, + true ); leftoverBalance = balanceObj.balance; @@ -102,18 +102,18 @@ describe(chalk.yellowBright("Top ups"), () => { const cusRes = await AutumnCli.getCustomer(customerId); // Get product const product = cusRes.add_ons.find( - (p: any) => p.id === alexProducts.topUpMessages.id, + (p: any) => p.id === alexProducts.topUpMessages.id ); expect(product).to.exist; expect(product.status).to.equal(CusProductStatus.Active); // Check quantity is correct let cusEnt = cusRes.entitlements.find( - (e: any) => e.feature_id === alexFeatures.topUpMessage.id, + (e: any) => e.feature_id === alexFeatures.topUpMessage.id ); expect(cusEnt).to.exist; expect(cusEnt.balance).to.equal( - leftoverBalance + overrideQuantity * billingUnits, + leftoverBalance + overrideQuantity * billingUnits ); expect(cusEnt.interval).to.equal(prodEnt.interval); }); @@ -166,7 +166,7 @@ describe(chalk.yellowBright("Testing o1 message top up"), () => { const cusRes = await AutumnCli.getCustomer(customerId); // Get product const product = cusRes.add_ons.find( - (p: any) => p.id === alexProducts.o1TopUps.id, + (p: any) => p.id === alexProducts.o1TopUps.id ); expect(product).to.exist; diff --git a/server/tests/alex/utils.ts b/server/tests/alex/utils.ts index cca366f74..03de6e957 100644 --- a/server/tests/alex/utils.ts +++ b/server/tests/alex/utils.ts @@ -184,7 +184,7 @@ export const runEventsAndCheckBalances = async ({ ); } - let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 1000; + let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500; await timeout(timeoutMilli); await Promise.all(batchUpdate);