diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 2e7491b67..95f9a1d56 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,7 +1,7 @@ services: valkey: - image: docker.io/bitnami/valkey:8.0 + image: docker.io/valkey/valkey:8.0 environment: - ALLOW_EMPTY_PASSWORD=yes - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 6fb796a6d..6be24242e 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -1,7 +1,7 @@ services: valkey: - image: docker.io/bitnami/valkey:8.0 + image: docker.io/valkey/valkey:8.0 environment: - ALLOW_EMPTY_PASSWORD=yes - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL diff --git a/docker-compose.unix.yml b/docker-compose.unix.yml index b4abbb58d..5a2c876bd 100644 --- a/docker-compose.unix.yml +++ b/docker-compose.unix.yml @@ -1,6 +1,6 @@ services: valkey: - image: docker.io/bitnami/valkey:8.0 + image: docker.io/valkey/valkey:8.0 environment: - ALLOW_EMPTY_PASSWORD=yes - VALKEY_DISABLE_COMMANDS=FLUSHDB,FLUSHALL diff --git a/server/src/cron.ts b/server/src/cron.ts index 0a955e45f..0c0e2dc9e 100644 --- a/server/src/cron.ts +++ b/server/src/cron.ts @@ -1,15 +1,12 @@ -import dotenv from "dotenv"; -import { - CustomerEntitlement, - FullCusEntWithProduct, - ResetCusEnt, -} from "@autumn/shared"; -import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; -import { format } from "date-fns"; -import { CronJob } from "cron"; +import type { CustomerEntitlement, ResetCusEnt } from "@autumn/shared"; import { UTCDate } from "@date-fns/utc"; -import { initDrizzle } from "./db/initDrizzle.js"; +import { CronJob } from "cron"; +import { format } from "date-fns"; +import dotenv from "dotenv"; import { resetCustomerEntitlement } from "./cron/cronUtils.js"; +import { runProductCron } from "./cron/productCron/runProductCron.js"; +import { initDrizzle } from "./db/initDrizzle.js"; +import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { OrgService } from "./internal/orgs/OrgService.js"; import { notNullish } from "./utils/genUtils.js"; @@ -45,9 +42,9 @@ export const cronTask = async () => { ); } - let results = await Promise.all(batchResets); + const results = await Promise.all(batchResets); - let toUpsert = results.filter(notNullish); + const toUpsert = results.filter(notNullish); await CusEntService.upsert({ db, data: toUpsert as CustomerEntitlement[], @@ -68,17 +65,22 @@ export const cronTask = async () => { // await client.end(); }; -const job = new CronJob( +const main = async () => { + await Promise.all([ + // cronTask(), + runProductCron(), + ]); +}; + +new CronJob( "* * * * *", // Run every minute - function () { - cronTask(); - }, + main, null, // onComplete true, // start immediately "UTC", // timezone (adjust as needed) ); -cronTask(); +main(); process.on("SIGTERM", async () => { console.log("Received SIGTERM signal, closing database connection..."); diff --git a/server/src/cron/productCron/runProductCron.ts b/server/src/cron/productCron/runProductCron.ts new file mode 100644 index 000000000..5d8c801f0 --- /dev/null +++ b/server/src/cron/productCron/runProductCron.ts @@ -0,0 +1,67 @@ +import { customerPrices, customerProducts, customers } from "@autumn/shared"; +import { and, eq, gt, isNotNull, notExists, sql } from "drizzle-orm"; +import { db } from "@/db/initDrizzle.js"; + +export const runProductCron = async () => { + console.log("Running product cron"); + // Get customer_products that have 0 customer_prices, and trial_ends_at is not null, and trial_ends_at > now + const results = await db + .select() + .from(customerProducts) + .innerJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .where( + and( + // No customer_prices exist for this customer_product + notExists( + db + .select() + .from(customerPrices) + .where(eq(customerPrices.customer_product_id, customerProducts.id)), + ), + // trial_ends_at is not null + isNotNull(customerProducts.trial_ends_at), + // trial_ends_at > now (comparing epoch timestamps in milliseconds) + gt( + customerProducts.trial_ends_at, + sql`(EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`, + ), + ), + ); + + console.log( + `Found ${results.length} customer products with no prices and active trials`, + ); + + const uniqueOrgIds = [...new Set(results.map((r) => r.customers.org_id))]; + console.log("Unique org IDs:", uniqueOrgIds); + + // for (const result of results) { + // console.log( + // `Customer ${result.customers.id}, Org: ${result.customers.org_id}, product: ${result.customer_products.product_id}`, + // ); + // } + // const expireCusProduct = async (customerProduct: CustomerProduct) => { + // await CusProductService.update({ + // db, + // cusProductId: customerProduct.id, + // updates: { + // status: CusProductStatus.Expired, + // }, + // }); + // }; + + // const batchSize = 50; + // for (let i = 0; i < results.length; i += batchSize) { + // const batch = results.slice(i, i + batchSize); + // const batchExpires = batch.map((cusProduct) => + // expireCusProduct(cusProduct), + // ); + // await Promise.all(batchExpires); + // console.log(`Expired batch of ${batch.length} customer products`); + // } + + return results; +}; diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts index b75eab067..032895c41 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts @@ -1,26 +1,25 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { - activateFutureProduct, - activateDefaultProduct, - cancelCusProductSubscriptions, -} from "@/internal/customers/cusProducts/cusProductUtils.js"; -import { cusProductToPrices } from "@autumn/shared"; -import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - FullCusProduct, APIVersion, + AttachScenario, BillingType, CusProductStatus, - AttachScenario, + cusProductToPrices, + type FullCusProduct, } from "@autumn/shared"; -import Stripe from "stripe"; -import { getCusPaymentMethod } from "../../stripeCusUtils.js"; -import { webhookToAttachParams } from "../../webhookUtils/webhookUtils.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { createUsageInvoice } from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoice.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { + activateDefaultProduct, + activateFutureProduct, +} from "@/internal/customers/cusProducts/cusProductUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { getCusPaymentMethod } from "../../stripeCusUtils.js"; +import { webhookToAttachParams } from "../../webhookUtils/webhookUtils.js"; export const handleCusProductDeleted = async ({ req, @@ -137,13 +136,13 @@ export const handleCusProductDeleted = async ({ return; } - let cusProducts = await CusProductService.list({ + const cusProducts = await CusProductService.list({ db, - internalCustomerId: cusProduct.customer!.internal_id, + internalCustomerId: cusProduct.internal_customer_id, inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue], }); - let { curMainProduct } = getExistingCusProducts({ + const { curMainProduct } = getExistingCusProducts({ product: cusProduct.product, cusProducts, }); @@ -154,12 +153,4 @@ export const handleCusProductDeleted = async ({ fullCus, curCusProduct: curMainProduct || undefined, }); - - // await cancelCusProductSubscriptions({ - // cusProduct, - // org, - // env, - // excludeIds: [subscription.id], - // logger, - // }); }; diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts index e180ba223..f5c477f6f 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts @@ -1,15 +1,17 @@ -import Stripe from "stripe"; -import { AttachScenario, CusProductStatus } from "@autumn/shared"; -import { createStripeCli } from "../../utils.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; - -import { notNullish } from "@/utils/genUtils.js"; -import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; -import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { cusProductToProduct } from "@autumn/shared"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { + AttachScenario, + CusProductStatus, + cusProductToProduct, +} from "@autumn/shared"; +import type Stripe from "stripe"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; +import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; +import { createStripeCli } from "../../utils.js"; export const handleSchedulePhaseCompleted = async ({ req, @@ -127,9 +129,4 @@ export const handleSchedulePhaseCompleted = async ({ ); } } - // const currentPhase = schedule.phases.find( - // (phase) => - // phase.start_date <= Math.floor(now / 1000) && - // phase.end_date > Math.floor(now / 1000) - // ); }; diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index 555aa2533..4bdac2366 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -1,4 +1,5 @@ import { + ACTIVE_STATUSES, APIVersion, type AppEnv, AttachScenario, @@ -9,6 +10,7 @@ import { type FixedPriceConfig, type FullCusProduct, type FullCustomer, + type FullProduct, type Organization, PriceType, type Subscription, @@ -31,7 +33,6 @@ import { notNullish, nullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js"; import { newCusToAttachParams } from "../attach/attachUtils/attachParams/convertToParams.js"; -import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js"; import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js"; import { CusProductService, RELEVANT_STATUSES } from "./CusProductService.js"; import { getRelatedCusEnt } from "./cusPrices/cusPriceUtils.js"; @@ -133,6 +134,7 @@ export const getDefaultProduct = async ({ return defaultProd; }; +// This function is only used in cancellation flows export const activateDefaultProduct = async ({ req, productGroup, @@ -153,44 +155,38 @@ export const activateDefaultProduct = async ({ }); // Look for a paid default trial first, then fall back to free default - let defaultProd = defaultProducts.find( + const defaultProd: FullProduct | undefined = defaultProducts.find( (p) => - p.group === productGroup && isDefaultTrialFullProduct({ product: p }), + p.group === productGroup && !isDefaultTrialFullProduct({ product: p }), ); - const defaultableProducts = { - free: defaultProducts.filter( - (p) => p.group === productGroup && isFreeProduct(p.prices), - ), - paid: defaultProducts.filter( - (p) => - p.group === productGroup && isDefaultTrialFullProduct({ product: p }), - ), - }; + // const defaultableProducts = { + // free: defaultProducts.filter( + // (p) => p.group === productGroup && isFreeProduct(p.prices), + // ), + // paid: defaultProducts.filter( + // (p) => + // p.group === productGroup && isDefaultTrialFullProduct({ product: p }), + // ), + // }; - // console.log("Found defaultable products:", { - // free: defaultableProducts.free.map((p) => p.name), - // paid: defaultableProducts.paid.map((p) => p.name), - // }); + // if (defaultableProducts.paid.length > 0) { + // defaultProd = defaultableProducts.paid[0]; + // } else if (defaultableProducts.free.length > 0) { + // defaultProd = defaultableProducts.free[0]; + // } - if (defaultableProducts.paid.length > 0) { - defaultProd = defaultableProducts.paid[0]; - } else if (defaultableProducts.free.length > 0) { - defaultProd = defaultableProducts.free[0]; - } else { - return false; - } + if (!defaultProd) return false; - if (curCusProduct?.internal_product_id == defaultProd.internal_id) { + if (curCusProduct?.internal_product_id === defaultProd.internal_id) { return false; } const stripeCli = createStripeCli({ org, env }); const defaultIsFree = isFreeProduct(defaultProd.prices); - const isDefaultTrial = isDefaultTrialFullProduct({ product: defaultProd }); // Initialize Stripe customer and products if needed (for paid non-trial products) - if (!defaultIsFree && !isDefaultTrial) { + if (!defaultIsFree) { await initStripeCusAndProducts({ db, org, @@ -201,83 +197,87 @@ export const activateDefaultProduct = async ({ }); } - if (!isDefaultTrial) { - const existingDefaultProduct = fullCus.customer_products.find( - (cp) => - cp.product.internal_id === defaultProd!.internal_id && - (cp.status === CusProductStatus.Active || - cp.status === CusProductStatus.PastDue || - cp.status === CusProductStatus.Trialing), + // If default is already active, skip + const existingDefaultProduct = fullCus.customer_products.find( + (cp) => + cp.product.id === defaultProd?.id && ACTIVE_STATUSES.includes(cp.status), + ); + + if (existingDefaultProduct) { + logger.info( + `Default product ${defaultProd?.name} already exists for customer`, ); - - if (existingDefaultProduct) { - logger.info( - `Default product ${defaultProd!.name} already exists for customer`, - ); - return false; - } - - await handleAddProduct({ - req, - attachParams: newCusToAttachParams({ - req, - newCus: fullCus, - products: [defaultProd], - stripeCli, - }), - }); - - // await createFullCusProduct({ - // db, - // attachParams: { - // org, - // customer, - // product: defaultProd, - // prices: defaultProd.prices, - // entitlements: defaultProd.entitlements, - // freeTrial: defaultProd.free_trial || null, - // optionsList: [], - // entities: [], - // features: [], - // replaceables: [], - // }, - // scenario: AttachScenario.New, - // logger, - // }); - - // console.log(` ✅ activated default product: ${defaultProd.group}`); - return true; - } else if (isDefaultTrial && defaultableProducts.free.length > 0) { - defaultProd = defaultableProducts.free[0]; - - // Check if the free default product already exists to prevent duplicates - const existingFreeProduct = fullCus.customer_products.find( - (cp) => - cp.product.internal_id === defaultProd!.internal_id && - (cp.status === CusProductStatus.Active || - cp.status === CusProductStatus.PastDue), - ); - - if (existingFreeProduct) { - logger.info( - `Free default product ${defaultProd!.name} already exists for customer`, - ); - return false; - } - - await handleAddProduct({ - req, - attachParams: newCusToAttachParams({ - req, - newCus: fullCus, - products: [defaultProd], - stripeCli, - }), - config: getDefaultAttachConfig(), - }); - - return true; + return false; } + + await handleAddProduct({ + req, + attachParams: newCusToAttachParams({ + req, + newCus: fullCus, + products: [defaultProd], + stripeCli, + }), + }); + + return true; + + // if (!isDefaultTrial) { + // const existingDefaultProduct = fullCus.customer_products.find( + // (cp) => + // cp.product.internal_id === defaultProd?.internal_id && + // ACTIVE_STATUSES.includes(cp.status), + // ); + + // if (existingDefaultProduct) { + // logger.info( + // `Default product ${defaultProd?.name} already exists for customer`, + // ); + // return false; + // } + + // await handleAddProduct({ + // req, + // attachParams: newCusToAttachParams({ + // req, + // newCus: fullCus, + // products: [defaultProd], + // stripeCli, + // }), + // }); + + // return true; + // } else if (isDefaultTrial && defaultableProducts.free.length > 0) { + // defaultProd = defaultableProducts.free[0]; + + // // Check if the free default product already exists to prevent duplicates + // const existingFreeProduct = fullCus.customer_products.find( + // (cp) => + // cp.product.internal_id === defaultProd!.internal_id && + // (cp.status === CusProductStatus.Active || + // cp.status === CusProductStatus.PastDue), + // ); + + // if (existingFreeProduct) { + // logger.info( + // `Free default product ${defaultProd?.name} already exists for customer`, + // ); + // return false; + // } + + // await handleAddProduct({ + // req, + // attachParams: newCusToAttachParams({ + // req, + // newCus: fullCus, + // products: [defaultProd], + // stripeCli, + // }), + // config: getDefaultAttachConfig(), + // }); + + // return true; + // } }; export const expireAndActivate = async ({ diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts index c06a4f64e..a329df627 100644 --- a/server/src/internal/customers/cusUtils/createNewCustomer.ts +++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts @@ -196,51 +196,5 @@ export const createNewCustomer = async ({ } } - // if (nonFreeProds.length > 0) { - // const stripeCli = createStripeCli({ org, env }); - - // await initStripeCusAndProducts({ - // db, - // org, - // env, - // customer: newCustomer, - // products: nonFreeProds, - // logger, - // }); - - // await handleAddProduct({ - // req, - // attachParams: newCusToAttachParams({ - // req, - // newCus: newCustomer as FullCustomer, - // products: nonFreeProds, - // stripeCli, - // freeTrial: defaultPaidTrialProd?.free_trial || null, - // }), - // }); - // } - - // if (!defaultPaidTrialProd) { - // for (const product of freeProds) { - // await createFullCusProduct({ - // db, - // attachParams: newCusToInsertParams({ - // req, - // newCus: newCustomer, - // product, - // }), - // nextResetAt, - // anchorToUnix: org.config.anchor_start_of_month - // ? getNextStartOfMonthUnix({ - // interval: BillingInterval.Month, - // intervalCount: 1, - // }) - // : undefined, - // scenario: AttachScenario.New, - // logger, - // }); - // } - // } - return newCustomer; }; diff --git a/server/src/internal/products/productUtils/classifyProduct.ts b/server/src/internal/products/productUtils/classifyProduct.ts index 863601e0a..62381d088 100644 --- a/server/src/internal/products/productUtils/classifyProduct.ts +++ b/server/src/internal/products/productUtils/classifyProduct.ts @@ -1,4 +1,4 @@ -import { FullProduct, Price, ProductV2 } from "@autumn/shared"; +import type { FullProduct, Price, ProductV2 } from "@autumn/shared"; import { pricesOnlyOneOff } from "../prices/priceUtils.js"; import { isFeatureItem } from "../product-items/productItemUtils/getItemType.js"; import { isFreeProduct } from "../productUtils.js"; @@ -57,6 +57,9 @@ export const isDefaultTrialFullProduct = ({ product: FullProduct; skipDefault?: boolean; }) => { + // If it's free + trial, also consider it default trial + if (isFreeProduct(product.prices) && product.free_trial) return true; + return ( product.free_trial && !product.free_trial?.card_required && diff --git a/server/src/utils/scriptUtils/initCustomer.ts b/server/src/utils/scriptUtils/initCustomer.ts index 9c58a730b..54e06b1ea 100644 --- a/server/src/utils/scriptUtils/initCustomer.ts +++ b/server/src/utils/scriptUtils/initCustomer.ts @@ -7,6 +7,7 @@ import { import type { Autumn } from "autumn-js"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; import { @@ -14,7 +15,6 @@ import { createStripeCustomer, } from "../../external/stripe/stripeCusUtils.js"; import { createStripeCli } from "../../external/stripe/utils.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; export const createCusInStripe = async ({ customer, @@ -188,7 +188,7 @@ export const initCustomerV2 = async ({ attachPm, withTestClock = true, }: { - autumn: Autumn; + autumn: Autumn | AutumnInt; customerId: string; org: Organization; env: AppEnv; @@ -201,7 +201,7 @@ export const initCustomerV2 = async ({ const fingerprint_ = ""; const stripeCli = createStripeCli({ org, env }); - let testClockId; + let testClockId: string | undefined; if (withTestClock) { const testClock = await stripeCli.testHelpers.testClocks.create({ @@ -220,7 +220,8 @@ export const initCustomerV2 = async ({ // 2. Create customer try { await autumn.customers.delete(customerId); - } catch (error) {} + } catch (_error) {} + await autumn.customers.create({ id: customerId, name, diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index b55dd1f6e..58e45400a 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -1,8 +1,10 @@ import "dotenv/config"; -import { timeout } from "./genUtils.js"; -import { Stripe } from "stripe"; -import { BillingInterval, Customer, FullProduct } from "@autumn/shared"; +import { + BillingInterval, + type Customer, + type FullProduct, +} from "@autumn/shared"; import { addDays, addHours, @@ -12,9 +14,11 @@ import { format, } from "date-fns"; import puppeteer from "puppeteer-core"; -import Browserbase from "@browserbasehq/sdk"; +import type { Stripe } from "stripe"; +import { timeout } from "./genUtils.js"; const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s + import { Hyperbrowser } from "@hyperbrowser/sdk"; const client = new Hyperbrowser({ @@ -186,7 +190,7 @@ export const deleteStripeProduct = async ({ if (product.processor) { // console.log("Stripe product", stripeProd.active, stripeProd.id); - let stripeProdId = product.processor.id; + const stripeProdId = product.processor.id; try { await stripeCli.products.del(stripeProdId); } catch (error) { @@ -260,7 +264,7 @@ export const advanceTestClock = async ({ } console.log(" - Advancing to: ", format(advanceTo, "dd MMM yyyy HH:mm:ss")); - await stripeCli.testHelpers.testClocks.advance(testClockId, { + const res = await stripeCli.testHelpers.testClocks.advance(testClockId, { frozen_time: Math.floor(advanceTo / 1000), }); @@ -384,7 +388,7 @@ export const checkBillingMeterEventSummary = async ({ stripeMeterId: string; stripeCustomerId: string; }) => { - let endTime = addMonths(startTime, 1); + const endTime = addMonths(startTime, 1); const event = await stripeCli.billing.meters.listEventSummaries( stripeMeterId, {