From d5a226e8fd4914e6afba04de89ff3e4b7b968c7e Mon Sep 17 00:00:00 2001 From: John Yeo Date: Tue, 24 Feb 2026 13:34:34 +0000 Subject: [PATCH] feat: atomic update usage --- scripts/testGroups/g2.sh | 30 ++--- .../src/cron/oneoffCron/runOneOffCleanup.ts | 48 +++++-- .../resetCron/resetCustomerEntitlement.ts | 31 +++-- .../resetShortDurationCustomerEntitlement.ts | 8 +- server/src/cron/resetCron/runResetCron.ts | 2 +- server/src/db/repoContext.ts | 14 +++ .../webhookHandlers/handleRevenucatRenewal.ts | 6 +- .../handleRevenuecatBillingIssue.ts | 2 +- .../handleRevenuecatCancellation.ts | 2 +- .../handleRevenuecatExpiration.ts | 2 +- .../handleRevenuecatInitialPurchase.ts | 2 +- .../handleRevenuecatUncancellation.ts | 2 +- .../handleContUsePrices.ts | 15 ++- .../handleInvoiceCreated.ts | 118 ++---------------- .../handlePrepaidPrices.ts | 18 +-- .../handleInvoiceCreated/handleUsagePrices.ts | 18 ++- ...processAllocatedPricesForInvoiceCreated.ts | 8 +- ...rocessConsumablePricesForInvoiceCreated.ts | 4 +- .../processPrepaidPricesForInvoiceCreated.ts | 10 +- .../handleInvoiceActionRequiredCompleted.ts | 2 +- ...sConsumablePricesForSubscriptionDeleted.ts | 3 +- .../handleStripeSubscriptionCanceled.ts | 2 +- .../handleStripeSubscriptionRenewed.ts | 6 +- .../syncCustomerProductStatus.ts | 3 +- .../webhookHandlers/handleSubCreated.ts | 2 +- .../handleMarketplaceInvoicePaid.ts | 23 ++-- .../balances/handlers/handleCreateBalance.ts | 17 ++- .../balances/handlers/handleUpdateBalance.ts | 2 +- .../updateBalance/updateGrantedBalance.ts | 4 +- .../paidAllocatedFeature/adjustAllowance.ts | 26 +--- .../createUpgradeProrationInvoice.ts | 23 ++-- .../handleProratedDowngrade.ts | 27 ++-- .../handleProratedUpgrade.ts | 17 +-- .../handlePaidAllocatedCusEnt.ts | 7 +- .../paidAllocatedFeature/rollbackDeduction.ts | 2 +- .../insertNewCusProducts.ts | 4 +- .../updateCustomerEntitlements.ts | 6 +- .../v2/execute/executeAutumnBillingPlan.ts | 4 +- ...applyExistingRolloversToCustomerProduct.ts | 3 +- .../reapplyExistingUsagesToCustomerProduct.ts | 2 +- .../finalizeCreateCustomer.ts | 5 +- .../applyResetResults.ts | 4 +- .../add-product/createFullCusProduct.ts | 36 ++++-- .../add-product/createOneTimeCusProduct.ts | 30 ++--- .../multiAttach/handleMultiAttachFlow.ts | 4 +- .../handleQuantityDowngrade.ts | 4 +- .../handleQuantityUpgrade.ts | 4 +- .../createUsageInvoiceItems.ts | 7 +- .../upgradeFlow/handleLegacyUpgradeFlow.ts | 4 +- .../upgradeFlow/updateStripeSub2.ts | 2 +- .../attach/attachUtils/getAttachFunction.ts | 2 +- .../cusProducts/CusProductService.ts | 12 +- .../cusProducts/actions/activateScheduled.ts | 2 +- .../actions/cleanupOneOff/cleanupOneOff.ts | 4 +- .../actions/deleteScheduledCustomerProduct.ts | 2 +- .../actions/expireAndActivateDefault.ts | 2 +- .../cusEnts/CusEntitlementService.ts | 81 ++++++++++-- .../cusProducts/cusEnts/RepService.ts | 17 ++- .../cusEnts/cusRollovers/RolloverService.ts | 21 ++-- .../repos/batchUpdateCustomerProducts.ts | 14 ++- .../handleDecreaseAndTransfer.ts | 4 +- .../handlers/handleTransferProductV2.ts | 2 +- .../internal/entities/actions/deleteEntity.ts | 9 +- .../createEntityForCusProduct.ts | 20 ++- .../handleDeleteEntity/cancelSubsForEntity.ts | 4 +- .../handleDeleteEntity/handleDeleteEntity.ts | 43 ++++--- .../invoiceItemUtils/invoiceItemUtils.ts | 15 +-- .../migrateRevenuecatCustomer.ts | 2 +- server/src/utils/importUtils/updateUsages.ts | 8 +- .../advanced/customInterval/reset1.test.ts | 4 +- .../tests/balances/cron/loose-reset.test.ts | 2 +- .../track-race-condition3.test.ts | 2 +- .../track/rollovers/rolloverTestUtils.ts | 9 +- .../track/rollovers/track-rollover1.test.ts | 4 +- .../track/rollovers/track-rollover2.test.ts | 4 +- .../track/rollovers/track-rollover5.test.ts | 4 +- .../track/rollovers/track-rollover6.test.ts | 4 +- .../balances/legacy/legacy-set-usage.test.ts | 2 +- .../legacy/attach/addon/legacy-addon.test.ts | 9 +- .../checkout/legacy-checkout-advanced.test.ts | 10 +- .../legacy-downgrade-merged-clock.test.ts | 8 +- .../attach/downgrade/legacy-downgrade.test.ts | 15 +-- .../attach/trial/legacy-trial-merged.test.ts | 2 +- .../legacy/attach/trial/legacy-trial.test.ts | 4 +- .../tests/utils/testInitUtils/initScenario.ts | 2 +- 85 files changed, 484 insertions(+), 490 deletions(-) create mode 100644 server/src/db/repoContext.ts diff --git a/scripts/testGroups/g2.sh b/scripts/testGroups/g2.sh index 134123f91..10a106163 100755 --- a/scripts/testGroups/g2.sh +++ b/scripts/testGroups/g2.sh @@ -2,26 +2,26 @@ source "$(dirname "$0")/config.sh" -export TEST_FILE_CONCURRENCY=6 +export TEST_FILE_CONCURRENCY=3 BUN_PARALLEL_V2 \ 'billing/legacy/attach' -BUN_PARALLEL_V2 \ - 'attach/basic' \ - 'attach/upgrade' \ - 'attach/downgrade' \ - 'attach/free' \ - 'attach/addOn' \ - 'attach/checkout' \ - 'attach/others' \ - 'attach/upgradeOld' \ - 'attach/response' \ - 'interval/upgrade' \ - 'interval/multiSub' \ - 'server/tests/attach/entities' \ - --max=6 +# BUN_PARALLEL_V2 \ +# 'attach/basic' \ +# 'attach/upgrade' \ +# 'attach/downgrade' \ +# 'attach/free' \ +# 'attach/addOn' \ +# 'attach/checkout' \ +# 'attach/others' \ +# 'attach/upgradeOld' \ +# 'attach/response' \ +# 'interval/upgrade' \ +# 'interval/multiSub' \ +# 'server/tests/attach/entities' \ +# --max=6 # # 'billing/invoice-action-required' \ \ No newline at end of file diff --git a/server/src/cron/oneoffCron/runOneOffCleanup.ts b/server/src/cron/oneoffCron/runOneOffCleanup.ts index 637a2009c..a7e3303c4 100644 --- a/server/src/cron/oneoffCron/runOneOffCleanup.ts +++ b/server/src/cron/oneoffCron/runOneOffCleanup.ts @@ -1,4 +1,5 @@ -import { CusProductStatus } from "@autumn/shared"; +import { type AppEnv, CusProductStatus } from "@autumn/shared"; +import type { RepoContext } from "@/db/repoContext.js"; import { getOneOffCustomerProductsToCleanup } from "@/internal/customers/cusProducts/actions/cleanupOneOff/getOneOffToCleanup.js"; import { batchUpdateCustomerProducts } from "@/internal/customers/cusProducts/repos/batchUpdateCustomerProducts.js"; import type { CronContext } from "../utils/CronContext.js"; @@ -33,13 +34,44 @@ export const runOneOffCleanup = async ({ ctx }: { ctx: CronContext }) => { ); } - await batchUpdateCustomerProducts({ - db: ctx.db, - updates: toCleanup.map((result) => ({ - id: result.customer_product.id, - updates: { status: CusProductStatus.Expired }, - })), - }); + // Group customer products by org and env + const groupedByOrgEnv = new Map< + string, + { orgId: string; env: AppEnv; customerProductIds: string[] } + >(); + + for (const result of toCleanup) { + const key = `${result.org.id}:${result.customer.env}`; + if (!groupedByOrgEnv.has(key)) { + groupedByOrgEnv.set(key, { + orgId: result.org.id, + env: result.customer.env, + customerProductIds: [], + }); + } + groupedByOrgEnv + .get(key)! + .customerProductIds.push(result.customer_product.id); + } + + // Process each org/env group + for (const [_key, group] of groupedByOrgEnv) { + const repoContext: RepoContext = { + db: ctx.db, + org: { + id: group.orgId, + }, + env: group.env, + logger: logger, + }; + await batchUpdateCustomerProducts({ + ctx: repoContext, + updates: group.customerProductIds.map((id) => ({ + id, + updates: { status: CusProductStatus.Expired }, + })), + }); + } logger.info(`Expired ${toCleanup.length} customer products`); console.log(`Expired ${toCleanup.length} customer products`); diff --git a/server/src/cron/resetCron/resetCustomerEntitlement.ts b/server/src/cron/resetCron/resetCustomerEntitlement.ts index 19341f78a..6019a40e9 100644 --- a/server/src/cron/resetCron/resetCustomerEntitlement.ts +++ b/server/src/cron/resetCron/resetCustomerEntitlement.ts @@ -7,7 +7,7 @@ import { } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; import { format } from "date-fns"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService"; import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService"; @@ -16,20 +16,31 @@ import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js"; import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; import { getNextResetAt } from "@/utils/timeUtils.js"; +import type { CronContext } from "../utils/CronContext"; import { getStripeSubscriptionAnchor } from "./getStripeSubscriptionAnchor"; import { resetShortDurationCustomerEntitlement } from "./resetShortDurationCustomerEntitlement"; const shortDurations = [EntInterval.Minute, EntInterval.Hour, EntInterval.Day]; export const resetCustomerEntitlement = async ({ - db, + ctx, cusEnt, updatedCusEnts, }: { - db: DrizzleCli; + ctx: CronContext; cusEnt: ResetCusEnt; updatedCusEnts: ResetCusEnt[]; }) => { + const repoContext: RepoContext = { + db: ctx.db, + logger: ctx.logger, + org: { + id: cusEnt.customer.org_id, + }, + env: cusEnt.customer.env, + customerId: cusEnt.customer_id ?? "", + }; + try { const ent = cusEnt.entitlement as FullEntitlement; @@ -38,7 +49,7 @@ export const resetCustomerEntitlement = async ({ shortDurations.includes(ent.interval as EntInterval) ) { return await resetShortDurationCustomerEntitlement({ - db, + ctx: repoContext, cusEnt, updatedCusEnts, }); @@ -48,7 +59,7 @@ export const resetCustomerEntitlement = async ({ let relatedCusPrice = null; if (cusEnt.customer_product_id) { const cusPrices = await CusPriceService.getByCustomerProductId({ - db, + db: ctx.db, customerProductId: cusEnt.customer_product_id, }); relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices); @@ -66,7 +77,7 @@ export const resetCustomerEntitlement = async ({ const entitlement = cusEnt.entitlement; if (entitlement.allowance_type === AllowanceType.Unlimited) { await CusEntService.update({ - db, + ctx: repoContext, id: cusEnt.id, updates: { unlimited: true, @@ -82,7 +93,7 @@ export const resetCustomerEntitlement = async ({ if (entitlement.interval === EntInterval.Lifetime) { await CusEntService.update({ - db, + ctx: repoContext, id: cusEnt.id, updates: { next_reset_at: null, @@ -125,7 +136,7 @@ export const resetCustomerEntitlement = async ({ if (cusEnt.customer_product) { try { nextResetAt = await getStripeSubscriptionAnchor({ - db, + db: ctx.db, cusEnt, nextResetAt, }); @@ -138,7 +149,7 @@ export const resetCustomerEntitlement = async ({ } await CusEntService.update({ - db, + ctx: repoContext, id: cusEnt.id, updates: { ...resetBalanceUpdate, @@ -149,7 +160,7 @@ export const resetCustomerEntitlement = async ({ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { await RolloverService.insert({ - db, + ctx: repoContext, rows: rolloverUpdate.toInsert, fullCusEnt: cusEnt, }); diff --git a/server/src/cron/resetCron/resetShortDurationCustomerEntitlement.ts b/server/src/cron/resetCron/resetShortDurationCustomerEntitlement.ts index d09c79595..faf6c6639 100644 --- a/server/src/cron/resetCron/resetShortDurationCustomerEntitlement.ts +++ b/server/src/cron/resetCron/resetShortDurationCustomerEntitlement.ts @@ -1,18 +1,18 @@ import type { EntInterval, FullEntitlement, ResetCusEnt } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; import { Decimal } from "decimal.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService"; import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils"; import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils"; import { getNextResetAt } from "@/utils/timeUtils.js"; export const resetShortDurationCustomerEntitlement = async ({ - db, + ctx, cusEnt, updatedCusEnts, }: { - db: DrizzleCli; + ctx: RepoContext; cusEnt: ResetCusEnt; updatedCusEnts: ResetCusEnt[]; }) => { @@ -44,7 +44,7 @@ export const resetShortDurationCustomerEntitlement = async ({ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { await RolloverService.insert({ - db, + ctx, rows: rolloverUpdate.toInsert, fullCusEnt: cusEnt, }); diff --git a/server/src/cron/resetCron/runResetCron.ts b/server/src/cron/resetCron/runResetCron.ts index f674f5f17..f868cfe72 100644 --- a/server/src/cron/resetCron/runResetCron.ts +++ b/server/src/cron/resetCron/runResetCron.ts @@ -48,7 +48,7 @@ export const runResetCron = async ({ ctx }: { ctx: CronContext }) => { for (const cusEnt of batch) { batchResets.push( resetCustomerEntitlement({ - db, + ctx, cusEnt: cusEnt, updatedCusEnts, }), diff --git a/server/src/db/repoContext.ts b/server/src/db/repoContext.ts new file mode 100644 index 000000000..78cae9a87 --- /dev/null +++ b/server/src/db/repoContext.ts @@ -0,0 +1,14 @@ +import type { AppEnv } from "@autumn/shared"; +import type { Logger } from "@/external/logtail/logtailUtils"; +import type { DrizzleCli } from "./initDrizzle.js"; + +export interface RepoContext { + org: { + id: string; + }; + env: AppEnv; + + db: DrizzleCli; + logger: Logger; + customerId?: string; +} diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenucatRenewal.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenucatRenewal.ts index a3ea69163..88e41af4a 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenucatRenewal.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenucatRenewal.ts @@ -55,7 +55,7 @@ export const handleRenewal = async ({ `Renewal for existing past due product ${product.id}, marking as active`, ); await CusProductService.update({ - db, + ctx, cusProductId: curSameProduct.id, updates: { status: CusProductStatus.Active, @@ -86,7 +86,7 @@ export const handleRenewal = async ({ // Expire old cus_product await CusProductService.update({ - db, + ctx, cusProductId: curMainProduct.id, updates: { status: CusProductStatus.Expired, @@ -98,7 +98,7 @@ export const handleRenewal = async ({ } else if (curSameProduct) { // Reactivate the same product if it was expired/cancelled await CusProductService.update({ - db, + ctx, cusProductId: curSameProduct.id, updates: { status: CusProductStatus.Active, diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatBillingIssue.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatBillingIssue.ts index 9efad9362..1171ae280 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatBillingIssue.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatBillingIssue.ts @@ -48,7 +48,7 @@ export const handleBillingIssue = async ({ if (ACTIVE_STATUSES.includes(curSameProduct.status)) { await CusProductService.update({ - db, + ctx, cusProductId: curSameProduct.id, updates: { status: CusProductStatus.PastDue, diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatCancellation.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatCancellation.ts index 4a5f9dc90..cdac3c5b8 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatCancellation.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatCancellation.ts @@ -36,7 +36,7 @@ export const handleCancellation = async ({ } await CusProductService.update({ - db, + ctx, cusProductId: curSameProduct.id, updates: { canceled_at: Date.now(), diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatExpiration.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatExpiration.ts index a8f487f7c..0e8b30fdc 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatExpiration.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatExpiration.ts @@ -37,7 +37,7 @@ export const handleExpiration = async ({ // Expire the cus_product await CusProductService.update({ - db, + ctx, cusProductId: curSameProduct.id, updates: { status: CusProductStatus.Expired, diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatInitialPurchase.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatInitialPurchase.ts index 0e8f7f84d..672fb98f4 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatInitialPurchase.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatInitialPurchase.ts @@ -73,7 +73,7 @@ export const handleInitialPurchase = async ({ // Expire old cus_product await CusProductService.update({ - db, + ctx, cusProductId: curMainProduct.id, updates: { status: CusProductStatus.Expired, diff --git a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatUncancellation.ts b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatUncancellation.ts index 9f3d2c743..3c78fcbda 100644 --- a/server/src/external/revenueCat/webhookHandlers/handleRevenuecatUncancellation.ts +++ b/server/src/external/revenueCat/webhookHandlers/handleRevenuecatUncancellation.ts @@ -33,7 +33,7 @@ export const handleUncancellation = async ({ if (cusProduct) { await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { canceled_at: null, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts index 9b6b5bd9d..28bafb477 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts @@ -4,7 +4,7 @@ import type { } from "@autumn/shared"; import { customerPriceToCustomerEntitlement } from "@autumn/shared"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { findLinkedCusEnts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js"; import { removeReplaceablesFromCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.js"; @@ -12,22 +12,21 @@ import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService. import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; export const handleContUsePrices = async ({ - db, + ctx, cusEnts, cusPrice, invoice, usageSub, - logger, resetBalance = true, }: { - db: DrizzleCli; + ctx: AutumnContext; cusEnts: FullCustomerEntitlement[]; cusPrice: FullCustomerPrice; invoice: Stripe.Invoice; usageSub: Stripe.Subscription; - logger: any; resetBalance?: boolean; }): Promise => { + const { logger } = ctx; const cusEnt = customerPriceToCustomerEntitlement({ customerPrice: cusPrice, customerEntitlements: cusEnts, @@ -74,7 +73,7 @@ export const handleContUsePrices = async ({ }); await CusEntService.update({ - db, + ctx, id: linkedCusEnt.id, updates: { entities: newEntities, @@ -83,13 +82,13 @@ export const handleContUsePrices = async ({ } await CusEntService.increment({ - db, + ctx, id: cusEnt.id, amount: replaceables.length, }); await RepService.deleteInIds({ - db, + ctx, ids: replaceables.map((r) => r.id), }); diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts index 4450ca428..e810641bf 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts @@ -1,23 +1,16 @@ import { - type AppEnv, BillingType, - CusProductStatus, customerPriceToCustomerEntitlement, type FullCusProduct, isFixedPrice, - type Organization, } from "@autumn/shared"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import { FeatureService } from "@/internal/features/FeatureService.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { stripeInvoiceToStripeSubscriptionId } from "../../invoices/utils/convertStripeInvoice"; -import { getFullStripeInvoice } from "../../stripeInvoiceUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; -import { getStripeSubs } from "../../stripeSubUtils.js"; import { handleContUsePrices } from "./handleContUsePrices.js"; import { handlePrepaidPrices } from "./handlePrepaidPrices.js"; import { handleUsagePrices } from "./handleUsagePrices.js"; @@ -28,24 +21,19 @@ import { handleUsagePrices } from "./handleUsagePrices.js"; // For upgrade, bill_immediately: invoice period start = sub period start (cur cycle), invoice period end cancel immediately date export const sendUsageAndReset = async ({ - db, + ctx, activeProduct, - org, - env, invoice, - logger, submitUsage = true, resetBalance = true, }: { - db: DrizzleCli; + ctx: AutumnContext; activeProduct: FullCusProduct; - org: Organization; - env: AppEnv; invoice: Stripe.Invoice; - logger: any; submitUsage?: boolean; resetBalance?: boolean; }) => { + const { org, env, logger } = ctx; const stripeCli = createStripeCli({ org, env }); const cusEnts = activeProduct.customer_entitlements; @@ -85,15 +73,13 @@ export const sendUsageAndReset = async ({ if (billingType === BillingType.UsageInArrear) { const handledUsage = await handleUsagePrices({ - db, - org, + ctx, invoice, customer, relatedCusEnt, stripeCli, price, usageSub: usageBasedSub, - logger, activeProduct, submitUsage, resetBalance, @@ -104,12 +90,11 @@ export const sendUsageAndReset = async ({ if (billingType === BillingType.InArrearProrated) { const handledContUse = await handleContUsePrices({ - db, + ctx, cusEnts, cusPrice, invoice, usageSub: usageBasedSub, - logger, resetBalance, }); @@ -118,12 +103,11 @@ export const sendUsageAndReset = async ({ if (billingType === BillingType.UsageInAdvance) { const handledPrepaid = await handlePrepaidPrices({ - db, + ctx, cusPrice, cusProduct: activeProduct, usageSub: usageBasedSub, invoice, - logger, resetBalance, }); @@ -131,91 +115,3 @@ export const sendUsageAndReset = async ({ } } }; - -const handleInvoiceCreated = async ({ - db, - org, - data, - env, - logger, -}: { - db: DrizzleCli; - org: Organization; - data: Stripe.Invoice; - env: AppEnv; - logger: any; -}) => { - const stripeCli = createStripeCli({ org, env }); - const invoice = await getFullStripeInvoice({ - stripeCli, - stripeId: data.id!, - }); - - const subId = stripeInvoiceToStripeSubscriptionId(invoice); - - if (subId) { - const activeProducts = await CusProductService.getByStripeSubId({ - db, - stripeSubId: subId, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - // CusProductStatus.Expired, - CusProductStatus.PastDue, - ], - }); - - if (activeProducts.length === 0) { - logger.warn( - `Stripe invoice.created -- no active products found (${org.slug})`, - ); - return; - } - - await FeatureService.list({ - db, - orgId: org.id, - env, - }); - - const stripeSubs = await getStripeSubs({ - stripeCli: createStripeCli({ org, env }), - subIds: activeProducts.flatMap((p) => p.subscription_ids || []), - }); - - for (const activeProduct of activeProducts) { - const subId = stripeInvoiceToStripeSubscriptionId(invoice); - const subscription = stripeSubs.find((s) => s.id === subId); - - await sendUsageAndReset({ - db, - activeProduct, - org, - env, - invoice, - logger, - submitUsage: true, // Always submit usage during invoice.created - resetBalance: validateProductShouldReset({ - subscription, - _invoice: invoice, - }), // Skip balance reset for Vercel (wait for payment confirmation) - }); - } - } -}; - -const validateProductShouldReset = ({ - subscription, - _invoice, -}: { - subscription?: Stripe.Subscription; - _invoice: Stripe.Invoice; -}) => { - /** - * This was separated so as to give us headroom to add further Custom Payment Methods in the future. - * e.g RevenueCat etc... - */ - if (subscription?.metadata?.vercel_installation_id) return false; - return true; -}; diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts index 5668684f4..8f7fcd36f 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts @@ -7,7 +7,7 @@ import { type UsagePriceConfig, } from "@autumn/shared"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.js"; @@ -18,22 +18,22 @@ import { notNullish } from "@/utils/genUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; export const handlePrepaidPrices = async ({ - db, + ctx, cusProduct, cusPrice, usageSub, invoice, - logger, resetBalance = true, }: { - db: DrizzleCli; + ctx: AutumnContext; cusProduct: FullCusProduct; cusPrice: FullCustomerPrice; usageSub: Stripe.Subscription; invoice: Stripe.Invoice; - logger: any; resetBalance?: boolean; }): Promise => { + const { logger } = ctx; + const { org } = ctx; const { start, end } = subToPeriodStartEnd({ sub: usageSub }); const isNewPeriod = invoice.period_start !== start; @@ -93,7 +93,7 @@ export const handlePrepaidPrices = async ({ }); await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { options: newOptions as FeatureOptions[], @@ -104,7 +104,7 @@ export const handlePrepaidPrices = async ({ const difference = (options?.quantity ?? 0) - (options?.upcoming_quantity ?? 0); await CusEntService.decrement({ - db, + ctx, id: cusEnt.id, amount: difference, }); @@ -118,14 +118,14 @@ export const handlePrepaidPrices = async ({ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { await RolloverService.insert({ - db, + ctx, rows: rolloverUpdate.toInsert, fullCusEnt: cusEnt, }); } await CusEntService.update({ - db, + ctx, id: cusEnt.id, updates: { ...resetUpdate, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts index 853d92208..9071780f1 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts @@ -4,13 +4,12 @@ import { EntInterval, type FullCusProduct, type FullCustomerEntitlement, - type Organization, type Price, type UsagePriceConfig, } from "@autumn/shared"; import { differenceInMinutes, subDays } from "date-fns"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.js"; import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js"; @@ -21,32 +20,31 @@ import { getInvoiceItemForUsage } from "../../stripePriceUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; export const handleUsagePrices = async ({ - db, - org, + ctx, invoice, customer, relatedCusEnt, stripeCli, price, usageSub, - logger, activeProduct, submitUsage = true, resetBalance = true, }: { - db: DrizzleCli; - org: Organization; + ctx: AutumnContext; invoice: Stripe.Invoice; customer: Customer; relatedCusEnt: FullCustomerEntitlement; stripeCli: Stripe; price: Price; usageSub: Stripe.Subscription; - logger: any; activeProduct: FullCusProduct; submitUsage?: boolean; resetBalance?: boolean; }): Promise => { + const { logger } = ctx; + const { org } = ctx; + const invoiceCreatedRecently = Math.abs( differenceInMinutes( @@ -153,7 +151,7 @@ export const handleUsagePrices = async ({ const { end } = subToPeriodStartEnd({ sub: usageSub }); await CusEntService.update({ - db, + ctx, id: relatedCusEnt.id, updates: { ...resetBalancesUpdate, @@ -169,7 +167,7 @@ export const handleUsagePrices = async ({ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { await RolloverService.insert({ - db, + ctx, rows: rolloverUpdate.toInsert, fullCusEnt: relatedCusEnt, }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processAllocatedPricesForInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processAllocatedPricesForInvoiceCreated.ts index fd9b8b40b..ad424ad46 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processAllocatedPricesForInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processAllocatedPricesForInvoiceCreated.ts @@ -27,7 +27,7 @@ const processAllocatedPrice = async ({ customerEntitlement: FullCusEntWithFullCusProduct; }) => { const { db } = ctx; - const { stripeInvoice } = eventContext; + const { stripeInvoice, fullCustomer } = eventContext; const customerProduct = customerEntitlement.customer_product; const customerEntitlements = customerProduct?.customer_entitlements ?? []; @@ -54,7 +54,7 @@ const processAllocatedPrice = async ({ }); await CusEntService.update({ - db, + ctx, id: linkedCusEnt.id, updates: { entities: newEntities, @@ -63,13 +63,13 @@ const processAllocatedPrice = async ({ } await CusEntService.increment({ - db, + ctx, id: customerEntitlement.id, amount: replaceables.length, }); await RepService.deleteInIds({ - db, + ctx, ids: replaceables.map((r) => r.id), }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts index 7761f3bb0..684c93c66 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processConsumablePricesForInvoiceCreated.ts @@ -88,7 +88,7 @@ export const processConsumablePricesForInvoiceCreated = async ({ } await CusEntService.batchUpdate({ - db: ctx.db, + ctx, data: updateCustomerEntitlements, }); @@ -100,7 +100,7 @@ export const processConsumablePricesForInvoiceCreated = async ({ }); await RolloverService.insert({ - db: ctx.db, + ctx, rows: rolloverUpdates.toInsert, fullCusEnt: update.customerEntitlement, }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts index b2cf00aaf..5384211f5 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceCreated/tasks/processPrepaidPricesForInvoiceCreated.ts @@ -41,7 +41,7 @@ const processPrepaidPrice = async ({ const customerProduct = customerEntitlement.customer_product; - const { stripeSubscription } = eventContext; + const { stripeSubscription, fullCustomer } = eventContext; const { db } = ctx; if (!options) return; @@ -80,7 +80,7 @@ const processPrepaidPrice = async ({ }); await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates: { options: newOptions, @@ -91,7 +91,7 @@ const processPrepaidPrice = async ({ const difference = (options?.quantity ?? 0) - (options?.upcoming_quantity ?? 0); await CusEntService.decrement({ - db, + ctx, id: customerEntitlement.id, amount: difference, }); @@ -105,14 +105,14 @@ const processPrepaidPrice = async ({ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) { await RolloverService.insert({ - db, + ctx, rows: rolloverUpdate.toInsert, fullCusEnt: customerEntitlement, }); } await CusEntService.update({ - db, + ctx, id: customerEntitlement.id, updates: { ...resetUpdate, diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceMetadata/handleInvoiceActionRequiredCompleted.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceMetadata/handleInvoiceActionRequiredCompleted.ts index 2b13a87bf..d11a827d1 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceMetadata/handleInvoiceActionRequiredCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeInvoicePaid/tasks/handleStripeInvoiceMetadata/handleInvoiceActionRequiredCompleted.ts @@ -53,7 +53,7 @@ export const handleInvoiceActionRequiredCompleted = async ({ const curCusProduct = attachParamsToCurCusProduct({ attachParams }); if (attachParams.cusEntIds && curCusProduct) { await resetUsageBalances({ - db: ctx.db, + ctx, cusEntIds: attachParams.cusEntIds, cusProduct: curCusProduct, }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/processConsumablePricesForSubscriptionDeleted.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/processConsumablePricesForSubscriptionDeleted.ts index 9f31245c5..06e9417bb 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/processConsumablePricesForSubscriptionDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionDeleted/tasks/processConsumablePricesForSubscriptionDeleted.ts @@ -47,7 +47,6 @@ export const processConsumablePricesForSubscriptionDeleted = async ({ ctx: StripeWebhookContext; eventContext: StripeSubscriptionDeletedContext; }): Promise => { - const { db } = ctx; const { stripeSubscription, fullCustomer, customerProducts } = eventContext; // Skip if subscription has metered items - Stripe handles metered billing automatically @@ -106,7 +105,7 @@ export const processConsumablePricesForSubscriptionDeleted = async ({ // 4. Reset usage balances for all affected customer entitlements (only if payment succeeded) await CusEntService.batchUpdate({ - db, + ctx, data: updateCustomerEntitlements, }); }; diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionCanceled/handleStripeSubscriptionCanceled.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionCanceled/handleStripeSubscriptionCanceled.ts index 8ce98b54a..d5a5210a6 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionCanceled/handleStripeSubscriptionCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionCanceled/handleStripeSubscriptionCanceled.ts @@ -75,7 +75,7 @@ export const handleStripeSubscriptionCanceled = async ({ }; await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionRenewed/handleStripeSubscriptionRenewed.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionRenewed/handleStripeSubscriptionRenewed.ts index 41ffec877..09e462c0a 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionRenewed/handleStripeSubscriptionRenewed.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/handleStripeSubscriptionRenewed/handleStripeSubscriptionRenewed.ts @@ -32,7 +32,7 @@ export const handleStripeSubscriptionRenewed = async ({ ctx: StripeWebhookContext; subscriptionUpdatedContext: StripeSubscriptionUpdatedContext; }): Promise => { - const { db, org, env, logger } = ctx; + const { org, env, logger } = ctx; const { stripeSubscription, previousAttributes, @@ -80,7 +80,7 @@ export const handleStripeSubscriptionRenewed = async ({ }; await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); @@ -112,7 +112,7 @@ export const handleStripeSubscriptionRenewed = async ({ if (scheduledProduct) { await CusProductService.delete({ - db, + ctx, cusProductId: scheduledProduct.id, }); diff --git a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/syncCustomerProductStatus/syncCustomerProductStatus.ts b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/syncCustomerProductStatus/syncCustomerProductStatus.ts index 791988876..23bc181b8 100644 --- a/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/syncCustomerProductStatus/syncCustomerProductStatus.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeSubscriptionUpdated/tasks/syncCustomerProductStatus/syncCustomerProductStatus.ts @@ -3,7 +3,6 @@ import { type CollectionMethod, CusProductStatus, cp, - type FullCusProduct, type InsertCustomerProduct, } from "@autumn/shared"; import { @@ -99,7 +98,7 @@ export const syncCustomerProductStatus = async ({ ); await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); diff --git a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts index 361ab5252..cb02f392e 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts @@ -106,7 +106,7 @@ export const handleSubCreated = async ({ const updateCusProd = async () => { await CusProductService.update({ - db, + ctx, cusProductId: cusProd.id, updates: { subscription_ids: subIds, diff --git a/server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoicePaid.ts b/server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoicePaid.ts index 6981986e9..d90db048b 100644 --- a/server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoicePaid.ts +++ b/server/src/external/vercel/handlers/marketplace/handleMarketplaceInvoicePaid.ts @@ -1,12 +1,5 @@ -import { - type AppEnv, - AttachScenario, - type FeatureOptions, - type Organization, -} from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AttachScenario, type FeatureOptions } from "@autumn/shared"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; -import type { Logger } from "@/external/logtail/logtailUtils.js"; import { sendUsageAndReset } from "@/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js"; import { parseVercelPrepaidQuantities } from "@/external/vercel/misc/vercelInvoicing.js"; import { VercelResourceService } from "@/external/vercel/services/VercelResourceService.js"; @@ -138,10 +131,11 @@ export const handleMarketplaceInvoicePaid = async ({ status: "ready", }, }); - } catch (error: any) { - logger.warn("Could not fetch or parse resource metadata", { - error: error.message, - resourceId: vercelResourceId, + } catch (error) { + logger.warn(`Could not fetch or parse resource metadata ${error}`, { + data: { + resourceId: vercelResourceId, + }, }); // Continue with empty optionsList } @@ -151,12 +145,9 @@ export const handleMarketplaceInvoicePaid = async ({ const activeProduct = existingCusProducts[0]; await sendUsageAndReset({ - db, + ctx, activeProduct, - org, - env, invoice, - logger, submitUsage: false, // Usage already submitted in invoice.created resetBalance: true, // Payment confirmed - now safe to reset balance }); diff --git a/server/src/internal/balances/handlers/handleCreateBalance.ts b/server/src/internal/balances/handlers/handleCreateBalance.ts index 68710c4f6..03d13a329 100644 --- a/server/src/internal/balances/handlers/handleCreateBalance.ts +++ b/server/src/internal/balances/handlers/handleCreateBalance.ts @@ -1,6 +1,5 @@ import { CreateBalanceParamsV0Schema } from "@autumn/shared"; import { FeatureNotFoundError } from "@shared/index"; -import type { DrizzleCli } from "@/db/initDrizzle"; import { createRoute } from "@/honoMiddlewares/routeHandler"; import { prepareNewBalanceForInsertion } from "@/internal/balances/createBalance/prepareNewBalanceForInsertion"; import { validateCreateBalanceParams } from "@/internal/balances/createBalance/validateCreateBalance"; @@ -44,16 +43,14 @@ export const handleCreateBalance = createRoute({ params: createBalanceParams, }); - await ctx.db.transaction(async (tx) => { - await EntitlementService.insert({ - db: tx as unknown as DrizzleCli, - data: [newEntitlement], - }); + await EntitlementService.insert({ + db: ctx.db, + data: [newEntitlement], + }); - await CusEntService.insert({ - db: tx as unknown as DrizzleCli, - data: [newCustomerEntitlement], - }); + await CusEntService.insert({ + ctx, + data: [newCustomerEntitlement], }); return c.json({ success: true }); diff --git a/server/src/internal/balances/handlers/handleUpdateBalance.ts b/server/src/internal/balances/handlers/handleUpdateBalance.ts index b86f8d564..196b5ecaa 100644 --- a/server/src/internal/balances/handlers/handleUpdateBalance.ts +++ b/server/src/internal/balances/handlers/handleUpdateBalance.ts @@ -66,7 +66,7 @@ export const handleUpdateBalance = createRoute({ if (notNullish(params.next_reset_at) && params.customer_entitlement_id) { await CusEntService.update({ - db: ctx.db, + ctx, id: params.customer_entitlement_id, updates: { next_reset_at: params.next_reset_at, diff --git a/server/src/internal/balances/updateBalance/updateGrantedBalance.ts b/server/src/internal/balances/updateBalance/updateGrantedBalance.ts index b5ed038e8..1684c6fcb 100644 --- a/server/src/internal/balances/updateBalance/updateGrantedBalance.ts +++ b/server/src/internal/balances/updateBalance/updateGrantedBalance.ts @@ -88,7 +88,7 @@ export const updateGrantedBalance = async ({ }; await CusEntService.update({ - db: ctx.db, + ctx, id: targetCusEnt.id, updates: { entities: newEntities }, }); @@ -97,7 +97,7 @@ export const updateGrantedBalance = async ({ targetCusEnt.entities = newEntities; } else { await CusEntService.update({ - db: ctx.db, + ctx, id: targetCusEnt.id, updates: { adjustment: requiredAdjustment }, }); diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/adjustAllowance.ts b/server/src/internal/balances/utils/paidAllocatedFeature/adjustAllowance.ts index 313c1115f..6ee4bbd55 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/adjustAllowance.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/adjustAllowance.ts @@ -1,5 +1,4 @@ import { - type AppEnv, BillingType, type Customer, type Entitlement, @@ -7,16 +6,15 @@ import { type Feature, type FullCusEntWithFullCusProduct, type FullCustomerPrice, - type Organization, type Price, type UsagePriceConfig, } from "@autumn/shared"; import { Decimal } from "decimal.js"; import { StatusCodes } from "http-status-codes"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; @@ -59,33 +57,25 @@ export const getUsageFromBalance = ({ }; export const adjustAllowance = async ({ - db, - env, - org, + ctx, affectedFeature, cusEnt, cusPrices, customer, originalBalance, newBalance, - logger, errorIfIncomplete = false, - // deduction, - // product, - // fromEntities = false, }: { - db: DrizzleCli; - env: AppEnv; + ctx: AutumnContext; affectedFeature: Feature; - org: Organization; cusEnt: FullCusEntWithFullCusProduct; cusPrices: FullCustomerPrice[]; customer: Customer; originalBalance: number; newBalance: number; - logger: any; errorIfIncomplete?: boolean; }) => { + const { logger, org, env } = ctx; const cusPrice = getRelatedCusPrice(cusEnt, cusPrices); const billingType = cusPrice ? getBillingType(cusPrice.price.config!) : null; const cusProduct = cusEnt.customer_product; @@ -139,7 +129,7 @@ export const adjustAllowance = async ({ if (isUpgrade) { return await handleProratedUpgrade({ - db, + ctx, stripeCli, cusEnt, cusPrice, @@ -147,13 +137,10 @@ export const adjustAllowance = async ({ subItem: subItem as Stripe.SubscriptionItem, newBalance, prevBalance: originalBalance, - org, - logger, }); } else { return await handleProratedDowngrade({ - db, - org, + ctx, stripeCli, cusEnt, cusPrice, @@ -161,7 +148,6 @@ export const adjustAllowance = async ({ subItem: subItem as Stripe.SubscriptionItem, newBalance, prevBalance: originalBalance, - logger, }); } }; diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/createUpgradeProrationInvoice.ts b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/createUpgradeProrationInvoice.ts index 8b55623cd..c36270560 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/createUpgradeProrationInvoice.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/createUpgradeProrationInvoice.ts @@ -3,7 +3,6 @@ import { type FullCustomerPrice, getFeatureInvoiceDescription, type OnIncrease, - type Organization, type Price, type Product, shouldBillNow, @@ -12,41 +11,41 @@ import { } from "@autumn/shared"; import { Decimal } from "decimal.js"; import type Stripe from "stripe"; -import type { Logger } from "@/external/logtail/logtailUtils.js"; import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { stripeSubscriptionToNowMs } from "@/external/stripe/subscriptions/utils/convertStripeSubscription"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js"; import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"; import { formatUnixToDate } from "@/utils/genUtils.js"; const getUpgradeProrationInvoiceItem = ({ + ctx, prevPrice, newPrice, now, feature, newRoundedUsage, price, - org, onIncrease, product, stripeSub, subItem, - logger, }: { + ctx: AutumnContext; prevPrice: number; newPrice: number; now: number; feature: Feature; newRoundedUsage: number; price: Price; - org: Organization; onIncrease: OnIncrease; product: Product; stripeSub: Stripe.Subscription; subItem: Stripe.SubscriptionItem; - logger: Logger; }) => { + const { logger, org } = ctx; + const billingUnits = (price.config as UsagePriceConfig).billing_units; let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber(); let invoiceDescription = getFeatureInvoiceDescription({ @@ -76,7 +75,7 @@ const getUpgradeProrationInvoiceItem = ({ const invoiceItem = constructStripeInvoiceItem({ product, amount: invoiceAmount, - org, + ctx, price: price, description: invoiceDescription, stripeSubId: stripeSub.id, @@ -93,7 +92,7 @@ const getUpgradeProrationInvoiceItem = ({ }; export const createUpgradeProrationInvoice = async ({ - org, + ctx, cusPrice, stripeCli, sub, @@ -105,9 +104,8 @@ export const createUpgradeProrationInvoice = async ({ product, config, onIncrease, - logger, }: { - org: Organization; + ctx: AutumnContext; cusPrice: FullCustomerPrice; stripeCli: Stripe; sub: Stripe.Subscription; @@ -119,8 +117,8 @@ export const createUpgradeProrationInvoice = async ({ product: Product; config: UsagePriceConfig; onIncrease: OnIncrease; - logger: Logger; }) => { + const { logger } = ctx; const now = await stripeSubscriptionToNowMs({ stripeSubscription: sub, stripeCli, @@ -132,18 +130,17 @@ export const createUpgradeProrationInvoice = async ({ }); const invoiceItem = getUpgradeProrationInvoiceItem({ + ctx, prevPrice, newPrice, now, feature, newRoundedUsage, price: cusPrice.price, - org, onIncrease, product, stripeSub: sub, subItem, - logger, }); const invoiceAmount = diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedDowngrade.ts b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedDowngrade.ts index 529d39178..3d95b8712 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedDowngrade.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedDowngrade.ts @@ -7,7 +7,6 @@ import { InternalError, OnDecrease, OnIncrease, - type Organization, type Product, priceToInvoiceAmount, shouldProrate, @@ -16,9 +15,8 @@ import { } from "@autumn/shared"; import { Decimal } from "decimal.js"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import type { Logger } from "@/external/logtail/logtailUtils.js"; import { stripeSubscriptionToNowMs } from "@/external/stripe/subscriptions/index.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js"; import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js"; import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js"; @@ -29,7 +27,7 @@ import { formatUnixToDate } from "@/utils/genUtils.js"; import { getUsageFromBalance } from "../adjustAllowance.js"; const createDowngradeProrationInvoice = async ({ - org, + ctx, cusPrice, stripeCli, sub, @@ -41,9 +39,8 @@ const createDowngradeProrationInvoice = async ({ product, onIncrease, onDecrease, - logger, }: { - org: Organization; + ctx: AutumnContext; cusPrice: FullCustomerPrice; stripeCli: Stripe; sub: Stripe.Subscription; @@ -55,8 +52,8 @@ const createDowngradeProrationInvoice = async ({ product: Product; onIncrease: OnIncrease; onDecrease: OnDecrease; - logger: any; }) => { + const { logger } = ctx; const config = cusPrice.price.config as UsagePriceConfig; const now = await stripeSubscriptionToNowMs({ @@ -94,9 +91,9 @@ const createDowngradeProrationInvoice = async ({ ); const invoiceItem = constructStripeInvoiceItem({ + ctx, product, amount: invoiceAmount, - org, price: cusPrice.price, description: invoiceDescription, stripeSubId: sub.id, @@ -129,8 +126,7 @@ const createDowngradeProrationInvoice = async ({ }; export const handleProratedDowngrade = async ({ - db, - org, + ctx, stripeCli, cusEnt, cusPrice, @@ -138,10 +134,8 @@ export const handleProratedDowngrade = async ({ subItem, newBalance, prevBalance, - logger, }: { - db: DrizzleCli; - org: Organization; + ctx: AutumnContext; stripeCli: Stripe; cusEnt: FullCusEntWithFullCusProduct; cusPrice: FullCustomerPrice; @@ -149,8 +143,8 @@ export const handleProratedDowngrade = async ({ subItem: Stripe.SubscriptionItem; newBalance: number; prevBalance: number; - logger: Logger; }) => { + const { logger } = ctx; logger.info(`Handling quantity decrease`); if (!cusEnt.customer_product) { @@ -200,7 +194,7 @@ export const handleProratedDowngrade = async ({ }); invoice = await createDowngradeProrationInvoice({ - org, + ctx, cusPrice, stripeCli, sub, @@ -217,7 +211,6 @@ export const handleProratedDowngrade = async ({ cusPrice.price.proration_config?.on_increase || OnIncrease.ProrateImmediately, onDecrease, - logger, }); } else { if (prevOverage > 0) { @@ -228,7 +221,7 @@ export const handleProratedDowngrade = async ({ }); await RepService.insert({ - db, + ctx, data: newReplaceables, }); } diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedUpgrade.ts b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedUpgrade.ts index 7a6098672..bafc2711f 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedUpgrade.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/createPaidAllocatedInvoice/handleProratedUpgrade.ts @@ -4,14 +4,13 @@ import { type FullCustomerPrice, InternalError, OnIncrease, - type Organization, type Price, priceToInvoiceAmount, shouldCreateInvoiceItem, type UsagePriceConfig, } from "@autumn/shared"; import type Stripe from "stripe"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js"; import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; import { getUsageFromBalance } from "../adjustAllowance.js"; @@ -94,28 +93,25 @@ export function getReps({ } export const handleProratedUpgrade = async ({ - db, + ctx, stripeCli, cusEnt, - org, cusPrice, sub, subItem, newBalance, prevBalance, - logger, }: { - db: DrizzleCli; + ctx: AutumnContext; stripeCli: Stripe; - org: Organization; cusEnt: FullCusEntWithFullCusProduct; cusPrice: FullCustomerPrice; sub: Stripe.Subscription; subItem: Stripe.SubscriptionItem; newBalance: number; prevBalance: number; - logger: any; }) => { + const { logger } = ctx; logger.info(`Handling quantity increase`); if (!cusEnt.customer_product) { @@ -156,7 +152,7 @@ export const handleProratedUpgrade = async ({ let invoice = null; if (shouldCreateInvoiceItem(onIncrease) && sub.status !== "trialing") { invoice = await createUpgradeProrationInvoice({ - org, + ctx, cusPrice, stripeCli, sub, @@ -168,12 +164,11 @@ export const handleProratedUpgrade = async ({ product, config, onIncrease, - logger, }); } const deleted = await RepService.deleteInIds({ - db, + ctx, ids: reps.map((r) => r.id), }); diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/handlePaidAllocatedCusEnt.ts b/server/src/internal/balances/utils/paidAllocatedFeature/handlePaidAllocatedCusEnt.ts index 08988df0d..8934b7f3c 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/handlePaidAllocatedCusEnt.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/handlePaidAllocatedCusEnt.ts @@ -43,16 +43,13 @@ export const handlePaidAllocatedCusEnt = async ({ }); const { newReplaceables, deletedReplaceables } = await adjustAllowance({ - db, - env, - org, + ctx, cusPrices: cusPrices, customer: fullCus, affectedFeature: cusEnt.entitlement.feature, cusEnt: cusEnt, originalBalance: originalGrpBalance, newBalance: newGrpBalance, - logger: ctx.logger, }); // Adjust balance based on replaceables @@ -70,7 +67,7 @@ export const handlePaidAllocatedCusEnt = async ({ ); await CusEntService.update({ - db, + ctx, id: cusEnt.id, updates: { balance: reUpdatedBalance, diff --git a/server/src/internal/balances/utils/paidAllocatedFeature/rollbackDeduction.ts b/server/src/internal/balances/utils/paidAllocatedFeature/rollbackDeduction.ts index 78ca1225e..e052d3e93 100644 --- a/server/src/internal/balances/utils/paidAllocatedFeature/rollbackDeduction.ts +++ b/server/src/internal/balances/utils/paidAllocatedFeature/rollbackDeduction.ts @@ -40,7 +40,7 @@ export const rollbackDeduction = async ({ // Restore the entitlement to original values await CusEntService.update({ - db, + ctx, id: cusEntId, updates: { balance: originalCusEnt.balance ?? 0, diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/insertNewCusProducts.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/insertNewCusProducts.ts index ad70f2a2d..f7bb6d9a0 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnActions/insertNewCusProducts.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/insertNewCusProducts.ts @@ -30,7 +30,7 @@ export const insertNewCusProducts = async ({ // 2. Insert cusEnts await CusEntService.insert({ - db: ctx.db, + ctx, data: cusEnts, }); @@ -51,7 +51,7 @@ export const insertNewCusProducts = async ({ if (cusEnt.rollovers.length > 0) { await RolloverService.insert({ - db: ctx.db, + ctx, rows: cusEnt.rollovers, fullCusEnt: cusEnt, }); diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts index 2e54ec378..544fea3df 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts @@ -25,7 +25,7 @@ export const updateCustomerEntitlements = async ({ if (updates) { await CusEntService.update({ - db, + ctx, id: customerEntitlement.id, updates, }); @@ -34,7 +34,7 @@ export const updateCustomerEntitlements = async ({ if (balanceChange > 0) { await CusEntService.increment({ - db, + ctx, id: customerEntitlement.id, amount: balanceChange, }); @@ -42,7 +42,7 @@ export const updateCustomerEntitlements = async ({ const absoluteDecrement = Math.abs(balanceChange); await CusEntService.decrement({ - db, + ctx, id: customerEntitlement.id, amount: absoluteDecrement, }); diff --git a/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts b/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts index 800c248c2..b7ca6cf1d 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts @@ -61,7 +61,7 @@ export const executeAutumnBillingPlan = async ({ const { customerProduct, updates } = updateCustomerProduct; await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); @@ -73,7 +73,7 @@ export const executeAutumnBillingPlan = async ({ `[executeAutumnBillingPlan] deleting scheduled customer product: ${deleteCustomerProduct.product.id}`, ); await CusProductService.delete({ - db, + ctx, cusProductId: deleteCustomerProduct.id, }); } diff --git a/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingRolloversToCustomerProduct.ts b/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingRolloversToCustomerProduct.ts index 00d6b77c3..e029c5389 100644 --- a/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingRolloversToCustomerProduct.ts +++ b/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingRolloversToCustomerProduct.ts @@ -44,11 +44,10 @@ export const reapplyExistingRolloversToCustomerProduct = async ({ existingRollovers: currentRollovers, }); - try { for (const cusEnt of customerProduct.customer_entitlements) { await RolloverService.insert({ - db, + ctx, rows: cusEnt.rollovers, fullCusEnt: cusEnt, }); diff --git a/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingUsagesToCustomerProduct.ts b/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingUsagesToCustomerProduct.ts index 6701bd67b..006632ab2 100644 --- a/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingUsagesToCustomerProduct.ts +++ b/server/src/internal/billing/v2/utils/initFullCustomerProduct/reapplyExistingUsagesToCustomerProduct.ts @@ -74,7 +74,7 @@ export const reapplyExistingUsagesToCustomerProduct = async ({ for (const cusEnt of customerProduct.customer_entitlements) { await CusEntService.update({ - db, + ctx, id: cusEnt.id, updates: { balance: cusEnt.balance ?? 0, diff --git a/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts b/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts index 1be4b9843..a9ec230da 100644 --- a/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts +++ b/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts @@ -1,7 +1,6 @@ -import type { FullCustomer } from "@autumn/shared"; +import type { AutumnBillingPlan, FullCustomer } from "@autumn/shared"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; -import type { AutumnBillingPlan } from "@autumn/shared"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { initSubscriptionFromStripe } from "@/internal/subscriptions/utils/initSubscriptionFromStripe.js"; import type { CreateCustomerContext } from "./createCustomerContext.js"; @@ -28,7 +27,7 @@ export const finalizeCreateCustomer = async ({ // Link subscription_ids to customer products for (const customerProduct of autumnBillingPlan.insertCustomerProducts) { await CusProductService.update({ - db: ctx.db, + ctx, cusProductId: customerProduct.id, updates: { subscription_ids: customerProduct.subscription_ids }, }); diff --git a/server/src/internal/customers/actions/resetCustomerEntitlements/applyResetResults.ts b/server/src/internal/customers/actions/resetCustomerEntitlements/applyResetResults.ts index 32103b9ed..8fb8bb323 100644 --- a/server/src/internal/customers/actions/resetCustomerEntitlements/applyResetResults.ts +++ b/server/src/internal/customers/actions/resetCustomerEntitlements/applyResetResults.ts @@ -59,7 +59,7 @@ export const applyResetResults = async ({ // Winner: we inserted the rollover into DB. Clear excess and // update the in-memory array to include the new rollovers. const clearedRollovers = await RolloverService.clearExcessRollovers({ - db, + ctx, newRows: result.rolloverInsert.rows, fullCusEnt: original, }); @@ -68,7 +68,7 @@ export const applyResetResults = async ({ // Loser: the winning request already inserted the rollover and // cleared excess. Re-read from DB to get the authoritative state. original.rollovers = await RolloverService.getCurrentRollovers({ - db, + ctx, cusEntID: cusEntId, }); } diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts index 430145bba..7bc81222e 100644 --- a/server/src/internal/customers/add-product/createFullCusProduct.ts +++ b/server/src/internal/customers/add-product/createFullCusProduct.ts @@ -20,6 +20,7 @@ import { type ProductOptions, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { triggerVerifyCacheConsistency } from "@/internal/billing/v2/workflows/verifyCacheConsistency/triggerVerifyCacheConsistency.js"; import { searchCusProducts } from "@/internal/customers/cusProducts/cusProductUtils.js"; @@ -157,25 +158,26 @@ const initCusProduct = ({ }; const insertFullCusProduct = async ({ - db, + ctx, cusProd, cusEnts, cusPrices, replaceables, }: { - db: DrizzleCli; + ctx: RepoContext; cusProd: CusProduct; cusEnts: CustomerEntitlement[]; cusPrices: CustomerPrice[]; replaceables: InsertReplaceable[]; }) => { + const { db } = ctx; await CusProductService.insert({ db, data: cusProd, }); await CusEntService.insert({ - db, + ctx, data: cusEnts as InsertCustomerEntitlement[], }); @@ -185,19 +187,19 @@ const insertFullCusProduct = async ({ }); await RepService.insert({ - db, + ctx, data: replaceables, }); }; const expireOrDeleteCusProduct = async ({ - db, + ctx, startsAt, product, cusProducts, internalEntityId, }: { - db: DrizzleCli; + ctx: RepoContext; startsAt?: number; product: FullProduct; cusProducts?: FullCusProduct[]; @@ -216,7 +218,7 @@ const expireOrDeleteCusProduct = async ({ if (curScheduledProduct) { await CusProductService.delete({ - db, + ctx, cusProductId: curScheduledProduct.id, }); } @@ -229,7 +231,7 @@ const expireOrDeleteCusProduct = async ({ if (curMainProduct) { await CusProductService.update({ - db, + ctx, cusProductId: curMainProduct.id, updates: { status: CusProductStatus.Expired, @@ -352,6 +354,15 @@ export const createFullCusProduct = async ({ internalEntityId: attachParams.internalEntityId, }); + const repoContext: RepoContext = { + db, + org: { + id: customer.org_id, + }, + env: customer.env, + logger, + }; + if ( (isOneOff(prices) || (isFreeProduct(prices) && product.is_add_on)) && notNullish(existingCusProduct) && @@ -361,9 +372,8 @@ export const createFullCusProduct = async ({ ACTIVE_STATUSES.includes(existingCusProduct.status) ) { await updateOneTimeCusProduct({ - db, + ctx: repoContext, attachParams, - logger, }); return; @@ -476,7 +486,7 @@ export const createFullCusProduct = async ({ if (!isOneOff(prices) && !product.is_add_on) { await expireOrDeleteCusProduct({ - db, + ctx: repoContext, startsAt, product, cusProducts: attachParams.cusProducts, @@ -485,7 +495,7 @@ export const createFullCusProduct = async ({ } await insertFullCusProduct({ - db, + ctx: repoContext, cusProd, cusEnts: deductedCusEnts, cusPrices, @@ -497,7 +507,7 @@ export const createFullCusProduct = async ({ for (const operation of rolloverOps) { rolloverInserts.push( RolloverService.insert({ - db, + ctx: repoContext, rows: operation.toInsert, fullCusEnt: operation.cusEnt, }), diff --git a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts index d79974366..75caf3ffb 100644 --- a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts +++ b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts @@ -11,39 +11,35 @@ import { type Organization, type Price, } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { triggerVerifyCacheConsistency } from "@/internal/billing/v2/workflows/verifyCacheConsistency/triggerVerifyCacheConsistency.js"; import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js"; import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; import { nullish } from "@/utils/genUtils.js"; -import type { Logger } from "../../../external/logtail/logtailUtils.js"; import type { InsertCusProductParams } from "../cusProducts/AttachParams.js"; import { CusProductService } from "../cusProducts/CusProductService.js"; import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js"; import { initCusEntitlement } from "./initCusEnt.js"; const updateOneOffExistingEntitlement = async ({ - db, + ctx, cusEnt, entitlement, org, env, options, relatedPrice, - logger, - attachParams, }: { - db: DrizzleCli; + ctx: RepoContext; cusEnt: FullCustomerEntitlement; entitlement: EntitlementWithFeature; org: Organization; env: AppEnv; options?: FeatureOptions; relatedPrice?: Price; - logger: any; - attachParams: InsertCusProductParams; }) => { + const { db, logger } = ctx; if (entitlement.allowance_type === AllowanceType.Unlimited) { return; } @@ -71,7 +67,7 @@ const updateOneOffExistingEntitlement = async ({ } await CusEntService.update({ - db, + ctx, id: updatedCusEnt.id, updates: { balance: updatedCusEnt.balance! + resetBalance!, @@ -82,13 +78,11 @@ const updateOneOffExistingEntitlement = async ({ }; export const updateOneTimeCusProduct = async ({ - db, + ctx, attachParams, - logger, }: { - db: DrizzleCli; + ctx: RepoContext; attachParams: InsertCusProductParams; - logger: Logger; }) => { // 1. Sort cus products by created_at attachParams.cusProducts?.sort((a, b) => b.created_at - a.created_at); @@ -122,15 +116,13 @@ export const updateOneTimeCusProduct = async ({ if (existingCusEnt) { await updateOneOffExistingEntitlement({ - db, + ctx, cusEnt: existingCusEnt, entitlement, org: attachParams.org, env: attachParams.customer.env, options: options || undefined, relatedPrice, - logger, - attachParams, }); } else { const newCusEnt = initCusEntitlement({ @@ -147,7 +139,7 @@ export const updateOneTimeCusProduct = async ({ console.log("Inserting new cus ent"); await CusEntService.insert({ - db, + ctx, data: [newCusEnt as any], }); } @@ -173,7 +165,7 @@ export const updateOneTimeCusProduct = async ({ } await CusProductService.update({ - db, + ctx, cusProductId: existingCusProduct.id, updates: { options: newOptionsList, @@ -197,7 +189,7 @@ export const updateOneTimeCusProduct = async ({ await triggerVerifyCacheConsistency({ newCustomerProduct: existingCusProduct, previousFullCustomer: attachParams.customer as FullCustomer, - logger, + logger: ctx.logger, source: "updateOneTimeCusProduct", }); }; diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts index 68474d108..cfa07d0ba 100644 --- a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts @@ -130,7 +130,7 @@ export const handleMultiAttachFlow = async ({ for (const cusProduct of removeCusProducts) { if (cusProduct.status === CusProductStatus.Scheduled) { await CusProductService.delete({ - db, + ctx, cusProductId: cusProduct.id, }); } @@ -138,7 +138,7 @@ export const handleMultiAttachFlow = async ({ for (const cusProduct of expireCusProducts) { await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { status: CusProductStatus.Expired, diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts index 89d435333..049abb6ed 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts @@ -101,9 +101,9 @@ export const handleQuantityDowngrade = async ({ (f: Feature) => f.internal_id === newOptions.internal_feature_id, )!; const invoiceItem = constructStripeInvoiceItem({ + ctx, product, amount: amount, - org: org, price: cusPrice.price, description: getFeatureInvoiceDescription({ feature: feature, @@ -189,7 +189,7 @@ export const handleQuantityDowngrade = async ({ .toNumber(); await CusEntService.decrement({ - db, + ctx, id: cusEnt.id, amount: decrementBy, }); diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts index 01d7497bc..236a8f184 100644 --- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts +++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts @@ -103,9 +103,9 @@ export const handleQuantityUpgrade = async ({ const product = cusProductToProduct({ cusProduct }); const invoiceItem = constructStripeInvoiceItem({ + ctx, product, amount: amount, - org: org, price: cusPrice.price, description: getFeatureInvoiceDescription({ feature: feature, @@ -197,7 +197,7 @@ export const handleQuantityUpgrade = async ({ `🔥 Incrementing feature ${cusEnt.entitlement.feature.id} balance by ${incrementBy}`, ); await CusEntService.increment({ - db, + ctx, id: cusEnt.id, amount: incrementBy, }); diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts index 99ad78cff..3638043d3 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts @@ -10,6 +10,7 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; import type { Logger } from "@/external/logtail/logtailUtils"; import { subToAutumnInterval } from "@/external/stripe/utils.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; @@ -168,17 +169,17 @@ export const createUsageInvoiceItems = async ({ }; export const resetUsageBalances = async ({ - db, + ctx, cusEntIds, cusProduct, }: { - db: DrizzleCli; + ctx: RepoContext; cusEntIds: string[]; cusProduct: FullCusProduct; }) => { for (const cusEntId of cusEntIds) { await CusEntService.update({ - db, + ctx, id: cusEntId, updates: { balance: 0, diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleLegacyUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleLegacyUpgradeFlow.ts index c38378afd..3b45d312a 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleLegacyUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleLegacyUpgradeFlow.ts @@ -99,7 +99,7 @@ export const handleLegacyUpgradeFlow = async ({ if (curScheduledProduct) { await CusProductService.delete({ - db, + ctx, cusProductId: curScheduledProduct.id, }); } @@ -219,7 +219,7 @@ export const handleLegacyUpgradeFlow = async ({ ) { logger.info(`UPGRADE FLOW: expiring previous cus product`); await CusProductService.update({ - db, + ctx, cusProductId: curCusProduct.id, updates: { subscription_ids: canceled ? undefined : [], diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts index 3b4d7cda8..f07267548 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts @@ -157,7 +157,7 @@ export const updateStripeSub2 = async ({ if (curCusProduct) { if (!url) { await resetUsageBalances({ - db, + ctx, cusEntIds, cusProduct: curCusProduct, }); diff --git a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts index 8466de3c0..e22242175 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts @@ -180,7 +180,7 @@ export const runAttachFunction = async ({ // 2. If main is trial, cancel it... if (branch === AttachBranch.MainIsTrial) { await CusProductService.update({ - db, + ctx, cusProductId: curMainProduct!.id, updates: { ended_at: attachParams.now, diff --git a/server/src/internal/customers/cusProducts/CusProductService.ts b/server/src/internal/customers/cusProducts/CusProductService.ts index 6fc35b898..a0e5d0b5c 100644 --- a/server/src/internal/customers/cusProducts/CusProductService.ts +++ b/server/src/internal/customers/cusProducts/CusProductService.ts @@ -21,6 +21,7 @@ import { or, } from "drizzle-orm"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; export const ACTIVE_STATUSES = [ CusProductStatus.Active, @@ -397,14 +398,15 @@ export class CusProductService { } static async update({ - db, + ctx, cusProductId, updates, }: { - db: DrizzleCli; + ctx: RepoContext; cusProductId: string; updates: Partial; }) { + const { db } = ctx; return await db .update(customerProducts) .set(updates) @@ -497,13 +499,13 @@ export class CusProductService { } static async delete({ - db, + ctx, cusProductId, }: { - db: DrizzleCli; + ctx: RepoContext; cusProductId: string; }) { - return await db + return await ctx.db .delete(customerProducts) .where(eq(customerProducts.id, cusProductId)) .returning(); diff --git a/server/src/internal/customers/cusProducts/actions/activateScheduled.ts b/server/src/internal/customers/cusProducts/actions/activateScheduled.ts index 443206817..937166041 100644 --- a/server/src/internal/customers/cusProducts/actions/activateScheduled.ts +++ b/server/src/internal/customers/cusProducts/actions/activateScheduled.ts @@ -64,7 +64,7 @@ export const activateScheduledCustomerProduct = async ({ }; await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); diff --git a/server/src/internal/customers/cusProducts/actions/cleanupOneOff/cleanupOneOff.ts b/server/src/internal/customers/cusProducts/actions/cleanupOneOff/cleanupOneOff.ts index b2ae587c7..95ce9778a 100644 --- a/server/src/internal/customers/cusProducts/actions/cleanupOneOff/cleanupOneOff.ts +++ b/server/src/internal/customers/cusProducts/actions/cleanupOneOff/cleanupOneOff.ts @@ -27,7 +27,7 @@ export const cleanupOneOffCustomerProducts = async ({ }: { ctx: AutumnContext; }): Promise => { - const { logger, db } = ctx; + const { logger } = ctx; // 1. Get customer products eligible for cleanup const toCleanup = await getOneOffCustomerProductsToCleanup({ ctx }); @@ -47,7 +47,7 @@ export const cleanupOneOffCustomerProducts = async ({ ); await batchUpdateCustomerProducts({ - db, + ctx, updates: uniqueIds.map((id) => ({ id, updates: { status: CusProductStatus.Expired }, diff --git a/server/src/internal/customers/cusProducts/actions/deleteScheduledCustomerProduct.ts b/server/src/internal/customers/cusProducts/actions/deleteScheduledCustomerProduct.ts index 9d1dd532f..75bda82bb 100644 --- a/server/src/internal/customers/cusProducts/actions/deleteScheduledCustomerProduct.ts +++ b/server/src/internal/customers/cusProducts/actions/deleteScheduledCustomerProduct.ts @@ -47,7 +47,7 @@ export const deleteScheduledCustomerProduct = async ({ ); await CusProductService.delete({ - db, + ctx, cusProductId: scheduledCustomerProduct.id, }); diff --git a/server/src/internal/customers/cusProducts/actions/expireAndActivateDefault.ts b/server/src/internal/customers/cusProducts/actions/expireAndActivateDefault.ts index fbc9f6a7a..df24eca09 100644 --- a/server/src/internal/customers/cusProducts/actions/expireAndActivateDefault.ts +++ b/server/src/internal/customers/cusProducts/actions/expireAndActivateDefault.ts @@ -43,7 +43,7 @@ export const expireCustomerProductAndActivateDefault = async ({ }; await CusProductService.update({ - db, + ctx, cusProductId: customerProduct.id, updates, }); diff --git a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts index 89a5cdf62..b1ea6c75a 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts @@ -20,6 +20,10 @@ import { and, eq, gt, inArray, isNull, lt, or, sql } from "drizzle-orm"; import { StatusCodes } from "http-status-codes"; import { buildConflictUpdateColumns } from "@/db/dbUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; +import { redis } from "@/external/redis/initRedis.js"; +import { buildFullCustomerCacheKey } from "@/internal/customers/cusUtils/fullCustomerCacheUtils/fullCustomerCacheConfig.js"; +import { tryRedisWrite } from "@/utils/cacheUtils/cacheUtils.js"; import RecaseError from "@/utils/errorUtils.js"; export class CusEntService { @@ -81,12 +85,13 @@ export class CusEntService { } static async insert({ - db, + ctx, data, }: { - db: DrizzleCli; + ctx: RepoContext; data: InsertCustomerEntitlement[] | FullCustomerEntitlement[]; }) { + const { db } = ctx; if (Array.isArray(data) && data.length === 0) { return; } @@ -176,14 +181,16 @@ export class CusEntService { } static async update({ - db, + ctx, id, updates, }: { - db: DrizzleCli; + ctx: RepoContext; id: string; updates: Partial; }) { + const { db } = ctx; + const data = await db .update(customerEntitlements) .set({ @@ -196,11 +203,60 @@ export class CusEntService { return data; } + static async syncUpdateToCache({ + ctx, + cusEntId, + updates, + }: { + ctx: RepoContext; + cusEntId: string; + updates: Partial; + }) { + const { org, env, customerId } = ctx; + + if (!customerId) { + ctx.logger.warn( + `skipping cusEnt sync update to cache, customerId not known`, + ); + return; + } + + const cacheKey = buildFullCustomerCacheKey({ + orgId: org.id, + env, + customerId: customerId ?? "", + }); + + const cacheUpdates = [ + { + cus_ent_id: cusEntId, + balance: updates.balance ?? null, + additional_balance: updates.additional_balance ?? null, + adjustment: updates.adjustment ?? null, + entities: updates.entities ?? null, + next_reset_at: updates.next_reset_at ?? null, + expected_next_reset_at: null, + rollover_insert: null, + rollover_overwrites: null, + rollover_delete_ids: null, + new_replaceables: null, + deleted_replaceable_ids: null, + }, + ]; + + await tryRedisWrite(() => + redis.updateCustomerEntitlements( + cacheKey, + JSON.stringify({ updates: cacheUpdates }), + ), + ); + } + static async batchUpdate({ - db, + ctx, data, }: { - db: DrizzleCli; + ctx: RepoContext; data: UpdateCustomerEntitlement[]; }) { if (Array.isArray(data) && data.length === 0) { @@ -215,7 +271,7 @@ export class CusEntService { updatePromises.push( CusEntService.update({ - db, + ctx, id: customerEntitlement.id, updates: updates as Partial, }), @@ -273,14 +329,15 @@ export class CusEntService { } static async increment({ - db, + ctx, id, amount, }: { - db: DrizzleCli; + ctx: RepoContext; id: string; amount: number; }) { + const { db } = ctx; const data = await db .update(customerEntitlements) .set({ @@ -294,14 +351,16 @@ export class CusEntService { } static async decrement({ - db, + ctx, id, amount, }: { - db: DrizzleCli; + ctx: RepoContext; id: string; amount: number; }) { + const { db } = ctx; + const data = await db .update(customerEntitlements) .set({ diff --git a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts index 8c002acd6..538304e50 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts @@ -5,17 +5,19 @@ import { } from "@autumn/shared"; import { eq, inArray } from "drizzle-orm"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; export class RepService { static async insert({ - db, + ctx, data, }: { - db: DrizzleCli; + ctx: RepoContext; data: InsertReplaceable[]; }) { if (data.length === 0) return []; - const inserted = await db.insert(replaceables).values(data).returning(); + const inserted = await ctx.db.insert(replaceables).values(data).returning(); return inserted as Replaceable[]; } @@ -36,7 +38,14 @@ export class RepService { return updated as Replaceable[]; } - static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) { + static async deleteInIds({ + ctx, + ids, + }: { + ctx: AutumnContext; + ids: string[]; + }) { + const { db } = ctx; if (ids.length === 0) return []; const deleted = await db .delete(replaceables) diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts index baebd26f9..e4aea2911 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts @@ -4,8 +4,10 @@ import { rollovers, } from "@autumn/shared"; import { and, eq, gte, inArray } from "drizzle-orm"; +import type { CronContext } from "@/cron/utils/CronContext.js"; import { buildConflictUpdateColumns } from "@/db/dbUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { RepoContext } from "@/db/repoContext.js"; import { performMaximumClearing } from "./rolloverUtils.js"; export class RolloverService { @@ -58,12 +60,15 @@ export class RolloverService { // } static async getCurrentRollovers({ - db, + // db, + ctx, cusEntID, }: { - db: DrizzleCli; + // db: DrizzleCli; + ctx: CronContext; cusEntID: string; }) { + const { db } = ctx; return await db .select() .from(rollovers) @@ -76,20 +81,21 @@ export class RolloverService { } static async insert({ - db, + ctx, rows, fullCusEnt, }: { - db: DrizzleCli; + ctx: RepoContext; rows: Rollover[]; fullCusEnt: FullCustomerEntitlement; }) { + const { db } = ctx; if (rows.length === 0) return {}; await db.insert(rollovers).values(rows).returning(); return RolloverService.clearExcessRollovers({ - db, + ctx, newRows: rows, fullCusEnt, }); @@ -97,14 +103,15 @@ export class RolloverService { /** Enforces the rollover max cap after new rollovers have been inserted into the DB. */ static async clearExcessRollovers({ - db, + ctx, newRows, fullCusEnt, }: { - db: DrizzleCli; + ctx: RepoContext; newRows: Rollover[]; fullCusEnt: FullCustomerEntitlement; }): Promise { + const { db } = ctx; const curRollovers = [...fullCusEnt.rollovers, ...newRows]; const { toDelete, toUpdate } = performMaximumClearing({ diff --git a/server/src/internal/customers/cusProducts/repos/batchUpdateCustomerProducts.ts b/server/src/internal/customers/cusProducts/repos/batchUpdateCustomerProducts.ts index 77a7d1831..744833c49 100644 --- a/server/src/internal/customers/cusProducts/repos/batchUpdateCustomerProducts.ts +++ b/server/src/internal/customers/cusProducts/repos/batchUpdateCustomerProducts.ts @@ -1,16 +1,18 @@ -import type { InsertCustomerProduct } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { CusProductService } from "../CusProductService.js"; +import { type InsertCustomerProduct } from "@autumn/shared"; +import type { RepoContext } from "@/db/repoContext.js"; +import { CusProductService } from "../CusProductService"; /** * Batch update customer products by their IDs. * Each update is executed in parallel using Promise.all. */ export const batchUpdateCustomerProducts = async ({ - db, + ctx, + updates, }: { - db: DrizzleCli; + ctx: RepoContext; + updates: { id: string; updates: Partial; @@ -26,7 +28,7 @@ export const batchUpdateCustomerProducts = async ({ } return CusProductService.update({ - db, + ctx, cusProductId: id, updates: updateData, }); diff --git a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts index 03d01dacc..128c94429 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts @@ -50,7 +50,7 @@ export const handleDecreaseAndTransfer = async ({ batchDecrement.push( CusEntService.decrement({ - db, + ctx, id: cusEnt.id, amount: resetBalance, }), @@ -60,7 +60,7 @@ export const handleDecreaseAndTransfer = async ({ await Promise.all(batchDecrement); await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { quantity: cusProduct.quantity - 1, diff --git a/server/src/internal/customers/handlers/handleTransferProductV2.ts b/server/src/internal/customers/handlers/handleTransferProductV2.ts index decd0e210..ab404f663 100644 --- a/server/src/internal/customers/handlers/handleTransferProductV2.ts +++ b/server/src/internal/customers/handlers/handleTransferProductV2.ts @@ -118,7 +118,7 @@ export const handleTransferProductV2 = createRoute({ }); } else { await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { entity_id: toEntity?.id || null, diff --git a/server/src/internal/entities/actions/deleteEntity.ts b/server/src/internal/entities/actions/deleteEntity.ts index 9fc4a3513..1d6490940 100644 --- a/server/src/internal/entities/actions/deleteEntity.ts +++ b/server/src/internal/entities/actions/deleteEntity.ts @@ -62,16 +62,13 @@ export const deleteEntity = async ({ if (!mainCusEnt) continue; const { newReplaceables } = await adjustAllowance({ - db, - env, - org, + ctx, cusPrices: cusProduct.customer_prices, customer: fullCus, affectedFeature: mainCusEnt.entitlement.feature, cusEnt: { ...mainCusEnt, customer_product: cusProduct }, originalBalance: mainCusEnt.balance!, newBalance: mainCusEnt.balance! + 1, - logger, }); const linkedCusEnts = findLinkedCusEnts({ @@ -113,7 +110,7 @@ export const deleteEntity = async ({ } await CusEntService.update({ - db, + ctx, id: linkedCusEnt.id, updates: { entities: newEntities, @@ -123,7 +120,7 @@ export const deleteEntity = async ({ if (!replaceable) { await CusEntService.increment({ - db, + ctx, id: mainCusEnt.id, amount: 1, }); diff --git a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts index 96f52b27b..f8a03ac7e 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts @@ -11,7 +11,6 @@ import { findFeatureById, type Replaceable, } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { acquireLock, clearLock } from "@/external/redis/redisUtils.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { adjustAllowance } from "@/internal/balances/utils/paidAllocatedFeature/adjustAllowance.js"; @@ -22,12 +21,12 @@ import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; const updateLinkedCusEnt = async ({ - db, + ctx, linkedCusEnt, inputEntities, entityToReplacement, }: { - db: DrizzleCli; + ctx: AutumnContext; linkedCusEnt: FullCustomerEntitlement; inputEntities: CreateEntityParams[]; entityToReplacement: Record; @@ -57,7 +56,7 @@ const updateLinkedCusEnt = async ({ } await CusEntService.update({ - db, + ctx, id: linkedCusEnt.id, updates: { entities: newEntities, @@ -161,23 +160,20 @@ export const createEntityForCusProduct = async ({ const { deletedReplaceables: deletedReplaceables_ } = await adjustAllowance({ - db, - env, - org, + ctx, cusPrices, customer, - affectedFeature: feature, + affectedFeature: feature!, cusEnt: mainCusEntWithCusProduct, originalBalance, - newBalance, - logger, + newBalance: innerNewBalance, errorIfIncomplete: true, }); deletedReplaceables = deletedReplaceables_ || []; await CusEntService.decrement({ - db, + ctx, id: mainCusEntWithCusProduct.id, amount: inputEntities.length - deletedReplaceables.length, }); @@ -203,7 +199,7 @@ export const createEntityForCusProduct = async ({ for (const linkedCusEnt of linkedCusEnts) { await updateLinkedCusEnt({ - db, + ctx, linkedCusEnt, inputEntities, entityToReplacement, diff --git a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts index 4e1fe0971..9eab91141 100644 --- a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts +++ b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts @@ -15,7 +15,7 @@ export const cancelSubsForEntity = async ({ cusProducts: FullCusProduct[]; entity: Entity; }) => { - const { db, logger } = ctx; + const { logger } = ctx; try { for (const cusProduct of cusProducts) { if (cusProduct.internal_entity_id !== entity.internal_id) { @@ -24,7 +24,7 @@ export const cancelSubsForEntity = async ({ if (cusProduct.status === CusProductStatus.Scheduled) { await CusProductService.delete({ - db, + ctx, cusProductId: cusProduct.id, }); } diff --git a/server/src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntity.ts b/server/src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntity.ts index aa50df461..6aaefb61d 100644 --- a/server/src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntity.ts +++ b/server/src/internal/entities/handlers/handleDeleteEntity/handleDeleteEntity.ts @@ -55,16 +55,13 @@ export const handleDeleteEntity = createRoute({ if (!mainCusEnt) continue; const { newReplaceables } = await adjustAllowance({ - db, - env, - org, + ctx, cusPrices: cusProduct.customer_prices, customer: fullCus, - affectedFeature: mainCusEnt.entitlement.feature, + affectedFeature: mainCusEnt.entitlement.feature!, cusEnt: { ...mainCusEnt, customer_product: cusProduct }, originalBalance: mainCusEnt.balance!, newBalance: mainCusEnt.balance! + 1, - logger, }); const linkedCusEnts = findLinkedCusEnts({ @@ -107,22 +104,34 @@ export const handleDeleteEntity = createRoute({ newEntities = newEntities_; } - await CusEntService.update({ + await CusEntService.update({ + ctx: { db, - id: linkedCusEnt.id, - updates: { - entities: newEntities, - }, - }); + logger, + org, + env, + customerId: customer_id, + }, + id: linkedCusEnt.id, + updates: { + entities: newEntities, + }, + }); } - if (!replaceable) { - await CusEntService.increment({ + if (!replaceable) { + await CusEntService.increment({ + ctx: { db, - id: mainCusEnt.id, - amount: 1, - }); - } + logger, + org, + env, + customerId: customer_id, + }, + id: mainCusEnt.id, + amount: 1, + }); + } } // Cancel any subs diff --git a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts index 8788fd432..1711f7834 100644 --- a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts +++ b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts @@ -1,16 +1,12 @@ -import type { - Organization, - Price, - Product, - UsagePriceConfig, -} from "@autumn/shared"; +import type { Price, Product, UsagePriceConfig } from "@autumn/shared"; import { Decimal } from "decimal.js"; import type Stripe from "stripe"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; export const constructStripeInvoiceItem = ({ + ctx, product, amount, - org, price, description, stripeSubId, @@ -18,9 +14,9 @@ export const constructStripeInvoiceItem = ({ periodStart, periodEnd, }: { + ctx: AutumnContext; product: Product; amount: number; - org: Organization; price: Price; description: string; stripeSubId: string; @@ -28,6 +24,7 @@ export const constructStripeInvoiceItem = ({ periodStart: number; periodEnd: number; }) => { + const { org } = ctx; const config = price.config as UsagePriceConfig; const amountInCents = Math.floor( @@ -40,7 +37,7 @@ export const constructStripeInvoiceItem = ({ price_data: { unit_amount: amountInCents, currency: org.default_currency || "usd", - product: config.stripe_product_id || product.processor?.id!, + product: config.stripe_product_id || (product.processor?.id ?? ""), }, } : { diff --git a/server/src/internal/migrations/migrationSteps/migrateRevenuecatCustomer.ts b/server/src/internal/migrations/migrationSteps/migrateRevenuecatCustomer.ts index 33742df94..7a82e24f4 100644 --- a/server/src/internal/migrations/migrationSteps/migrateRevenuecatCustomer.ts +++ b/server/src/internal/migrations/migrationSteps/migrateRevenuecatCustomer.ts @@ -46,7 +46,7 @@ export const migrateRevenueCatCustomer = async ({ }); await CusProductService.update({ - db, + ctx, cusProductId: cusProduct.id, updates: { status: CusProductStatus.Expired, diff --git a/server/src/utils/importUtils/updateUsages.ts b/server/src/utils/importUtils/updateUsages.ts index 6885e730a..a5c9bdba4 100644 --- a/server/src/utils/importUtils/updateUsages.ts +++ b/server/src/utils/importUtils/updateUsages.ts @@ -2,20 +2,20 @@ import { type FullCustomer, fullCustomerToCustomerEntitlements, } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; export const updateUsages = async ({ + ctx, featureId, usage, fullCus, - db, }: { + ctx: AutumnContext; featureId: string; usage: number; fullCus: FullCustomer; - db: DrizzleCli; }) => { const cusEnts = fullCustomerToCustomerEntitlements({ fullCustomer: fullCus, @@ -30,7 +30,7 @@ export const updateUsages = async ({ const newBalance = cusEnt.balance! - usage; await CusEntService.update({ - db, + ctx, id: cusEnt.id, updates: { balance: newBalance, diff --git a/server/tests/advanced/customInterval/reset1.test.ts b/server/tests/advanced/customInterval/reset1.test.ts index 42149dfff..9b06ee720 100644 --- a/server/tests/advanced/customInterval/reset1.test.ts +++ b/server/tests/advanced/customInterval/reset1.test.ts @@ -96,7 +96,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`, await timeout(3000); await resetAndGetCusEnt({ - db, + ctx, customer, productGroup: free.group || "", featureId: TestFeature.Messages, @@ -113,7 +113,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`, test("should reset words feature and have correct next reset at", async () => { await resetAndGetCusEnt({ - db, + ctx, customer, productGroup: free.group || "", featureId: TestFeature.Words, diff --git a/server/tests/balances/cron/loose-reset.test.ts b/server/tests/balances/cron/loose-reset.test.ts index d86596f47..c276d995b 100644 --- a/server/tests/balances/cron/loose-reset.test.ts +++ b/server/tests/balances/cron/loose-reset.test.ts @@ -104,7 +104,7 @@ describe(`${chalk.yellowBright("loose-reset: test getActiveResetPassed for loose // 3. Call resetCustomerEntitlement const updatedCusEnt = await resetCustomerEntitlement({ - db: ctx.db, + ctx, cusEnt: resetCusEnt, updatedCusEnts: [], }); diff --git a/server/tests/balances/track/race-condition/track-race-condition3.test.ts b/server/tests/balances/track/race-condition/track-race-condition3.test.ts index 6b0774d55..458945f4a 100644 --- a/server/tests/balances/track/race-condition/track-race-condition3.test.ts +++ b/server/tests/balances/track/race-condition/track-race-condition3.test.ts @@ -124,7 +124,7 @@ describe(`${chalk.yellowBright("track-race-condition3: track runs when credits a chalk.yellow("\nStep 3: Resetting credits (simulates cron reset)..."), ); await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer: fullCustomer, productGroup: pro.group!, featureId: TestFeature.Messages, diff --git a/server/tests/balances/track/rollovers/rolloverTestUtils.ts b/server/tests/balances/track/rollovers/rolloverTestUtils.ts index f49faac58..db6e30573 100644 --- a/server/tests/balances/track/rollovers/rolloverTestUtils.ts +++ b/server/tests/balances/track/rollovers/rolloverTestUtils.ts @@ -1,24 +1,25 @@ import type { Customer } from "@autumn/shared"; +import type { TestContext } from "@tests/utils/testInitUtils/createTestContext"; import { clearCusEntsFromCache } from "@/cron/resetCron/clearCusEntsFromCache"; import { resetCustomerEntitlement } from "@/cron/resetCron/resetCustomerEntitlement.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { cusProductToCusEnt } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; export const resetAndGetCusEnt = async ({ - db, + ctx, customer, productGroup, featureId, skipCacheDeletion = false, }: { - db: DrizzleCli; + ctx: TestContext; customer: Customer; productGroup: string; featureId: string; skipCacheDeletion?: boolean; }) => { + const { db } = ctx; // Run reset cusEnt on ... let mainCusProduct = await getMainCusProduct({ db, @@ -37,7 +38,7 @@ export const resetAndGetCusEnt = async ({ }; const updatedCusEnt = await resetCustomerEntitlement({ - db, + ctx, cusEnt: resetCusEnt, updatedCusEnts: [], }); diff --git a/server/tests/balances/track/rollovers/track-rollover1.test.ts b/server/tests/balances/track/rollovers/track-rollover1.test.ts index 4556b424d..f78b7a5cc 100644 --- a/server/tests/balances/track/rollovers/track-rollover1.test.ts +++ b/server/tests/balances/track/rollovers/track-rollover1.test.ts @@ -82,7 +82,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` await timeout(3000); await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer, productGroup: free.group!, featureId: TestFeature.Messages, @@ -119,7 +119,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item` // let usage2 = 50; test("should reset again and have correct rollover", async () => { await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer, productGroup: free.group!, featureId: TestFeature.Messages, diff --git a/server/tests/balances/track/rollovers/track-rollover2.test.ts b/server/tests/balances/track/rollovers/track-rollover2.test.ts index df2b8f9f3..c9dc58dbb 100644 --- a/server/tests/balances/track/rollovers/track-rollover2.test.ts +++ b/server/tests/balances/track/rollovers/track-rollover2.test.ts @@ -117,7 +117,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item // Run reset cusEnt on ... await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer, productGroup: free.group!, featureId: TestFeature.Messages, @@ -157,7 +157,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item test("should reset again and have correct rollovers", async () => { await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer, productGroup: free.group!, featureId: TestFeature.Messages, diff --git a/server/tests/balances/track/rollovers/track-rollover5.test.ts b/server/tests/balances/track/rollovers/track-rollover5.test.ts index 91faabf39..479b3dff3 100644 --- a/server/tests/balances/track/rollovers/track-rollover5.test.ts +++ b/server/tests/balances/track/rollovers/track-rollover5.test.ts @@ -85,14 +85,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, test("should create rollovers", async () => { await resetAndGetCusEnt({ + ctx, customer, - db: ctx.db, productGroup: testCase, featureId: TestFeature.Messages, }); await resetAndGetCusEnt({ + ctx, customer, - db: ctx.db, productGroup: testCase, featureId: TestFeature.Messages, }); diff --git a/server/tests/balances/track/rollovers/track-rollover6.test.ts b/server/tests/balances/track/rollovers/track-rollover6.test.ts index 2efdec7a6..768df612d 100644 --- a/server/tests/balances/track/rollovers/track-rollover6.test.ts +++ b/server/tests/balances/track/rollovers/track-rollover6.test.ts @@ -92,14 +92,14 @@ describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, test("should create rollovers", async () => { await resetAndGetCusEnt({ + ctx, customer, - db: ctx.db, productGroup: testCase, featureId: TestFeature.Messages, }); await resetAndGetCusEnt({ + ctx, customer, - db: ctx.db, productGroup: testCase, featureId: TestFeature.Messages, }); diff --git a/server/tests/integration/balances/legacy/legacy-set-usage.test.ts b/server/tests/integration/balances/legacy/legacy-set-usage.test.ts index 6f6886bac..dd424900b 100644 --- a/server/tests/integration/balances/legacy/legacy-set-usage.test.ts +++ b/server/tests/integration/balances/legacy/legacy-set-usage.test.ts @@ -398,7 +398,7 @@ test.concurrent(`${chalk.yellowBright("legacy-set-usage-prepaid1: set usage with s.attach({ productId: freeProd.id }), s.billing.attach({ productId: prepaidAddOn.id, - options: { quantity: 2 }, + options: [{ feature_id: TestFeature.Messages, quantity: 200 }], }), ], }); diff --git a/server/tests/integration/billing/legacy/attach/addon/legacy-addon.test.ts b/server/tests/integration/billing/legacy/attach/addon/legacy-addon.test.ts index 7569a6332..286e66619 100644 --- a/server/tests/integration/billing/legacy/attach/addon/legacy-addon.test.ts +++ b/server/tests/integration/billing/legacy/attach/addon/legacy-addon.test.ts @@ -141,7 +141,7 @@ test.concurrent(`${chalk.yellowBright("legacy-addon 2: attach pro then free add- // Attach the same free add-on a second time await autumnV1.attach({ customer_id: customerId, - product_id: `${addOn.id}_${customerId}`, + product_id: addOn.id, }); const customerAfter = await autumnV1.customers.get(customerId); @@ -216,7 +216,7 @@ test.concurrent(`${chalk.yellowBright("legacy-addon 3: attach pro then monthly p }); // Use AutumnCli.getCustomer for V1 response format (entitlements, add_ons) - const cusRes = await AutumnCli.getCustomer(customerId) as ApiCustomerV1; + const cusRes = (await AutumnCli.getCustomer(customerId)) as ApiCustomerV1; // Pro gives 10 Messages, add-on gives 500 const expectedBalance = 10 + monthlyQuantity; @@ -233,7 +233,10 @@ test.concurrent(`${chalk.yellowBright("legacy-addon 3: attach pro then monthly p expect(cusRes.invoices.length).toBe(2); // Verify /entitled returns correct balance (V0 endpoint with balances array) - const entitledRes = await AutumnCli.entitled(customerId, TestFeature.Messages) as { + const entitledRes = (await AutumnCli.entitled( + customerId, + TestFeature.Messages, + )) as { allowed: boolean; balances: { feature_id: string; balance: number }[]; }; diff --git a/server/tests/integration/billing/legacy/attach/checkout/legacy-checkout-advanced.test.ts b/server/tests/integration/billing/legacy/attach/checkout/legacy-checkout-advanced.test.ts index d3a57dc7e..30b2f979b 100644 --- a/server/tests/integration/billing/legacy/attach/checkout/legacy-checkout-advanced.test.ts +++ b/server/tests/integration/billing/legacy/attach/checkout/legacy-checkout-advanced.test.ts @@ -213,7 +213,8 @@ test.concurrent(`${chalk.yellowBright("legacy-checkout-adv 3: force checkout upg product_id: pro.id, }); - const customerAfterPro = await autumnV1.customers.get(customerId); + const customerAfterPro = + await autumnV1.customers.get(customerId); await expectCustomerProducts({ customer: customerAfterPro, @@ -231,9 +232,11 @@ test.concurrent(`${chalk.yellowBright("legacy-checkout-adv 3: force checkout upg customer_id: customerId, product_id: pro.id, cancel_immediately: true, + prorate: false, }); - const customerAfterCancel = await autumnV1.customers.get(customerId); + const customerAfterCancel = + await autumnV1.customers.get(customerId); await expectCustomerProducts({ customer: customerAfterCancel, @@ -249,7 +252,8 @@ test.concurrent(`${chalk.yellowBright("legacy-checkout-adv 3: force checkout upg await completeStripeCheckoutFormV2({ url: res.checkout_url }); - const customerAfterPremium = await autumnV1.customers.get(customerId); + const customerAfterPremium = + await autumnV1.customers.get(customerId); await expectCustomerProducts({ customer: customerAfterPremium, diff --git a/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade-merged-clock.test.ts b/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade-merged-clock.test.ts index bc6fef8cb..6262b834d 100644 --- a/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade-merged-clock.test.ts +++ b/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade-merged-clock.test.ts @@ -148,7 +148,7 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 4: annual + monthly, adv status: CusProductStatus.Active, }); expect( - entity1.products.filter((p: any) => p.group === premiumAnnualProduct.group) + entity1.products.filter((p) => p.group === premiumAnnualProduct.group) .length, ).toBe(1); @@ -159,9 +159,9 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 4: annual + monthly, adv productId: pro.id, status: CusProductStatus.Active, }); - expect( - entity2.products.filter((p: any) => p.group === premium.group).length, - ).toBe(1); + expect(entity2.products.filter((p) => p.group === premium.group).length).toBe( + 1, + ); }); // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade.test.ts b/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade.test.ts index 1d5b8fdd4..e38e56ee0 100644 --- a/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade.test.ts +++ b/server/tests/integration/billing/legacy/attach/downgrade/legacy-downgrade.test.ts @@ -29,6 +29,7 @@ import { items } from "@tests/utils/fixtures/items"; import { products } from "@tests/utils/fixtures/products"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; import chalk from "chalk"; +import { addWeeks } from "date-fns"; import { constructProduct } from "@/utils/scriptUtils/createTestProducts"; import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils"; @@ -89,7 +90,7 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 1: premium -> pro, advan stripeCli: ctx.stripeCli, testClockId: testClockId!, numberOfMonths: 1, - waitForSeconds: 10, + waitForSeconds: 30, }); // Verify: Pro is active, Premium is gone @@ -164,7 +165,7 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 2: premium -> free, adva stripeCli: ctx.stripeCli, testClockId: testClockId!, numberOfMonths: 1, - waitForSeconds: 10, + waitForSeconds: 30, }); // Verify: Free is active @@ -330,17 +331,17 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 4: pro-quarter -> premiu await expectProductScheduled({ customer, productId: pro.id }); // Advance clock 3 months (end of quarter) - advance 1.5 months twice - await advanceTestClock({ + const advancedTo = await advanceTestClock({ stripeCli: ctx.stripeCli, testClockId: testClockId!, numberOfWeeks: 6, - waitForSeconds: 10, + waitForSeconds: 30, }); await advanceTestClock({ stripeCli: ctx.stripeCli, testClockId: testClockId!, - numberOfWeeks: 6, - waitForSeconds: 10, + advanceTo: addWeeks(advancedTo, 7).getTime(), + waitForSeconds: 30, }); // Verify: Pro (monthly) is active @@ -443,7 +444,7 @@ test.concurrent(`${chalk.yellowBright("legacy-downgrade 5: premium -> pro schedu stripeCli: ctx.stripeCli, testClockId: testClockId!, numberOfMonths: 1, - waitForSeconds: 10, + waitForSeconds: 30, }); // Verify: Pro is active with correct features diff --git a/server/tests/integration/billing/legacy/attach/trial/legacy-trial-merged.test.ts b/server/tests/integration/billing/legacy/attach/trial/legacy-trial-merged.test.ts index 93daa4984..59919ef9b 100644 --- a/server/tests/integration/billing/legacy/attach/trial/legacy-trial-merged.test.ts +++ b/server/tests/integration/billing/legacy/attach/trial/legacy-trial-merged.test.ts @@ -243,7 +243,7 @@ test.concurrent(`${chalk.yellowBright("legacy-trial-merged 3: upgrade entities f entity_id: entities[1].id, }); - await timeout(4000); + await timeout(5000); const entity2 = await autumnV1.entities.get(customerId, entities[1].id); expectProductAttached({ customer: entity2, diff --git a/server/tests/integration/billing/legacy/attach/trial/legacy-trial.test.ts b/server/tests/integration/billing/legacy/attach/trial/legacy-trial.test.ts index 6c32932aa..b4450a1c3 100644 --- a/server/tests/integration/billing/legacy/attach/trial/legacy-trial.test.ts +++ b/server/tests/integration/billing/legacy/attach/trial/legacy-trial.test.ts @@ -501,9 +501,11 @@ test.concurrent(`${chalk.yellowBright("legacy-trial 5: paid to trial upgrade")}` // 2 invoices: Pro paid ($20) + Premium trial ($0) await expectCustomerInvoiceCorrect({ customer: customerAfterPremium, - count: 2, + count: 3, latestTotal: 0, // Premium trial - $0 }); + + expect(customerAfterPremium.invoices?.[1].total).toBe(-20); // refund for pro }); // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/server/tests/utils/testInitUtils/initScenario.ts b/server/tests/utils/testInitUtils/initScenario.ts index 3e89f3eea..5abc096b3 100644 --- a/server/tests/utils/testInitUtils/initScenario.ts +++ b/server/tests/utils/testInitUtils/initScenario.ts @@ -1462,7 +1462,7 @@ export async function initScenario({ const productGroup = productPrefix; await resetAndGetCusEnt({ - db: ctx.db, + ctx, customer, productGroup, featureId: action.featureId,