import { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js"; import { findStripePriceFromPrices } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; import { CusService } from "@/internal/customers/CusService.js"; import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils.js"; import { AppEnv, AttachBranch, AttachPreview, FullCusProduct, FullCustomer, Organization, ProductV2, } from "@autumn/shared"; import { expect } from "chai"; import Stripe from "stripe"; import { expectSubItemsCorrect } from "./expectSubUtils.js"; import { expectProductAttached } from "./expectProductAttached.js"; import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js"; import { advanceTestClock } from "../stripeUtils.js"; import { hoursToFinalizeInvoice } from "../constants.js"; import { addHours } from "date-fns"; export const expectNextCycleCorrect = async ({ autumn, preview, stripeCli, customerId, testClockId, product, db, org, env, advanceClock = true, }: { autumn: AutumnInt; preview: AttachPreview; stripeCli: Stripe; customerId: string; testClockId: string; product: ProductV2; db: DrizzleCli; org: Organization; env: AppEnv; advanceClock?: boolean; }) => { if (advanceClock) { await advanceTestClock({ stripeCli, testClockId, advanceTo: addHours( preview!.due_next_cycle.due_at, hoursToFinalizeInvoice, ).getTime(), }); } const customer = await autumn.customers.get(customerId); expectProductAttached({ customer, product, }); await expectSubItemsCorrect({ stripeCli, customerId, product, db, org, env, }); }; export const expectDowngradeCorrect = async ({ customerId, curProduct, newProduct, autumn, stripeCli, db, org, env, }: { customerId: string; curProduct: ProductV2; newProduct: ProductV2; autumn: AutumnInt; stripeCli: Stripe; db: DrizzleCli; org: Organization; env: AppEnv; }) => { const preview = await autumn.attachPreview({ customer_id: customerId, product_id: newProduct.id, }); await autumn.attach({ customer_id: customerId, product_id: newProduct.id, }); const customer = await autumn.customers.get(customerId); const productCount = customer.products.reduce((acc: number, product: any) => { if (product.group == curProduct.group) { return acc + 1; } else return acc; }, 0); expect( productCount, "customer should only have 2 products (from this group)", ).to.equal(2); expectProductAttached({ customer, product: curProduct, }); const { fullCus } = await expectSubItemsCorrect({ stripeCli, customerId, product: curProduct, db, org, env, isCanceled: true, }); const newProductIsFree = isFreeProductV2({ product: newProduct }); if (newProductIsFree) { let res = await stripeCli.subscriptionSchedules.list({ customer: fullCus.processor?.id, }); let data = res.data.filter((s) => s.status != "canceled"); expect(data.length, "should have no sub schedules").to.equal(0); await expectSubScheduleCorrect({ stripeCli, customerId, productId: newProduct.id, db, org, env, }); expectProductAttached({ customer, product: newProduct, }); } expect(preview.branch).to.equal(AttachBranch.Downgrade); return { preview, }; }; export const expectSubScheduleCorrect = async ({ stripeCli, customerId, productId, db, org, env, fullCus, }: { stripeCli: Stripe; customerId: string; productId: string; db: DrizzleCli; org: Organization; env: AppEnv; fullCus?: FullCustomer; }) => { // 1. Check schedule if (!fullCus) { fullCus = await CusService.getFull({ db, idOrInternalId: customerId, orgId: org.id, env, }); } const cusProduct = fullCus.customer_products.find( (cp: FullCusProduct) => cp.product.id == productId, )!; const scheduleSets = await getStripeSchedules({ stripeCli, scheduleIds: cusProduct?.scheduled_ids || [], }); const stripePrices = scheduleSets.flatMap( (schedule) => schedule?.prices || [], ); const autumnPrices = cusProductToPrices({ cusProduct }); let missingUsageCount = 0; for (const autumnPrice of autumnPrices) { const stripePrice = findStripePriceFromPrices({ stripePrices, autumnPrice, }); if (isV4Usage({ price: autumnPrice!, cusProduct })) { missingUsageCount++; continue; } else { expect(stripePrice).to.exist; } } expect( stripePrices.length, "number of schedule items equivalent to number of autumn prices", ).to.equal(autumnPrices.length - missingUsageCount); };