diff --git a/package.json b/package.json index 171fc76d8..21f73e418 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "js:build": "bun -F @useautumn/sdk build && bun -F autumn-js build", "js:publish": "bun js:build && cd packages/autumn-js && npm publish", "js:publish-beta": "bun js:build && cd packages/autumn-js && npm publish --tag beta", - "js:publish": "bun js:build && cd packages/autumn-js && npm publish", + "js:publish-dry": "bun js:build && cd packages/autumn-js && npm publish --dry-run", "js:version": "git fetch --tags origin && git tag -l 'autumn-js-v*' --sort=-v:refname | head -1" }, diff --git a/server/experiments/explainOneOffCleanup.ts b/server/experiments/explainOneOffCleanup.ts new file mode 100644 index 000000000..4633884c4 --- /dev/null +++ b/server/experiments/explainOneOffCleanup.ts @@ -0,0 +1,181 @@ +import { + AllowanceType, + BillingInterval, + CusProductStatus, + FeatureType, + FeatureUsageType, +} from "@autumn/shared"; +import { sql } from "drizzle-orm"; +import { initDrizzle } from "./experimentEnv"; + +// Run with `bun run experiments/explainOneOffCleanup.ts` +// Source: getOneOffCustomerProductsToCleanup +// File: server/src/internal/customers/cusProducts/actions/cleanupOneOff/getOneOffToCleanup.ts:48 + +const RELEVANT_TABLES = [ + "customer_products", + "customer_prices", + "customer_entitlements", + "entitlements", + "features", + "prices", + "products", + "customers", + "organizations", +]; + +const listIndexes = async ({ db }: { db: ReturnType["db"] }) => { + console.log("--- Existing indexes on relevant tables ---\n"); + const indexResult = await db.execute<{ + tablename: string; + indexname: string; + indexdef: string; + }>(sql` + SELECT tablename, indexname, indexdef + FROM pg_indexes + WHERE tablename IN (${sql.join( + RELEVANT_TABLES.map((t) => sql`${t}`), + sql`, `, + )}) + ORDER BY tablename, indexname + `); + + for (const row of indexResult) { + console.log(`[${row.tablename}] ${row.indexname}`); + console.log(` ${row.indexdef}\n`); + } +}; + +const main = async () => { + const { db } = initDrizzle(); + + await listIndexes({ db }); + + const rawQuery = sql` + WITH + active_cus_products_with_prices AS ( + SELECT DISTINCT cp.id + FROM customer_products cp + WHERE cp.status IN (${CusProductStatus.Active}, ${CusProductStatus.PastDue}) + AND EXISTS ( + SELECT 1 FROM customer_prices cpr WHERE cpr.customer_product_id = cp.id + ) + ), + + cus_products_with_non_one_off_prices AS ( + SELECT DISTINCT cpr.customer_product_id + FROM customer_prices cpr + INNER JOIN prices p ON p.id = cpr.price_id + WHERE cpr.customer_product_id IN (SELECT id FROM active_cus_products_with_prices) + AND COALESCE(p.config->>'interval', '') != ${BillingInterval.OneOff} + ), + + one_off_cus_products AS ( + SELECT id FROM active_cus_products_with_prices + WHERE id NOT IN (SELECT customer_product_id FROM cus_products_with_non_one_off_prices) + ), + + cus_products_with_entitlements AS ( + SELECT DISTINCT cp.id + FROM customer_products cp + WHERE cp.id IN (SELECT id FROM one_off_cus_products) + AND EXISTS ( + SELECT 1 FROM customer_entitlements ce WHERE ce.customer_product_id = cp.id + ) + ), + + valid_one_off_cus_products AS ( + SELECT cp.id, cp.internal_customer_id, cp.internal_entity_id, cp.created_at, cp.internal_product_id + FROM customer_products cp + WHERE cp.id IN (SELECT id FROM cus_products_with_entitlements) + AND NOT EXISTS ( + SELECT 1 + FROM customer_entitlements ce + INNER JOIN entitlements e ON e.id = ce.entitlement_id + INNER JOIN features f ON f.internal_id = e.internal_feature_id + WHERE ce.customer_product_id = cp.id + AND f.type != ${FeatureType.Boolean} + AND NOT ( + COALESCE(f.config->>'usage_type', '') = ${FeatureUsageType.Single} + AND COALESCE(e.allowance_type, '') = ${AllowanceType.Fixed} + AND COALESCE(ce.balance, 0) = 0 + AND ce.usage_allowed = false + ) + ) + ), + + cus_products_with_newer_active_product AS ( + SELECT DISTINCT oo.id + FROM valid_one_off_cus_products oo + INNER JOIN products prod1 ON prod1.internal_id = oo.internal_product_id + WHERE EXISTS ( + SELECT 1 + FROM customer_products cp2 + INNER JOIN products prod2 ON prod2.internal_id = cp2.internal_product_id + WHERE cp2.internal_customer_id = oo.internal_customer_id + AND ( + (cp2.internal_entity_id IS NULL AND oo.internal_entity_id IS NULL) + OR cp2.internal_entity_id = oo.internal_entity_id + ) + AND prod2.id = prod1.id + AND cp2.created_at > oo.created_at + AND cp2.status IN (${CusProductStatus.Active}, ${CusProductStatus.PastDue}) + AND cp2.id != oo.id + AND NOT EXISTS ( + SELECT 1 + FROM customer_entitlements ce1 + INNER JOIN entitlements e1 ON e1.id = ce1.entitlement_id + INNER JOIN features f1 ON f1.internal_id = e1.internal_feature_id + WHERE ce1.customer_product_id = oo.id + AND f1.type = ${FeatureType.Boolean} + AND NOT EXISTS ( + SELECT 1 + FROM customer_entitlements ce2 + INNER JOIN entitlements e2 ON e2.id = ce2.entitlement_id + INNER JOIN features f2 ON f2.internal_id = e2.internal_feature_id + WHERE ce2.customer_product_id = cp2.id + AND f2.id = f1.id + AND f2.type = ${FeatureType.Boolean} + ) + ) + ) + ) + + SELECT + row_to_json(cp.*) as customer_product, + row_to_json(cpr.*) as customer_price, + row_to_json(p.*) as price, + row_to_json(c.*) as customer, + row_to_json(prod.*) as product, + row_to_json(o.*) as org + FROM customer_products cp + INNER JOIN customer_prices cpr ON cpr.customer_product_id = cp.id + INNER JOIN prices p ON p.id = cpr.price_id + INNER JOIN customers c ON c.internal_id = cp.internal_customer_id + INNER JOIN products prod ON prod.internal_id = cp.internal_product_id + INNER JOIN organizations o ON o.id = c.org_id + WHERE cp.id IN (SELECT id FROM cus_products_with_newer_active_product) + `; + + // Run the actual query to measure wall-clock time + console.log("--- Running getOneOffCustomerProductsToCleanup query ---"); + const start = performance.now(); + const result = await db.execute(rawQuery); + const elapsed = performance.now() - start; + console.log(`Rows returned: ${result.length}`); + console.log(`Wall-clock time: ${elapsed.toFixed(2)}ms\n`); + + // Run EXPLAIN ANALYZE + console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n"); + const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${rawQuery}`; + const explainResult = await db.execute(explainQuery); + + for (const row of explainResult) { + const line = (row as Record)["QUERY PLAN"]; + console.log(line); + } + + process.exit(0); +}; + +await main(); diff --git a/server/experiments/explainResetCron.ts b/server/experiments/explainResetCron.ts new file mode 100644 index 000000000..3fc26c386 --- /dev/null +++ b/server/experiments/explainResetCron.ts @@ -0,0 +1,205 @@ +import { + CusProductStatus, + customerEntitlements, + customerProducts, + customers, + entitlements, + features, +} from "@autumn/shared"; +import { and, eq, gt, isNull, lt, or, sql } from "drizzle-orm"; +import { initDrizzle } from "./experimentEnv"; + +// Run with `bun run experiments/explainResetCron.ts` +// Source: CusEntitlementService.getActiveResetPassed +// File: server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts:161 + +const RELEVANT_TABLES = [ + "customer_entitlements", + "entitlements", + "features", + "customers", + "customer_products", +]; + +const listIndexes = async ({ db }: { db: ReturnType["db"] }) => { + console.log("--- Existing indexes on relevant tables ---\n"); + const indexResult = await db.execute<{ + tablename: string; + indexname: string; + indexdef: string; + }>(sql` + SELECT tablename, indexname, indexdef + FROM pg_indexes + WHERE tablename IN (${sql.join( + RELEVANT_TABLES.map((t) => sql`${t}`), + sql`, `, + )}) + ORDER BY tablename, indexname + `); + + for (const row of indexResult) { + console.log(`[${row.tablename}] ${row.indexname}`); + console.log(` ${row.indexdef}\n`); + } +}; + +const main = async () => { + const { db } = initDrizzle({ replica: true }); + const now = Date.now(); + + await listIndexes({ db }); + + // Reproduce the exact Drizzle query from getActiveResetPassed + const query = db + .select() + .from(customerEntitlements) + .innerJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .innerJoin( + features, + eq(entitlements.internal_feature_id, features.internal_id), + ) + .innerJoin( + customers, + eq(customerEntitlements.internal_customer_id, customers.internal_id), + ) + .leftJoin( + customerProducts, + eq(customerEntitlements.customer_product_id, customerProducts.id), + ) + .where( + and( + or( + isNull(customerEntitlements.customer_product_id), + eq(customerProducts.status, CusProductStatus.Active), + ), + lt(customerEntitlements.next_reset_at, now), + or( + isNull(customerEntitlements.expires_at), + gt(customerEntitlements.expires_at, now), + ), + ), + ) + .limit(5000); + + // ── Original query ────────────────────────────────────────────────── + console.log("=== ORIGINAL: getActiveResetPassed (LEFT JOIN + OR) ===\n"); + const startOrig = performance.now(); + const resultOrig = await db.execute(query.getSQL()); + const elapsedOrig = performance.now() - startOrig; + console.log(`Rows returned: ${resultOrig.length}`); + console.log(`Wall-clock time: ${elapsedOrig.toFixed(2)}ms\n`); + + console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n"); + const explainOrig = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query.getSQL()}`; + const explainOrigResult = await db.execute(explainOrig); + for (const row of explainOrigResult) { + console.log((row as Record)["QUERY PLAN"]); + } + + const expiryFilter = or( + isNull(customerEntitlements.expires_at), + gt(customerEntitlements.expires_at, now), + ); + + // ── Optimized Sub-query A: Loose entitlements ─────────────────────── + console.log( + "\n=== OPTIMIZED A: Loose entitlements (customer_product_id IS NULL) ===\n", + ); + const looseQuery = db + .select() + .from(customerEntitlements) + .innerJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .innerJoin( + features, + eq(entitlements.internal_feature_id, features.internal_id), + ) + .innerJoin( + customers, + eq(customerEntitlements.internal_customer_id, customers.internal_id), + ) + .where( + and( + isNull(customerEntitlements.customer_product_id), + lt(customerEntitlements.next_reset_at, now), + expiryFilter, + ), + ) + .limit(5000); + + const startLoose = performance.now(); + const resultLoose = await db.execute(looseQuery.getSQL()); + const elapsedLoose = performance.now() - startLoose; + console.log(`Rows returned: ${resultLoose.length}`); + console.log(`Wall-clock time: ${elapsedLoose.toFixed(2)}ms\n`); + + console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n"); + const explainLoose = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${looseQuery.getSQL()}`; + const explainLooseResult = await db.execute(explainLoose); + for (const row of explainLooseResult) { + console.log((row as Record)["QUERY PLAN"]); + } + + // ── Optimized Sub-query B: Active product entitlements ────────────── + console.log( + "\n=== OPTIMIZED B: Active product entitlements (INNER JOIN) ===\n", + ); + const activeQuery = db + .select() + .from(customerEntitlements) + .innerJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .innerJoin( + features, + eq(entitlements.internal_feature_id, features.internal_id), + ) + .innerJoin( + customers, + eq(customerEntitlements.internal_customer_id, customers.internal_id), + ) + .innerJoin( + customerProducts, + sql`${customerEntitlements.customer_product_id} COLLATE "C" = ${customerProducts.id}`, + ) + .where( + and( + eq(customerProducts.status, CusProductStatus.Active), + lt(customerEntitlements.next_reset_at, now), + expiryFilter, + ), + ) + .limit(5000); + + const startActive = performance.now(); + const resultActive = await db.execute(activeQuery.getSQL()); + const elapsedActive = performance.now() - startActive; + console.log(`Rows returned: ${resultActive.length}`); + console.log(`Wall-clock time: ${elapsedActive.toFixed(2)}ms\n`); + + console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n"); + const explainActive = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${activeQuery.getSQL()}`; + const explainActiveResult = await db.execute(explainActive); + for (const row of explainActiveResult) { + console.log((row as Record)["QUERY PLAN"]); + } + + // ── Summary ───────────────────────────────────────────────────────── + console.log("\n=== SUMMARY ===\n"); + console.log(`Original: ${elapsedOrig.toFixed(2)}ms (${resultOrig.length} rows)`); + console.log(`Optimized A: ${elapsedLoose.toFixed(2)}ms (${resultLoose.length} rows)`); + console.log(`Optimized B: ${elapsedActive.toFixed(2)}ms (${resultActive.length} rows)`); + console.log( + `Optimized total: ${(elapsedLoose + elapsedActive).toFixed(2)}ms (${resultLoose.length + resultActive.length} rows)`, + ); + + process.exit(0); +}; + +await main(); diff --git a/server/experiments/explainResetCronCollation.ts b/server/experiments/explainResetCronCollation.ts new file mode 100644 index 000000000..f43e73ef3 --- /dev/null +++ b/server/experiments/explainResetCronCollation.ts @@ -0,0 +1,101 @@ +import { + CusProductStatus, + customerEntitlements, + customerProducts, + customers, + entitlements, + features, +} from "@autumn/shared"; +import { and, eq, gt, isNull, lt, or, sql } from "drizzle-orm"; +import { initDrizzle } from "./experimentEnv"; + +// Run with `bun run experiments/explainResetCronCollation.ts` +// Tests the getActiveResetPassed query with COLLATE "C" fix on the LEFT JOIN. + +const RELEVANT_TABLES = ["customer_entitlements", "customer_products"]; + +const listIndexes = async ({ + db, +}: { db: ReturnType["db"] }) => { + console.log("--- Existing indexes on relevant tables ---\n"); + const indexResult = await db.execute<{ + tablename: string; + indexname: string; + indexdef: string; + }>(sql` + SELECT tablename, indexname, indexdef + FROM pg_indexes + WHERE tablename IN (${sql.join( + RELEVANT_TABLES.map((t) => sql`${t}`), + sql`, `, + )}) + ORDER BY tablename, indexname + `); + + for (const row of indexResult) { + console.log(`[${row.tablename}] ${row.indexname}`); + console.log(` ${row.indexdef}\n`); + } +}; + +const main = async () => { + const { db } = initDrizzle(); + const now = Date.now(); + + await listIndexes({ db }); + + const expiryFilter = or( + isNull(customerEntitlements.expires_at), + gt(customerEntitlements.expires_at, now), + ); + + const query = db + .select() + .from(customerEntitlements) + .innerJoin( + entitlements, + eq(customerEntitlements.entitlement_id, entitlements.id), + ) + .innerJoin( + features, + eq(entitlements.internal_feature_id, features.internal_id), + ) + .innerJoin( + customers, + eq(customerEntitlements.internal_customer_id, customers.internal_id), + ) + .leftJoin( + customerProducts, + sql`${customerEntitlements.customer_product_id} COLLATE "C" = ${customerProducts.id}`, + ) + .where( + and( + or( + isNull(customerEntitlements.customer_product_id), + eq(customerProducts.status, CusProductStatus.Active), + ), + lt(customerEntitlements.next_reset_at, now), + expiryFilter, + ), + ) + .limit(5000); + + console.log("=== getActiveResetPassed + COLLATE fix ===\n"); + const start = performance.now(); + const result = await db.execute(query.getSQL()); + const elapsed = performance.now() - start; + console.log(`Rows returned: ${result.length}`); + console.log(`Wall-clock time: ${elapsed.toFixed(2)}ms\n`); + + console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n"); + const explainResult = await db.execute( + sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query.getSQL()}`, + ); + for (const row of explainResult) { + console.log((row as Record)["QUERY PLAN"]); + } + + process.exit(0); +}; + +await main(); diff --git a/server/src/cron/invoiceCron/runInvoiceCron.ts b/server/src/cron/invoiceCron/runInvoiceCron.ts index 35df40c14..6392f9078 100644 --- a/server/src/cron/invoiceCron/runInvoiceCron.ts +++ b/server/src/cron/invoiceCron/runInvoiceCron.ts @@ -1,6 +1,7 @@ import type { DeferredAutumnBillingPlanData } from "@autumn/shared"; import { type Metadata, MetadataType, metadata } from "@autumn/shared"; import { and, eq, isNotNull, lt, or } from "drizzle-orm"; +import type { Stripe } from "stripe"; import { OrgService } from "@/internal/orgs/OrgService"; import { createStripeCli } from "../../external/connect/createStripeCli"; import { stripeInvoiceToStripeSubscriptionId } from "../../external/stripe/invoices/utils/convertStripeInvoice"; @@ -55,7 +56,14 @@ export const handleVoidInvoiceCron = async ({ if (!metadata.stripe_invoice_id) return; - const invoice = await stripeCli.invoices.retrieve(metadata.stripe_invoice_id); + let invoice: Stripe.Invoice | undefined; + try { + invoice = await stripeCli.invoices.retrieve(metadata.stripe_invoice_id); + } catch { + logger.warn(`Failed to retrieve invoice ${metadata.stripe_invoice_id}`); + return; + } + const subId = stripeInvoiceToStripeSubscriptionId(invoice); const voidSub = metadata.type === MetadataType.InvoiceCheckout; @@ -85,6 +93,17 @@ export const handleVoidInvoiceCron = async ({ }); } catch (error) { logger.error(`Error voiding invoice: ${error}`); + if ( + error instanceof Error && + error.message.includes("cannot be voided") + ) { + await MetadataService.delete({ + db, + id: metadata.id, + }); + return; + } + logger.error(`Error voiding invoice: ${error}`); } } else if (invoice.status === "void" || invoice.status === "uncollectible") { await MetadataService.delete({ diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts index 3d6869003..062784a1d 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts @@ -4,6 +4,7 @@ import type { StripeCheckoutSessionAction, } from "@autumn/shared"; import { msToSeconds, orgToReturnUrl } from "@autumn/shared"; +import { addMinutes } from "date-fns"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { buildStripeCheckoutSessionItems } from "@/internal/billing/v2/providers/stripe/utils/checkoutSessions/buildStripeCheckoutSessionItems"; @@ -49,7 +50,7 @@ export const buildStripeCheckoutSessionAction = ({ // 4. Trial handling (only for subscription mode) const trialEnd = mode === "subscription" && trialContext?.trialEndsAt - ? msToSeconds(trialContext.trialEndsAt) + ? msToSeconds(addMinutes(trialContext.trialEndsAt, 10).getTime()) : undefined; // 5. Build subscription_data (only for subscription mode) diff --git a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts index 506d393d0..907962d82 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts @@ -175,7 +175,7 @@ export class CusEntService { ) .leftJoin( customerProducts, - eq(customerEntitlements.customer_product_id, customerProducts.id), + sql`${customerEntitlements.customer_product_id} COLLATE "C" = ${customerProducts.id}`, ) .where( and( diff --git a/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts b/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts new file mode 100644 index 000000000..8cfc9b574 --- /dev/null +++ b/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts @@ -0,0 +1 @@ +export const customerEntitlementRepo = {}; diff --git a/server/tests/integration/billing/attach/free-trial/trial-conversion.test.ts b/server/tests/integration/billing/attach/free-trial/trial-conversion.test.ts index 6498d91fd..6949f6e81 100644 --- a/server/tests/integration/billing/attach/free-trial/trial-conversion.test.ts +++ b/server/tests/integration/billing/attach/free-trial/trial-conversion.test.ts @@ -282,6 +282,7 @@ test.concurrent(`${chalk.yellowBright("trial-conversion 5: scheduled downgrade a balance: 500, usage: 0, resetsAt: addMonths(Date.now() + ms.days(14), 1).getTime(), + toleranceMs: ms.hours(1) + ms.minutes(10), }); // Verify Stripe subscription state diff --git a/server/tests/integration/billing/multi-attach/multi-attach-trial.test.ts b/server/tests/integration/billing/multi-attach/multi-attach-trial.test.ts index b09f921a4..edeb42408 100644 --- a/server/tests/integration/billing/multi-attach/multi-attach-trial.test.ts +++ b/server/tests/integration/billing/multi-attach/multi-attach-trial.test.ts @@ -267,6 +267,7 @@ test.concurrent(`${chalk.yellowBright("multi-attach trial 3: explicit free_trial customer, productId: pro.id, trialEndsAt: advancedTo + ms.days(14), + toleranceMs: ms.hours(1) + ms.minutes(10), }); expectCustomerFeatureCorrect({ diff --git a/server/tests/integration/billing/update-subscription/free-trial/update-paid-trials.test.ts b/server/tests/integration/billing/update-subscription/free-trial/update-paid-trials.test.ts index c8a5c71af..d6a1c4537 100644 --- a/server/tests/integration/billing/update-subscription/free-trial/update-paid-trials.test.ts +++ b/server/tests/integration/billing/update-subscription/free-trial/update-paid-trials.test.ts @@ -404,6 +404,7 @@ test.concurrent(`${chalk.yellowBright("p2p-trial: paid no trial -> paid with tri customer, productId: pro.id, trialEndsAt: advancedTo + ms.days(14), + toleranceMs: ms.hours(1) + ms.minutes(10), }); // Feature should still have correct values with usage preserved @@ -595,6 +596,7 @@ test.concurrent(`${chalk.yellowBright("p2f-trial: paid no trial -> free with tri customer, productId: pro.id, trialEndsAt: advancedTo + ms.days(14), + toleranceMs: ms.hours(1) + ms.minutes(10), }); // Usage should be preserved, reset should follow new trial end diff --git a/server/tests/integration/crud/customers/create-customer-paid-defaults.test.ts b/server/tests/integration/crud/customers/create-customer-paid-defaults.test.ts index 19e981329..6ea937458 100644 --- a/server/tests/integration/crud/customers/create-customer-paid-defaults.test.ts +++ b/server/tests/integration/crud/customers/create-customer-paid-defaults.test.ts @@ -1,5 +1,10 @@ import { expect, test } from "bun:test"; -import { type ApiCustomerV3, BillingVersion } from "@autumn/shared"; +import { + type ApiCustomerV3, + BillingVersion, + FreeTrialDuration, + ms, +} from "@autumn/shared"; import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect.js"; import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { expectProductNotPresent } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; @@ -13,7 +18,6 @@ import { items } from "@tests/utils/fixtures/items.js"; import { products } from "@tests/utils/fixtures/products.js"; import { advanceTestClock } from "@tests/utils/stripeUtils"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js"; -import { FreeTrialDuration } from "@autumn/shared"; import chalk from "chalk"; import { CusService } from "@/internal/customers/CusService"; @@ -47,6 +51,7 @@ test.concurrent(`${chalk.yellowBright("paid-defaults: trial product")}`, async ( customer, productId: trialDefault.id, trialEndsAt: calculateTrialEndMs({ trialDays: 14 }), + toleranceMs: ms.hours(1) + ms.minutes(10), }); expectCustomerFeatureCorrect({ diff --git a/server/tests/utils/testAttachUtils/testAttachUtils.ts b/server/tests/utils/testAttachUtils/testAttachUtils.ts index 051f9294c..9c41aa1c5 100644 --- a/server/tests/utils/testAttachUtils/testAttachUtils.ts +++ b/server/tests/utils/testAttachUtils/testAttachUtils.ts @@ -176,7 +176,7 @@ export const advanceToNextInvoice = async ({ stripeCli, testClockId, advanceTo: addMonths(baseTime, 1).getTime(), - waitForSeconds: parallel ? 60 : 30, + waitForSeconds: parallel ? 80 : 50, }); await advanceTestClock({