From 5c92d1b491c9c4dbe37bfb3643ef2f8332d954e7 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Fri, 23 Jan 2026 18:18:50 +0000 Subject: [PATCH 001/110] feat: initial attach endpoint setup --- server/src/internal/billing/billingRouter.ts | 2 +- .../billing/v2/attach/handleAttachV2.ts | 9 ++ .../v2/attach/logs/logAttachContext.ts | 62 ++++++++++ .../attach/setup/setupAttachBillingContext.ts | 106 ++++++++++++++++++ .../attach/setup/setupAttachCheckoutMode.ts | 19 ++++ .../attach/setup/setupAttachEndOfCycleMs.ts | 55 +++++++++ .../attach/setup/setupAttachProductContext.ts | 43 +++++++ .../setup/setupAttachTransitionContext.ts | 71 ++++++++++++ .../v2/attach/types/attachBillingContext.ts | 23 ++++ .../stripe/setup/setupStripeBillingContext.ts | 40 ++++--- .../v2/setup/setupFullCustomerContext.ts | 6 +- .../attach/basic-attach-scenario.test.ts | 29 +++++ shared/api/billing/attachV2/attachV0Params.ts | 19 ++-- .../api/billing/common/billingParamsBase.ts | 12 ++ shared/api/billing/common/checkoutMode.ts | 7 ++ shared/api/billing/common/planTiming.ts | 5 + shared/api/billing/index.ts | 3 + .../updateSubscriptionV0Params.ts | 57 +++++----- 18 files changed, 505 insertions(+), 63 deletions(-) create mode 100644 server/src/internal/billing/v2/attach/logs/logAttachContext.ts create mode 100644 server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts create mode 100644 server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts create mode 100644 server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts create mode 100644 server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts create mode 100644 server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts create mode 100644 server/src/internal/billing/v2/attach/types/attachBillingContext.ts create mode 100644 server/tests/scenarios/attach/basic-attach-scenario.test.ts create mode 100644 shared/api/billing/common/billingParamsBase.ts create mode 100644 shared/api/billing/common/checkoutMode.ts create mode 100644 shared/api/billing/common/planTiming.ts diff --git a/server/src/internal/billing/billingRouter.ts b/server/src/internal/billing/billingRouter.ts index 8887b3f0d..2eb3d01dc 100644 --- a/server/src/internal/billing/billingRouter.ts +++ b/server/src/internal/billing/billingRouter.ts @@ -25,4 +25,4 @@ billingRouter.post( ); // V2 Attach -billingRouter.post("/v2/attach", ...handleAttachV2); +billingRouter.post("/billing/attach", ...handleAttachV2); diff --git a/server/src/internal/billing/v2/attach/handleAttachV2.ts b/server/src/internal/billing/v2/attach/handleAttachV2.ts index f3c953b47..c6d25e5f8 100644 --- a/server/src/internal/billing/v2/attach/handleAttachV2.ts +++ b/server/src/internal/billing/v2/attach/handleAttachV2.ts @@ -1,5 +1,7 @@ import { AttachV0ParamsSchema } from "@autumn/shared"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; +import { logAttachContext } from "./logs/logAttachContext"; +import { setupAttachBillingContext } from "./setup/setupAttachBillingContext"; export const handleAttachV2 = createRoute({ body: AttachV0ParamsSchema, @@ -24,6 +26,13 @@ export const handleAttachV2 = createRoute({ `=============== RUNNING ATTACH V2 FOR ${body.customer_id} ===============`, ); + // 1. Setup + const billingContext = await setupAttachBillingContext({ + ctx, + params: body, + }); + logAttachContext({ ctx, billingContext }); + return c.json({ customer_id: body.customer_id }, 200); }, }); diff --git a/server/src/internal/billing/v2/attach/logs/logAttachContext.ts b/server/src/internal/billing/v2/attach/logs/logAttachContext.ts new file mode 100644 index 000000000..50417f5ba --- /dev/null +++ b/server/src/internal/billing/v2/attach/logs/logAttachContext.ts @@ -0,0 +1,62 @@ +import { formatMs } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; +import type { AttachBillingContext } from "../types/attachBillingContext"; + +export const logAttachContext = ({ + ctx, + billingContext, +}: { + ctx: AutumnContext; + billingContext: AttachBillingContext; +}) => { + const { + attachProduct, + currentCustomerProduct, + scheduledCustomerProduct, + planTiming, + endOfCycleMs, + checkoutMode, + featureQuantities, + currentEpochMs, + invoiceMode, + stripeSubscription, + stripeSubscriptionSchedule, + isCustom, + } = billingContext; + + addToExtraLogs({ + ctx, + extras: { + attachContext: { + product: `${attachProduct.id} (v${attachProduct.version}) ${isCustom ? "custom" : "standard"}`, + + transition: currentCustomerProduct + ? `${currentCustomerProduct.product.id} -> ${attachProduct.id} (${planTiming})` + : "new attachment", + + currentCustomerProduct: currentCustomerProduct?.id ?? "none", + scheduledCustomerProduct: scheduledCustomerProduct?.id ?? "none", + + planTiming, + endOfCycleMs: endOfCycleMs ? formatMs(endOfCycleMs) : "n/a", + checkoutMode: checkoutMode ?? "direct billing", + + timestamps: `Current: ${formatMs(currentEpochMs)}`, + + invoiceMode: invoiceMode + ? `enable immediately: ${invoiceMode.enableProductImmediately} | finalize invoice: ${invoiceMode.finalizeInvoice}` + : "default", + + stripe: `${stripeSubscription?.id ?? "no sub"} | ${stripeSubscriptionSchedule?.id ?? "no schedule"}`, + + featureQuantities: + featureQuantities.length > 0 + ? featureQuantities + .map((fq) => `${fq.feature_id}: ${fq.quantity}`) + .join(", ") + : "none", + }, + }, + }); +}; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts new file mode 100644 index 000000000..ac76777ed --- /dev/null +++ b/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts @@ -0,0 +1,106 @@ +import { type AttachV0Params, notNullish } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext"; +import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext"; +import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext"; +import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext"; +import type { AttachBillingContext } from "../types/attachBillingContext"; +import { setupAttachCheckoutMode } from "./setupAttachCheckoutMode"; +import { setupAttachEndOfCycleMs } from "./setupAttachEndOfCycleMs"; +import { setupAttachProductContext } from "./setupAttachProductContext"; +import { setupAttachTransitionContext } from "./setupAttachTransitionContext"; + +/** + * Assembles the full billing context for attaching a product. + */ +export const setupAttachBillingContext = async ({ + ctx, + params, +}: { + ctx: AutumnContext; + params: AttachV0Params; +}): Promise => { + const fullCustomer = await setupFullCustomerContext({ + ctx, + params, + }); + + const { attachProduct, customPrices, customEnts } = + await setupAttachProductContext({ + ctx, + params, + }); + + const { currentCustomerProduct, scheduledCustomerProduct, planTiming } = + setupAttachTransitionContext({ + fullCustomer, + attachProduct, + }); + + const { + stripeSubscription, + stripeSubscriptionSchedule, + stripeCustomer, + stripeDiscounts, + paymentMethod, + testClockFrozenTime, + } = await setupStripeBillingContext({ + ctx, + fullCustomer, + targetCustomerProduct: currentCustomerProduct, + }); + + const currentEpochMs = testClockFrozenTime ?? Date.now(); + + const featureQuantities = setupFeatureQuantitiesContext({ + ctx, + featureQuantitiesParams: params, + fullProduct: attachProduct, + currentCustomerProduct: undefined, + }); + + const invoiceMode = setupInvoiceModeContext({ params }); + const isCustom = notNullish(params.items); + + const endOfCycleMs = setupAttachEndOfCycleMs({ + planTiming, + currentCustomerProduct, + stripeSubscription, + currentEpochMs, + }); + + const checkoutMode = setupAttachCheckoutMode({ + paymentMethod, + redirectMode: params.redirect_mode, + }); + + return { + fullCustomer, + fullProducts: [attachProduct], + attachProduct, + + currentCustomerProduct, + scheduledCustomerProduct, + + planTiming, + endOfCycleMs, + checkoutMode, + + stripeCustomer, + stripeSubscription, + stripeSubscriptionSchedule, + stripeDiscounts, + paymentMethod, + + currentEpochMs, + billingCycleAnchorMs: "now", + resetCycleAnchorMs: "now", + + invoiceMode, + featureQuantities, + + customPrices, + customEnts, + isCustom, + }; +}; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts b/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts new file mode 100644 index 000000000..c00167cfc --- /dev/null +++ b/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts @@ -0,0 +1,19 @@ +import type { CheckoutMode, RedirectMode } from "@autumn/shared"; +import type Stripe from "stripe"; + +/** + * Determines the checkout mode based on payment method availability and redirect preference. + */ +export const setupAttachCheckoutMode = ({ + paymentMethod, + redirectMode, +}: { + paymentMethod?: Stripe.PaymentMethod; + redirectMode?: RedirectMode; +}): CheckoutMode => { + const hasPaymentMethod = !!paymentMethod; + + if (!hasPaymentMethod) return "stripe_checkout"; + if (redirectMode === "always") return "autumn_checkout"; + return null; +}; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts b/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts new file mode 100644 index 000000000..58d35396b --- /dev/null +++ b/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts @@ -0,0 +1,55 @@ +import { + cusProductToPrices, + type FullCusProduct, + getCycleEnd, + type PlanTiming, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils"; +import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils"; + +/** + * Computes the end of cycle timestamp for downgrades. + * Returns undefined if planTiming is "immediate" or no billing interval found. + */ +export const setupAttachEndOfCycleMs = ({ + planTiming, + currentCustomerProduct, + stripeSubscription, + currentEpochMs, +}: { + planTiming: PlanTiming; + currentCustomerProduct?: FullCusProduct; + stripeSubscription?: Stripe.Subscription; + currentEpochMs: number; +}): number | undefined => { + if (planTiming !== "end_of_cycle" || !currentCustomerProduct) { + return undefined; + } + + const currentPrices = cusProductToPrices({ + cusProduct: currentCustomerProduct, + }); + + const largestInterval = getLargestInterval({ + prices: currentPrices, + excludeOneOff: true, + }); + + if (!largestInterval) { + return undefined; + } + + // Use Stripe subscription's earliest period end if available + const billingAnchor = stripeSubscription + ? getEarliestPeriodEnd({ sub: stripeSubscription }) * 1000 + : currentEpochMs; + + return getCycleEnd({ + anchor: billingAnchor, + interval: largestInterval.interval, + intervalCount: largestInterval.intervalCount, + now: currentEpochMs, + floor: billingAnchor, + }); +}; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts new file mode 100644 index 000000000..21da87607 --- /dev/null +++ b/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts @@ -0,0 +1,43 @@ +import type { AttachV0Params } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { ProductService } from "@/internal/products/ProductService"; +import { setupCustomFullProduct } from "../../setup/setupCustomFullProduct"; + +/** + * Loads the product being attached, handling version and custom items params. + */ +export const setupAttachProductContext = async ({ + ctx, + params, +}: { + ctx: AutumnContext; + params: AttachV0Params; +}) => { + const { db, org, env } = ctx; + + // 1. Fetch the product being attached + const fullProduct = await ProductService.getFull({ + db, + idOrInternalId: params.product_id, + orgId: org.id, + env, + version: params.version, + }); + + // 2. Handle custom items if provided + const { + fullProduct: attachProduct, + customPrices, + customEnts, + } = await setupCustomFullProduct({ + ctx, + currentFullProduct: fullProduct, + customItems: params.items, + }); + + return { + attachProduct, + customPrices, + customEnts, + }; +}; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts new file mode 100644 index 000000000..98b5be486 --- /dev/null +++ b/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts @@ -0,0 +1,71 @@ +import { + cusProductToPrices, + type FullCustomer, + type FullProduct, + findMainActiveCustomerProductByGroup, + findMainScheduledCustomerProductByGroup, + isOneOffProduct, + isProductUpgrade, + type PlanTiming, +} from "@autumn/shared"; + +/** + * Sets up the transition context for attaching a product. + * Determines if there's an existing product to transition from (upgrade/downgrade). + */ +export const setupAttachTransitionContext = ({ + fullCustomer, + attachProduct, +}: { + fullCustomer: FullCustomer; + attachProduct: FullProduct; +}) => { + // Only main recurring products can trigger transitions + const isMainRecurring = + !attachProduct.is_add_on && + !isOneOffProduct({ prices: attachProduct.prices }); + + if (!isMainRecurring) { + return { + currentCustomerProduct: undefined, + scheduledCustomerProduct: undefined, + planTiming: "immediate" as PlanTiming, + }; + } + + const internalEntityId = fullCustomer.entity?.internal_id; + + const currentCustomerProduct = findMainActiveCustomerProductByGroup({ + fullCus: fullCustomer, + productGroup: attachProduct.group, + internalEntityId, + }); + + const scheduledCustomerProduct = findMainScheduledCustomerProductByGroup({ + fullCustomer, + productGroup: attachProduct.group, + internalEntityId, + }); + + // Compute planTiming (upgrade = immediate, downgrade = end_of_cycle) + let planTiming: PlanTiming = "immediate"; + + if (currentCustomerProduct) { + const currentPrices = cusProductToPrices({ + cusProduct: currentCustomerProduct, + }); + + const isUpgrade = isProductUpgrade({ + prices1: currentPrices, + prices2: attachProduct.prices, + }); + + planTiming = isUpgrade ? "immediate" : "end_of_cycle"; + } + + return { + currentCustomerProduct, + scheduledCustomerProduct, + planTiming, + }; +}; diff --git a/server/src/internal/billing/v2/attach/types/attachBillingContext.ts b/server/src/internal/billing/v2/attach/types/attachBillingContext.ts new file mode 100644 index 000000000..53aa67f27 --- /dev/null +++ b/server/src/internal/billing/v2/attach/types/attachBillingContext.ts @@ -0,0 +1,23 @@ +import type { + CheckoutMode, + FullCusProduct, + FullProduct, + PlanTiming, +} from "@autumn/shared"; +import type { BillingContext } from "../../billingContext"; + +export interface AttachBillingContext extends BillingContext { + // The product being attached + attachProduct: FullProduct; + + // Transition context (only for main recurring products) + currentCustomerProduct?: FullCusProduct; // To transition from + scheduledCustomerProduct?: FullCusProduct; // To delete + + // Timing + planTiming: PlanTiming; + endOfCycleMs?: number; // Only needed if planTiming === "end_of_cycle" + + // Checkout + checkoutMode: CheckoutMode; +} diff --git a/server/src/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext.ts b/server/src/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext.ts index 4a44e89b1..642e6a9f6 100644 --- a/server/src/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext.ts +++ b/server/src/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext.ts @@ -12,26 +12,30 @@ export const setupStripeBillingContext = async ({ }: { ctx: AutumnContext; fullCustomer: FullCustomer; - targetCustomerProduct: FullCusProduct; + targetCustomerProduct?: FullCusProduct; }) => { - const stripeSubscription = await fetchStripeSubscriptionForBilling({ - ctx, - fullCus: fullCustomer, - products: [], - targetCusProductId: targetCustomerProduct.id, - }); + // If no target customer product, skip subscription/schedule fetching + const stripeSubscription = targetCustomerProduct + ? await fetchStripeSubscriptionForBilling({ + ctx, + fullCus: fullCustomer, + products: [], + targetCusProductId: targetCustomerProduct.id, + }) + : undefined; - const stripeSubscriptionSchedule = - await fetchStripeSubscriptionScheduleForBilling({ - ctx, - fullCus: fullCustomer, - subscriptionScheduleId: - typeof stripeSubscription?.schedule === "string" - ? stripeSubscription.schedule - : undefined, - products: [], - targetCusProductId: targetCustomerProduct.id, - }); + const stripeSubscriptionSchedule = targetCustomerProduct + ? await fetchStripeSubscriptionScheduleForBilling({ + ctx, + fullCus: fullCustomer, + subscriptionScheduleId: + typeof stripeSubscription?.schedule === "string" + ? stripeSubscription.schedule + : undefined, + products: [], + targetCusProductId: targetCustomerProduct.id, + }) + : undefined; const { stripeCus: stripeCustomer, diff --git a/server/src/internal/billing/v2/setup/setupFullCustomerContext.ts b/server/src/internal/billing/v2/setup/setupFullCustomerContext.ts index 8d53cc639..eb74ffed6 100644 --- a/server/src/internal/billing/v2/setup/setupFullCustomerContext.ts +++ b/server/src/internal/billing/v2/setup/setupFullCustomerContext.ts @@ -1,15 +1,13 @@ -import type { UpdateSubscriptionV0Params } from "@autumn/shared"; +import type { BillingParamsBase } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; import { CusService } from "@server/internal/customers/CusService"; export const setupFullCustomerContext = async ({ ctx, params, - autoCreateCustomer = false, }: { ctx: AutumnContext; - params: UpdateSubscriptionV0Params; - autoCreateCustomer?: boolean; + params: BillingParamsBase; }) => { const { db, org, env } = ctx; const { customer_id: customerId } = params; diff --git a/server/tests/scenarios/attach/basic-attach-scenario.test.ts b/server/tests/scenarios/attach/basic-attach-scenario.test.ts new file mode 100644 index 000000000..4e51f700e --- /dev/null +++ b/server/tests/scenarios/attach/basic-attach-scenario.test.ts @@ -0,0 +1,29 @@ +import { test } from "bun:test"; +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"; + +/** + * Uncancel Tests (cancel_action: "uncancel") + * + * Tests the uncancel functionality which removes a scheduled cancellation + * from a subscription via the update subscription API. + * + * Usage: subscriptions.update({ customer_id, product_id, cancel_action: "uncancel" }) + */ + +test(`${chalk.yellowBright("uncancel: basic - canceling product → uncancel → active")}`, async () => { + const customerId = "attach-basic"; + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ items: [messagesItem] }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [], + }); +}); diff --git a/shared/api/billing/attachV2/attachV0Params.ts b/shared/api/billing/attachV2/attachV0Params.ts index 599b5bafb..49221f236 100644 --- a/shared/api/billing/attachV2/attachV0Params.ts +++ b/shared/api/billing/attachV2/attachV0Params.ts @@ -1,17 +1,14 @@ import { z } from "zod/v4"; import { FeatureOptionsSchema } from "../../../models/cusProductModels/cusProductModels.js"; import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels.js"; -import { CustomerDataSchema } from "../../common/customerData.js"; -import { EntityDataSchema } from "../../common/entityData.js"; +import { BillingParamsBaseSchema } from "../common/billingParamsBase.js"; -export const ExtAttachV0ParamsSchema = z.object({ - // Customer / Entity Info - customer_id: z.string(), +export const RedirectModeSchema = z.enum(["always", "if_required"]); +export type RedirectMode = z.infer; + +export const ExtAttachV0ParamsSchema = BillingParamsBaseSchema.extend({ + // Product identification product_id: z.string(), - entity_id: z.string().nullish(), - - customer_data: CustomerDataSchema.optional(), - entity_data: EntityDataSchema.optional(), // Invoice mode invoice: z.boolean().optional(), @@ -21,6 +18,10 @@ export const ExtAttachV0ParamsSchema = z.object({ // Product config options: z.array(FeatureOptionsSchema).nullish(), version: z.number().optional(), + + // Checkout behavior + redirect_mode: RedirectModeSchema.optional(), + success_url: z.string().optional(), }); export const AttachV0ParamsSchema = ExtAttachV0ParamsSchema.extend({ diff --git a/shared/api/billing/common/billingParamsBase.ts b/shared/api/billing/common/billingParamsBase.ts new file mode 100644 index 000000000..de8a1b17d --- /dev/null +++ b/shared/api/billing/common/billingParamsBase.ts @@ -0,0 +1,12 @@ +import { z } from "zod/v4"; +import { CustomerDataSchema } from "../../common/customerData.js"; +import { EntityDataSchema } from "../../common/entityData.js"; + +export const BillingParamsBaseSchema = z.object({ + customer_id: z.string(), + entity_id: z.string().nullish(), + customer_data: CustomerDataSchema.optional(), + entity_data: EntityDataSchema.optional(), +}); + +export type BillingParamsBase = z.infer; diff --git a/shared/api/billing/common/checkoutMode.ts b/shared/api/billing/common/checkoutMode.ts new file mode 100644 index 000000000..9dc3d5b44 --- /dev/null +++ b/shared/api/billing/common/checkoutMode.ts @@ -0,0 +1,7 @@ +import { z } from "zod/v4"; + +export const CheckoutModeSchema = z + .enum(["stripe_checkout", "autumn_checkout"]) + .nullable(); + +export type CheckoutMode = z.infer; diff --git a/shared/api/billing/common/planTiming.ts b/shared/api/billing/common/planTiming.ts new file mode 100644 index 000000000..1a7f03528 --- /dev/null +++ b/shared/api/billing/common/planTiming.ts @@ -0,0 +1,5 @@ +import { z } from "zod/v4"; + +export const PlanTimingSchema = z.enum(["immediate", "end_of_cycle"]); + +export type PlanTiming = z.infer; diff --git a/shared/api/billing/index.ts b/shared/api/billing/index.ts index f5201e865..a8120b83b 100644 --- a/shared/api/billing/index.ts +++ b/shared/api/billing/index.ts @@ -9,8 +9,11 @@ export * from "./checkout/checkoutParamsV1.js"; export * from "./checkout/prevVersions/checkoutParamsV0.js"; export * from "./checkout/prevVersions/checkoutResponseV0.js"; // Common +export * from "./common/billingParamsBase.js"; export * from "./common/billingPreviewResponse.js"; export * from "./common/billingResponse.js"; +export * from "./common/checkoutMode.js"; +export * from "./common/planTiming.js"; export * from "./common/refundBehavior.js"; export * from "./updateSubscription/previewUpdateSubscriptionResponse.js"; // Update Subscription diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts index 445cefccc..c2449a7c9 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts @@ -4,46 +4,41 @@ import { z } from "zod/v4"; import { FeatureOptionsSchema } from "../../../models/cusProductModels/cusProductModels"; import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels"; import { CancelActionSchema } from "../../common/cancelMode"; -import { CustomerDataSchema } from "../../common/customerData"; -import { EntityDataSchema } from "../../models"; import { BillingBehaviorSchema } from "../common/billingBehavior"; +import { BillingParamsBaseSchema } from "../common/billingParamsBase"; import { RefundBehaviorSchema } from "../common/refundBehavior"; -export const ExtUpdateSubscriptionV0ParamsSchema = z.object({ - // Customer / Entity Info - customer_id: z.string(), - product_id: z.string().nullish(), - entity_id: z.string().nullish(), +export const ExtUpdateSubscriptionV0ParamsSchema = + BillingParamsBaseSchema.extend({ + // Product identification (optional for update subscription - can target by customer_product_id) + product_id: z.string().nullish(), - customer_data: CustomerDataSchema.optional(), - entity_data: EntityDataSchema.optional(), + invoice: z.boolean().optional(), + enable_product_immediately: z.boolean().optional(), + finalize_invoice: z.boolean().optional(), + options: z.array(FeatureOptionsSchema).nullish(), // used for update quantity etc (in api - feature_quantities) - invoice: z.boolean().optional(), - enable_product_immediately: z.boolean().optional(), - finalize_invoice: z.boolean().optional(), - options: z.array(FeatureOptionsSchema).nullish(), // used for update quantity etc (in api - feature_quantities) + // New + version: z.number().optional(), + items: z.array(ProductItemSchema).optional(), // used for custom configuration of a plan (in api - plan_override) + free_trial: CreateFreeTrialSchema.nullable().optional(), - // New - version: z.number().optional(), - items: z.array(ProductItemSchema).optional(), // used for custom configuration of a plan (in api - plan_override) - free_trial: CreateFreeTrialSchema.nullable().optional(), + // Cancel action: 'cancel_immediately' | 'cancel_end_of_cycle' | 'uncancel' + cancel_action: CancelActionSchema.optional(), - // Cancel action: 'cancel_immediately' | 'cancel_end_of_cycle' | 'uncancel' - cancel_action: CancelActionSchema.optional(), + // Billing behavior for subscription updates: + // - 'prorate_immediately' (default): Invoice line items are charged immediately + // - 'next_cycle_only': Do NOT create any charges due to the update + billing_behavior: BillingBehaviorSchema.optional(), - // Billing behavior for subscription updates: - // - 'prorate_immediately' (default): Invoice line items are charged immediately - // - 'next_cycle_only': Do NOT create any charges due to the update - billing_behavior: BillingBehaviorSchema.optional(), + // Refund behavior for negative invoice totals (downgrades): + // - 'grant_invoice_credits' (default): Apply credits to customer balance + // - 'refund_payment_method': Issue refund to payment method + refund_behavior: RefundBehaviorSchema.optional(), - // Refund behavior for negative invoice totals (downgrades): - // - 'grant_invoice_credits' (default): Apply credits to customer balance - // - 'refund_payment_method': Issue refund to payment method - refund_behavior: RefundBehaviorSchema.optional(), - - // reset_billing_cycle_anchor: z.boolean().optional(), - // new_billing_subscription: z.boolean().optional(), -}); + // reset_billing_cycle_anchor: z.boolean().optional(), + // new_billing_subscription: z.boolean().optional(), + }); export const UpdateSubscriptionV0ParamsSchema = ExtUpdateSubscriptionV0ParamsSchema.extend({ From e32a2edd7dbe8934771c432cde33dd0efef8e749 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Fri, 23 Jan 2026 18:56:40 +0000 Subject: [PATCH 002/110] chore: scaffold attach compute layer --- .../computeAttachNewCustomerProduct.ts | 77 +++++++++++++++++++ .../v2/attach/compute/computeAttachPlan.ts | 71 +++++++++++++++++ .../compute/computeAttachTransitionUpdates.ts | 37 +++++++++ .../v2/attach/compute/finalizeAttachPlan.ts | 35 +++++++++ .../billing/v2/attach/handleAttachV2.ts | 24 ++++++ 5 files changed, 244 insertions(+) create mode 100644 server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts create mode 100644 server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts create mode 100644 server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts create mode 100644 server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts b/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts new file mode 100644 index 000000000..52ae983d1 --- /dev/null +++ b/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts @@ -0,0 +1,77 @@ +import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { cusProductToExistingRollovers } from "@/internal/billing/v2/utils/handleExistingRollovers/cusProductToExistingRollovers"; +import { cusProductToExistingUsages } from "@/internal/billing/v2/utils/handleExistingUsages/cusProductToExistingUsages"; +import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct"; +import type { AttachBillingContext } from "../types/attachBillingContext"; + +/** + * Creates the new FullCusProduct to insert when attaching a product. + * + * For upgrades (planTiming === "immediate"): creates an active product + * For downgrades (planTiming === "end_of_cycle"): creates a scheduled product that starts at endOfCycleMs + */ +export const computeAttachNewCustomerProduct = ({ + ctx, + attachBillingContext, +}: { + ctx: AutumnContext; + attachBillingContext: AttachBillingContext; +}): FullCusProduct => { + const { + attachProduct, + fullCustomer, + currentCustomerProduct, + planTiming, + endOfCycleMs, + stripeSubscription, + stripeSubscriptionSchedule, + resetCycleAnchorMs, + currentEpochMs, + featureQuantities, + trialContext, + isCustom, + } = attachBillingContext; + + // Get existing usages/rollovers if transitioning from an existing product + const existingUsages = cusProductToExistingUsages({ + cusProduct: currentCustomerProduct, + entityId: fullCustomer.entity?.id, + }); + + const existingRollovers = cusProductToExistingRollovers({ + cusProduct: currentCustomerProduct, + }); + + ctx.logger.debug( + `[computeAttachNewCustomerProduct] existing usages:`, + existingUsages, + ); + + // Determine if this is a scheduled product (downgrade) + const isScheduled = planTiming === "end_of_cycle"; + + const newFullCustomerProduct = initFullCustomerProduct({ + ctx, + initContext: { + fullCustomer, + fullProduct: attachProduct, + featureQuantities, + existingUsages, + existingRollovers, + resetCycleAnchor: resetCycleAnchorMs, + now: currentEpochMs, + freeTrial: trialContext?.freeTrial ?? null, + trialEndsAt: trialContext?.trialEndsAt ?? undefined, + }, + initOptions: { + isCustom, + subscriptionId: isScheduled ? undefined : stripeSubscription?.id, + subscriptionScheduleId: stripeSubscriptionSchedule?.id, + status: isScheduled ? CusProductStatus.Scheduled : undefined, + startsAt: isScheduled ? endOfCycleMs : undefined, + }, + }); + + return newFullCustomerProduct; +}; diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts b/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts new file mode 100644 index 000000000..c097e4035 --- /dev/null +++ b/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts @@ -0,0 +1,71 @@ +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AttachBillingContext } from "../types/attachBillingContext"; +import { computeAttachNewCustomerProduct } from "./computeAttachNewCustomerProduct"; +import { computeAttachTransitionUpdates } from "./computeAttachTransitionUpdates"; +import { finalizeAttachPlan } from "./finalizeAttachPlan"; + +/** + * Computes the billing plan for attaching a product. + * + * Scenarios: + * - Add-on/One-time (no currentCustomerProduct): Just insert new product + * - First main product (no currentCustomerProduct): Just insert new product + * - Upgrade (currentCustomerProduct exists, planTiming=immediate): Expire current, insert new active + * - Downgrade (currentCustomerProduct exists, planTiming=end_of_cycle): Cancel current at end of cycle, insert new scheduled + */ +export const computeAttachPlan = ({ + ctx, + attachBillingContext, +}: { + ctx: AutumnContext; + attachBillingContext: AttachBillingContext; +}): AutumnBillingPlan => { + const { + currentCustomerProduct, + scheduledCustomerProduct, + planTiming, + customPrices, + customEnts, + trialContext, + } = attachBillingContext; + + const newCustomerProduct = computeAttachNewCustomerProduct({ + ctx, + attachBillingContext, + }); + + const updateCustomerProduct = computeAttachTransitionUpdates({ + attachBillingContext, + }); + + const lineItems = + planTiming === "immediate" + ? buildAutumnLineItems({ + ctx, + newCustomerProducts: [newCustomerProduct], + deletedCustomerProduct: currentCustomerProduct, + billingContext: attachBillingContext, + }) + : []; + + let plan: AutumnBillingPlan = { + insertCustomerProducts: [newCustomerProduct], + updateCustomerProduct, + deleteCustomerProduct: scheduledCustomerProduct, + customPrices, + customEntitlements: customEnts, + customFreeTrial: trialContext?.customFreeTrial, + lineItems, + updateCustomerEntitlements: undefined, + }; + + plan = finalizeAttachPlan({ + ctx, + plan, + attachBillingContext, + }); + + return plan; +}; diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts b/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts new file mode 100644 index 000000000..26fa0b330 --- /dev/null +++ b/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts @@ -0,0 +1,37 @@ +import { CusProductStatus } from "@autumn/shared"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AttachBillingContext } from "../types/attachBillingContext"; + +/** + * Computes the updates to apply to the current customer product during an attach transition. + * + * - Upgrade (immediate): Expire the current product + * - Downgrade (end_of_cycle): Mark as canceling at end of cycle + */ +export const computeAttachTransitionUpdates = ({ + attachBillingContext, +}: { + attachBillingContext: AttachBillingContext; +}): AutumnBillingPlan["updateCustomerProduct"] => { + const { currentCustomerProduct, planTiming, currentEpochMs, endOfCycleMs } = + attachBillingContext; + + if (!currentCustomerProduct) return undefined; + + if (planTiming === "immediate") { + return { + customerProduct: currentCustomerProduct, + updates: { status: CusProductStatus.Expired }, + }; + } + + // Downgrade: mark as canceling at end of cycle + return { + customerProduct: currentCustomerProduct, + updates: { + canceled: true, + canceled_at: currentEpochMs, + ended_at: endOfCycleMs, + }, + }; +}; diff --git a/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts b/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts new file mode 100644 index 000000000..6ff77443b --- /dev/null +++ b/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts @@ -0,0 +1,35 @@ +import { filterUnchangedPricesFromLineItems } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { applyStripeDiscountsToLineItems } from "@/internal/billing/v2/providers/stripe/utils/discounts/applyStripeDiscountsToLineItems"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AttachBillingContext } from "../types/attachBillingContext"; + +/** + * Finalizes the attach billing plan by: + * 1. Filtering out unchanged prices (refund + charge pairs that cancel out) + * 2. Applying Stripe discounts to line items + */ +export const finalizeAttachPlan = ({ + ctx: _ctx, + plan, + attachBillingContext, +}: { + ctx: AutumnContext; + plan: AutumnBillingPlan; + attachBillingContext: AttachBillingContext; +}): AutumnBillingPlan => { + // 1. Filter out unchanged prices (refund + charge pairs that cancel out) + plan.lineItems = filterUnchangedPricesFromLineItems({ + lineItems: plan.lineItems ?? [], + }); + + // 2. Apply Stripe discounts if present + if (attachBillingContext.stripeDiscounts?.length) { + plan.lineItems = applyStripeDiscountsToLineItems({ + lineItems: plan.lineItems ?? [], + discounts: attachBillingContext.stripeDiscounts, + }); + } + + return plan; +}; diff --git a/server/src/internal/billing/v2/attach/handleAttachV2.ts b/server/src/internal/billing/v2/attach/handleAttachV2.ts index c6d25e5f8..011b3f5f6 100644 --- a/server/src/internal/billing/v2/attach/handleAttachV2.ts +++ b/server/src/internal/billing/v2/attach/handleAttachV2.ts @@ -1,5 +1,6 @@ import { AttachV0ParamsSchema } from "@autumn/shared"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; +import { computeAttachPlan } from "./compute/computeAttachPlan"; import { logAttachContext } from "./logs/logAttachContext"; import { setupAttachBillingContext } from "./setup/setupAttachBillingContext"; @@ -33,6 +34,29 @@ export const handleAttachV2 = createRoute({ }); logAttachContext({ ctx, billingContext }); + // 2. Compute + const autumnPlan = computeAttachPlan({ + ctx, + attachBillingContext: billingContext, + }); + + ctx.logger.info("Attach V2 autumn plan:", { + insertCustomerProducts: autumnPlan.insertCustomerProducts.map((p) => ({ + id: p.id, + productId: p.product.id, + status: p.status, + startsAt: p.starts_at, + })), + updateCustomerProduct: autumnPlan.updateCustomerProduct + ? { + id: autumnPlan.updateCustomerProduct.customerProduct.id, + updates: autumnPlan.updateCustomerProduct.updates, + } + : undefined, + deleteCustomerProduct: autumnPlan.deleteCustomerProduct?.id, + lineItemsCount: autumnPlan.lineItems?.length ?? 0, + }); + return c.json({ customer_id: body.customer_id }, 200); }, }); From 45c690821afa43da4b9625aabe4a95ab24908b59 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 26 Jan 2026 18:37:44 +0000 Subject: [PATCH 003/110] resolved merge conflicts from cherry picks --- .../billing/updateSubscription/updateSubscriptionV0Params.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts index c2449a7c9..a54ef33cf 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts @@ -6,7 +6,6 @@ import { ProductItemSchema } from "../../../models/productV2Models/productItemMo import { CancelActionSchema } from "../../common/cancelMode"; import { BillingBehaviorSchema } from "../common/billingBehavior"; import { BillingParamsBaseSchema } from "../common/billingParamsBase"; -import { RefundBehaviorSchema } from "../common/refundBehavior"; export const ExtUpdateSubscriptionV0ParamsSchema = BillingParamsBaseSchema.extend({ @@ -34,7 +33,7 @@ export const ExtUpdateSubscriptionV0ParamsSchema = // Refund behavior for negative invoice totals (downgrades): // - 'grant_invoice_credits' (default): Apply credits to customer balance // - 'refund_payment_method': Issue refund to payment method - refund_behavior: RefundBehaviorSchema.optional(), + // refund_behavior: RefundBehaviorSchema.optional(), // reset_billing_cycle_anchor: z.boolean().optional(), // new_billing_subscription: z.boolean().optional(), From 42752336916706221585062453a73edcf67d2198 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 28 Jan 2026 15:36:39 +0000 Subject: [PATCH 004/110] wip --- server/src/external/autumn/autumnCli.ts | 39 +++ .../integration/billing/attach/add-ons.md | 259 ++++++++++++++ .../integration/billing/attach/attachTests.md | 164 +++++++++ .../integration/billing/attach/checkout.md | 208 +++++++++++ .../integration/billing/attach/errors.md | 290 +++++++++++++++ .../billing/attach/future-plans.md | 329 ++++++++++++++++++ .../billing/attach/immediate-switch.md | 118 +++++++ .../integration/billing/attach/new-plan.md | 51 +++ .../attach/new-plan/attach-free.test.ts | 143 ++++++++ .../billing/attach/scheduled-switch.md | 120 +++++++ .../integration/billing/attach/trials.md | 312 +++++++++++++++++ server/tests/utils/fixtures/products.ts | 63 ++++ .../tests/utils/testInitUtils/initScenario.ts | 13 +- 13 files changed, 2108 insertions(+), 1 deletion(-) create mode 100644 server/tests/integration/billing/attach/add-ons.md create mode 100644 server/tests/integration/billing/attach/attachTests.md create mode 100644 server/tests/integration/billing/attach/checkout.md create mode 100644 server/tests/integration/billing/attach/errors.md create mode 100644 server/tests/integration/billing/attach/future-plans.md create mode 100644 server/tests/integration/billing/attach/immediate-switch.md create mode 100644 server/tests/integration/billing/attach/new-plan.md create mode 100644 server/tests/integration/billing/attach/new-plan/attach-free.test.ts create mode 100644 server/tests/integration/billing/attach/scheduled-switch.md create mode 100644 server/tests/integration/billing/attach/trials.md diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index e6b72a038..c9e90d355 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -795,4 +795,43 @@ export class AutumnInt { return data; }, }; + + billing = { + attach: async ( + params: AttachBodyV0, + { + skipWebhooks, + idempotencyKey, + timeout, + }: { + skipWebhooks?: boolean; + idempotencyKey?: string; + timeout?: number; + } = {}, + ) => { + const headers: Record = {}; + if (skipWebhooks !== undefined) { + headers["x-skip-webhooks"] = skipWebhooks ? "true" : "false"; + } + if (idempotencyKey !== undefined) { + headers["idempotency-key"] = idempotencyKey; + } + + const data = await this.post( + `/billing/attach`, + params, + Object.keys(headers).length > 0 ? headers : undefined, + ); + + if (timeout) { + await new Promise((resolve) => setTimeout(resolve, timeout)); + } + return data; + }, + + previewAttach: async (params: AttachBodyV0) => { + const data = await this.post(`/billing/attach/preview`, params); + return data; + }, + }; } diff --git a/server/tests/integration/billing/attach/add-ons.md b/server/tests/integration/billing/attach/add-ons.md new file mode 100644 index 000000000..983ab1261 --- /dev/null +++ b/server/tests/integration/billing/attach/add-ons.md @@ -0,0 +1,259 @@ +# Add-Ons Test Plan + +Tests for supplementary products attached alongside base products. + +--- + +## File Structure + +| File | Test Count | Description | +|------|------------|-------------| +| `addons-basic.test.ts` | 6 | Basic add-on attachment and behavior | +| `addons-cancel.test.ts` | 6 | Canceling add-ons (immediately, end-of-cycle) | +| `addons-upgrade.test.ts` | 4 | Upgrading/changing add-on products | +| `addons-entities.test.ts` | 5 | Entity-scoped add-ons | +| `addons-discounts.test.ts` | 4 | Discounts applied to add-ons | + +**Total: 25 tests** + +--- + +## Critical Rule + +**Without `isAddOn: true`, second product REPLACES the first:** + +```typescript +// ❌ BAD - Second attach replaces first product +const prod1 = constructProduct({ type: "pro", id: "prod1", items: [...] }); +const prod2 = constructProduct({ type: "pro", id: "prod2", items: [...] }); + +// ✅ GOOD - Second product is an add-on +const prod1 = constructProduct({ type: "pro", id: "prod1", items: [...] }); +const prod2 = constructProduct({ type: "pro", id: "prod2", isAddOn: true, items: [...] }); +``` + +--- + +## Test Details + +### `addons-basic.test.ts` (6 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | addon: attach free add-on | Pro product + free add-on | Both products exist, features combined | +| 2 | addon: attach paid add-on | Pro product + paid add-on ($20/mo) | Both products active, subscription has both items | +| 3 | addon: attach prepaid add-on | Base product + prepaid credits add-on | Credits added, both products on customer | +| 4 | addon: attach one-time add-on | Pro product + one-off credits | One-off not recurring, credits granted | +| 5 | addon: attach multiple add-ons | Pro + addon1 + addon2 | 3 products on customer | +| 6 | addon: replace add-on with new version | Attach addon v1, then addon v2 (same ID) | Only v2 on customer | + +**Setup:** +```typescript +const pro = products.pro({ id: "pro", items: [messagesItem] }); +const addon = products.base({ + id: "addon", + isAddOn: true, + items: [items.monthlyCredits({ includedUsage: 50 })], +}); + +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [pro, addon] }), + ], + actions: [ + s.attach({ productId: pro.id }), + s.attach({ productId: addon.id }), + ], +}); + +const customer = await autumnV1.customers.get(customerId); +expect(customer.products.length).toBe(2); +expectProductAttached({ customer, product: pro }); +expectProductAttached({ customer, product: addon }); +``` + +--- + +### `addons-cancel.test.ts` (6 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | cancel-addon: immediately - basic | Cancel add-on immediately | Add-on removed, base product remains | +| 2 | cancel-addon: immediately - with refund | Cancel paid add-on mid-cycle | Refund invoice created for unused time | +| 3 | cancel-addon: end-of-cycle | Schedule add-on cancellation | Add-on canceling, removed after cycle | +| 4 | cancel-addon: main product remains | Cancel add-on, verify base unaffected | Base product unchanged | +| 5 | cancel-addon: cancel both | Cancel main + add-on | Both products removed | +| 6 | cancel-addon: uncancel add-on | Cancel then uncancel add-on | Add-on restored | + +**Cancel Pattern:** +```typescript +// Cancel add-on immediately +await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: addon.id, + cancel_action: "cancel_immediately", +}); + +// Verify +const customer = await autumnV1.customers.get(customerId); +expectProductNotPresent({ customer, productId: addon.id }); +expectProductActive({ customer, productId: pro.id }); // Base still active + +// End-of-cycle cancel +await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: addon.id, + cancel_action: "cancel_end_of_cycle", +}); + +expectProductCanceling({ customer, productId: addon.id }); +``` + +--- + +### `addons-upgrade.test.ts` (4 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | upgrade-addon: free to paid | Free add-on → paid add-on | Payment charged, add-on upgraded | +| 2 | upgrade-addon: paid to paid | $10 add-on → $20 add-on | Proration applied | +| 3 | upgrade-addon: update quantity | Prepaid add-on quantity change | New quantity reflected | +| 4 | upgrade-addon: main upgrade preserves addon | Pro → Premium (main upgrade) | Add-on still attached | + +**Upgrade Pattern:** +```typescript +const addonBasic = products.base({ + id: "addon-basic", + isAddOn: true, + items: [items.monthlyPrice({ unitAmount: 10_00 })], +}); + +const addonPremium = products.base({ + id: "addon-premium", + isAddOn: true, + items: [items.monthlyPrice({ unitAmount: 20_00 })], +}); + +// Upgrade add-on +await autumnV1.attach({ + customer_id: customerId, + product_id: addonPremium.id, +}); + +// Verify only new addon +const customer = await autumnV1.customers.get(customerId); +expectProductAttached({ customer, product: addonPremium }); +expectProductNotPresent({ customer, productId: addonBasic.id }); +``` + +--- + +### `addons-entities.test.ts` (5 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | entity-addon: different entities different addons | Entity1 has addon A, Entity2 has addon B | Each entity has own add-on | +| 2 | entity-addon: shared addon across entities | Same add-on on multiple entities | Each entity charged separately | +| 3 | entity-addon: update addon on one entity | Change add-on quantity on Entity1 | Entity2 unchanged | +| 4 | entity-addon: cancel addon on one entity | Cancel add-on on Entity1 | Entity2 still has add-on | +| 5 | entity-addon: entity inherits customer addon | Customer-level add-on | All entities have access | + +**Entity Pattern:** +```typescript +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [pro, addon] }), + s.entities({ ids: ["entity-1", "entity-2"] }), + ], + actions: [ + s.attach({ productId: pro.id, entityId: "entity-1" }), + s.attach({ productId: addon.id, entityId: "entity-1" }), + s.attach({ productId: pro.id, entityId: "entity-2" }), + // Entity-2 does NOT have add-on + ], +}); + +// Verify entity-1 has both products +const entity1 = await autumnV1.entities.get(customerId, "entity-1"); +expect(entity1.products.length).toBe(2); + +// Verify entity-2 only has pro +const entity2 = await autumnV1.entities.get(customerId, "entity-2"); +expect(entity2.products.length).toBe(1); +``` + +--- + +### `addons-discounts.test.ts` (4 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | addon-discount: discount on add-on | 20% discount on add-on only | Add-on price reduced, main unchanged | +| 2 | addon-discount: customer discount applies | Customer has 10% discount | Applies to both main + add-on | +| 3 | addon-discount: separate subscription own discount | Add-on on separate subscription | Own discount scope | +| 4 | addon-discount: main discount doesn't affect isolated addon | Main product discount | Isolated add-on not affected | + +**Discount Pattern:** +```typescript +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [pro, addon] }), + s.reward({ discountPercent: 20 }), // Customer-level discount + ], + actions: [ + s.attach({ productId: pro.id }), + s.attach({ productId: addon.id }), + ], +}); + +// Both products should have discount applied +await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, +}); +``` + +--- + +## Add-On Product Types + +| Type | Example | Notes | +|------|---------|-------| +| Free Add-On | Credits, features | No base price | +| Recurring Add-On | $20/mo support tier | Monthly charge | +| Prepaid Add-On | Credit packs | Billing units | +| One-Time Add-On | One-off credits | Single purchase | +| Usage Add-On | Metered features | Overage pricing | + +--- + +## Key Utilities + +**Expectation Helpers:** +```typescript +expectProductAttached({ customer, product: addon }); +expectProductActive({ customer, productId: addon.id }); +expectProductCanceling({ customer, productId: addon.id }); +expectProductNotPresent({ customer, productId: addon.id }); +expectCustomerProducts({ + customer, + active: [pro.id], + canceling: [addon.id], + notPresent: [oldAddon.id], +}); +``` + +**Subscription Verification:** +```typescript +await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, +}); +``` diff --git a/server/tests/integration/billing/attach/attachTests.md b/server/tests/integration/billing/attach/attachTests.md new file mode 100644 index 000000000..f8044f42f --- /dev/null +++ b/server/tests/integration/billing/attach/attachTests.md @@ -0,0 +1,164 @@ +# Attach V2 Test Guide + +## Key Gotchas + +1. **Always use `product.id`, never string literals** + ```typescript + // ✅ GOOD + s.attach({ productId: pro.id }) + + // ❌ BAD + s.attach({ productId: "pro" }) + ``` + +2. **Multiple products need unique IDs** + - Without `isAddOn: true`, second product **replaces** the first + ```typescript + const prod1 = constructProduct({ type: "free", id: "prod1", items: [...] }); + const prod2 = constructProduct({ type: "free", id: "prod2", isAddOn: true, items: [...] }); + ``` + +3. **Payment method required for paid features** + ```typescript + s.customer({ paymentMethod: "success" }) // Required for overage, per-seat, usage-based, base price + ``` + +4. **Wait 2000ms after `track` before `attach`** + ```typescript + await autumnV1.track({ ... }); + await new Promise(r => setTimeout(r, 2000)); // track syncs to Postgres async + await autumnV1.attach({ ... }); + ``` + +5. **Prepaid items require `options` with `quantity` on attach** + ```typescript + s.attach({ + productId: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 200 }] + }) + ``` + +6. **Use `products.base()` for free products** (no base price) + - `products.pro()` already includes $20/mo base price — don't add `monthlyPrice()` + +7. **Lifetime interval: `null` vs `"one_off"`** + - Constructing: use `null` → `constructFeatureItem({ interval: null })` + - In API responses: use `ResetInterval.OneOff` + +8. **Canceling/Downgrading is NOT a status** + - Use `expectProductCanceling` helper, not `expect(product.status).toBe("canceling")` + +9. **Server logs not visible in tests** + - Console logs in server code don't appear in test output + +10. **Always verify subscription state when billing is involved** + - Anytime prices are involved (base price, prepaid, allocated, etc.), use `expectSubToBeCorrect` to verify subscription state + ```typescript + await expectSubToBeCorrect({ + db: ctx.db, + customerId, + org: ctx.org, + env: ctx.env, + entityId?: string, // For entity-level subscription + }); + ``` + +11. **Always call attach preview before attach to verify `preview.total`** + - The preview endpoint validates pricing before the actual attach + ```typescript + const preview = await autumn.attachPreview({ + customer_id: customerId, + product_id: productId, + entity_id: entityId, // Optional for entity-level + }); + expect(preview.total).toBe(expectedTotal); + + // Then perform the actual attach + await autumn.attach({ ... }); + ``` + +12. **Scheduled-switch tests must advance test clock with `advanceToNextInvoice()`** + - After scheduling a downgrade, advance the test clock to verify: + - A. Next cycle invoice is correct + - B. Products on customer are correct after cycle + ```typescript + // Schedule the downgrade + await s.attach({ productId: basic.id }); // Schedules switch to basic at end of cycle + + // Advance to next billing cycle + await advanceToNextInvoice({ + stripeCli: ctx.stripeCli, + testClockId: ctx.testClockId, + currentEpochMs, // Use return value for consecutive advances + }); + + // Verify invoice and customer state + const customer = await autumnV1.customers.get(customerId); + expect(customer.products[0].id).toBe(basic.id); + ``` + +--- + +## AutumnInt Generic Types + +Always use generic type parameters for proper type safety: + +- `autumnV1.customers.get()` +- `autumnV1.entities.get()` +- `autumnV2.customers.get()` +- `autumnV2.entities.get()` + +--- + +## Folder Structure + +| Folder | Description | +|--------|-------------| +| `new-plan/` | Attach when customer has no existing product | +| `immediate-switch/` | Upgrades (immediate) | +| `scheduled-switch/` | Downgrades (scheduled for end of cycle) | +| `checkout/` | Payment collection flows | +| `add-ons/` | Supplementary products | +| `trials/` | Free trial logic | +| `carry-over-usage/` | (TBD) | +| `groups/` | (TBD) | +| `errors/` | Validation and payment errors | +| `misc/` | Edge cases | + +--- + +## Test File Guides + +### Planned (Ready for Implementation) +- [new-plan.md](./new-plan.md) — Tests for attaching products when customer has no existing product (20 tests) +- [immediate-switch.md](./immediate-switch.md) — Tests for upgrades/immediate effect (34 tests) +- [scheduled-switch.md](./scheduled-switch.md) — Tests for downgrades/scheduled for end of cycle (32 tests) +- [checkout.md](./checkout.md) — Payment collection flows (13 tests in stripe-checkout, more TBD) +- [add-ons.md](./add-ons.md) — Supplementary products (25 tests) +- [trials.md](./trials.md) — Free trial logic (27 tests) +- [errors.md](./errors.md) — Validation and payment errors (33 tests) + +### Needs Planning +- [future-plans.md](./future-plans.md) — Planning prompts for remaining folders: + - `carry-existing-usages/` — Usage carryover on upgrade/downgrade + - `invoice/` — `invoice: true` mode + - `new-billing-subscription/` — Force new Stripe subscription + - `billing-behavior/` — Proration control + - `plan-schedule/` — Override upgrade/downgrade timing + +--- + +## Test Count Summary + +| Category | Tests | +|----------|-------| +| new-plan | 20 | +| immediate-switch | 34 | +| scheduled-switch | 32 | +| checkout (stripe-checkout) | 13 | +| checkout (mode-decision) | TBD | +| checkout (autumn-checkout) | TBD | +| add-ons | 25 | +| trials | 27 | +| errors | 33 | +| **Total** | **184+** | diff --git a/server/tests/integration/billing/attach/checkout.md b/server/tests/integration/billing/attach/checkout.md new file mode 100644 index 000000000..0027ff37e --- /dev/null +++ b/server/tests/integration/billing/attach/checkout.md @@ -0,0 +1,208 @@ +# Checkout Test Plan + +Tests for payment collection flows during attach operations. + +--- + +## Folder Structure + +``` +checkout/ +├── checkout-mode-decision/ # Tests for computeCheckoutMode logic (TBD) +│ └── (ad-hoc tests once we finalize the checkout mode decision logic) +│ +├── stripe-checkout/ # When checkoutMode = "stripe_checkout" +│ ├── stripe-checkout-basic.test.ts +│ ├── stripe-checkout-entities.test.ts +│ ├── stripe-checkout-one-off.test.ts +│ ├── stripe-checkout-prepaid.test.ts +│ ├── stripe-checkout-trial.test.ts +│ └── stripe-checkout-promo.test.ts +│ +└── autumn-checkout/ # When checkoutMode = "autumn_checkout" (future) + └── (future work per ENG-1013) +``` + +**Note:** `invoice/` mode tests are in a top-level folder at `/attach/invoice/` (same pattern as `update-subscription/invoice/`). + +--- + +## Checkout Mode Decision (TBD) + +The `computeCheckoutMode` function determines which checkout flow to use. Once we finalize the logic, we'll add comprehensive tests for each branch. + +**Current understanding from ENG-1013:** +```typescript +type CheckoutMode = + | "stripe_checkout" // No payment method → Stripe Checkout session + | "autumn_checkout" // Has payment method + redirect_mode: "always" → Autumn confirmation page + | null; // Has payment method, no redirect → direct billing +``` + +**Key variables to consider:** +- `hasPaymentMethod` — Does customer have a PM on file? +- `redirect_mode` — `"when_required"` (default) | `"always"` +- `needsSubscriptionUpdate` — Is there an existing subscription to modify? + +**Known constraint:** Stripe Checkout can only handle new subscriptions, not updates. If no PM + update needed → Error. + +--- + +## Stripe Checkout Tests + +**Prerequisite for all tests:** Customer has NO payment method → triggers `stripe_checkout` mode + +### `stripe-checkout-basic.test.ts` (3 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: no product → pro | New customer, no PM, attach pro | `checkout_url` returned, product attached after completion | +| 2 | stripe-checkout: free → pro | Customer on free product, no PM, attach pro | `checkout_url` returned, pro replaces free after completion | +| 3 | stripe-checkout: multi-interval product | No PM, attach product with monthly + annual prices | Checkout handles multi-interval, product attached after completion | + +**Setup:** +```typescript +const pro = products.pro({ id: "pro", items: [messagesItem] }); + +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true }), // No payment method + s.products({ list: [pro] }), + ], + actions: [], +}); + +const result = await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, +}); + +expect(result.checkout_url).toBeDefined(); +await completeCheckoutForm(result.checkout_url); + +const customer = await autumnV1.customers.get(customerId); +expectProductAttached({ customer, product: pro }); +``` + +--- + +### `stripe-checkout-entities.test.ts` (2 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: entity attach | Customer has no PM, attach to entity-1 | `checkout_url` returned, entity gets product after completion | +| 2 | stripe-checkout: second entity | Entity-1 has product via direct billing, entity-2 needs checkout (no PM) | Entity-2 gets its own checkout flow | + +**Setup:** +```typescript +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true }), // No PM + s.products({ list: [pro] }), + s.entities({ ids: ["entity-1"] }), + ], + actions: [], +}); + +const result = await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: "entity-1", +}); + +expect(result.checkout_url).toBeDefined(); +``` + +--- + +### `stripe-checkout-one-off.test.ts` (2 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: one-off credits | No PM, attach one-off credits product | Checkout `mode: "payment"` (not subscription), credits granted after | +| 2 | stripe-checkout: one-off with quantity | One-off with `options: [{ quantity: 5 }]` | Quantity reflected in checkout line items | + +**Key difference:** One-off products use Stripe Checkout in `mode: "payment"`, not `mode: "subscription"`. + +--- + +### `stripe-checkout-prepaid.test.ts` (2 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: prepaid quantity | No PM, attach prepaid with `options: [{ feature_id, quantity: 200 }]` | Checkout includes prepaid line item with quantity | +| 2 | stripe-checkout: prepaid on free product | Customer has free, attach prepaid pack (no PM) | Checkout for prepaid, free product remains | + +--- + +### `stripe-checkout-trial.test.ts` (2 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: trial card required | Product with `trialDays: 7, cardRequired: true`, no PM | Checkout captures card, trial starts after completion | +| 2 | stripe-checkout: trial subscription_data | Verify checkout has correct `trial_end` and `trial_settings` | `subscription_data.trial_end` set correctly | + +**Note:** If `cardRequired: false`, the product might not need checkout at all (no payment collected). TBD on exact behavior. + +--- + +### `stripe-checkout-promo.test.ts` (2 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | stripe-checkout: allow_promotion_codes | Verify Stripe checkout has `allow_promotion_codes: true` | Customer can enter promo code in checkout UI | +| 2 | stripe-checkout: reward applied | Attach with reward/coupon pre-applied | Discount reflected in first invoice after checkout | + +--- + +## Stripe Checkout Test Count + +| File | Tests | +|------|-------| +| stripe-checkout-basic.test.ts | 3 | +| stripe-checkout-entities.test.ts | 2 | +| stripe-checkout-one-off.test.ts | 2 | +| stripe-checkout-prepaid.test.ts | 2 | +| stripe-checkout-trial.test.ts | 2 | +| stripe-checkout-promo.test.ts | 2 | +| **Total** | **13** | + +--- + +## Key Utilities + +**Checkout Completion:** +```typescript +import { completeCheckoutForm } from "@tests/utils/puppeteer/completeCheckoutForm"; +import { completeInvoiceCheckout } from "@tests/utils/puppeteer/completeInvoiceCheckout"; +import { completeInvoiceConfirmation } from "@tests/utils/puppeteer/completeInvoiceConfirmation"; + +// Stripe checkout session (new PM) +await completeCheckoutForm(checkout_url); + +// Invoice payment page (with existing PM) +await completeInvoiceCheckout({ url: payment_url }); + +// 3DS authentication flow +await completeInvoiceConfirmation({ url: payment_url }); +``` + +**Payment Method Setup:** +```typescript +// In initScenario: +s.customer({ paymentMethod: "success" }) // Valid card +s.customer({ paymentMethod: "fail" }) // Card that declines +s.customer({ paymentMethod: "authenticate" }) // Card requiring 3DS + +// Actions: +s.attachPaymentMethod({ type: "success" | "fail" | "authenticate" }) +s.removePaymentMethod() +``` + +--- + +## Future Work + +- **`checkout-mode-decision/`** — Once `computeCheckoutMode` logic is finalized, add tests for each decision branch +- **`autumn-checkout/`** — Tests for `redirect_mode: "always"` with payment method (Autumn confirmation page) +- **`invoice/`** — Top-level folder for `invoice: true` mode (see `update-subscription/invoice/` for patterns) diff --git a/server/tests/integration/billing/attach/errors.md b/server/tests/integration/billing/attach/errors.md new file mode 100644 index 000000000..73d4aa8a2 --- /dev/null +++ b/server/tests/integration/billing/attach/errors.md @@ -0,0 +1,290 @@ +# Errors Test Plan + +Tests for validation errors, payment failures, and edge cases in attach operations. + +--- + +## File Structure + +| File | Test Count | Description | +|------|------------|-------------| +| `errors-validation.test.ts` | 8 | Missing/invalid parameters | +| `errors-payment.test.ts` | 5 | Payment failures and recovery | +| `errors-product.test.ts` | 6 | Product-related errors | +| `errors-options.test.ts` | 5 | Invalid feature options | +| `errors-transition.test.ts` | 6 | Invalid product transitions | +| `errors-idempotency.test.ts` | 3 | Duplicate requests | + +**Total: 33 tests** + +--- + +## Common Error Codes + +```typescript +import { ErrCode, AttachErrCode } from "@autumn/shared"; + +// General errors +ErrCode.InvalidRequest // General validation error +ErrCode.InvalidOptions // Missing/invalid options +ErrCode.NotFound // Resource not found +ErrCode.ProductNotFound // Product/version not found +ErrCode.CustomerNotFound // Customer not found +ErrCode.DuplicateIdempotencyKey // Duplicate request + +// Attach-specific errors +AttachErrCode.ProductAlreadyAttached // Already has this product +``` + +--- + +## Test Details + +### `errors-validation.test.ts` (8 tests) + +| # | Test Name | Scenario | Expected Error | +|---|-----------|----------|----------------| +| 1 | error: missing customer_id | Attach without customer_id | `ErrCode.InvalidRequest` | +| 2 | error: missing product_id | Attach without product_id | `ErrCode.InvalidRequest` | +| 3 | error: invalid customer_id | Non-existent customer | `ErrCode.CustomerNotFound` | +| 4 | error: invalid product_id | Non-existent product | `ErrCode.ProductNotFound` | +| 5 | error: invalid entity_id | Non-existent entity | `ErrCode.NotFound` | +| 6 | error: invalid version | Product version doesn't exist | `ErrCode.ProductNotFound` | +| 7 | error: negative version | `version: -1` | `ErrCode.InvalidRequest` | +| 8 | error: empty options array | `options: []` when required | `ErrCode.InvalidOptions` | + +**Pattern:** +```typescript +import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils"; + +await expectAutumnError({ + errCode: ErrCode.CustomerNotFound, + func: async () => { + await autumnV1.attach({ + customer_id: "non-existent-customer", + product_id: pro.id, + }); + }, +}); +``` + +--- + +### `errors-payment.test.ts` (5 tests) + +| # | Test Name | Scenario | Expected Behavior | +|---|-----------|----------|-------------------| +| 1 | error: no payment method | Paid product, no PM | `checkout_url` returned or error | +| 2 | error: payment declined | PM set to fail | `checkout_url` returned, product not upgraded | +| 3 | error: 3ds required | PM requires 3DS | `required_action.code: "3ds_required"` | +| 4 | error: payment fails mid-upgrade | Upgrade with failed PM | Customer stays on old product | +| 5 | error: insufficient funds | Card declined for amount | `checkout_url` for retry | + +**Pattern:** +```typescript +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "fail" }), + s.products({ list: [pro] }), + ], +}); + +const result = await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, +}); + +// Payment failed, checkout URL provided +expect(result.checkout_url).toBeDefined(); +expect(result.code).toBe(SuccessCode.InvoiceActionRequired); +``` + +--- + +### `errors-product.test.ts` (6 tests) + +| # | Test Name | Scenario | Expected Error | +|---|-----------|----------|----------------| +| 1 | error: product already attached | Attach same product twice | `AttachErrCode.ProductAlreadyAttached` | +| 2 | error: attach same options | Prepaid with identical options | `AttachErrCode.ProductAlreadyAttached` | +| 3 | error: archived product | Attach archived product | `ErrCode.ProductNotFound` | +| 4 | error: draft product | Attach draft/unpublished product | `ErrCode.ProductNotFound` | +| 5 | error: entity product without entity_id | Entity-scoped product, no entity_id | `ErrCode.InvalidRequest` | +| 6 | error: non-entity product with entity_id | Customer product with entity_id | `ErrCode.InvalidRequest` | + +**Pattern:** +```typescript +// Already attached +await autumnV1.attach({ customer_id: customerId, product_id: pro.id }); + +await expectAutumnError({ + errCode: AttachErrCode.ProductAlreadyAttached, + func: async () => { + await autumnV1.attach({ customer_id: customerId, product_id: pro.id }); + }, +}); +``` + +--- + +### `errors-options.test.ts` (5 tests) + +| # | Test Name | Scenario | Expected Error | +|---|-----------|----------|----------------| +| 1 | error: prepaid missing quantity | Prepaid product, no options | `ErrCode.InvalidOptions` | +| 2 | error: invalid feature_id in options | Non-existent feature_id | `ErrCode.InvalidOptions` | +| 3 | error: negative quantity | `quantity: -10` | `ErrCode.InvalidOptions` | +| 4 | error: zero quantity | `quantity: 0` | `ErrCode.InvalidOptions` | +| 5 | error: options for non-prepaid | Options on usage-based product | `ErrCode.InvalidOptions` | + +**Pattern:** +```typescript +const prepaid = products.pro({ + id: "prepaid", + items: [items.prepaidCredits({ billingUnits: 100, pricePerUnit: 10_00 })], +}); + +// Missing options +await expectAutumnError({ + errCode: ErrCode.InvalidOptions, + errMessage: "missing options", + func: async () => { + await autumnV1.attach({ + customer_id: customerId, + product_id: prepaid.id, + // No options provided! + }); + }, +}); + +// Negative quantity +await expectAutumnError({ + errCode: ErrCode.InvalidOptions, + func: async () => { + await autumnV1.attach({ + customer_id: customerId, + product_id: prepaid.id, + options: [{ feature_id: TestFeature.Credits, quantity: -10 }], + }); + }, +}); +``` + +--- + +### `errors-transition.test.ts` (6 tests) + +| # | Test Name | Scenario | Expected Error/Behavior | +|---|-----------|----------|-------------------------| +| 1 | error: recurring to one-off | Pro (recurring) → one-off credits | `ErrCode.InvalidRequest` | +| 2 | error: one-off to recurring | One-off → Pro (recurring) | `ErrCode.InvalidRequest` | +| 3 | error: paid recurring to one-off | Paid monthly → one-off | `ErrCode.InvalidRequest` | +| 4 | error: downgrade with invoice mode | Downgrade with `invoice: true` | `ErrCode.InvalidRequest` | +| 5 | error: free to paid next_cycle_only | Free → paid with `next_cycle_only: true` | `ErrCode.InvalidRequest` | +| 6 | error: remove trial with next_cycle_only | Remove trial + `next_cycle_only: true` | `ErrCode.InvalidRequest` | + +**Pattern:** +```typescript +const recurring = products.pro({ id: "recurring", items: [...] }); +const oneOff = products.oneOff({ id: "one-off", items: [...] }); + +// Attach recurring first +await autumnV1.attach({ customer_id: customerId, product_id: recurring.id }); + +// Try to switch to one-off +await expectAutumnError({ + errCode: ErrCode.InvalidRequest, + errMessage: "Cannot transition from recurring to one-off", + func: async () => { + await autumnV1.attach({ + customer_id: customerId, + product_id: oneOff.id, + }); + }, +}); +``` + +--- + +### `errors-idempotency.test.ts` (3 tests) + +| # | Test Name | Scenario | Expected Behavior | +|---|-----------|----------|-------------------| +| 1 | idempotency: same key same request | Duplicate request with same key | Returns cached response (200) | +| 2 | idempotency: same key different request | Same key, different params | `ErrCode.DuplicateIdempotencyKey` (409) | +| 3 | idempotency: concurrent requests | Two requests same key simultaneously | One succeeds, one returns 409 | + +**Pattern:** +```typescript +const idempotencyKey = `attach-${Date.now()}`; + +// First request +const result1 = await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, + idempotency_key: idempotencyKey, +}); + +// Same request, same key - returns cached +const result2 = await autumnV1.attach({ + customer_id: customerId, + product_id: pro.id, + idempotency_key: idempotencyKey, +}); + +expect(result1).toEqual(result2); + +// Different request, same key - error +await expectAutumnError({ + errCode: ErrCode.DuplicateIdempotencyKey, + func: async () => { + await autumnV1.attach({ + customer_id: customerId, + product_id: premium.id, // Different product! + idempotency_key: idempotencyKey, + }); + }, +}); +``` + +--- + +## Key Utilities + +**Error Expectation:** +```typescript +import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils"; + +await expectAutumnError({ + errCode: ErrCode.InvalidRequest, // Optional: specific error code + errMessage: "substring to match", // Optional: error message substring + func: async () => { + // Code that should throw + }, +}); +``` + +**Payment Method Setup for Error Testing:** +```typescript +// In initScenario setup: +s.customer({ paymentMethod: "fail" }) // Will decline +s.customer({ paymentMethod: "authenticate" }) // Requires 3DS + +// In actions: +s.attachPaymentMethod({ type: "fail" }) +s.removePaymentMethod() +``` + +**Verify Customer State Unchanged:** +```typescript +// Before error +const before = await autumnV1.customers.get(customerId); + +// Trigger error +await expectAutumnError({ ... }); + +// After error - state unchanged +const after = await autumnV1.customers.get(customerId); +expect(after.products).toEqual(before.products); +expect(after.entitlements).toEqual(before.entitlements); +``` diff --git a/server/tests/integration/billing/attach/future-plans.md b/server/tests/integration/billing/attach/future-plans.md new file mode 100644 index 000000000..a04c6d765 --- /dev/null +++ b/server/tests/integration/billing/attach/future-plans.md @@ -0,0 +1,329 @@ +# Future Test Plans & Implementation Prompts + +This file contains: +1. **Implementation Prompt Template** — For writing tests from a completed plan +2. **Planning Prompt Template** — For planning tests for folders that need it +3. **Folders Needing Planning** — List of folders with key questions + +--- + +## Implementation Prompt Template + +Use this prompt to start writing tests from a **completed test plan** (e.g., `new-plan.md`, `immediate-switch.md`): + +``` +I need to implement tests for the Attach V2 `{FOLDER_NAME}` folder. + +## Required Reading (Read ALL of these first) + +### 1. Test Writing Skill (CRITICAL - read the entire folder) +Read all files in `/.claude/skills/write-test/`: +- `SKILL.md` — Main skill file +- `references/SCENARIO.md` — How to use initScenario +- `references/FIXTURES.md` — Product fixtures (products.pro, items.monthlyCredits, etc.) +- `references/EXPECTATIONS.md` — Expectation helpers +- `references/GOTCHAS.md` — Common pitfalls +- `references/ENTITIES.md` — Entity-level testing +- `references/STRIPE-BEHAVIORS.md` — Stripe-specific behaviors +- `references/WEBHOOKS.md` — Webhook testing +- `references/TRACK-CHECK.md` — Track/check endpoint testing + +### 2. Attach V2 Test Guide +Read `/server/tests/integration/billing/attach/attachTests.md` for: +- 12 key gotchas specific to attach tests +- AutumnInt generic types +- Folder structure + +### 3. The Test Plan for This Folder +Read `/server/tests/integration/billing/attach/{FOLDER_NAME}.md` for: +- File structure and test cases to implement +- Setup patterns and key assertions + +### 4. Reference: New Testing Style +Look at `/server/tests/integration/billing/update-subscription/` for the NEW testing style: +- How tests are organized into folders/files +- How `initScenario` is used +- How expectations are structured + +Specifically look at a few example files: +- `cancel/end-of-cycle/cancel-end-of-cycle.test.ts` +- `custom-plan/update-paid-basic.test.ts` +- `errors/update-errors-basic.test.ts` + +## Your Task + +1. **Create the folder structure** at `/server/tests/integration/billing/attach/{FOLDER_NAME}/` + +2. **Implement test files** one at a time, following: + - The test cases specified in the plan + - The `initScenario` pattern from the write-test skill + - The NEW testing style from update-subscription/ + +3. **For each test file:** + - Use `initScenario` for setup (NOT the old `beforeAll` pattern) + - Use product fixtures from `@tests/utils/fixtures/products.ts` + - Use expectation helpers from `@tests/utils/expectUtils/` + - Follow the 12 gotchas in `attachTests.md` + +4. **Run tests after each file** to verify they pass: + ```bash + bun test server/tests/integration/billing/attach/{FOLDER_NAME}/{FILE_NAME}.test.ts + ``` + +## Important Notes + +- Use `initScenario` — this is the NEW pattern, not `beforeAll` + manual setup +- Always use `product.id`, never string literals +- Payment method required for paid features: `s.customer({ paymentMethod: "success" })` +- Prepaid requires `options` on attach +- Use `expectSubToBeCorrect` when billing is involved +- Server logs are NOT visible in test output — ask user to paste logs if needed +``` + +--- + +## Example: Implementing `new-plan/` Tests + +``` +I need to implement tests for the Attach V2 `new-plan` folder. + +## Required Reading (Read ALL of these first) + +### 1. Test Writing Skill (CRITICAL - read the entire folder) +Read all files in `/.claude/skills/write-test/`: +- `SKILL.md` — Main skill file +- `references/SCENARIO.md` — How to use initScenario +- `references/FIXTURES.md` — Product fixtures (products.pro, items.monthlyCredits, etc.) +- `references/EXPECTATIONS.md` — Expectation helpers +- `references/GOTCHAS.md` — Common pitfalls +- `references/ENTITIES.md` — Entity-level testing +- `references/STRIPE-BEHAVIORS.md` — Stripe-specific behaviors +- `references/WEBHOOKS.md` — Webhook testing +- `references/TRACK-CHECK.md` — Track/check endpoint testing + +### 2. Attach V2 Test Guide +Read `/server/tests/integration/billing/attach/attachTests.md` for: +- 12 key gotchas specific to attach tests +- AutumnInt generic types +- Folder structure + +### 3. The Test Plan for This Folder +Read `/server/tests/integration/billing/attach/new-plan.md` for: +- File structure and test cases to implement +- Setup patterns and key assertions + +### 4. Reference: New Testing Style +Look at `/server/tests/integration/billing/update-subscription/` for the NEW testing style: +- How tests are organized into folders/files +- How `initScenario` is used +- How expectations are structured + +Specifically look at a few example files: +- `cancel/end-of-cycle/cancel-end-of-cycle.test.ts` +- `custom-plan/update-paid-basic.test.ts` +- `errors/update-errors-basic.test.ts` + +## Your Task + +1. **Create the folder structure** at `/server/tests/integration/billing/attach/new-plan/` + +2. **Implement test files** one at a time, following: + - The test cases specified in the plan + - The `initScenario` pattern from the write-test skill + - The NEW testing style from update-subscription/ + +3. **For each test file:** + - Use `initScenario` for setup (NOT the old `beforeAll` pattern) + - Use product fixtures from `@tests/utils/fixtures/products.ts` + - Use expectation helpers from `@tests/utils/expectUtils/` + - Follow the 12 gotchas in `attachTests.md` + +4. **Run tests after each file** to verify they pass: + ```bash + bun test server/tests/integration/billing/attach/new-plan/{FILE_NAME}.test.ts + ``` + +## Important Notes + +- Use `initScenario` — this is the NEW pattern, not `beforeAll` + manual setup +- Always use `product.id`, never string literals +- Payment method required for paid features: `s.customer({ paymentMethod: "success" })` +- Prepaid requires `options` on attach +- Use `expectSubToBeCorrect` when billing is involved +- Server logs are NOT visible in test output — ask user to paste logs if needed +``` + +--- + +# Future Test Plans (To Be Planned) + +These folders need detailed test planning before implementation. Use the prompt template at the bottom to start a planning session for each. + +--- + +## Folders Needing Planning + +### 1. `carry-existing-usages/` +**What it covers:** When upgrading/downgrading, how existing usage (consumable, prepaid, allocated) carries over or resets. + +**Key questions to answer:** +- Does consumable usage reset on upgrade? (Current understanding: YES, resets, overage NOT charged) +- Does allocated usage carry over? (Current understanding: YES) +- Does prepaid balance carry over? How is it converted between billing units? +- What happens to usage when scheduled switch activates at end of cycle? + +**Related code:** Look for `carryOverUsages`, `existingUsages`, `rollovers` in the codebase. + +--- + +### 2. `trials/` +**What it covers:** Free trial logic during attach operations. + +**Key questions to answer:** +- Trial with card required vs no card required +- Trial to paid conversion (natural end vs early removal) +- Upgrading/downgrading while in trial +- Trial on entities +- Preventing duplicate trials (fingerprint check) +- `free_trial` param override (from ENG-1013 follow-up) + +**Note:** We have a draft in `trials.md` but need to verify against actual implementation. + +--- + +### 3. `invoice/` +**What it covers:** The `invoice: true` mode where product is NOT granted until invoice is paid. + +**Key questions to answer:** +- Product not attached until invoice status = "paid" +- Invoice mode with upgrades vs new subscriptions +- Invoice mode with prepaid/one-off +- `enable_product_immediately` and `finalize_invoice` params (from ENG-1013) + +**Reference:** See `update-subscription/invoice/` for existing patterns. + +--- + +### 4. `new-billing-subscription/` +**What it covers:** The `new_billing_subscription` param that forces creation of a new Stripe subscription instead of merging. + +**Key questions to answer:** +- When does a new subscription get created vs merging into existing? +- Entity1 has pro, Entity2 attaches pro → same or different subscription? +- Add-on on separate subscription +- Billing anchor alignment across subscriptions + +**From ENG-1013:** +> Use case: User has pro plan and attaches recurring add-on, or entity1 has pro and entity2 attaches pro + +--- + +### 5. `billing-behavior/` +**What it covers:** The `billing_behavior` param that controls proration. + +**Key questions to answer:** +- `"prorate_immediately"` — Default, charges prorated amount now +- `"next_cycle_only"` — No immediate proration, changes apply next cycle +- How does this interact with upgrades vs downgrades? +- How does this interact with prepaid quantities? + +**Reference:** See PR #614 for `prorate_billing` in update subscription. + +--- + +### 6. `plan-schedule/` +**What it covers:** The `plan_schedule` param that overrides default upgrade/downgrade timing. + +**Key questions to answer:** +- Default: upgrades = immediate, downgrades = end_of_cycle +- Override: `"immediate"` forces immediate downgrade +- Override: `"end_of_cycle"` forces scheduled upgrade +- Should immediate downgrades allow proration refunds? + +**From ENG-1013:** +> Override allows forcing immediate downgrades or scheduled upgrades + +--- + +## Planning Session Prompt Template + +Copy and customize this prompt to start a planning session for any of the above folders: + +``` +I need to plan tests for the Attach V2 `{FOLDER_NAME}` folder. + +## Context + +Read the following files first: +1. `/server/tests/integration/billing/attach/attachTests.md` — Main test guide with gotchas +2. `/server/tests/integration/billing/attach/new-plan.md` — Example of a completed test plan +3. Linear ticket ENG-1013: https://linear.app/useautumn/issue/ENG-1013/implement-v2-attach-endpoint + +## What This Folder Covers + +{BRIEF_DESCRIPTION_FROM_ABOVE} + +## Key Questions to Answer + +{COPY_KEY_QUESTIONS_FROM_ABOVE} + +## Your Task + +1. **Research the codebase** to understand how this feature currently works: + - Search for relevant functions/types in `server/src/internal/billing/` + - Look at existing tests in `server/tests/attach/` and `server/tests/merged/` + - Check `server/tests/integration/billing/update-subscription/` for similar patterns + +2. **Create a test plan** in `/server/tests/integration/billing/attach/{FOLDER_NAME}.md` with: + - File structure (which test files to create) + - Test cases in table format (Test Name | Scenario | Key Assertions) + - Code snippets showing setup patterns + - Any open questions or undefined behaviors + +3. **Update `attachTests.md`** to add a link to the new test plan file + +Do NOT write actual test code yet — this is a planning session only. +``` + +--- + +## Example: Starting a Planning Session for `carry-existing-usages/` + +``` +I need to plan tests for the Attach V2 `carry-existing-usages` folder. + +## Context + +Read the following files first: +1. `/server/tests/integration/billing/attach/attachTests.md` — Main test guide with gotchas +2. `/server/tests/integration/billing/attach/new-plan.md` — Example of a completed test plan +3. Linear ticket ENG-1013: https://linear.app/useautumn/issue/ENG-1013/implement-v2-attach-endpoint + +## What This Folder Covers + +When upgrading/downgrading, how existing usage (consumable, prepaid, allocated) carries over or resets. + +## Key Questions to Answer + +- Does consumable usage reset on upgrade? (Current understanding: YES, resets, overage NOT charged) +- Does allocated usage carry over? (Current understanding: YES) +- Does prepaid balance carry over? How is it converted between billing units? +- What happens to usage when scheduled switch activates at end of cycle? + +## Your Task + +1. **Research the codebase** to understand how this feature currently works: + - Search for relevant functions/types in `server/src/internal/billing/` + - Look at existing tests in `server/tests/attach/` and `server/tests/merged/` + - Check `server/tests/integration/billing/update-subscription/` for similar patterns + +2. **Create a test plan** in `/server/tests/integration/billing/attach/carry-existing-usages.md` with: + - File structure (which test files to create) + - Test cases in table format (Test Name | Scenario | Key Assertions) + - Code snippets showing setup patterns + - Any open questions or undefined behaviors + +3. **Update `attachTests.md`** to add a link to the new test plan file + +Do NOT write actual test code yet — this is a planning session only. +``` diff --git a/server/tests/integration/billing/attach/immediate-switch.md b/server/tests/integration/billing/attach/immediate-switch.md new file mode 100644 index 000000000..2e7af7580 --- /dev/null +++ b/server/tests/integration/billing/attach/immediate-switch.md @@ -0,0 +1,118 @@ +# immediate-switch/ Test Cases + +Covers **upgrades** — when attaching a higher-tier product that takes effect **immediately**. + +--- + +## immediate-switch-basic.test.ts + +Basic upgrade scenarios. + +- **immediate-switch: free to pro** — Free product → Pro. Verify pro is active, free is removed, invoice for pro base price. +- **immediate-switch: pro to premium** — Pro ($20/mo) → Premium ($50/mo). Verify prorated charge for price difference. +- **immediate-switch: pro to premium mid-cycle** — Attach pro, advance 15 days, upgrade to premium. Verify prorated charge. +- **immediate-switch: pro to free to premium** — Pro → Free (downgrade, scheduled) → Premium (upgrade). Verify scheduled downgrade is cancelled, premium is active immediately. +- **immediate-switch: premium to pro to ultra** — Premium → Pro (downgrade, scheduled) → Ultra. Verify scheduled downgrade is cancelled, ultra is active immediately. +- **immediate-switch: upgrade with consumable features, verify usage resets** — Pro with consumable + free consumable + prepaid consumable → Premium. Verify all **usage resets**. +- **immediate-switch: upgrade with allocated features, verify usage carries over** — Pro with free allocated + allocated + prepaid allocated → Premium. Verify all **usage carries over**. + +Fixtures: `products.pro()`, `products.premium()`, `products.ultra()`, `products.base()` + +--- + +## immediate-switch-consumable.test.ts + +Upgrades involving consumable features. + +- **immediate-switch: pro with consumable, track usage, to premium** — Pro with consumable messages, track some usage (and also into overage). Upgrade to premium. Verify overage NOT charged on upgrade (billed at cycle end), and **usage resets** after upgrade. + +--- + +## immediate-switch-allocated.test.ts + +Upgrades involving allocated (seat-based) features. + +### Same included usage (pro → pro-variant) +- **immediate-switch: free with free allocated to pro with allocated** — Free with free allocated users → Pro with allocated users (same included). Verify usage carries over. +- **immediate-switch: pro with allocated, under limit, to pro-variant** — Pro with 3 allocated (using 2) → Pro-variant with 3 allocated. Verify no overage, usage carries over. +- **immediate-switch: pro with allocated, at limit, to pro-variant** — Pro with 3 allocated (using 3) → Pro-variant with 3 allocated. Verify usage carries over. + +### Included usage changes (pro → premium with higher limit) +- **immediate-switch: pro with allocated, under limit, to premium with higher limit** — Pro with 3 allocated (using 2) → Premium with 5 allocated. Verify no overage charge, usage carries over. +- **immediate-switch: pro with allocated, over limit, to premium with higher limit** — Pro with 3 allocated (using 5) → Premium with 10 allocated. Verify existing overage handled, usage carries over. + +### Replaceable (TBD) +- **immediate-switch: allocated with replaceable entities (track negative), upgrade** — Error: "behavior undefined". Will implement later. + +--- + +## immediate-switch-prepaid.test.ts + +Upgrades involving prepaid features. + +### No options passed +- **immediate-switch: free to pro with prepaid, no options** — Free → Pro with prepaid, no options passed. Verify quantity defaults to 0, only base price charged. + +### Same config (quantity change only) +- **immediate-switch: pro with prepaid, increase quantity** — Pro with prepaid messages (2 packs) → same product with 5 packs. Verify refund old + charge new. +- **immediate-switch: pro with prepaid, decrease quantity** — Pro with prepaid (5 packs) → same with 2 packs. Verify credit issued. + +### Billing units change +- **immediate-switch: prepaid billing units change (100 → 50)** — Pro prepaid (100 units/pack) → Premium prepaid (50 units/pack). Verify correct recalculation. +- **immediate-switch: prepaid billing units change (50 → 100)** — Pro prepaid (50 units/pack) → Premium prepaid (100 units/pack). Verify correct recalculation. + +### Price change +- **immediate-switch: prepaid price increase** — Pro prepaid ($10/pack) → Premium prepaid ($15/pack). Verify correct charge difference. +- **immediate-switch: prepaid price decrease** — Pro prepaid ($15/pack) → Premium prepaid ($10/pack). Verify credit issued. + +### Included usage change +- **immediate-switch: prepaid included usage increase** — Pro prepaid (0 included) → Premium prepaid (100 included). Verify correct handling. +- **immediate-switch: prepaid included usage decrease** — Pro prepaid (100 included) → Premium prepaid (0 included). Verify correct handling. + +### Upcoming quantity (proration None) +- **immediate-switch: prepaid with upcoming_quantity populated, upgrade** — Pro with prepaid, decrease quantity with proration `None` (sets `upcoming_quantity`), then upgrade to premium. Verify correct handling of pending quantity change. + +--- + +## immediate-switch-billing-interval.test.ts + +Upgrades involving billing interval changes. + +- **immediate-switch: monthly to annual** — Pro monthly → Pro annual. Verify correct charge for annual. +- **immediate-switch: monthly to monthly + annual** — Pro monthly → Pro with both monthly and annual components. + +--- + +## immediate-switch-entities.test.ts + +Multi-entity upgrade scenarios. + +### Basic entity upgrades +- **immediate-switch: entity 1 free, entity 2 free, upgrade entity 2 to pro** — Two entities on free, upgrade one to pro. Verify independent states. +- **immediate-switch: entity 1 pro, entity 2 free, upgrade entity 2 to pro** — Mixed entity states, upgrade the free one. +- **immediate-switch: entity 1 pro, entity 2 pro, upgrade entity 2 to premium** — Both on pro, upgrade one to premium. +- **immediate-switch: entity 1 pro, entity 2 pro, upgrade entity 2 to pro annual** — Both on pro monthly, upgrade one to annual. + +### Upgrade with scheduled downgrade +- **immediate-switch: entity 1 premium, entity 2 premium, downgrade entity 1 to pro, then upgrade entity 1 to growth** — Premium on both, downgrade one (scheduled), then upgrade that same entity. Verify scheduled downgrade is cancelled, growth is active. +- **immediate-switch: entity 1 premium, entity 2 premium, downgrade both to pro, upgrade entity 2 to growth** — Both scheduled for downgrade, upgrade one. Verify one still scheduled, one upgraded. + +### Upgrade when cancel is scheduled +- **immediate-switch: entity 1 pro, entity 2 pro, cancel entity 1 (to free), upgrade entity 1 to premium** — Pro on both, cancel one (scheduled to free), then upgrade that entity to premium. Verify cancel is overridden, premium is active. + +### Track and upgrade +- **immediate-switch: entity 1 pro, entity 2 pro, track usage on both, advance 2 weeks, upgrade entity 1 to premium** — Both on pro with tracked usage, mid-cycle upgrade one. Verify correct invoice at end of cycle (entity 2 overage + base prices). + +--- + +## Summary + +| File | Test Count | +|------|------------| +| `immediate-switch-basic.test.ts` | 7 | +| `immediate-switch-consumable.test.ts` | 1 | +| `immediate-switch-allocated.test.ts` | 6 | +| `immediate-switch-prepaid.test.ts` | 10 | +| `immediate-switch-billing-interval.test.ts` | 2 | +| `immediate-switch-entities.test.ts` | 8 | +| **Total** | **34** | diff --git a/server/tests/integration/billing/attach/new-plan.md b/server/tests/integration/billing/attach/new-plan.md new file mode 100644 index 000000000..fd6cd2843 --- /dev/null +++ b/server/tests/integration/billing/attach/new-plan.md @@ -0,0 +1,51 @@ +# new-plan/ Test Cases + +Covers attaching products when customer has **no existing product** for that group. + +--- + +## attach-free.test.ts + +- **new-plan: attach free product** — Free product with monthly messages. Verify balance, usage, no invoice. +- **new-plan: attach free with multiple features** — Free with messages + words + dashboard + unlimited. Verify all features present with correct balances. + +Fixtures: `items.monthlyMessages()`, `items.monthlyWords()`, `items.dashboard()`, `items.unlimitedMessages()`, `products.base()` + +--- + +## attach-paid.test.ts + +- **new-plan: attach pro with mixed features** — Pro ($20/mo) with consumable words + prepaid messages + allocated users. Verify invoice = base + prepaid, all features correct. +- **new-plan: attach pro with allocated, create entities** — Pro with allocated users (3 included). Create 5 user entities via track. Verify users usage = 5, overage invoice created. +- **new-plan: attach base with prepaid messages, no options** — Base product with prepaid messages, attach without passing `options`. Expect error: "behavior undefined". +- **new-plan: attach pro with prepaid messages, no options** — Pro with prepaid messages, attach without passing `options`. Expect error: "behavior undefined". +- **new-plan: attach pro with prepaid messages, quantity 0** — Pro with prepaid messages, pass `options` with `quantity: 0`. Verify no prepaid charged, only base price. + +Fixtures: `items.consumableMessages()`, `items.prepaidMessages()`, `items.allocatedUsers()`, `products.pro()`, `products.base()` + +--- + +## attach-one-time.test.ts + +- **new-plan: attach one-time purchase** — One-time product with prepaid messages. Verify invoice, balance added, no recurring subscription. +- **new-plan: attach one-time purchase twice** — Attach same one-time product twice. Verify balance is cumulative (not replaced). +- **new-plan: attach pro then one-time as main** — Attach pro, then attach one-time **without** `isAddOn`. Should replace pro (user forgot to toggle). +- **new-plan: attach one-time with quantity=0 for one feature** — One-time with messages (qty=100) + words (qty=0). Verify messages added, words not charged. +- **new-plan: attach one-time as add-on to pro** — Attach pro, then attach one-time with `isAddOn: true`. Verify both products exist, balances combined. +- **new-plan: attach one-time with multiple features** — One-time with messages + words + storage (all one-off). Verify all balances correct. +- **new-plan: attach one-time to entity** — Create entity, attach one-time to entity. Verify entity has balance, customer does not. + +Fixtures: `items.oneOffMessages()`, `items.oneOffPrice()`, `products.oneOff()` + +--- + +## attach-entities.test.ts + +- **new-plan: create entity, attach pro to entity** — Create entity, attach pro to entity (not customer). Verify entity has product, customer does not. +- **new-plan: create 2 entities, attach pro to each** — Create 2 entities, attach pro to each. Verify independent balances, 2 separate subscriptions. +- **new-plan: attach pro to entity 1, advance 2 weeks, attach pro to entity 2** — Mid-cycle attach to second entity. Verify prorated billing for entity 2. +- **new-plan: attach pro annual to entity** — Attach annual product to entity. Verify correct billing interval. +- **new-plan: attach pro to customer, then pro to entity** — Attach pro to customer first, then attach pro to entity. Verify both have product independently. +- **new-plan: attach free to customer, then free to entity** — Attach free to customer first, then attach free to entity. Verify both have product independently. + +Fixtures: `s.entities({ count: 2 })`, `products.pro()`, `products.proAnnual()`, `products.base()` diff --git a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts new file mode 100644 index 000000000..5d7a815ed --- /dev/null +++ b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts @@ -0,0 +1,143 @@ +/** + * Attach Free Product Tests + * + * Tests for attaching free products when customer has no existing product. + * Free products have no base price and only provide included usage/features. + * + * Key behaviors: + * - No invoice is created for free products + * - Features are granted immediately + * - Usage resets according to billing interval + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3 } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +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"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: Attach free product with monthly messages +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer has no existing product + * - Attach free product with monthly messages (100 included) + * + * Expected Result: + * - Product is active + * - Messages feature has balance = 100, usage = 0 + * - No invoice created (free product) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async () => { + const customerId = "new-plan-attach-free"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const free = products.base({ + id: "free", + items: [messagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [s.customer({}), s.products({ list: [free] })], + actions: [s.attach({ productId: free.id })], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: free.id, + }); + + // Verify messages feature has correct balance + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Verify no invoice created (free product) + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 2: Attach free product with multiple features +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer has no existing product + * - Attach free product with: messages (100), words (200), dashboard (boolean), unlimited messages + * + * Expected Result: + * - Product is active + * - All features present with correct balances + * - No invoice created (free product) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple features")}`, async () => { + const customerId = "new-plan-attach-free-multi"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const wordsItem = items.monthlyWords({ includedUsage: 200 }); + const dashboardItem = items.dashboard(); + + const free = products.base({ + id: "free-multi", + items: [messagesItem, wordsItem, dashboardItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [s.customer({}), s.products({ list: [free] })], + actions: [s.attach({ productId: free.id })], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: free.id, + }); + + // Verify messages feature + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Verify words feature + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Words, + includedUsage: 200, + balance: 200, + usage: 0, + }); + + // Verify dashboard feature (boolean - just check it exists) + expect(customer.features[TestFeature.Dashboard]).toBeDefined(); + + // Verify no invoice created (free product) + expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); diff --git a/server/tests/integration/billing/attach/scheduled-switch.md b/server/tests/integration/billing/attach/scheduled-switch.md new file mode 100644 index 000000000..0ac10673f --- /dev/null +++ b/server/tests/integration/billing/attach/scheduled-switch.md @@ -0,0 +1,120 @@ +# scheduled-switch/ Test Cases + +Covers **downgrades** — when attaching a lower-tier product that takes effect at **end of billing cycle**. + +--- + +## scheduled-switch-basic.test.ts + +Basic downgrade scenarios. + +- **scheduled-switch: pro to free** — Pro → Free. Verify pro is "canceling" (active with canceled_at), free is "scheduled". At cycle end, pro removed, free active. +- **scheduled-switch: premium to pro** — Premium ($50/mo) → Pro ($20/mo). Verify premium canceling, pro scheduled. At cycle end, premium removed, pro active. +- **scheduled-switch: premium to pro to free** — Premium → Pro (scheduled) → Free (scheduled). Verify pro scheduled is replaced by free scheduled. At cycle end, premium removed, free active. +- **scheduled-switch: premium to free to pro** — Premium → Free (scheduled) → Pro (upgrade, immediate). Verify scheduled downgrade cancelled, pro active immediately. +- **scheduled-switch: premium to pro, then upgrade to growth** — Premium → Pro (scheduled) → Growth (immediate). Verify scheduled pro is cancelled, growth active. +- **scheduled-switch: premium to free, then upgrade to pro** — Premium → Free (scheduled) → Pro (immediate). Verify scheduled free is cancelled, pro active. +- **scheduled-switch: premium annual + monthly to premium monthly** — Premium with annual + monthly components → Premium monthly only. Verify correct handling of mixed intervals on downgrade. + +--- + +## scheduled-switch-prepaid.test.ts + +Downgrades with prepaid quantities. + +> **Key behavior:** Total prepaid quantity is preserved (rounded to new billing units). +> Example: 5 packs × 100 units = 500 units → new plan with 50 units/pack = 10 packs. + +### Quantity handling +- **scheduled-switch: prepaid 5 packs to 2 packs (explicit options)** — Premium 5 packs (100 units/pack) → Pro 2 packs. Verify 2 packs on next cycle. +- **scheduled-switch: prepaid, no options passed in** — Premium 5 packs → Pro (no options). Verify total units preserved, quantity converted to new billing units. +- **scheduled-switch: prepaid, no options, different billing units** — Premium 5 packs (100 units/pack = 500 units) → Pro (50 units/pack). Verify 10 packs on next cycle. +- **scheduled-switch: prepaid to quantity 0** — Premium 5 packs → Pro with quantity: 0. Verify no prepaid charged on next cycle. + +### Feature changes +- **scheduled-switch: prepaid to product without prepaid feature** — Premium with prepaid → Free (no prepaid). Verify balance lost at cycle end. +- **scheduled-switch: prepaid with different price per pack** — Premium ($15/pack) → Pro ($10/pack). Verify next cycle uses new price. + +### Included usage change +- **scheduled-switch: prepaid included usage increase** — Premium prepaid (0 included) → Pro prepaid (100 included). Verify included usage changes on next cycle. +- **scheduled-switch: prepaid included usage decrease** — Premium prepaid (100 included) → Pro prepaid (0 included). Verify included usage changes on next cycle. + +--- + +## scheduled-switch-consumable.test.ts + +Downgrades with consumable features. + +> **Note:** Consumable overage is charged at cycle end via invoice-created webhook. These tests verify the downgrade flow works correctly with consumable usage. + +- **scheduled-switch: pro with consumable, usage under limit, to free** — Pro with consumable (used 50/100 included) → Free. Verify scheduled downgrade, no overage charged at cycle end. +- **scheduled-switch: pro with consumable, into overage, to free** — Pro with consumable (used 150/100, 50 overage) → Free. Verify overage charged at cycle end when downgrade completes. +- **scheduled-switch: premium with consumable overage, downgrade to pro** — Premium ($50/mo) with consumable (200 used, 100 overage) → Pro ($20/mo). Advance cycle. Verify overage billed to Premium, Pro active with balance reset. (from invoice-created-consumable-edge-cases.test.ts) + +--- + +## scheduled-switch-allocated.test.ts + +Downgrades with allocated (seat-based) features. + +> **Note:** These cases have undefined behavior. Tests should throw error "behavior undefined" until we clarify how allocated seats are handled on scheduled downgrade. + +- **scheduled-switch: pro with allocated, under limit, to free** — Pro with 5 allocated (using 3) → Free. Error: "behavior undefined". TBD: How are seats handled at cycle end? +- **scheduled-switch: pro with allocated, over limit, to free** — Pro with 5 allocated (using 7) → Free. Error: "behavior undefined". TBD: How is existing overage handled on downgrade? + +--- + +## scheduled-switch-entities.test.ts + +Multi-entity downgrade scenarios. + +### Basic entity downgrades +- **scheduled-switch: entity 1 pro, entity 2 pro, downgrade entity 1 to free** — Both on pro, downgrade one. Verify entity 1 has pro canceling + free scheduled, entity 2 unchanged. +- **scheduled-switch: entity 1 pro, entity 2 pro, downgrade both to free** — Both on pro, downgrade both. Verify both have free scheduled. Advance cycle, verify both on free. + +### Downgrade + upgrade on different entities simultaneously +- **scheduled-switch: entity 1 premium to pro, entity 2 pro to premium** — Premium on entity 1 → Pro (scheduled), Pro on entity 2 → Premium (immediate). Verify independent states. +- **scheduled-switch: entity 1 pro to premium, entity 2 premium to pro** — Pro on entity 1 → Premium (immediate), Premium on entity 2 → Pro (scheduled). Verify independent states. + +### Change scheduled product (replace) +- **scheduled-switch: entity 1 & 2 premium, downgrade both to free, entity 2 changes to pro** — Premium on both → Free scheduled on both → Entity 2 changes scheduled to pro. Verify entity 1 has free scheduled, entity 2 has pro scheduled. + +### Post-cycle upgrade +- **scheduled-switch: entity 1 premium to free, entity 2 premium to pro, advance cycle, upgrade entity 1 to premium** — After downgrade completes (entity 1 now free, entity 2 now pro), upgrade entity 1 back to premium. +- **scheduled-switch: entity 1 premiumAnnual to pro, entity 2 premium to pro, advance cycle, upgrade entity 2 to premium** — After monthly downgrade completes (entity 2 now pro), upgrade entity 2 back to premium. Entity 1 still has annual + scheduled pro. + +### Chained downgrades +- **scheduled-switch: entity 1 premium, entity 2 premium, downgrade both to pro, then downgrade entity 1 to free** — Premium on both → Pro scheduled on both → Free scheduled on entity 1 (replaces pro). Verify entity 1 has free scheduled, entity 2 has pro scheduled. + +--- + +## scheduled-switch-multi-interval.test.ts + +Mixed billing interval scenarios (annual + monthly entities). + +- **scheduled-switch: entity 1 premiumAnnual, entity 2 premium, downgrade both to pro, advance monthly cycle** — Annual on entity 1, monthly on entity 2 → both scheduled for pro. Advance 1 month. Entity 1 still on premiumAnnual + pro scheduled (annual not ended), entity 2 now on pro. +- **scheduled-switch: entity 1 premiumAnnual, entity 2 premium, downgrade both to pro, re-upgrade both** — Annual on entity 1, monthly on entity 2 → both scheduled for pro → both re-upgrade (premiumAnnual and premium). Verify scheduled downgrades cancelled, both back to original products. +- **scheduled-switch: entity 1 premiumAnnual, entity 2 premium, downgrade both to pro, advance full year** — Same setup but advance a full year to see the annual downgrade complete as well. Verify both entities now on pro. + +--- + +## scheduled-switch-edge-cases.test.ts + +Edge cases and complex scenarios. + +- **scheduled-switch: multiple scheduled changes on same entity** — Growth → Free (scheduled) → Pro (replaces) → Premium (replaces) → Free (replaces). Verify each change replaces the previous scheduled product. + +--- + +## Summary + +| File | Test Count | +|------|------------| +| `scheduled-switch-basic.test.ts` | 7 | +| `scheduled-switch-prepaid.test.ts` | 8 | +| `scheduled-switch-consumable.test.ts` | 3 | +| `scheduled-switch-allocated.test.ts` | 2 | +| `scheduled-switch-entities.test.ts` | 8 | +| `scheduled-switch-multi-interval.test.ts` | 3 | +| `scheduled-switch-edge-cases.test.ts` | 1 | +| **Total** | **32** | diff --git a/server/tests/integration/billing/attach/trials.md b/server/tests/integration/billing/attach/trials.md new file mode 100644 index 000000000..e48e069f0 --- /dev/null +++ b/server/tests/integration/billing/attach/trials.md @@ -0,0 +1,312 @@ +# Trials Test Plan + +Tests for free trial logic in attach operations. + +--- + +## File Structure + +| File | Test Count | Description | +|------|------------|-------------| +| `trials-basic.test.ts` | 5 | Basic trial attachment and states | +| `trials-conversion.test.ts` | 5 | Trial to paid conversion | +| `trials-cancel.test.ts` | 5 | Canceling trials (immediately, end-of-cycle) | +| `trials-upgrade.test.ts` | 4 | Upgrading while in trial | +| `trials-entities.test.ts` | 4 | Entity-scoped trials | +| `trials-payment-method.test.ts` | 4 | Card required vs not required | + +**Total: 27 tests** + +--- + +## Trial Product Types + +| Type | Property | Description | +|------|----------|-------------| +| Card Required | `cardRequired: true` | Customer must have PM before trial starts | +| No Card Required | `cardRequired: false` | Customer can start trial without PM | + +```typescript +// Card required trial (default for proWithTrial) +const proTrial = products.proWithTrial({ + id: "pro-trial", + items: [messagesItem], + trialDays: 7, + cardRequired: true, +}); + +// No card required trial (default for baseWithTrial) +const freeTrial = products.baseWithTrial({ + id: "free-trial", + items: [messagesItem], + trialDays: 14, + cardRequired: false, +}); +``` + +--- + +## Test Details + +### `trials-basic.test.ts` (5 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | trial: attach product with trial | Attach proTrial | Product status = trialing, trialEndsAt correct | +| 2 | trial: features available during trial | Check entitlements during trial | All features accessible | +| 3 | trial: trial end date calculation | 7-day trial attached today | trialEndsAt = now + 7 days | +| 4 | trial: usage tracking during trial | Track usage during trial | Usage recorded, balance updated | +| 5 | trial: trial with prepaid features | Trial product with prepaid credits | Credits available during trial | + +**Setup:** +```typescript +const proTrial = products.proWithTrial({ + id: "pro-trial", + items: [messagesItem], + trialDays: 7, + cardRequired: true, +}); + +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [proTrial] }), + ], + actions: [s.attach({ productId: proTrial.id })], +}); + +const customer = await autumnV1.customers.get(customerId); +expectProductTrialing({ + customer, + productId: proTrial.id, + trialEndsAt: addDays(new Date(), 7).getTime(), + toleranceMs: 60_000, // 1 minute tolerance +}); +``` + +--- + +### `trials-conversion.test.ts` (5 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | conversion: trial ends naturally | Advance clock past trial end | Status = active, invoice generated | +| 2 | conversion: remove trial early | Call remove trial action | Trial ends immediately, payment charged | +| 3 | conversion: trial ends without PM | No card trial ends | Product removed or checkout required | +| 4 | conversion: trial ends with failed PM | PM fails at conversion | Invoice open, action required | +| 5 | conversion: invoice amount after trial | Trial ends, verify invoice | First invoice = full price (no proration) | + +**Conversion Pattern:** +```typescript +const { customerId, autumnV1, ctx } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [proTrial] }), + ], + actions: [s.attach({ productId: proTrial.id })], +}); + +// Advance past trial (7 days + buffer) +const advancedTo = await advanceToNextInvoice({ + stripeCli: ctx.stripeCli, + testClockId: ctx.testClockId, + currentEpochMs: addDays(new Date(), 7).getTime(), +}); + +// Verify conversion +const customer = await autumnV1.customers.get(customerId); +expectProductNotTrialing({ customer, productId: proTrial.id, nowMs: advancedTo }); +expectProductActive({ customer, productId: proTrial.id }); +``` + +--- + +### `trials-cancel.test.ts` (5 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | cancel-trial: immediately | Cancel trial immediately | Product removed, no invoice | +| 2 | cancel-trial: end-of-cycle (trial period) | Cancel during trial | Canceling status, removed at trial end | +| 3 | cancel-trial: uncancel during trial | Cancel then uncancel | Trial restored, same end date | +| 4 | cancel-trial: usage not charged | Cancel trial with usage | No overage charged | +| 5 | cancel-trial: verify no refund | Cancel free trial | No refund invoice (nothing charged) | + +**Cancel Pattern:** +```typescript +// Cancel trial immediately +await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: proTrial.id, + cancel_action: "cancel_immediately", +}); + +const customer = await autumnV1.customers.get(customerId); +expectProductNotPresent({ customer, productId: proTrial.id }); + +// Cancel at end of trial +await autumnV1.subscriptions.update({ + customer_id: customerId, + product_id: proTrial.id, + cancel_action: "cancel_end_of_cycle", +}); + +expectProductCanceling({ customer, productId: proTrial.id }); +expectProductTrialing({ customer, productId: proTrial.id }); // Still trialing until end +``` + +--- + +### `trials-upgrade.test.ts` (4 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | upgrade-trial: trial to trial | Pro trial → Premium trial | New trial starts, original trial replaced | +| 2 | upgrade-trial: trial to paid | Trial → paid (no trial) | Trial ends, paid immediately | +| 3 | upgrade-trial: trial to free | Trial → free product | Trial ends, free product attached | +| 4 | upgrade-trial: preserve trial days | Upgrade mid-trial | Remaining trial days preserved (if configured) | + +**Upgrade Pattern:** +```typescript +const proTrial = products.proWithTrial({ id: "pro-trial", items: [...], trialDays: 14 }); +const premiumTrial = products.premiumWithTrial({ id: "premium-trial", items: [...], trialDays: 14 }); + +// Attach pro trial +await autumnV1.attach({ customer_id: customerId, product_id: proTrial.id }); + +// Upgrade to premium trial after 7 days +await advanceTestClock({ ... addDays(7) }); +await autumnV1.attach({ customer_id: customerId, product_id: premiumTrial.id }); + +// Verify new trial +const customer = await autumnV1.customers.get(customerId); +expectProductTrialing({ customer, productId: premiumTrial.id }); +expectProductNotPresent({ customer, productId: proTrial.id }); +``` + +--- + +### `trials-entities.test.ts` (4 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | entity-trial: new entity starts trial | Entity1 with trial | Entity1 trialing | +| 2 | entity-trial: second entity mid-trial | Entity2 joins while Entity1 in trial | Entity2 starts own trial | +| 3 | entity-trial: entity trial conversion | Advance past entity trial end | Entity-level invoice generated | +| 4 | entity-trial: cancel one entity trial | Cancel Entity1 trial | Entity2 trial unaffected | + +**Entity Pattern:** +```typescript +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true, paymentMethod: "success" }), + s.products({ list: [proTrial] }), + s.entities({ ids: ["entity-1", "entity-2"] }), + ], + actions: [ + s.attach({ productId: proTrial.id, entityId: "entity-1" }), + ], +}); + +// Entity-1 trialing +const entity1 = await autumnV1.entities.get(customerId, "entity-1"); +expectProductTrialing({ customer: entity1, productId: proTrial.id }); + +// Entity-2 not attached yet +const entity2 = await autumnV1.entities.get(customerId, "entity-2"); +expect(entity2.products.length).toBe(0); +``` + +--- + +### `trials-payment-method.test.ts` (4 tests) + +| # | Test Name | Scenario | Key Assertions | +|---|-----------|----------|----------------| +| 1 | pm-trial: card required - no PM | Attach cardRequired trial without PM | Error or checkout required | +| 2 | pm-trial: card required - with PM | Attach cardRequired trial with PM | Trial starts successfully | +| 3 | pm-trial: no card required - start | Attach no-card trial without PM | Trial starts without PM | +| 4 | pm-trial: no card required - conversion | No-card trial ends | Checkout required to continue | + +**Payment Method Pattern:** +```typescript +// Card required - needs PM +const cardRequiredTrial = products.proWithTrial({ + items: [...], + trialDays: 7, + cardRequired: true, +}); + +// No card required - no PM needed +const noCardTrial = products.baseWithTrial({ + items: [...], + trialDays: 7, + cardRequired: false, +}); + +// Without PM - cardRequired fails, noCard succeeds +const { customerId, autumnV1 } = await initScenario({ + setup: [ + s.customer({ testClock: true }), // No payment method + s.products({ list: [cardRequiredTrial, noCardTrial] }), + ], +}); + +// This should require checkout or fail +const result1 = await autumnV1.attach({ + customer_id: customerId, + product_id: cardRequiredTrial.id, +}); +expect(result1.checkout_url).toBeDefined(); + +// This should succeed +await autumnV1.attach({ + customer_id: customerId, + product_id: noCardTrial.id, +}); +``` + +--- + +## Key Utilities + +**Product Fixtures:** +```typescript +products.proWithTrial({ items, trialDays, cardRequired }) +products.premiumWithTrial({ items, trialDays, cardRequired }) +products.baseWithTrial({ items, trialDays, cardRequired }) +products.defaultTrial({ items, trialDays, cardRequired }) +``` + +**Expectation Helpers:** +```typescript +expectProductTrialing({ + customer, + productId, + trialEndsAt, // Expected trial end timestamp + toleranceMs, // Tolerance for date comparison (default 60000) +}); + +expectProductNotTrialing({ + customer, + productId, + nowMs, // Current time to compare against +}); +``` + +**Test Clock Advancement:** +```typescript +// Advance to end of trial +await advanceTestClock({ + stripeCli, + testClockId, + advanceTo: addDays(new Date(), trialDays).getTime(), + waitForSeconds: 30, +}); + +// Or use advanceToNextInvoice for full cycle +await advanceToNextInvoice({ + stripeCli, + testClockId, + currentEpochMs, +}); +``` diff --git a/server/tests/utils/fixtures/products.ts b/server/tests/utils/fixtures/products.ts index be0ef5f1b..37e24fa56 100644 --- a/server/tests/utils/fixtures/products.ts +++ b/server/tests/utils/fixtures/products.ts @@ -226,6 +226,66 @@ const defaultTrial = ({ } as unknown as FreeTrial, }); +/** + * Premium product - $50/month base price + * @param items - Product items (features) + * @param id - Product ID (default: "premium") + */ +const premium = ({ + items, + id = "premium", +}: { + items: ProductItem[]; + id?: string; +}): ProductV2 => + constructProduct({ + id, + items: [...items], + type: "premium", + isDefault: false, + }); + +/** + * Growth product - $100/month base price + * @param items - Product items (features) + * @param id - Product ID (default: "growth") + */ +const growth = ({ + items, + id = "growth", +}: { + items: ProductItem[]; + id?: string; +}): ProductV2 => + constructProduct({ + id, + items: [...items], + type: "growth", + isDefault: false, + }); + +/** + * Ultra product - $200/month base price (uses base with custom price) + * @param items - Product items (features) + * @param id - Product ID (default: "ultra") + */ +const ultra = ({ + items, + id = "ultra", +}: { + items: ProductItem[]; + id?: string; +}): ProductV2 => ({ + ...constructRawProduct({ + id, + items: [ + ...items, + constructPriceItem({ price: 200, interval: BillingInterval.Month }), + ], + }), + is_default: false, +}); + /** * One-off product - one-time purchase with $10 base price * @param items - Product items (features) @@ -272,7 +332,10 @@ export const products = { pro, proAnnual, proWithTrial, + premium, premiumWithTrial, + growth, + ultra, oneOff, recurringAddOn, } as const; diff --git a/server/tests/utils/testInitUtils/initScenario.ts b/server/tests/utils/testInitUtils/initScenario.ts index 2267d173d..f001dc26b 100644 --- a/server/tests/utils/testInitUtils/initScenario.ts +++ b/server/tests/utils/testInitUtils/initScenario.ts @@ -86,6 +86,16 @@ type AdvanceToNextInvoiceAction = { withPause?: boolean; }; +type BillingAttachAction = { + type: "billingAttach"; + productId: string; + entityIndex?: number; + options?: FeatureOption[]; + newBillingSubscription?: boolean; + timeout?: number; + isAddOn?: boolean; +}; + type ScenarioAction = | AttachAction | CancelAction @@ -94,7 +104,8 @@ type ScenarioAction = | RemovePaymentMethodAction | TrackAction | UpdateSubscriptionAction - | AdvanceToNextInvoiceAction; + | AdvanceToNextInvoiceAction + | BillingAttachAction; type CleanupConfig = { customerIdsToDelete: string[]; From 7b6abc2fa4a4b38057c3e9f238d08406fe36a849 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 28 Jan 2026 17:04:06 +0000 Subject: [PATCH 005/110] chore: clean up files and write plans for tests --- server/src/external/autumn/autumnCli.ts | 5 +- .../common/buildBillingContextFromWebhook.ts | 2 +- .../common/eventContextToArrearLineItems.ts | 4 +- .../common/logs/logWebhookArrearLineItems.ts | 2 +- .../computeAttachNewCustomerProduct.ts | 2 +- .../v2/attach/compute/computeAttachPlan.ts | 6 +- .../compute/computeAttachTransitionUpdates.ts | 6 +- .../v2/attach/compute/finalizeAttachPlan.ts | 6 +- .../v2/attach/errors/handleAttachV2Errors.ts | 128 +++++ .../billing/v2/attach/handleAttachV2.ts | 73 ++- .../v2/attach/logs/logAttachContext.ts | 2 +- .../attach/setup/setupAttachBillingContext.ts | 8 +- .../attach/setup/setupAttachCheckoutMode.ts | 61 ++- .../attach/setup/setupAttachEndOfCycleMs.ts | 2 +- .../attach/setup/setupAttachProductContext.ts | 4 +- .../setup/setupAttachTransitionContext.ts | 6 +- .../v2/attach/types/attachBillingContext.ts | 23 - .../buildAutumnLineItems.ts | 2 +- .../buildSharedSubscriptionTrialLineItems.ts | 4 +- .../filterLineItemsForTrialTransition.ts | 2 +- .../addStripeSubscriptionIdToBillingPlan.ts | 2 +- ...ripeSubscriptionScheduleIdToBillingPlan.ts | 2 +- .../updateCustomerEntitlements.ts | 2 +- .../v2/execute/executeAutumnBillingPlan.ts | 2 +- .../billing/v2/execute/executeBillingPlan.ts | 6 +- .../v2/execute/executeDeferredBillingPlan.ts | 2 +- ...moveStripeSubscriptionIdFromBillingPlan.ts | 2 +- .../buildStripeInvoiceAction.ts | 2 +- .../buildStripeInvoiceItemsAction.ts | 4 +- .../buildStripeSubscriptionAction.ts | 4 +- .../buildStripeSubscriptionScheduleAction.ts | 4 +- .../evaluateStripeBillingPlan.ts | 4 +- .../errors/handleStripeBillingPlanErrors.ts | 2 +- .../execute/executeStripeBillingPlan.ts | 8 +- .../execute/executeStripeInvoiceAction.ts | 8 +- .../executeStripeSubscriptionAction.ts | 8 +- ...executeStripeSubscriptionScheduleAction.ts | 4 +- .../stripe/logs/logStripeBillingPlan.ts | 4 +- .../stripe/logs/logStripeBillingResult.ts | 2 +- .../common/initStripeResourcesForProducts.ts | 4 +- .../utils/common/shouldDeferBillingPlan.ts | 2 +- .../utils/invoices/createInvoiceForBilling.ts | 4 +- .../shouldCreateManualStripeInvoice.ts | 4 +- .../billingPlanToOneOffStripeItemSpecs.ts | 2 +- .../buildStripeSubscriptionItemsUpdate.ts | 2 +- .../customerProductToStripeItemSpecs.ts | 2 +- .../buildStripePhasesUpdate.ts | 2 +- .../logSubscriptionScheduleAction.ts | 4 +- .../buildStripeSubscriptionCreateAction.ts | 2 +- .../buildStripeSubscriptionUpdateAction.ts | 4 +- .../executeStripeSubscriptionOperation.ts | 4 +- .../getLatestInvoiceFromSubscriptionAction.ts | 4 +- ...llStripeSubscriptionUpdateCreateInvoice.ts | 4 +- .../v2/setup/setupBillingCycleAnchor.ts | 2 +- .../billing/v2/setup/setupCancelMode.ts | 2 +- .../billing/v2/setup/setupRefundBehavior.ts | 10 - .../billing/v2/setup/setupTrialContext.ts | 2 +- .../billing/v2/types/attachBillingContext.ts | 30 ++ .../billing/v2/types/autumnBillingPlan.ts | 4 +- .../billing/v2/{ => types}/billingContext.ts | 6 +- .../internal/billing/v2/types/cancelTypes.ts | 14 - server/src/internal/billing/v2/types/index.ts | 12 + .../compute/cancel/applyCancelPlan.ts | 2 +- .../compute/cancel/applyUncancelToPlan.ts | 4 +- .../compute/cancel/computeCancelFields.ts | 2 +- .../compute/cancel/computeCancelLineItems.ts | 2 +- .../compute/cancel/computeCancelPlan.ts | 4 +- .../compute/cancel/computeCancelUpdates.ts | 2 +- .../cancel/computeCustomerProductToDelete.ts | 2 +- .../cancel/computeDefaultCustomerProduct.ts | 2 +- .../compute/cancel/computeEndOfCycleMs.ts | 2 +- .../compute/computeUpdateSubscriptionPlan.ts | 4 +- .../compute/customPlan/computeCustomPlan.ts | 4 +- .../computeCustomPlanNewCustomerProduct.ts | 2 +- .../compute/finalizeUpdateSubscriptionPlan.ts | 4 +- .../computeUpdateQuantityDetails.ts | 2 +- .../computeUpdateQuantityLineItems.ts | 2 +- .../computeUpdateQuantityPlan.ts | 4 +- .../errors/handleBillingBehaviorErrors.ts | 4 +- .../errors/handleCancelEndOfCycleErrors.ts | 2 +- .../handleCurrentCustomerProductErrors.ts | 2 +- .../errors/handleCustomPlanErrors.ts | 4 +- .../errors/handleFeatureQuantityErrors.ts | 4 +- .../errors/handleOneOffErrors.ts | 4 +- .../handleProductTypeTransitionErrors.ts | 4 +- .../errors/handleRefundBehaviorErrors.ts | 48 -- .../errors/handleUncancelErrors.ts | 2 +- .../errors/handleUpdateSubscriptionErrors.ts | 14 +- .../handlePreviewUpdateSubscription.ts | 4 +- .../handleUpdateSubscription.ts | 4 +- .../logs/logUpdateSubscriptionContext.ts | 2 +- .../setupUpdateSubscriptionBillingContext.ts | 6 +- .../autumnBillingPlanToFinalFullCustomer.ts | 4 +- .../billingContext/billingContextHasTrial.ts | 2 +- .../getBillingCycleAnchorForDirection.ts | 2 +- .../getCurrentBillingCycleAnchorMs.ts | 2 +- .../billingContext/getTrialStateTransition.ts | 2 +- .../billingContext/isDeferredInvoiceMode.ts | 2 +- .../v2/utils/billingContextPriceLookup.ts | 2 +- .../billingPlanToNewActiveCustomerProduct.ts | 2 +- .../billingPlanToNextCyclePreview.ts | 3 +- .../billingPlanToUpdatedCustomerProduct.ts | 2 +- .../v2/utils/billingPlanToPreviewResponse.ts | 4 +- .../billingResult/billingResultToResponse.ts | 6 +- .../customerProductToArrearLineItems.ts | 6 +- .../lineItems/customerProductToLineItems.ts | 2 +- .../lineItems/getLineItemBillingPeriod.ts | 2 +- .../logs/logAutumnBillingPlan.ts} | 10 +- .../billingPlanToSendProductsUpdated.ts | 6 +- .../compute/computeCreateCustomerPlan.ts | 2 +- .../createCustomerContext.ts | 5 +- .../executeAutumnCreateCustomerPlan.ts | 2 +- .../finalizeCreateCustomer.ts | 2 +- .../setupCreateCustomerBillingContext.ts | 2 +- .../setup/setupCreateCustomerTrialContext.ts | 2 +- .../customers/cancel/handleCancelV2.ts | 4 +- .../cusEnts/CusEntitlementService.ts | 2 +- .../utils/insertMetadataFromBillingPlan.ts | 6 +- .../integration/billing/attach/attachTests.md | 17 +- .../attach/new-plan/attach-entities.test.ts | 479 ++++++++++++++++++ .../attach/new-plan/attach-free.test.ts | 12 +- .../attach/new-plan/attach-one-time.test.ts | 470 +++++++++++++++++ .../attach/new-plan/attach-paid.test.ts | 331 ++++++++++++ server/tests/utils/fixtures/db/contexts.ts | 2 +- server/tests/utils/fixtures/products.ts | 4 + .../tests/utils/testInitUtils/initScenario.ts | 78 ++- .../{attachV0Params.ts => attachParamsV0.ts} | 10 +- .../common/cancelAction.ts} | 0 shared/api/billing/common/checkoutMode.ts | 7 - .../api/billing/common/featureQuantities.ts | 2 - shared/api/billing/common/planTiming.ts | 5 - shared/api/billing/index.ts | 5 +- .../updateSubscriptionV0Params.ts | 4 +- shared/index.ts | 1 + .../updateSubscriptionFormSchema.ts | 13 +- vite/src/main.tsx | 5 - vite/vite.config.ts | 4 - 137 files changed, 1869 insertions(+), 360 deletions(-) create mode 100644 server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts delete mode 100644 server/src/internal/billing/v2/attach/types/attachBillingContext.ts delete mode 100644 server/src/internal/billing/v2/setup/setupRefundBehavior.ts create mode 100644 server/src/internal/billing/v2/types/attachBillingContext.ts rename server/src/internal/billing/v2/{ => types}/billingContext.ts (91%) delete mode 100644 server/src/internal/billing/v2/types/cancelTypes.ts create mode 100644 server/src/internal/billing/v2/types/index.ts delete mode 100644 server/src/internal/billing/v2/updateSubscription/errors/handleRefundBehaviorErrors.ts rename server/src/internal/billing/v2/{updateSubscription/logs/logUpdateSubscriptionPlan.ts => utils/logs/logAutumnBillingPlan.ts} (85%) create mode 100644 server/tests/integration/billing/attach/new-plan/attach-entities.test.ts create mode 100644 server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts create mode 100644 server/tests/integration/billing/attach/new-plan/attach-paid.test.ts rename shared/api/billing/attachV2/{attachV0Params.ts => attachParamsV0.ts} (78%) rename shared/api/{common/cancelMode.ts => billing/common/cancelAction.ts} (100%) delete mode 100644 shared/api/billing/common/checkoutMode.ts delete mode 100644 shared/api/billing/common/planTiming.ts diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index c9e90d355..41e105fb3 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -9,6 +9,7 @@ import { type ApiCusProductV3, type ApiEntityV0, type AttachBodyV0, + type AttachParamsV0, type BalancesUpdateParams, type BillingResponse, type CheckQuery, @@ -798,7 +799,7 @@ export class AutumnInt { billing = { attach: async ( - params: AttachBodyV0, + params: AttachParamsV0, { skipWebhooks, idempotencyKey, @@ -829,7 +830,7 @@ export class AutumnInt { return data; }, - previewAttach: async (params: AttachBodyV0) => { + previewAttach: async (params: AttachParamsV0) => { const data = await this.post(`/billing/attach/preview`, params); return data; }, diff --git a/server/src/external/stripe/webhookHandlers/common/buildBillingContextFromWebhook.ts b/server/src/external/stripe/webhookHandlers/common/buildBillingContextFromWebhook.ts index f0be0f58e..a02da80e8 100644 --- a/server/src/external/stripe/webhookHandlers/common/buildBillingContextFromWebhook.ts +++ b/server/src/external/stripe/webhookHandlers/common/buildBillingContextFromWebhook.ts @@ -7,7 +7,7 @@ import { import type Stripe from "stripe"; import type { ExpandedStripeCustomer } from "@/external/stripe/customers/operations/getExpandedStripeCustomer"; import type { ExpandedStripeSubscription } from "@/external/stripe/subscriptions/operations/getExpandedStripeSubscription"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; /** * Common fields between InvoiceCreatedContext and StripeSubscriptionDeletedContext. diff --git a/server/src/external/stripe/webhookHandlers/common/eventContextToArrearLineItems.ts b/server/src/external/stripe/webhookHandlers/common/eventContextToArrearLineItems.ts index a39582653..9f2dd5d4e 100644 --- a/server/src/external/stripe/webhookHandlers/common/eventContextToArrearLineItems.ts +++ b/server/src/external/stripe/webhookHandlers/common/eventContextToArrearLineItems.ts @@ -1,9 +1,9 @@ import type { FullCusEntWithFullCusProduct, LineItem } from "@autumn/shared"; import type { StripeWebhookContext } from "@/external/stripe/webhookMiddlewares/stripeWebhookContext"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { setupStripeDiscountsForBilling } from "@/internal/billing/v2/providers/stripe/setup/setupStripeDiscountsForBilling"; import { applyStripeDiscountsToLineItems } from "@/internal/billing/v2/providers/stripe/utils/discounts/applyStripeDiscountsToLineItems"; -import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types"; import { customerProductToArrearLineItems } from "@/internal/billing/v2/utils/lineItems/customerProductToArrearLineItems"; import { type BaseWebhookEventContext, diff --git a/server/src/external/stripe/webhookHandlers/common/logs/logWebhookArrearLineItems.ts b/server/src/external/stripe/webhookHandlers/common/logs/logWebhookArrearLineItems.ts index 9417e0f94..f1611a5d9 100644 --- a/server/src/external/stripe/webhookHandlers/common/logs/logWebhookArrearLineItems.ts +++ b/server/src/external/stripe/webhookHandlers/common/logs/logWebhookArrearLineItems.ts @@ -1,6 +1,6 @@ import { formatMs, type LineItem } from "@autumn/shared"; import type { StripeWebhookContext } from "@/external/stripe/webhookMiddlewares/stripeWebhookContext"; -import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; export const logWebhookArrearLineItems = ({ diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts b/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts index 52ae983d1..95232fe6f 100644 --- a/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts @@ -1,9 +1,9 @@ import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { AttachBillingContext } from "@/internal/billing/v2/types"; import { cusProductToExistingRollovers } from "@/internal/billing/v2/utils/handleExistingRollovers/cusProductToExistingRollovers"; import { cusProductToExistingUsages } from "@/internal/billing/v2/utils/handleExistingUsages/cusProductToExistingUsages"; import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct"; -import type { AttachBillingContext } from "../types/attachBillingContext"; /** * Creates the new FullCusProduct to insert when attaching a product. diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts b/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts index c097e4035..0b7827c8d 100644 --- a/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts +++ b/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts @@ -1,7 +1,9 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { AttachBillingContext } from "../types/attachBillingContext"; +import type { + AttachBillingContext, + AutumnBillingPlan, +} from "@/internal/billing/v2/types"; import { computeAttachNewCustomerProduct } from "./computeAttachNewCustomerProduct"; import { computeAttachTransitionUpdates } from "./computeAttachTransitionUpdates"; import { finalizeAttachPlan } from "./finalizeAttachPlan"; diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts b/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts index 26fa0b330..01685872e 100644 --- a/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts +++ b/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts @@ -1,6 +1,8 @@ import { CusProductStatus } from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { AttachBillingContext } from "../types/attachBillingContext"; +import type { + AttachBillingContext, + AutumnBillingPlan, +} from "@/internal/billing/v2/types"; /** * Computes the updates to apply to the current customer product during an attach transition. diff --git a/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts b/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts index 6ff77443b..4d8c48070 100644 --- a/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts +++ b/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts @@ -1,8 +1,10 @@ import { filterUnchangedPricesFromLineItems } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { applyStripeDiscountsToLineItems } from "@/internal/billing/v2/providers/stripe/utils/discounts/applyStripeDiscountsToLineItems"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { AttachBillingContext } from "../types/attachBillingContext"; +import type { + AttachBillingContext, + AutumnBillingPlan, +} from "@/internal/billing/v2/types"; /** * Finalizes the attach billing plan by: diff --git a/server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts b/server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts new file mode 100644 index 000000000..144bc9042 --- /dev/null +++ b/server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts @@ -0,0 +1,128 @@ +import { + type AttachParamsV0, + cusProductToPrices, + cusProductToProcessorType, + ErrCode, + isPrepaidPrice, + ProcessorType, + RecaseError, + type UsagePriceConfig, +} from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { + AttachBillingContext, + AutumnBillingPlan, +} from "@/internal/billing/v2/types"; + +/** + * Validates that we're not trying to modify a customer managed by an external PSP like RevenueCat. + */ +const handleExternalPSPErrors = ({ + billingContext, +}: { + billingContext: AttachBillingContext; +}) => { + const { currentCustomerProduct } = billingContext; + + if (!currentCustomerProduct) return; + + const processorType = cusProductToProcessorType(currentCustomerProduct); + if (processorType === ProcessorType.RevenueCat) { + throw new RecaseError({ + message: `Cannot attach '${billingContext.attachProduct.name}' because the customer's current product is managed by RevenueCat.`, + }); + } +}; + +/** + * Validates that prepaid prices have quantities specified in options. + */ +const handlePrepaidQuantityErrors = ({ + autumnBillingPlan, + billingContext, +}: { + autumnBillingPlan: AutumnBillingPlan; + billingContext: AttachBillingContext; +}) => { + // Skip validation if going to checkout (quantities can be collected there) + if (billingContext.checkoutMode === "stripe_checkout") return; + + const newCustomerProduct = autumnBillingPlan.insertCustomerProducts?.[0]; + if (!newCustomerProduct) return; + + const newPrices = cusProductToPrices({ cusProduct: newCustomerProduct }); + const prepaidPrices = newPrices.filter(isPrepaidPrice); + + if (prepaidPrices.length === 0) return; + + const options = newCustomerProduct.options ?? []; + const missingFeatures: string[] = []; + + for (const price of prepaidPrices) { + const config = price.config as UsagePriceConfig; + const internalFeatureId = config.internal_feature_id; + + const hasOption = options.some( + (opt) => opt.internal_feature_id === internalFeatureId, + ); + + if (!hasOption) { + const cusEnt = newCustomerProduct.customer_entitlements?.find( + (ce) => ce.entitlement.internal_feature_id === internalFeatureId, + ); + const featureId = cusEnt?.entitlement.feature_id ?? internalFeatureId; + missingFeatures.push(featureId); + } + } + + if (missingFeatures.length > 0) { + throw new RecaseError({ + message: `Missing quantity options for prepaid features: ${missingFeatures.join(", ")}`, + code: ErrCode.InvalidOptions, + statusCode: 400, + }); + } +}; + +/** + * Validates that negative quantities are not passed. + */ +const handleNegativeQuantityErrors = ({ + params, +}: { + params: AttachParamsV0; +}) => { + for (const option of params.options ?? []) { + if (option.quantity !== undefined && option.quantity < 0) { + throw new RecaseError({ + message: "Quantity cannot be negative", + code: ErrCode.InvalidOptions, + statusCode: 400, + }); + } + } +}; + +/** + * Validates attach v2 request before executing the billing plan. + */ +export const handleAttachV2Errors = ({ + ctx: _ctx, + billingContext, + autumnBillingPlan, + params, +}: { + ctx: AutumnContext; + billingContext: AttachBillingContext; + autumnBillingPlan: AutumnBillingPlan; + params: AttachParamsV0; +}) => { + // 1. External PSP errors (RevenueCat) + handleExternalPSPErrors({ billingContext }); + + // 2. Negative quantity errors + handleNegativeQuantityErrors({ params }); + + // 3. Prepaid quantity errors + handlePrepaidQuantityErrors({ autumnBillingPlan, billingContext }); +}; diff --git a/server/src/internal/billing/v2/attach/handleAttachV2.ts b/server/src/internal/billing/v2/attach/handleAttachV2.ts index 011b3f5f6..91c0beb35 100644 --- a/server/src/internal/billing/v2/attach/handleAttachV2.ts +++ b/server/src/internal/billing/v2/attach/handleAttachV2.ts @@ -1,11 +1,18 @@ -import { AttachV0ParamsSchema } from "@autumn/shared"; +import { AttachParamsV0Schema, RecaseError } from "@autumn/shared"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; +import { executeBillingPlan } from "../execute/executeBillingPlan"; +import { evaluateStripeBillingPlan } from "../providers/stripe/actionBuilders/evaluateStripeBillingPlan"; +import { logStripeBillingPlan } from "../providers/stripe/logs/logStripeBillingPlan"; +import { logStripeBillingResult } from "../providers/stripe/logs/logStripeBillingResult"; +import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse"; import { computeAttachPlan } from "./compute/computeAttachPlan"; +import { handleAttachV2Errors } from "./errors/handleAttachV2Errors"; import { logAttachContext } from "./logs/logAttachContext"; import { setupAttachBillingContext } from "./setup/setupAttachBillingContext"; export const handleAttachV2 = createRoute({ - body: AttachV0ParamsSchema, + body: AttachParamsV0Schema, lock: process.env.NODE_ENV !== "development" ? { @@ -35,28 +42,56 @@ export const handleAttachV2 = createRoute({ logAttachContext({ ctx, billingContext }); // 2. Compute - const autumnPlan = computeAttachPlan({ + const autumnBillingPlan = computeAttachPlan({ ctx, attachBillingContext: billingContext, }); - ctx.logger.info("Attach V2 autumn plan:", { - insertCustomerProducts: autumnPlan.insertCustomerProducts.map((p) => ({ - id: p.id, - productId: p.product.id, - status: p.status, - startsAt: p.starts_at, - })), - updateCustomerProduct: autumnPlan.updateCustomerProduct - ? { - id: autumnPlan.updateCustomerProduct.customerProduct.id, - updates: autumnPlan.updateCustomerProduct.updates, - } - : undefined, - deleteCustomerProduct: autumnPlan.deleteCustomerProduct?.id, - lineItemsCount: autumnPlan.lineItems?.length ?? 0, + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); + + // 3. Errors + handleAttachV2Errors({ + ctx, + billingContext, + autumnBillingPlan, + params: body, }); - return c.json({ customer_id: body.customer_id }, 200); + // 4. Handle checkout mode (redirect to Stripe checkout) + if (billingContext.checkoutMode !== null) { + throw new RecaseError({ + message: `Checkout flow not yet implemented for attach v2 (checkoutMode: ${billingContext.checkoutMode}). Please add a payment method to the customer first.`, + statusCode: 400, + }); + } + + // 5. Evaluate Stripe billing plan + const stripeBillingPlan = await evaluateStripeBillingPlan({ + ctx, + billingContext, + autumnBillingPlan, + }); + + logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); + + // 6. Execute billing plan + const billingResult = await executeBillingPlan({ + ctx, + billingContext, + billingPlan: { + autumn: autumnBillingPlan, + stripe: stripeBillingPlan, + }, + }); + + logStripeBillingResult({ ctx, result: billingResult.stripe }); + + // 7. Format response + const response = billingResultToResponse({ + billingContext, + billingResult, + }); + + return c.json(response, 200); }, }); diff --git a/server/src/internal/billing/v2/attach/logs/logAttachContext.ts b/server/src/internal/billing/v2/attach/logs/logAttachContext.ts index 50417f5ba..eca1457b9 100644 --- a/server/src/internal/billing/v2/attach/logs/logAttachContext.ts +++ b/server/src/internal/billing/v2/attach/logs/logAttachContext.ts @@ -1,7 +1,7 @@ import { formatMs } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { AttachBillingContext } from "@/internal/billing/v2/types"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; -import type { AttachBillingContext } from "../types/attachBillingContext"; export const logAttachContext = ({ ctx, diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts index ac76777ed..caa9c2a38 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts +++ b/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts @@ -1,10 +1,10 @@ -import { type AttachV0Params, notNullish } from "@autumn/shared"; +import { type AttachParamsV0, notNullish } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { setupStripeBillingContext } from "@/internal/billing/v2/providers/stripe/setup/setupStripeBillingContext"; import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext"; import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext"; import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext"; -import type { AttachBillingContext } from "../types/attachBillingContext"; +import type { AttachBillingContext } from "@/internal/billing/v2/types"; import { setupAttachCheckoutMode } from "./setupAttachCheckoutMode"; import { setupAttachEndOfCycleMs } from "./setupAttachEndOfCycleMs"; import { setupAttachProductContext } from "./setupAttachProductContext"; @@ -18,7 +18,7 @@ export const setupAttachBillingContext = async ({ params, }: { ctx: AutumnContext; - params: AttachV0Params; + params: AttachParamsV0; }): Promise => { const fullCustomer = await setupFullCustomerContext({ ctx, @@ -72,6 +72,8 @@ export const setupAttachBillingContext = async ({ const checkoutMode = setupAttachCheckoutMode({ paymentMethod, redirectMode: params.redirect_mode, + attachProduct, + stripeSubscription, }); return { diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts b/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts index c00167cfc..d1bfc5150 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts +++ b/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts @@ -1,19 +1,70 @@ -import type { CheckoutMode, RedirectMode } from "@autumn/shared"; +import { + type FullProduct, + isFreeProduct, + isOneOffProduct, + type RedirectMode, +} from "@autumn/shared"; import type Stripe from "stripe"; +import type { CheckoutMode } from "@/internal/billing/v2/types"; /** - * Determines the checkout mode based on payment method availability and redirect preference. + * Determines the checkout mode for attach operations. + * + * Checkout modes: + * - `stripe_checkout`: Redirect to Stripe Checkout to collect payment method + * - `autumn_checkout`: Redirect to Autumn confirmation page (has PM, but redirect_mode: "always") + * - `null`: Direct billing (charge existing PM or no payment needed) + * + * Decision tree for redirect_mode: "when_required" (default): + * + * NO PAYMENT METHOD: + * A. Product is one-off → stripe_checkout (mode: "payment") + * B. No existing subscription + product is paid recurring → stripe_checkout (mode: "subscription") + * C. Existing subscription + product is paid recurring → direct billing (update sub, invoice open) + * D. Product is free → direct billing (no action needed) + * + * HAS PAYMENT METHOD: + * → Always direct billing (charge PM, handle failures via open invoice) + * + * redirect_mode: "always": + * → autumn_checkout (regardless of PM status) - NOT YET IMPLEMENTED */ export const setupAttachCheckoutMode = ({ paymentMethod, redirectMode, + attachProduct, + stripeSubscription, }: { paymentMethod?: Stripe.PaymentMethod; redirectMode?: RedirectMode; + attachProduct: FullProduct; + stripeSubscription?: Stripe.Subscription; }): CheckoutMode => { const hasPaymentMethod = !!paymentMethod; + const hasExistingSubscription = !!stripeSubscription; - if (!hasPaymentMethod) return "stripe_checkout"; - if (redirectMode === "always") return "autumn_checkout"; - return null; + const prices = attachProduct.prices; + const productIsOneOff = isOneOffProduct({ prices }); + const productIsFree = isFreeProduct({ prices }); + const productIsPaidRecurring = !productIsOneOff && !productIsFree; + + const getStripeCheckoutOrDirectBilling = () => { + // A. if no payment method + if (hasPaymentMethod) return null; + + if (productIsOneOff) return "stripe_checkout"; + + if (!hasExistingSubscription && productIsPaidRecurring) + return "stripe_checkout"; + + return null; + }; + + const checkoutMode = getStripeCheckoutOrDirectBilling(); + + if (checkoutMode === null && redirectMode === "always") { + return "autumn_checkout"; + } + + return checkoutMode; }; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts b/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts index 58d35396b..9fabe3863 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts +++ b/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts @@ -2,10 +2,10 @@ import { cusProductToPrices, type FullCusProduct, getCycleEnd, - type PlanTiming, } from "@autumn/shared"; import type Stripe from "stripe"; import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils"; +import type { PlanTiming } from "@/internal/billing/v2/types"; import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils"; /** diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts index 21da87607..9bccfd4e4 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts +++ b/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts @@ -1,4 +1,4 @@ -import type { AttachV0Params } from "@autumn/shared"; +import type { AttachParamsV0 } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { ProductService } from "@/internal/products/ProductService"; import { setupCustomFullProduct } from "../../setup/setupCustomFullProduct"; @@ -11,7 +11,7 @@ export const setupAttachProductContext = async ({ params, }: { ctx: AutumnContext; - params: AttachV0Params; + params: AttachParamsV0; }) => { const { db, org, env } = ctx; diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts b/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts index 98b5be486..8a307aba6 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts +++ b/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts @@ -6,8 +6,8 @@ import { findMainScheduledCustomerProductByGroup, isOneOffProduct, isProductUpgrade, - type PlanTiming, } from "@autumn/shared"; +import type { PlanTiming } from "@/internal/billing/v2/types"; /** * Sets up the transition context for attaching a product. @@ -33,18 +33,14 @@ export const setupAttachTransitionContext = ({ }; } - const internalEntityId = fullCustomer.entity?.internal_id; - const currentCustomerProduct = findMainActiveCustomerProductByGroup({ fullCus: fullCustomer, productGroup: attachProduct.group, - internalEntityId, }); const scheduledCustomerProduct = findMainScheduledCustomerProductByGroup({ fullCustomer, productGroup: attachProduct.group, - internalEntityId, }); // Compute planTiming (upgrade = immediate, downgrade = end_of_cycle) diff --git a/server/src/internal/billing/v2/attach/types/attachBillingContext.ts b/server/src/internal/billing/v2/attach/types/attachBillingContext.ts deleted file mode 100644 index 53aa67f27..000000000 --- a/server/src/internal/billing/v2/attach/types/attachBillingContext.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { - CheckoutMode, - FullCusProduct, - FullProduct, - PlanTiming, -} from "@autumn/shared"; -import type { BillingContext } from "../../billingContext"; - -export interface AttachBillingContext extends BillingContext { - // The product being attached - attachProduct: FullProduct; - - // Transition context (only for main recurring products) - currentCustomerProduct?: FullCusProduct; // To transition from - scheduledCustomerProduct?: FullCusProduct; // To delete - - // Timing - planTiming: PlanTiming; - endOfCycleMs?: number; // Only needed if planTiming === "end_of_cycle" - - // Checkout - checkoutMode: CheckoutMode; -} diff --git a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts index 420bb1971..7d9bda21e 100644 --- a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts +++ b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts @@ -1,5 +1,5 @@ import type { FullCusProduct, LineItem } from "@autumn/shared"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import type { AutumnContext } from "../../../../../honoUtils/HonoEnv"; import { customerProductToLineItems } from "../../utils/lineItems/customerProductToLineItems"; import { logBuildAutumnLineItems } from "./logBuildAutumnLineItems"; diff --git a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildSharedSubscriptionTrialLineItems.ts b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildSharedSubscriptionTrialLineItems.ts index c6f2f5cc6..b07ae0d2a 100644 --- a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildSharedSubscriptionTrialLineItems.ts +++ b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildSharedSubscriptionTrialLineItems.ts @@ -2,8 +2,8 @@ import { cp, type FullCusProduct, type LineItem } from "@autumn/shared"; import chalk from "chalk"; import type { Logger } from "@/external/logtail/logtailUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; import { billingPlanToUpdatedCustomerProduct } from "@/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct"; import { customerProductToLineItems } from "@/internal/billing/v2/utils/lineItems/customerProductToLineItems"; diff --git a/server/src/internal/billing/v2/compute/computeAutumnUtils/filterLineItemsForTrialTransition.ts b/server/src/internal/billing/v2/compute/computeAutumnUtils/filterLineItemsForTrialTransition.ts index 2a1102867..a981e5cf4 100644 --- a/server/src/internal/billing/v2/compute/computeAutumnUtils/filterLineItemsForTrialTransition.ts +++ b/server/src/internal/billing/v2/compute/computeAutumnUtils/filterLineItemsForTrialTransition.ts @@ -1,6 +1,6 @@ import { isOneOffPrice, type LineItem } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; /** diff --git a/server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts b/server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts index b69bfa019..cfdd4a521 100644 --- a/server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan.ts @@ -1,5 +1,5 @@ import { cp } from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; /** * Adds a Stripe subscription ID to a billing plan. diff --git a/server/src/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan.ts b/server/src/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan.ts index 7c2432c48..823f6501e 100644 --- a/server/src/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan.ts @@ -2,7 +2,7 @@ import { CusProductStatus, cp } from "@autumn/shared"; import type { AutumnBillingPlan, StripeBillingPlan, -} from "@/internal/billing/v2/types/billingPlan"; +} from "@/internal/billing/v2/types"; export const addStripeSubscriptionScheduleIdToBillingPlan = ({ autumnBillingPlan, diff --git a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts index e1e1b314e..acdb84b2a 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements.ts @@ -1,5 +1,5 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService"; /** diff --git a/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts b/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts index c9905fc7a..613f9f17a 100644 --- a/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts @@ -1,7 +1,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { insertNewCusProducts } from "@/internal/billing/v2/execute/executeAutumnActions/insertNewCusProducts"; import { updateCustomerEntitlements } from "@/internal/billing/v2/execute/executeAutumnActions/updateCustomerEntitlements"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService"; import { FreeTrialService } from "@/internal/products/free-trials/FreeTrialService"; diff --git a/server/src/internal/billing/v2/execute/executeBillingPlan.ts b/server/src/internal/billing/v2/execute/executeBillingPlan.ts index 83d09a57f..b961989cb 100644 --- a/server/src/internal/billing/v2/execute/executeBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/executeBillingPlan.ts @@ -1,9 +1,9 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { executeAutumnBillingPlan } from "@/internal/billing/v2/execute/executeAutumnBillingPlan"; import { executeStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { BillingResult } from "@/internal/billing/v2/types/billingResult"; +import type { BillingPlan } from "@/internal/billing/v2/types"; +import type { BillingResult } from "@/internal/billing/v2/types"; import { billingPlanToSendProductsUpdated } from "@/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated"; export const executeBillingPlan = async ({ diff --git a/server/src/internal/billing/v2/execute/executeDeferredBillingPlan.ts b/server/src/internal/billing/v2/execute/executeDeferredBillingPlan.ts index 192d4513f..b52d4e894 100644 --- a/server/src/internal/billing/v2/execute/executeDeferredBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/executeDeferredBillingPlan.ts @@ -2,7 +2,7 @@ import type { Metadata } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { executeAutumnBillingPlan } from "@/internal/billing/v2/execute/executeAutumnBillingPlan"; import { executeStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan"; -import type { DeferredAutumnBillingPlanData } from "@/internal/billing/v2/types/billingPlan"; +import type { DeferredAutumnBillingPlanData } from "@/internal/billing/v2/types"; import { MetadataService } from "@/internal/metadata/MetadataService"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; diff --git a/server/src/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan.ts b/server/src/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan.ts index cd57ca62e..953f38c89 100644 --- a/server/src/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan.ts +++ b/server/src/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan.ts @@ -1,4 +1,4 @@ -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const removeStripeSubscriptionIdFromBillingPlan = ({ autumnBillingPlan, diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceAction.ts index 78371cd58..489a17ec6 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceAction.ts @@ -1,5 +1,5 @@ import type { LineItem } from "@autumn/shared"; -import type { StripeInvoiceAction } from "../../../types/billingPlan"; +import type { StripeInvoiceAction } from "../../../types"; import { lineItemsToInvoiceAddLinesParams } from "../utils/invoiceLines/lineItemsToInvoiceAddLinesParams"; /** diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceItemsAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceItemsAction.ts index 06f4712b7..5db28f276 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceItemsAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeInvoiceItemsAction.ts @@ -1,6 +1,6 @@ import type { LineItem } from "@autumn/shared"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { StripeInvoiceItemsAction } from "../../../types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { StripeInvoiceItemsAction } from "../../../types"; import { lineItemsToCreateInvoiceItemsParams } from "../utils/invoiceLines/lineItemsToCreateInvoiceItemsParams"; /** diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts index 9913c30fa..ed3a130e9 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts @@ -1,6 +1,6 @@ import type { FullCusProduct } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { buildStripeSubscriptionItemsUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; import { buildStripeSubscriptionCreateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction"; import { buildStripeSubscriptionUpdateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction"; @@ -9,7 +9,7 @@ import type { AutumnBillingPlan, StripeSubscriptionAction, StripeSubscriptionScheduleAction, -} from "@/internal/billing/v2/types/billingPlan"; +} from "@/internal/billing/v2/types"; export const buildStripeSubscriptionAction = ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts index 48ae87bc5..390b637ae 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts @@ -5,10 +5,10 @@ import { isCustomerProductOnStripeSubscriptionSchedule, } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { buildStripePhasesUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate"; import type Stripe from "stripe"; -import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types/billingPlan"; +import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types"; // ═══════════════════════════════════════════════════════════════════════════════ // TYPES diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts index dd63274ca..c075054eb 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts @@ -2,7 +2,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { buildStripeSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction"; import { shouldCreateManualStripeInvoice } from "@/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice"; import { autumnBillingPlanToFinalFullCustomer } from "@/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer"; -import type { BillingContext } from "../../../billingContext"; +import type { BillingContext } from "../../../types"; import { buildStripeInvoiceAction } from "../../../providers/stripe/actionBuilders/buildStripeInvoiceAction"; import { buildStripeInvoiceItemsAction } from "../../../providers/stripe/actionBuilders/buildStripeInvoiceItemsAction"; import { buildStripeSubscriptionAction } from "../../../providers/stripe/actionBuilders/buildStripeSubscriptionAction"; @@ -11,7 +11,7 @@ import type { StripeBillingPlan, StripeInvoiceAction, StripeInvoiceItemsAction, -} from "../../../types/billingPlan"; +} from "../../../types"; import { initStripeResourcesForBillingPlan } from "../utils/common/initStripeResourcesForProducts"; export const evaluateStripeBillingPlan = async ({ diff --git a/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts b/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts index 9a69e4c27..ed383db69 100644 --- a/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts +++ b/server/src/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors.ts @@ -1,5 +1,5 @@ import { ErrCode, InternalError } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; /** * Validates Stripe-specific billing context requirements before executing billing plan. diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts index ecfbd2cfb..ec80d7bf0 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts @@ -1,13 +1,13 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { addStripeSubscriptionScheduleIdToBillingPlan } from "@/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan"; import { executeStripeInvoiceAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction"; import { executeStripeSubscriptionAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction"; import { executeStripeSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction"; import { createStripeInvoiceItems } from "@/internal/billing/v2/providers/stripe/utils/invoices/stripeInvoiceOps"; -import { StripeBillingStage } from "@/internal/billing/v2/types/autumnBillingPlan"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types/billingResult"; +import { StripeBillingStage } from "@/internal/billing/v2/types"; +import type { BillingPlan } from "@/internal/billing/v2/types"; +import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; export const executeStripeBillingPlan = async ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts index d929eb222..62b67672b 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts @@ -1,14 +1,14 @@ import { ms } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { shouldDeferBillingPlan } from "@/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan"; import { createInvoiceForBilling } from "@/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling"; -import { StripeBillingStage } from "@/internal/billing/v2/types/autumnBillingPlan"; +import { StripeBillingStage } from "@/internal/billing/v2/types"; import type { BillingPlan, StripeInvoiceMetadata, -} from "@/internal/billing/v2/types/billingPlan"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types/billingResult"; +} from "@/internal/billing/v2/types"; +import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; import { isDeferredInvoiceMode } from "@/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode"; import { upsertInvoiceFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling"; import { insertMetadataFromBillingPlan } from "@/internal/metadata/utils/insertMetadataFromBillingPlan"; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts index ca9d10f92..b33a78985 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts @@ -3,7 +3,7 @@ import { createStripeCli } from "@/external/connect/createStripeCli"; import { isStripeSubscriptionCanceled } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils"; import { setStripeSubscriptionLock } from "@/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { addStripeSubscriptionIdToBillingPlan } from "@/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan"; import { removeStripeSubscriptionIdFromBillingPlan } from "@/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan"; import { shouldDeferBillingPlan } from "@/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan"; @@ -11,9 +11,9 @@ import { finalizeStripeInvoice } from "@/internal/billing/v2/providers/stripe/ut import { executeStripeSubscriptionOperation } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation"; import { getLatestInvoiceFromSubscriptionAction } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction"; import { getRequiredActionFromSubscriptionInvoice } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/getRequiredActionFromSubscriptionInvoice"; -import { StripeBillingStage } from "@/internal/billing/v2/types/autumnBillingPlan"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types/billingResult"; +import { StripeBillingStage } from "@/internal/billing/v2/types"; +import type { BillingPlan } from "@/internal/billing/v2/types"; +import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; import { upsertInvoiceFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling"; import { upsertSubscriptionFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertSubscriptionFromBilling"; import { insertMetadataFromBillingPlan } from "@/internal/metadata/utils/insertMetadataFromBillingPlan"; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts index 56689ca29..bb3454e8b 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts @@ -1,9 +1,9 @@ import { createStripeCli } from "@server/external/connect/createStripeCli"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import type Stripe from "stripe"; import { logSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction"; -import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types/billingPlan"; +import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; /** diff --git a/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan.ts index ee928a896..fb23edcf3 100644 --- a/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan.ts @@ -1,7 +1,7 @@ import { customerProductsToPricesWithProduct } from "@/external/stripe/subscriptionSchedules/utils/logStripeSchedulePhaseUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { StripeBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { StripeBillingPlan } from "@/internal/billing/v2/types"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; export const logStripeBillingPlan = ({ diff --git a/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingResult.ts b/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingResult.ts index f1d872a1e..cf36758bd 100644 --- a/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingResult.ts +++ b/server/src/internal/billing/v2/providers/stripe/logs/logStripeBillingResult.ts @@ -1,6 +1,6 @@ import { stripeInvoiceToStripeSubscriptionId } from "@/external/stripe/invoices/utils/convertStripeInvoice"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types/billingResult"; +import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; const formatInvoice = (invoice: StripeBillingPlanResult["stripeInvoice"]) => { diff --git a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts index aeec69fd7..4f87870d9 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/common/initStripeResourcesForProducts.ts @@ -2,8 +2,8 @@ import { cusProductToProduct } from "@autumn/shared"; import { createStripeCli } from "@/external/connect/createStripeCli"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { checkStripeProductExists } from "@/internal/products/productUtils"; export const initStripeResourcesForBillingPlan = async ({ diff --git a/server/src/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan.ts index 9b85d84a4..dfac03357 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan.ts @@ -1,6 +1,6 @@ import type { BillingResponseRequiredAction } from "@autumn/shared"; import type Stripe from "stripe"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { isDeferredInvoiceMode } from "@/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode"; export const shouldDeferBillingPlan = ({ diff --git a/server/src/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling.ts b/server/src/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling.ts index bd0bc7083..48f748461 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling.ts @@ -1,4 +1,4 @@ -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { type PayInvoiceResult, payStripeInvoice, @@ -13,7 +13,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { StripeInvoiceAction, StripeInvoiceMetadata, -} from "@/internal/billing/v2/types/billingPlan"; +} from "@/internal/billing/v2/types"; export const createInvoiceForBilling = async ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice.ts b/server/src/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice.ts index 55c2830ae..b4ed8f3bd 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice.ts @@ -1,6 +1,6 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { willStripeSubscriptionUpdateCreateInvoice } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice"; -import type { StripeSubscriptionAction } from "@/internal/billing/v2/types/billingPlan"; +import type { StripeSubscriptionAction } from "@/internal/billing/v2/types"; export const shouldCreateManualStripeInvoice = ({ billingContext, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs.ts b/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs.ts index 72a1563f0..56ae2a3be 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs.ts @@ -1,6 +1,6 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { customerProductToStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const billingPlanToOneOffStripeItemSpecs = ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts index b7386b6e7..dc675c4f3 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts @@ -9,7 +9,7 @@ import type Stripe from "stripe"; import { stripeSubscriptionItemToStripePriceId } from "@/external/stripe/subscriptions/subscriptionItems/utils/convertStripeSubscriptionItemUtils"; import { findStripeSubscriptionItemByStripePriceId } from "@/external/stripe/subscriptions/subscriptionItems/utils/findStripeSubscriptionItemUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { findStripeItemSpecByStripePriceId } from "./findStripeItemSpec"; /** diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs.ts index c9e348a82..0801a1542 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs.ts @@ -15,7 +15,7 @@ import { import { cusEntToInvoiceUsage } from "@shared/utils/cusEntUtils/overageUtils/cusEntToInvoiceUsage"; import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; /** * Convert a customer product to stripe item specs diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts index 8410dde6e..e034b0e4a 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts @@ -6,7 +6,7 @@ import { import type Stripe from "stripe"; import { logPhase } from "@/external/stripe/subscriptionSchedules/utils/logStripeSchedulePhaseUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { customerProductToStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs"; import { isCustomerProductActiveDuringPeriod } from "@/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/isCustomerProductActiveAtEpochMs"; import { buildTransitionPoints } from "./buildTransitionPoints"; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts index 2dcc76820..e65ea00e1 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction.ts @@ -1,7 +1,7 @@ import { formatSecondsToDate } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; -import type { StripeSubscriptionScheduleAction } from "@server/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { StripeSubscriptionScheduleAction } from "@server/internal/billing/v2/types"; import type Stripe from "stripe"; import { billingContextFormatPriceByStripePriceId } from "@/internal/billing/v2/utils/billingContextPriceLookup"; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction.ts index f202acce9..2b64c7442 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction.ts @@ -1,7 +1,7 @@ import { msToSeconds } from "@autumn/shared"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; export const buildStripeSubscriptionCreateAction = ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts index 9a52bdccc..e9cc9b425 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction.ts @@ -2,11 +2,11 @@ import { msToSeconds } from "@shared/utils/common/unixUtils"; import { notNullish } from "@shared/utils/utils"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import type { StripeSubscriptionAction, StripeSubscriptionScheduleAction, -} from "@/internal/billing/v2/types/billingPlan"; +} from "@/internal/billing/v2/types"; export const buildStripeSubscriptionUpdateAction = ({ // biome-ignore lint/correctness/noUnusedFunctionParameters: might be used in the future diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation.ts index 1391d3584..2c2f1d5ac 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation.ts @@ -1,8 +1,8 @@ import { InternalError, nullish } from "@autumn/shared"; import { createStripeCli } from "@/external/connect/createStripeCli"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { StripeSubscriptionAction } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { StripeSubscriptionAction } from "@/internal/billing/v2/types"; export const executeStripeSubscriptionOperation = async ({ ctx, diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction.ts index 833084d37..5a81d242c 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction.ts @@ -1,7 +1,7 @@ import type Stripe from "stripe"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { willStripeSubscriptionUpdateCreateInvoice } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice"; -import type { StripeSubscriptionAction } from "@/internal/billing/v2/types/billingPlan"; +import type { StripeSubscriptionAction } from "@/internal/billing/v2/types"; /** * Returns the latest invoice from a subscription action if one was created. diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice.ts index d37d153d2..3f17a71d1 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptions/willStripeSubscriptionUpdateCreateInvoice.ts @@ -1,5 +1,5 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { StripeSubscriptionAction } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { StripeSubscriptionAction } from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; export const willStripeSubscriptionUpdateCreateInvoice = ({ diff --git a/server/src/internal/billing/v2/setup/setupBillingCycleAnchor.ts b/server/src/internal/billing/v2/setup/setupBillingCycleAnchor.ts index f7e05846b..63f2f0f2e 100644 --- a/server/src/internal/billing/v2/setup/setupBillingCycleAnchor.ts +++ b/server/src/internal/billing/v2/setup/setupBillingCycleAnchor.ts @@ -9,7 +9,7 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import { isStripeSubscriptionTrialing } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils"; -import type { TrialContext } from "@/internal/billing/v2/billingContext"; +import type { TrialContext } from "@/internal/billing/v2/types"; /** * Determine the billing cycle anchor based on product transitions. diff --git a/server/src/internal/billing/v2/setup/setupCancelMode.ts b/server/src/internal/billing/v2/setup/setupCancelMode.ts index 5073e42ef..aca20bb52 100644 --- a/server/src/internal/billing/v2/setup/setupCancelMode.ts +++ b/server/src/internal/billing/v2/setup/setupCancelMode.ts @@ -1,5 +1,5 @@ import type { UpdateSubscriptionV0Params } from "@shared/api/billing/updateSubscription/updateSubscriptionV0Params"; -import type { CancelAction } from "@shared/api/common/cancelMode"; +import type { CancelAction } from "@autumn/shared"; /** * Setup cancel action from params diff --git a/server/src/internal/billing/v2/setup/setupRefundBehavior.ts b/server/src/internal/billing/v2/setup/setupRefundBehavior.ts deleted file mode 100644 index 2e2c0a18a..000000000 --- a/server/src/internal/billing/v2/setup/setupRefundBehavior.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { RefundBehavior } from "@autumn/shared"; -import type { UpdateSubscriptionV0Params } from "@shared/api/billing/updateSubscription/updateSubscriptionV0Params"; - -export const setupRefundBehavior = ({ - params, -}: { - params: UpdateSubscriptionV0Params; -}): RefundBehavior | undefined => { - return params.refund_behavior; -}; diff --git a/server/src/internal/billing/v2/setup/setupTrialContext.ts b/server/src/internal/billing/v2/setup/setupTrialContext.ts index d82135613..b595da803 100644 --- a/server/src/internal/billing/v2/setup/setupTrialContext.ts +++ b/server/src/internal/billing/v2/setup/setupTrialContext.ts @@ -11,7 +11,7 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import { isStripeSubscriptionTrialing } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils"; -import type { TrialContext } from "@/internal/billing/v2/billingContext"; +import type { TrialContext } from "@/internal/billing/v2/types"; import { initFreeTrial } from "@/internal/products/free-trials/initFreeTrial"; export const setupTrialContext = ({ diff --git a/server/src/internal/billing/v2/types/attachBillingContext.ts b/server/src/internal/billing/v2/types/attachBillingContext.ts new file mode 100644 index 000000000..e4b34fd82 --- /dev/null +++ b/server/src/internal/billing/v2/types/attachBillingContext.ts @@ -0,0 +1,30 @@ +import type { FullCusProduct, FullProduct } from "@autumn/shared"; +import { z } from "zod/v4"; +import type { BillingContext } from "./billingContext"; + +// Plan timing for attach operations +export const PlanTimingSchema = z.enum(["immediate", "end_of_cycle"]); +export type PlanTiming = z.infer; + +// Checkout mode for attach operations +export const CheckoutModeSchema = z + .enum(["stripe_checkout", "autumn_checkout"]) + .nullable(); + +export type CheckoutMode = z.infer; + +export interface AttachBillingContext extends BillingContext { + // The product being attached + attachProduct: FullProduct; + + // Transition context (only for main recurring products) + currentCustomerProduct?: FullCusProduct; // To transition from + scheduledCustomerProduct?: FullCusProduct; // To delete + + // Timing + planTiming: PlanTiming; + endOfCycleMs?: number; // Only needed if planTiming === "end_of_cycle" + + // Checkout + checkoutMode: CheckoutMode; +} diff --git a/server/src/internal/billing/v2/types/autumnBillingPlan.ts b/server/src/internal/billing/v2/types/autumnBillingPlan.ts index c7ac7b089..fdcadf58e 100644 --- a/server/src/internal/billing/v2/types/autumnBillingPlan.ts +++ b/server/src/internal/billing/v2/types/autumnBillingPlan.ts @@ -11,8 +11,8 @@ import { PriceSchema, } from "@autumn/shared"; import { z } from "zod/v4"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { BillingPlan } from "@/internal/billing/v2/types"; export const UpdateCustomerEntitlementSchema = z.object({ customerEntitlement: FullCustomerEntitlementSchema, diff --git a/server/src/internal/billing/v2/billingContext.ts b/server/src/internal/billing/v2/types/billingContext.ts similarity index 91% rename from server/src/internal/billing/v2/billingContext.ts rename to server/src/internal/billing/v2/types/billingContext.ts index 4029a9b90..e51eb62a9 100644 --- a/server/src/internal/billing/v2/billingContext.ts +++ b/server/src/internal/billing/v2/types/billingContext.ts @@ -1,14 +1,13 @@ import type { + CancelAction, Entitlement, FeatureOptions, FreeTrial, FullCusProduct, FullProduct, Price, - RefundBehavior, StripeDiscountWithCoupon, } from "@autumn/shared"; -import type { CancelAction } from "@shared/api/common/cancelMode"; import type { FullCustomer } from "@shared/models/cusModels/fullCusModel"; import type Stripe from "stripe"; import { z } from "zod/v4"; @@ -58,9 +57,6 @@ export interface BillingContext { // Cancel action (used by update subscription for uncancel) cancelAction?: CancelAction; - - // Refund behavior for negative invoice totals (downgrades) - refundBehavior?: RefundBehavior; } export interface UpdateSubscriptionBillingContext extends BillingContext { diff --git a/server/src/internal/billing/v2/types/cancelTypes.ts b/server/src/internal/billing/v2/types/cancelTypes.ts deleted file mode 100644 index cdfee7155..000000000 --- a/server/src/internal/billing/v2/types/cancelTypes.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CusProductStatus } from "@autumn/shared"; - -// Re-export CancelAction from shared for convenience -export type { CancelAction } from "@shared/api/common/cancelMode"; - -/** - * Updates to apply to a customer product when canceling or uncanceling. - */ -interface CancelUpdates { - canceled: boolean; - canceled_at: number | null; - ended_at: number | null; - status?: CusProductStatus; -} diff --git a/server/src/internal/billing/v2/types/index.ts b/server/src/internal/billing/v2/types/index.ts new file mode 100644 index 000000000..eb4427131 --- /dev/null +++ b/server/src/internal/billing/v2/types/index.ts @@ -0,0 +1,12 @@ +export * from "./attachBillingContext"; +export * from "./autumnBillingPlan"; +export * from "./billingContext"; +export * from "./billingPlan"; +export * from "./billingResult"; + +// Stripe billing plan types +export * from "./stripeBillingPlan/stripeBillingPlan"; +export * from "./stripeBillingPlan/stripeInvoiceAction"; +export * from "./stripeBillingPlan/stripeInvoiceItemsAction"; +export * from "./stripeBillingPlan/stripeSubscriptionAction"; +export * from "./stripeBillingPlan/stripeSubscriptionScheduleAction"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts index ad192e4a1..2d241d153 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts @@ -1,5 +1,5 @@ import type { FullCusProduct, LineItem } from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import type { CancelUpdates } from "./computeCancelUpdates"; /** diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts index a0992f4e3..4fb461f18 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts @@ -1,5 +1,5 @@ -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { computeCustomerProductToDelete } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete"; /** diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts index ffd70f811..d620f65e4 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts @@ -1,5 +1,5 @@ import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; -import type { CancelAction } from "@/internal/billing/v2/types/cancelTypes"; +import type { CancelAction } from "@autumn/shared"; /** * Computes cancel-related fields for a new customer product. diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts index 834eb56e3..1a77a8f99 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts @@ -1,6 +1,6 @@ import type { LineItem } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems"; /** diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts index e3f2c2f50..49a5df32a 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts @@ -1,6 +1,6 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { applyUncancelToPlan } from "@/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan"; import { applyCancelPlan } from "./applyCancelPlan"; import { computeCancelLineItems } from "./computeCancelLineItems"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts index 69e67ceb7..213789b88 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts @@ -1,5 +1,5 @@ import { CusProductStatus } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; export interface CancelUpdates { canceled: boolean; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts index 6be3ebcb4..ab3209480 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts @@ -3,7 +3,7 @@ import { type FullCusProduct, findMainScheduledCustomerProductByGroup, } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; /** * Finds an existing scheduled customer product in the same group to delete. diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts index 247b791cd..435350c3a 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts @@ -1,6 +1,6 @@ import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct"; /** diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts index 6e743999d..f427c710f 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts @@ -1,5 +1,5 @@ import { cusProductToPrices, getCycleEnd } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils"; /** diff --git a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts index b4baaf28d..1ced87dfb 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts @@ -1,7 +1,7 @@ import type { UpdateSubscriptionV0Params } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { computeCancelPlan } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts index 9711676d4..65b3b1cbf 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts @@ -3,9 +3,9 @@ import { type UpdateSubscriptionV0Params, } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@server/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { computeDeleteCustomerProduct } from "@/internal/billing/v2/updateSubscription/compute/computeDeleteCustomerProduct"; import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts index 600bc21f4..aa940a3f4 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts @@ -1,6 +1,6 @@ import type { FullCusProduct, FullProduct } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { computeCancelFields } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields"; import { cusProductToExistingRollovers } from "@/internal/billing/v2/utils/handleExistingRollovers/cusProductToExistingRollovers"; import { cusProductToExistingUsages } from "@/internal/billing/v2/utils/handleExistingUsages/cusProductToExistingUsages"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts index ec066bc98..9b00299bc 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts @@ -4,11 +4,11 @@ import { type UpdateSubscriptionV0Params, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { buildSharedSubscriptionTrialLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildSharedSubscriptionTrialLineItems"; import { filterLineItemsForTrialTransition } from "@/internal/billing/v2/compute/computeAutumnUtils/filterLineItemsForTrialTransition"; import { applyStripeDiscountsToLineItems } from "@/internal/billing/v2/providers/stripe/utils/discounts/applyStripeDiscountsToLineItems"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const finalizeUpdateSubscriptionPlan = ({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts index 015cc4389..e420c38ba 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts @@ -11,7 +11,7 @@ import { RecaseError, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { getLineItemBillingPeriod } from "@/internal/billing/v2/utils/lineItems/getLineItemBillingPeriod"; import { calculateUpdateQuantityDifferences } from "./calculateUpdateQuantityDifferences"; import { calculateUpdateQuantityEntitlementChange } from "./calculateUpdateQuantityEntitlementChange"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts index 43f827ca0..e2f41715f 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts @@ -14,7 +14,7 @@ import { usagePriceToLineItem, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; export const computeUpdateQuantityLineItems = ({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts index 1cca88a0c..f30dda2fe 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts +++ b/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts @@ -1,6 +1,6 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { computeUpdateQuantityDetails } from "./computeUpdateQuantityDetails"; export const computeUpdateQuantityPlan = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts index 3c461194c..c65d7dd8a 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts @@ -5,8 +5,8 @@ import { RecaseError, type UpdateSubscriptionV0Params, } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; export const handleBillingBehaviorErrors = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts index 4531bded8..44f23ea24 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts @@ -4,7 +4,7 @@ import { RecaseError, type UpdateSubscriptionV0Params, } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; /** * Validates cancel: 'end_of_cycle' requests. diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts index 6f47ebfd0..62e0a202f 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts @@ -3,7 +3,7 @@ import { isCustomerProductScheduled, RecaseError, } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; export const handleCurrentCustomerProductErrors = ({ billingContext, diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts index 72ecec011..fdd181f23 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts @@ -5,8 +5,8 @@ import { type UpdateSubscriptionV0Params, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const handleCustomPlanErrors = ({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts index 775f487e1..c901a36be 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts @@ -8,8 +8,8 @@ import { type UsagePriceConfig, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { billingPlanToNewActiveCustomerProduct } from "@/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct"; const checkInputFeatureQuantitiesAreValid = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts index 5f6d8006c..f9312b0de 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts @@ -9,8 +9,8 @@ import { } from "@autumn/shared"; import { cusProductToPrices } from "@shared/utils/cusProductUtils/convertCusProduct"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; export const handleOneOffErrors = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts index 50626adf8..6f5852e13 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts @@ -1,6 +1,6 @@ import { cusProductToPrices, ErrCode, RecaseError } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { isOneOff } from "@/internal/products/productUtils"; export const handleProductTypeTransitionErrors = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleRefundBehaviorErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleRefundBehaviorErrors.ts deleted file mode 100644 index 38f2f579f..000000000 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleRefundBehaviorErrors.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - ErrCode, - RecaseError, - sumValues, - type UpdateSubscriptionV0Params, -} from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; - -/** Computes expected invoice total from line items that will be charged immediately */ -const computeExpectedInvoiceTotal = ({ - autumnBillingPlan, -}: { - autumnBillingPlan: AutumnBillingPlan; -}): number => { - const lineItems = autumnBillingPlan.lineItems ?? []; - return sumValues( - lineItems - .filter((line) => line.chargeImmediately) - .map((line) => line.finalAmount), - ); -}; - -export const handleRefundBehaviorErrors = ({ - autumnBillingPlan, - params, -}: { - autumnBillingPlan: AutumnBillingPlan; - params: UpdateSubscriptionV0Params; -}) => { - if (params.refund_behavior !== "refund_payment_method") return; - - // Check 1: refund_payment_method + next_cycle_only are incompatible - if (params.billing_behavior === "next_cycle_only") { - throw new RecaseError({ - message: - "Cannot combine refund_behavior: 'refund_payment_method' with billing_behavior: 'next_cycle_only'. These behaviors are incompatible.", - }); - } - - // Check 2: Invoice total must be negative (credit due) to issue a refund - const expectedTotal = computeExpectedInvoiceTotal({ autumnBillingPlan }); - if (expectedTotal >= 0) { - throw new RecaseError({ - message: - "Cannot use refund_behavior: 'refund_payment_method' when invoice total is not negative. Refunds can only be issued when the customer is owed a credit (e.g., downgrade scenarios).", - }); - } -}; diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts index 403938a08..52997671f 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts @@ -1,5 +1,5 @@ import { CusProductStatus, RecaseError } from "@autumn/shared"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; /** * Validates uncancel operation and throws appropriate errors. diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts b/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts index b727a6973..19a901f51 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts +++ b/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts @@ -5,9 +5,11 @@ import { } from "@autumn/shared"; import { cusProductToProcessorType } from "@shared/utils/cusProductUtils/convertCusProduct"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; import { handleStripeBillingPlanErrors } from "@/internal/billing/v2/providers/stripe/errors/handleStripeBillingPlanErrors"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { + AutumnBillingPlan, + UpdateSubscriptionBillingContext, +} from "@/internal/billing/v2/types"; import { handleCancelEndOfCycleErrors } from "@/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors"; import { handleBillingBehaviorErrors } from "./handleBillingBehaviorErrors"; import { handleCurrentCustomerProductErrors } from "./handleCurrentCustomerProductErrors"; @@ -18,7 +20,7 @@ import { handleOneOffErrors, } from "./handleOneOffErrors"; import { handleProductTypeTransitionErrors } from "./handleProductTypeTransitionErrors"; -import { handleRefundBehaviorErrors } from "./handleRefundBehaviorErrors"; + import { handleUncancelErrors } from "./handleUncancelErrors"; export const handleUpdateSubscriptionErrors = async ({ @@ -77,12 +79,6 @@ export const handleUpdateSubscriptionErrors = async ({ params, }); - // 10. Refund behavior errors - handleRefundBehaviorErrors({ - autumnBillingPlan, - params, - }); - // 11. Stripe billing plan errors (validate Stripe resources) handleStripeBillingPlanErrors({ billingContext }); }; diff --git a/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts b/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts index 5c618f4ed..dcfeefce0 100644 --- a/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts +++ b/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts @@ -3,10 +3,10 @@ import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/strip import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; import { computeUpdateSubscriptionPlan } from "./compute/computeUpdateSubscriptionPlan"; import { logUpdateSubscriptionContext } from "./logs/logUpdateSubscriptionContext"; -import { logUpdateSubscriptionPlan } from "./logs/logUpdateSubscriptionPlan"; import { setupUpdateSubscriptionBillingContext } from "./setup/setupUpdateSubscriptionBillingContext"; export const handlePreviewUpdateSubscription = createRoute({ @@ -34,7 +34,7 @@ export const handlePreviewUpdateSubscription = createRoute({ billingContext: updateSubscriptionBillingContext, params: body, }); - logUpdateSubscriptionPlan({ + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext: updateSubscriptionBillingContext, diff --git a/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts b/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts index b965d7de6..428cfe4fe 100644 --- a/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts +++ b/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts @@ -4,7 +4,7 @@ import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/l import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan"; import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; import { logUpdateSubscriptionContext } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext"; -import { logUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionPlan"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; import { billingResultToResponse } from "@/internal/billing/v2/utils/billingResult/billingResultToResponse"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; import { executeBillingPlan } from "../execute/executeBillingPlan"; @@ -45,7 +45,7 @@ export const handleUpdateSubscription = createRoute({ billingContext, params: body, }); - logUpdateSubscriptionPlan({ ctx, plan: autumnBillingPlan, billingContext }); + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); await handleUpdateSubscriptionErrors({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts b/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts index 47ec7d961..f29ca1925 100644 --- a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts +++ b/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts @@ -1,6 +1,6 @@ import { formatMs } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; +import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; export const logUpdateSubscriptionContext = ({ diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts b/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts index 54716c2e2..4d1fa883d 100644 --- a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts +++ b/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts @@ -6,12 +6,12 @@ import { setupCancelAction } from "@/internal/billing/v2/setup/setupCancelMode"; import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext"; import { setupFullCustomerContext } from "@/internal/billing/v2/setup/setupFullCustomerContext"; import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoiceModeContext"; -import { setupRefundBehavior } from "@/internal/billing/v2/setup/setupRefundBehavior"; + import { setupResetCycleAnchor } from "@/internal/billing/v2/setup/setupResetCycleAnchor"; import { setupTrialContext } from "@/internal/billing/v2/setup/setupTrialContext"; import { setupDefaultProductContext } from "@/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext"; import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext"; -import type { UpdateSubscriptionBillingContext } from "../../billingContext"; +import type { UpdateSubscriptionBillingContext } from "../../types"; /** * Fetch the context for updating a subscription @@ -99,7 +99,6 @@ export const setupUpdateSubscriptionBillingContext = async ({ }); const cancelAction = setupCancelAction({ params }); - const refundBehavior = setupRefundBehavior({ params }); return { fullCustomer, @@ -124,6 +123,5 @@ export const setupUpdateSubscriptionBillingContext = async ({ customEnts, trialContext, isCustom, - refundBehavior, }; }; diff --git a/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts b/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts index f906d367b..4283fde23 100644 --- a/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts +++ b/server/src/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer.ts @@ -1,5 +1,5 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { billingPlanToUpdatedCustomerProduct } from "@/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct"; export const autumnBillingPlanToFinalFullCustomer = ({ diff --git a/server/src/internal/billing/v2/utils/billingContext/billingContextHasTrial.ts b/server/src/internal/billing/v2/utils/billingContext/billingContextHasTrial.ts index acd2a33c4..a9c28d98a 100644 --- a/server/src/internal/billing/v2/utils/billingContext/billingContextHasTrial.ts +++ b/server/src/internal/billing/v2/utils/billingContext/billingContextHasTrial.ts @@ -1,4 +1,4 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; /** * Check if the billing context will create a trial that ends later than the current epoch. diff --git a/server/src/internal/billing/v2/utils/billingContext/getBillingCycleAnchorForDirection.ts b/server/src/internal/billing/v2/utils/billingContext/getBillingCycleAnchorForDirection.ts index 21b17729e..f9281e4c4 100644 --- a/server/src/internal/billing/v2/utils/billingContext/getBillingCycleAnchorForDirection.ts +++ b/server/src/internal/billing/v2/utils/billingContext/getBillingCycleAnchorForDirection.ts @@ -1,4 +1,4 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { getCurrentBillingCycleAnchorMs } from "@/internal/billing/v2/utils/billingContext/getCurrentBillingCycleAnchorMs"; /** diff --git a/server/src/internal/billing/v2/utils/billingContext/getCurrentBillingCycleAnchorMs.ts b/server/src/internal/billing/v2/utils/billingContext/getCurrentBillingCycleAnchorMs.ts index ff1709111..9e33a6eb5 100644 --- a/server/src/internal/billing/v2/utils/billingContext/getCurrentBillingCycleAnchorMs.ts +++ b/server/src/internal/billing/v2/utils/billingContext/getCurrentBillingCycleAnchorMs.ts @@ -1,5 +1,5 @@ import { secondsToMs } from "@autumn/shared"; -import type { BillingContext } from "../../billingContext"; +import type { BillingContext } from "../../types"; export const getCurrentBillingCycleAnchorMs = ({ billingContext, diff --git a/server/src/internal/billing/v2/utils/billingContext/getTrialStateTransition.ts b/server/src/internal/billing/v2/utils/billingContext/getTrialStateTransition.ts index 4859cb278..a8806d6fc 100644 --- a/server/src/internal/billing/v2/utils/billingContext/getTrialStateTransition.ts +++ b/server/src/internal/billing/v2/utils/billingContext/getTrialStateTransition.ts @@ -1,5 +1,5 @@ import { isStripeSubscriptionTrialing } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { billingContextHasTrial } from "./billingContextHasTrial"; /** Gets the trial state transition for a billing context. */ diff --git a/server/src/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode.ts b/server/src/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode.ts index b25db68ec..514db1762 100644 --- a/server/src/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode.ts +++ b/server/src/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode.ts @@ -1,4 +1,4 @@ -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; export const isDeferredInvoiceMode = ({ billingContext, diff --git a/server/src/internal/billing/v2/utils/billingContextPriceLookup.ts b/server/src/internal/billing/v2/utils/billingContextPriceLookup.ts index 7f9cae139..3db7d74ea 100644 --- a/server/src/internal/billing/v2/utils/billingContextPriceLookup.ts +++ b/server/src/internal/billing/v2/utils/billingContextPriceLookup.ts @@ -6,7 +6,7 @@ import { PriceType, type UsagePriceConfig, } from "@autumn/shared"; -import type { BillingContext } from "@server/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { getBillingType } from "@server/internal/products/prices/priceUtils"; /** diff --git a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts index c8022e2fb..a078628c7 100644 --- a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts +++ b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts @@ -1,5 +1,5 @@ import { cp } from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const billingPlanToNewActiveCustomerProduct = ({ autumnBillingPlan, diff --git a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNextCyclePreview.ts b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNextCyclePreview.ts index 4b2bc5e8a..4f3d33bfe 100644 --- a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNextCyclePreview.ts +++ b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNextCyclePreview.ts @@ -8,8 +8,7 @@ import { sumValues, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext, BillingPlan } from "@/internal/billing/v2/types"; import { billingPlanToUpdatedCustomerProduct } from "@/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct"; import { customerProductToLineItems } from "../lineItems/customerProductToLineItems"; diff --git a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct.ts b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct.ts index da044a88f..030510ad2 100644 --- a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct.ts +++ b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToUpdatedCustomerProduct.ts @@ -1,5 +1,5 @@ import type { FullCusProduct } from "@autumn/shared"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; export const billingPlanToUpdatedCustomerProduct = ({ autumnBillingPlan, diff --git a/server/src/internal/billing/v2/utils/billingPlanToPreviewResponse.ts b/server/src/internal/billing/v2/utils/billingPlanToPreviewResponse.ts index 73e0ef29a..cd8b07a98 100644 --- a/server/src/internal/billing/v2/utils/billingPlanToPreviewResponse.ts +++ b/server/src/internal/billing/v2/utils/billingPlanToPreviewResponse.ts @@ -5,8 +5,8 @@ import { } from "@autumn/shared"; import { Decimal } from "decimal.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { BillingContext } from "@/internal/billing/v2/types"; +import type { BillingPlan } from "@/internal/billing/v2/types"; import { billingPlanToNextCyclePreview } from "./billingPlan/billingPlanToNextCyclePreview"; export const billingPlanToPreviewResponse = ({ diff --git a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts index 782d7dc5e..e0fdd3b40 100644 --- a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts +++ b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts @@ -1,6 +1,8 @@ import { type BillingResponse, stripeToAtmnAmount } from "@autumn/shared"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { BillingResult } from "@/internal/billing/v2/types/billingResult"; +import type { + BillingContext, + BillingResult, +} from "@/internal/billing/v2/types"; export const billingResultToResponse = ({ billingContext, diff --git a/server/src/internal/billing/v2/utils/lineItems/customerProductToArrearLineItems.ts b/server/src/internal/billing/v2/utils/lineItems/customerProductToArrearLineItems.ts index 544d531b8..df4e8651e 100644 --- a/server/src/internal/billing/v2/utils/lineItems/customerProductToArrearLineItems.ts +++ b/server/src/internal/billing/v2/utils/lineItems/customerProductToArrearLineItems.ts @@ -13,9 +13,11 @@ import { usagePriceToLineItem, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { + BillingContext, + UpdateCustomerEntitlement, +} from "@/internal/billing/v2/types"; import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils"; -import type { BillingContext } from "../../billingContext"; import { getLineItemBillingPeriod } from "./getLineItemBillingPeriod"; export const customerProductToArrearLineItems = ({ diff --git a/server/src/internal/billing/v2/utils/lineItems/customerProductToLineItems.ts b/server/src/internal/billing/v2/utils/lineItems/customerProductToLineItems.ts index 4deb9dba1..8e057e98f 100644 --- a/server/src/internal/billing/v2/utils/lineItems/customerProductToLineItems.ts +++ b/server/src/internal/billing/v2/utils/lineItems/customerProductToLineItems.ts @@ -16,8 +16,8 @@ import { usagePriceToLineItem, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { getBillingCycleAnchorForDirection } from "@/internal/billing/v2/utils/billingContext/getBillingCycleAnchorForDirection"; -import type { BillingContext } from "../../billingContext"; import { getLineItemBillingPeriod } from "./getLineItemBillingPeriod"; type LineItemDirection = "charge" | "refund"; diff --git a/server/src/internal/billing/v2/utils/lineItems/getLineItemBillingPeriod.ts b/server/src/internal/billing/v2/utils/lineItems/getLineItemBillingPeriod.ts index 787b88221..ba9ccad97 100644 --- a/server/src/internal/billing/v2/utils/lineItems/getLineItemBillingPeriod.ts +++ b/server/src/internal/billing/v2/utils/lineItems/getLineItemBillingPeriod.ts @@ -6,7 +6,7 @@ import { type Price, secondsToMs, } from "@autumn/shared"; -import type { BillingContext } from "../../billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; /** * Calculates the billing period (start and end) for a line item based on the billing context. diff --git a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/utils/logs/logAutumnBillingPlan.ts similarity index 85% rename from server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionPlan.ts rename to server/src/internal/billing/v2/utils/logs/logAutumnBillingPlan.ts index 3abb2e2c5..711dd1ee4 100644 --- a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionPlan.ts +++ b/server/src/internal/billing/v2/utils/logs/logAutumnBillingPlan.ts @@ -1,17 +1,19 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan"; +import type { + AutumnBillingPlan, + BillingContext, +} from "@/internal/billing/v2/types"; import { getTrialStateTransition } from "@/internal/billing/v2/utils/billingContext/getTrialStateTransition"; import { addToExtraLogs } from "@/utils/logging/addToExtraLogs"; -export const logUpdateSubscriptionPlan = ({ +export const logAutumnBillingPlan = ({ ctx, plan, billingContext, }: { ctx: AutumnContext; plan: AutumnBillingPlan; - billingContext: UpdateSubscriptionBillingContext; + billingContext: BillingContext; }) => { const { isTrialing, willBeTrialing } = getTrialStateTransition({ billingContext, diff --git a/server/src/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated.ts b/server/src/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated.ts index 69d50910f..3deaaa1c1 100644 --- a/server/src/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated.ts +++ b/server/src/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated.ts @@ -15,8 +15,10 @@ import { isCustomerProductScheduled, } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan.js"; +import type { + AutumnBillingPlan, + BillingContext, +} from "@/internal/billing/v2/types"; import type { CreateCustomerContext } from "@/internal/customers/actions/createWithDefaults/createCustomerContext"; import { workflows } from "@/queue/workflows.js"; diff --git a/server/src/internal/customers/actions/createWithDefaults/compute/computeCreateCustomerPlan.ts b/server/src/internal/customers/actions/createWithDefaults/compute/computeCreateCustomerPlan.ts index 2c9943b47..db75038f7 100644 --- a/server/src/internal/customers/actions/createWithDefaults/compute/computeCreateCustomerPlan.ts +++ b/server/src/internal/customers/actions/createWithDefaults/compute/computeCreateCustomerPlan.ts @@ -1,5 +1,5 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan.js"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { initFullCustomerProductFromProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProductFromProduct.js"; import type { CreateCustomerContext } from "../createCustomerContext.js"; diff --git a/server/src/internal/customers/actions/createWithDefaults/createCustomerContext.ts b/server/src/internal/customers/actions/createWithDefaults/createCustomerContext.ts index d59eeae5e..0c13ae7fe 100644 --- a/server/src/internal/customers/actions/createWithDefaults/createCustomerContext.ts +++ b/server/src/internal/customers/actions/createWithDefaults/createCustomerContext.ts @@ -1,8 +1,5 @@ import type { FullCustomer, FullProduct } from "@autumn/shared"; -import type { - BillingContext, - TrialContext, -} from "@/internal/billing/v2/billingContext"; +import type { BillingContext, TrialContext } from "@/internal/billing/v2/types"; export interface CreateCustomerContextFree { fullCustomer: FullCustomer; diff --git a/server/src/internal/customers/actions/createWithDefaults/execute/executeAutumnCreateCustomerPlan.ts b/server/src/internal/customers/actions/createWithDefaults/execute/executeAutumnCreateCustomerPlan.ts index 64b0c6c33..d310df08f 100644 --- a/server/src/internal/customers/actions/createWithDefaults/execute/executeAutumnCreateCustomerPlan.ts +++ b/server/src/internal/customers/actions/createWithDefaults/execute/executeAutumnCreateCustomerPlan.ts @@ -3,7 +3,7 @@ import { isUniqueConstraintError } from "@/db/dbUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { executeAutumnBillingPlan } from "@/internal/billing/v2/execute/executeAutumnBillingPlan.js"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/billingPlan.js"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { billingPlanToSendProductsUpdated } from "@/internal/billing/v2/workflows/sendProductsUpdated/billingPlanToSendProductsUpdated.js"; import type { CreateCustomerContext } from "@/internal/customers/actions/createWithDefaults/createCustomerContext.js"; import { captureOrgEvent } from "@/utils/posthog.js"; diff --git a/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts b/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts index abd0d8b52..7bcb28021 100644 --- a/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts +++ b/server/src/internal/customers/actions/createWithDefaults/finalizeCreateCustomer.ts @@ -1,7 +1,7 @@ import type { FullCustomer } from "@autumn/shared"; import type Stripe from "stripe"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; -import type { AutumnBillingPlan } from "@/internal/billing/v2/types/autumnBillingPlan.js"; +import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { initSubscriptionFromStripe } from "@/internal/subscriptions/utils/initSubscriptionFromStripe.js"; import type { CreateCustomerContext } from "./createCustomerContext.js"; diff --git a/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerBillingContext.ts b/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerBillingContext.ts index 83159f073..af1978814 100644 --- a/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerBillingContext.ts +++ b/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerBillingContext.ts @@ -1,6 +1,6 @@ import { getOrCreateStripeCustomer } from "@/external/stripe/customers/index.js"; import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; -import type { BillingContext } from "@/internal/billing/v2/billingContext.js"; +import type { BillingContext } from "@/internal/billing/v2/types"; import type { CreateCustomerContext } from "../createCustomerContext.js"; /** diff --git a/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerTrialContext.ts b/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerTrialContext.ts index 3c2eafbe4..f3537254a 100644 --- a/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerTrialContext.ts +++ b/server/src/internal/customers/actions/createWithDefaults/setup/setupCreateCustomerTrialContext.ts @@ -4,7 +4,7 @@ import { type FullProduct, InternalError, } from "@autumn/shared"; -import type { TrialContext } from "@/internal/billing/v2/billingContext.js"; +import type { TrialContext } from "@/internal/billing/v2/types"; export const setupCreateCustomerTrialContext = ({ paidProducts, diff --git a/server/src/internal/customers/cancel/handleCancelV2.ts b/server/src/internal/customers/cancel/handleCancelV2.ts index 7c651b9ff..daf9ac587 100644 --- a/server/src/internal/customers/cancel/handleCancelV2.ts +++ b/server/src/internal/customers/cancel/handleCancelV2.ts @@ -7,8 +7,8 @@ import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/l import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan"; import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; import { logUpdateSubscriptionContext } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext"; -import { logUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionPlan"; import { setupUpdateSubscriptionBillingContext } from "@/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; export const handleCancelV2 = createRoute({ // body: CancelBodySchema, @@ -48,7 +48,7 @@ export const handleCancelV2 = createRoute({ billingContext, params: updateSubscriptionBody, }); - logUpdateSubscriptionPlan({ ctx, plan: autumnBillingPlan, billingContext }); + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); await handleUpdateSubscriptionErrors({ ctx, diff --git a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts index 26589174f..772c6ae48 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts @@ -19,7 +19,7 @@ import { and, eq, gt, 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 { UpdateCustomerEntitlement } from "@/internal/billing/v2/types/autumnBillingPlan"; +import type { UpdateCustomerEntitlement } from "@/internal/billing/v2/types"; import RecaseError from "@/utils/errorUtils.js"; export class CusEntService { diff --git a/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts b/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts index c46d8e698..3b8cd17ac 100644 --- a/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts +++ b/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts @@ -3,12 +3,12 @@ import { addDays } from "date-fns"; import type Stripe from "stripe"; import { createStripeCli } from "@/external/connect/createStripeCli"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; -import type { StripeBillingStage } from "@/internal/billing/v2/types/autumnBillingPlan"; import type { + BillingContext, BillingPlan, DeferredAutumnBillingPlanData, -} from "@/internal/billing/v2/types/billingPlan"; + StripeBillingStage, +} from "@/internal/billing/v2/types"; import { generateId } from "@/utils/genUtils"; import { MetadataService } from "../MetadataService"; diff --git a/server/tests/integration/billing/attach/attachTests.md b/server/tests/integration/billing/attach/attachTests.md index f8044f42f..9c0b06236 100644 --- a/server/tests/integration/billing/attach/attachTests.md +++ b/server/tests/integration/billing/attach/attachTests.md @@ -77,7 +77,22 @@ await autumn.attach({ ... }); ``` -12. **Scheduled-switch tests must advance test clock with `advanceToNextInvoice()`** +12. **Add-on is defined at product level, NOT in attach params** + - Use `products.recurringAddOn()` or `products.base({ isAddOn: true })` when creating the product + - Do NOT pass `is_add_on` to the attach endpoint + ```typescript + // ✅ GOOD - define add-on at product creation + const addon = products.recurringAddOn({ id: "addon", items: [...] }); + // or + const addon = products.base({ id: "addon", items: [...], isAddOn: true }); + + s.billing.attach({ productId: addon.id }); + + // ❌ BAD - is_add_on is not an attach param + s.billing.attach({ productId: pro.id, isAddOn: true }); + ``` + +13. **Scheduled-switch tests must advance test clock with `advanceToNextInvoice()`** - After scheduling a downgrade, advance the test clock to verify: - A. Next cycle invoice is correct - B. Products on customer are correct after cycle diff --git a/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts new file mode 100644 index 000000000..e016df303 --- /dev/null +++ b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts @@ -0,0 +1,479 @@ +/** + * Attach Entity-Level Product Tests (Attach V2) + * + * Tests for attaching products to entities (sub-accounts) rather than customers. + * Entities have their own subscriptions and balances. + * + * Key behaviors: + * - Products attached to entities are independent from customer-level products + * - Each entity can have its own subscription + * - Mid-cycle attaches are prorated + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { advanceTestClock } from "@tests/utils/stripeUtils"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: Create entity, attach pro to entity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Create entity + * - Attach pro to entity (not customer) + * + * Expected Result: + * - Entity has product + * - Customer does not have product + */ +test.concurrent(`${chalk.yellowBright("new-plan: create entity, attach pro to entity")}`, async () => { + const customerId = "new-plan-attach-entity-pro"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-entity", + items: [messagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ + productId: pro.id, + entityIndex: 0, // Attach to first entity + }), + ], + }); + + // Get entity and verify it has the product + const entity = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + + await expectProductActive({ + customer: entity, + productId: `${pro.id}_${customerId}`, + }); + + // Verify entity has messages feature + expectCustomerFeatureCorrect({ + customer: entity, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Get customer and verify they don't have the product + const customer = await autumnV1.customers.get(customerId); + + // Customer should not have products array with this product + const customerProduct = customer.products?.find( + (p) => p.id === `${pro.id}_${customerId}`, + ); + expect(customerProduct).toBeUndefined(); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 2: Create 2 entities, attach pro to each +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Create 2 entities + * - Attach pro to each + * + * Expected Result: + * - Independent balances + * - 2 separate subscriptions + */ +test.concurrent(`${chalk.yellowBright("new-plan: create 2 entities, attach pro to each")}`, async () => { + const customerId = "new-plan-attach-2-entities"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-2ent", + items: [messagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: pro.id, entityIndex: 0 }), + s.billing.attach({ productId: pro.id, entityIndex: 1 }), + ], + }); + + // Get both entities and verify independent balances + const entity1 = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + const entity2 = await autumnV1.entities.get( + customerId, + entities[1].id, + ); + + // Both entities should have the product + await expectProductActive({ + customer: entity1, + productId: `${pro.id}_${customerId}`, + }); + await expectProductActive({ + customer: entity2, + productId: `${pro.id}_${customerId}`, + }); + + // Both should have independent balances + expectCustomerFeatureCorrect({ + customer: entity1, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + expectCustomerFeatureCorrect({ + customer: entity2, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Track usage on entity1 only + await autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Messages, + value: 30, + entity_id: entities[0].id, + }); + + // Re-fetch and verify independent balances after usage + const entity1After = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + const entity2After = await autumnV1.entities.get( + customerId, + entities[1].id, + ); + + expectCustomerFeatureCorrect({ + customer: entity1After, + featureId: TestFeature.Messages, + balance: 70, // 100 - 30 + usage: 30, + }); + expectCustomerFeatureCorrect({ + customer: entity2After, + featureId: TestFeature.Messages, + balance: 100, // Unchanged + usage: 0, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 3: Attach pro to entity 1, advance 2 weeks, attach pro to entity 2 +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro to entity 1 + * - Advance 2 weeks + * - Attach pro to entity 2 + * + * Expected Result: + * - Prorated billing for entity 2 + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance 2 weeks, attach pro to entity 2")}`, async () => { + const customerId = "new-plan-attach-entity-midcycle"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-midcycle", + items: [messagesItem], + }); + + const { autumnV1, entities, ctx, testClockId } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 2, featureId: TestFeature.Users }), + ], + actions: [s.billing.attach({ productId: pro.id, entityIndex: 0 })], + }); + + // Advance 2 weeks + await advanceTestClock({ + stripeCli: ctx.stripeCli, + testClockId: testClockId!, + numberOfWeeks: 2, + }); + + // Attach pro to entity 2 mid-cycle + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${pro.id}_${customerId}`, + entity_id: entities[1].id, + }); + + // Get both entities + const entity1 = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + const entity2 = await autumnV1.entities.get( + customerId, + entities[1].id, + ); + + // Both should have the product + await expectProductActive({ + customer: entity1, + productId: `${pro.id}_${customerId}`, + }); + await expectProductActive({ + customer: entity2, + productId: `${pro.id}_${customerId}`, + }); + + // Get customer to check invoices + const customer = await autumnV1.customers.get(customerId); + + // Should have 2 invoices: one full price, one prorated + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + }); + + // Entity 2's invoice should be prorated (roughly half of $20 = ~$10) + // Note: exact amount depends on billing cycle alignment +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 4: Attach pro annual to entity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach annual product to entity + * + * Expected Result: + * - Correct billing interval (annual) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro annual to entity")}`, async () => { + const customerId = "new-plan-attach-entity-annual"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const proAnnual = products.proAnnual({ + id: "pro-annual-ent", + items: [messagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [proAnnual] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ + productId: proAnnual.id, + entityIndex: 0, + }), + ], + }); + + // Get entity and verify product + const entity = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + + await expectProductActive({ + customer: entity, + productId: `${proAnnual.id}_${customerId}`, + }); + + // Verify messages feature + expectCustomerFeatureCorrect({ + customer: entity, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Get customer and verify invoice (annual = $200) + const customer = await autumnV1.customers.get(customerId); + + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 200, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 5: Attach pro to customer, then pro to entity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro to customer first + * - Then attach pro to entity + * + * Expected Result: + * - Both have product independently + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro to customer, then pro to entity")}`, async () => { + const customerId = "new-plan-attach-cust-then-entity"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-cust-ent", + items: [messagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: pro.id }), // Customer-level + s.billing.attach({ productId: pro.id, entityIndex: 0 }), // Entity-level + ], + }); + + // Get customer and entity + const customer = await autumnV1.customers.get(customerId); + const entity = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + + // Both should have the product + await expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, + }); + await expectProductActive({ + customer: entity, + productId: `${pro.id}_${customerId}`, + }); + + // Both should have independent balances + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + expectCustomerFeatureCorrect({ + customer: entity, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 6: Attach free to customer, then free to entity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach free to customer first + * - Then attach free to entity + * + * Expected Result: + * - Both have product independently + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach free to customer, then free to entity")}`, async () => { + const customerId = "new-plan-attach-free-cust-ent"; + + const messagesItem = items.monthlyMessages({ includedUsage: 50 }); + const free = products.base({ + id: "free-cust-ent", + items: [messagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({}), + s.products({ list: [free] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ productId: free.id }), // Customer-level + s.billing.attach({ productId: free.id, entityIndex: 0 }), // Entity-level + ], + }); + + // Get customer and entity + const customer = await autumnV1.customers.get(customerId); + const entity = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + + // Both should have the product + await expectProductActive({ + customer, + productId: `${free.id}_${customerId}`, + }); + await expectProductActive({ + customer: entity, + productId: `${free.id}_${customerId}`, + }); + + // Both should have independent balances + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 50, + balance: 50, + usage: 0, + }); + expectCustomerFeatureCorrect({ + customer: entity, + featureId: TestFeature.Messages, + includedUsage: 50, + balance: 50, + usage: 0, + }); + + // Verify no invoices (both free) + await expectCustomerInvoiceCorrect({ + customer, + count: 0, + }); +}); diff --git a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts index 5d7a815ed..0500dbbef 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts @@ -1,5 +1,5 @@ /** - * Attach Free Product Tests + * Attach Free Product Tests (Attach V2) * * Tests for attaching free products when customer has no existing product. * Free products have no base price and only provide included usage/features. @@ -47,7 +47,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async const { autumnV1 } = await initScenario({ customerId, setup: [s.customer({}), s.products({ list: [free] })], - actions: [s.attach({ productId: free.id })], + actions: [s.billing.attach({ productId: free.id })], }); const customer = await autumnV1.customers.get(customerId); @@ -55,7 +55,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async // Verify product is active await expectProductActive({ customer, - productId: free.id, + productId: `${free.id}_${customerId}`, }); // Verify messages feature has correct balance @@ -81,7 +81,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async /** * Scenario: * - Customer has no existing product - * - Attach free product with: messages (100), words (200), dashboard (boolean), unlimited messages + * - Attach free product with: messages (100), words (200), dashboard (boolean) * * Expected Result: * - Product is active @@ -103,7 +103,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple featu const { autumnV1 } = await initScenario({ customerId, setup: [s.customer({}), s.products({ list: [free] })], - actions: [s.attach({ productId: free.id })], + actions: [s.billing.attach({ productId: free.id })], }); const customer = await autumnV1.customers.get(customerId); @@ -111,7 +111,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple featu // Verify product is active await expectProductActive({ customer, - productId: free.id, + productId: `${free.id}_${customerId}`, }); // Verify messages feature diff --git a/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts new file mode 100644 index 000000000..0d72ad3d6 --- /dev/null +++ b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts @@ -0,0 +1,470 @@ +/** + * Attach One-Time Product Tests (Attach V2) + * + * Tests for attaching one-time (non-recurring) products. + * One-time products are single purchases with no subscription. + * + * Key behaviors: + * - Invoice is created for one-time purchase + * - No recurring subscription + * - Can be purchased multiple times (cumulative balance) + * - Can be attached as add-on to existing products + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { + expectProductActive, + expectProductNotPresent, +} from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +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"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: Attach one-time purchase +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer has no existing product + * - Attach one-time product with prepaid messages + * + * Expected Result: + * - Invoice created + * - Balance added + * - No recurring subscription + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase")}`, async () => { + const customerId = "new-plan-attach-one-time"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-messages", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [oneOff] }), + ], + actions: [ + s.billing.attach({ + productId: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }), + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: `${oneOff.id}_${customerId}`, + }); + + // Verify messages balance (100 from 1 pack) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 100, + usage: 0, + }); + + // Verify invoice: one-time charge ($10 base + $10 messages = $20) + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, // oneOff base ($10) + prepaid ($10) + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 2: Attach one-time purchase twice (cumulative) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach same one-time product twice + * + * Expected Result: + * - Balance is cumulative (not replaced) + * - Two invoices created + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase twice")}`, async () => { + const customerId = "new-plan-attach-one-time-twice"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-twice", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [oneOff] }), + ], + actions: [ + s.billing.attach({ + productId: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }), + ], + }); + + // Attach same product again + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${oneOff.id}_${customerId}`, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify messages balance is cumulative (200 = 100 + 100) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 200, + usage: 0, + }); + + // Verify two invoices created + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + latestTotal: 20, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 3: Attach pro then one-time as main (replaces pro) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro first + * - Attach one-time WITHOUT isAddOn flag + * + * Expected Result: + * - Should replace pro (user forgot to toggle isAddOn) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro then one-time as main")}`, async () => { + const customerId = "new-plan-attach-pro-then-one-time-main"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-main", + items: [messagesItem], + }); + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 50, + price: 5, + }); + const oneOff = products.oneOff({ + id: "one-off-main", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, oneOff] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + // Attach one-time without isAddOn - should replace pro + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${oneOff.id}_${customerId}`, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + // Note: NOT setting is_add_on: true + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify pro is no longer present (replaced) + await expectProductNotPresent({ + customer, + productId: `${pro.id}_${customerId}`, + }); + + // Verify one-time is active + await expectProductActive({ + customer, + productId: `${oneOff.id}_${customerId}`, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 4: Attach one-time with quantity=0 for one feature +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - One-time with messages (qty=100) + words (qty=0) + * + * Expected Result: + * - Messages added + * - Words not charged + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with quantity=0 for one feature")}`, async () => { + const customerId = "new-plan-attach-one-time-qty0"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-multi", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [oneOff] }), + ], + actions: [ + s.billing.attach({ + productId: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }), + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify messages added + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 100, + usage: 0, + }); + + // Verify invoice: only messages charged + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, // base ($10) + messages ($10) + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 5: Attach one-time as add-on to pro +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro first + * - Attach one-time product (defined as add-on at product level) + * + * Expected Result: + * - Both products exist + * - Balances combined + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time as add-on to pro")}`, async () => { + const customerId = "new-plan-attach-one-time-addon"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-addon", + items: [messagesItem], + }); + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 50, + price: 5, + }); + // Define as add-on at product level (isAddOn: true) + const oneOffAddon = products.oneOff({ + id: "one-off-addon", + items: [oneOffMessagesItem], + isAddOn: true, + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro, oneOffAddon] }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + // Attach one-time add-on (is_add_on defined at product level, not in attach params) + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${oneOffAddon.id}_${customerId}`, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify both products are active + await expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, + }); + await expectProductActive({ + customer, + productId: `${oneOffAddon.id}_${customerId}`, + }); + + // Verify combined messages balance (100 from pro + 50 from one-off = 150) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 150, + usage: 0, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 6: Attach one-time with multiple features +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - One-time with messages + words + storage (all one-off) + * + * Expected Result: + * - All balances correct + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with multiple features")}`, async () => { + const customerId = "new-plan-attach-one-time-multi-feat"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-multi-feat", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [oneOff] }), + ], + actions: [ + s.billing.attach({ + productId: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 2 }], // 2 packs = 200 messages + }), + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify messages balance (200 from 2 packs) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 200, + usage: 0, + }); + + // Verify invoice + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 30, // base ($10) + 2 packs ($20) + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 7: Attach one-time to entity +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Create entity + * - Attach one-time to entity (not customer) + * + * Expected Result: + * - Entity has balance + * - Customer does not have balance for this feature + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach one-time to entity")}`, async () => { + const customerId = "new-plan-attach-one-time-entity"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-entity", + items: [oneOffMessagesItem], + }); + + const { autumnV1, entities } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [oneOff] }), + s.entities({ count: 1, featureId: TestFeature.Users }), + ], + actions: [ + s.billing.attach({ + productId: oneOff.id, + entityIndex: 0, // Attach to first entity + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }), + ], + }); + + // Get entity to verify balance + const entity = await autumnV1.entities.get( + customerId, + entities[0].id, + ); + + // Verify entity has messages balance + expectCustomerFeatureCorrect({ + customer: entity, + featureId: TestFeature.Messages, + balance: 100, + usage: 0, + }); + + // Get customer and verify they don't have this balance at customer level + const customer = await autumnV1.customers.get(customerId); + + // Customer should not have messages feature (it's on the entity) + expect(customer.features[TestFeature.Messages]).toBeUndefined(); +}); diff --git a/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts new file mode 100644 index 000000000..e8aa3df43 --- /dev/null +++ b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts @@ -0,0 +1,331 @@ +/** + * Attach Paid Product Tests (Attach V2) + * + * Tests for attaching paid products when customer has no existing product. + * Paid products have base price and various feature types (consumable, prepaid, allocated). + * + * Key behaviors: + * - Invoice is created for base price + prepaid items + * - Prepaid features require options with quantity + * - Allocated features track entity usage + */ + +import { test } from "bun:test"; +import { type ApiCustomerV3, ErrCode } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils"; +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"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: Attach pro with mixed features +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer has no existing product + * - Attach pro ($20/mo) with consumable words + prepaid messages + allocated users + * + * Expected Result: + * - Invoice = base ($20) + prepaid (100 messages @ $10 = $10) + * - All features correctly configured + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro with mixed features")}`, async () => { + const customerId = "new-plan-attach-pro-mixed"; + + const consumableWordsItem = items.consumableWords({ includedUsage: 50 }); + const prepaidMessagesItem = items.prepaidMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + const allocatedUsersItem = items.allocatedUsers({ includedUsage: 3 }); + + const pro = products.pro({ + id: "pro-mixed", + items: [consumableWordsItem, prepaidMessagesItem, allocatedUsersItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ + productId: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], // 1 pack of 100 + }), + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, + }); + + // Verify consumable words feature (50 included, no prepaid) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Words, + includedUsage: 50, + balance: 50, + usage: 0, + }); + + // Verify prepaid messages feature (100 from 1 pack) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 100, + usage: 0, + }); + + // Verify allocated users feature (3 included) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Users, + includedUsage: 3, + balance: 3, + usage: 0, + }); + + // Verify invoice: base ($20) + prepaid ($10) = $30 + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 30, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 2: Attach pro with allocated, create entities +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro with allocated users (3 included) + * - Create 5 user entities via track + * + * Expected Result: + * - Users usage = 5 + * - Overage invoice created for 2 extra users + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro with allocated, create entities")}`, async () => { + const customerId = "new-plan-attach-pro-allocated"; + + const allocatedUsersItem = items.allocatedUsers({ includedUsage: 3 }); + + const pro = products.pro({ + id: "pro-allocated", + items: [allocatedUsersItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + s.entities({ count: 5, featureId: TestFeature.Users }), + ], + actions: [s.billing.attach({ productId: pro.id })], + }); + + // Track 5 users (creates overage of 2) + for (let i = 0; i < 5; i++) { + await autumnV1.track({ + customer_id: customerId, + feature_id: TestFeature.Users, + value: 1, + }); + } + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, + }); + + // Verify users feature: 3 included, 5 used, -2 balance (overage) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Users, + includedUsage: 3, + balance: -2, + usage: 5, + }); + + // Verify invoices: initial ($20) + overage (2 users @ $10 = $20) + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + latestTotal: 20, // Overage invoice + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 3: Attach base with prepaid messages, no options (error) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach base product with prepaid messages without passing options + * + * Expected Result: + * - Error: "behavior undefined" (prepaid requires quantity) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach base with prepaid messages, no options")}`, async () => { + const customerId = "new-plan-attach-base-prepaid-no-opts"; + + const prepaidMessagesItem = items.prepaidMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const base = products.base({ + id: "base-prepaid", + items: [prepaidMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [base] }), + ], + actions: [], + }); + + // Attempt to attach without options - should fail + await expectAutumnError({ + errCode: ErrCode.InvalidRequest, + func: async () => { + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${base.id}_${customerId}`, + }); + }, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 4: Attach pro with prepaid messages, no options (error) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro product with prepaid messages without passing options + * + * Expected Result: + * - Error: "behavior undefined" (prepaid requires quantity) + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid messages, no options")}`, async () => { + const customerId = "new-plan-attach-pro-prepaid-no-opts"; + + const prepaidMessagesItem = items.prepaidMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const pro = products.pro({ + id: "pro-prepaid", + items: [prepaidMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [], + }); + + // Attempt to attach without options - should fail + await expectAutumnError({ + errCode: ErrCode.InvalidRequest, + func: async () => { + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: `${pro.id}_${customerId}`, + }); + }, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 5: Attach pro with prepaid messages, quantity 0 +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Attach pro product with prepaid messages, pass options with quantity: 0 + * + * Expected Result: + * - No prepaid charged, only base price ($20) + * - Messages balance = 0 + */ +test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid messages, quantity 0")}`, async () => { + const customerId = "new-plan-attach-pro-prepaid-qty0"; + + const prepaidMessagesItem = items.prepaidMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const pro = products.pro({ + id: "pro-prepaid-qty0", + items: [prepaidMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), + s.products({ list: [pro] }), + ], + actions: [ + s.billing.attach({ + productId: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 0 }], + }), + ], + }); + + const customer = await autumnV1.customers.get(customerId); + + // Verify product is active + await expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, + }); + + // Verify messages feature: 0 prepaid purchased + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 0, + usage: 0, + }); + + // Verify invoice: only base price ($20), no prepaid + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); +}); diff --git a/server/tests/utils/fixtures/db/contexts.ts b/server/tests/utils/fixtures/db/contexts.ts index 5d31f8de5..1bb33d493 100644 --- a/server/tests/utils/fixtures/db/contexts.ts +++ b/server/tests/utils/fixtures/db/contexts.ts @@ -9,7 +9,7 @@ import { import type Stripe from "stripe"; import { logger } from "@/external/logtail/logtailUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/billingContext"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { stripeCustomers } from "../stripe/customers"; import { customers } from "./customers"; diff --git a/server/tests/utils/fixtures/products.ts b/server/tests/utils/fixtures/products.ts index 37e24fa56..88eaea933 100644 --- a/server/tests/utils/fixtures/products.ts +++ b/server/tests/utils/fixtures/products.ts @@ -290,19 +290,23 @@ const ultra = ({ * One-off product - one-time purchase with $10 base price * @param items - Product items (features) * @param id - Product ID (default: "one-off") + * @param isAddOn - Whether this is an add-on product (default: false) */ const oneOff = ({ items, id = "one-off", + isAddOn = false, }: { items: ProductItem[]; id?: string; + isAddOn?: boolean; }): ProductV2 => constructProduct({ id, items: [...items], type: "one_off", isDefault: false, + isAddOn, }); /** diff --git a/server/tests/utils/testInitUtils/initScenario.ts b/server/tests/utils/testInitUtils/initScenario.ts index f001dc26b..309975bdf 100644 --- a/server/tests/utils/testInitUtils/initScenario.ts +++ b/server/tests/utils/testInitUtils/initScenario.ts @@ -93,7 +93,6 @@ type BillingAttachAction = { options?: FeatureOption[]; newBillingSubscription?: boolean; timeout?: number; - isAddOn?: boolean; }; type ScenarioAction = @@ -525,6 +524,51 @@ const deleteCustomer = ( }; }; +/** + * Attach a product using the NEW billing/attach V2 endpoint. + * Product ID is auto-prefixed with customerId. + * + * NOTE: Add-on is defined at product level using `products.recurringAddOn()` or + * `products.base({ isAddOn: true })`, NOT in the attach params. + * + * @param productId - The product ID (without prefix) + * @param entityIndex - Optional entity index (0-based) to attach to (omit for customer-level) + * @param options - Optional feature options (e.g., prepaid quantity) + * @param newBillingSubscription - Create a separate Stripe subscription for this product + * @param timeout - Optional timeout in milliseconds for the attach request + * @example s.billing.attach({ productId: "pro" }) // customer-level + * @example s.billing.attach({ productId: "pro", entityIndex: 0 }) // attach to first entity + * @example s.billing.attach({ productId: "pro", options: [{ feature_id: "messages", quantity: 100 }] }) + */ +const billingAttach = ({ + productId, + entityIndex, + options, + newBillingSubscription, + timeout, +}: { + productId: string; + entityIndex?: number; + options?: FeatureOption[]; + newBillingSubscription?: boolean; + timeout?: number; +}): ConfigFn => { + return (config) => ({ + ...config, + actions: [ + ...config.actions, + { + type: "billingAttach" as const, + productId, + entityIndex, + options, + newBillingSubscription, + timeout, + }, + ], + }); +}; + /** * Scenario configuration functions. * Import and use with initScenario to configure test setup. @@ -555,6 +599,9 @@ export const s = { track, updateSubscription, deleteCustomer, + billing: { + attach: billingAttach, + }, } as const; // ═══════════════════════════════════════════════════════════════════ @@ -966,6 +1013,35 @@ export async function initScenario({ waitForSeconds: 30, }); } + } else if (action.type === "billingAttach") { + if (!customerId) { + throw new Error( + "Cannot attach product: customerId is required when using s.billing.attach()", + ); + } + const prefixedProductId = `${action.productId}_${productPrefix}`; + + // Resolve entityIndex to entityId + let entityId: string | undefined; + if (action.entityIndex !== undefined) { + if (action.entityIndex >= generatedEntities.length) { + throw new Error( + `entityIndex ${action.entityIndex} is out of bounds. Only ${generatedEntities.length} entities configured.`, + ); + } + entityId = generatedEntities[action.entityIndex].id; + } + + await autumnV1.billing.attach( + { + customer_id: customerId, + product_id: prefixedProductId, + entity_id: entityId, + options: action.options, + new_billing_subscription: action.newBillingSubscription, + }, + { timeout: action.timeout }, + ); } } diff --git a/shared/api/billing/attachV2/attachV0Params.ts b/shared/api/billing/attachV2/attachParamsV0.ts similarity index 78% rename from shared/api/billing/attachV2/attachV0Params.ts rename to shared/api/billing/attachV2/attachParamsV0.ts index 49221f236..e0291b692 100644 --- a/shared/api/billing/attachV2/attachV0Params.ts +++ b/shared/api/billing/attachV2/attachParamsV0.ts @@ -6,7 +6,7 @@ import { BillingParamsBaseSchema } from "../common/billingParamsBase.js"; export const RedirectModeSchema = z.enum(["always", "if_required"]); export type RedirectMode = z.infer; -export const ExtAttachV0ParamsSchema = BillingParamsBaseSchema.extend({ +export const ExtAttachParamsV0Schema = BillingParamsBaseSchema.extend({ // Product identification product_id: z.string(), @@ -22,9 +22,11 @@ export const ExtAttachV0ParamsSchema = BillingParamsBaseSchema.extend({ // Checkout behavior redirect_mode: RedirectModeSchema.optional(), success_url: z.string().optional(), + + new_billing_subscription: z.boolean().optional(), }); -export const AttachV0ParamsSchema = ExtAttachV0ParamsSchema.extend({ +export const AttachParamsV0Schema = ExtAttachParamsV0Schema.extend({ // Custom product configuration items: z.array(ProductItemSchema).optional(), }).refine( @@ -39,5 +41,5 @@ export const AttachV0ParamsSchema = ExtAttachV0ParamsSchema.extend({ }, ); -export type ExtAttachV0Params = z.infer; -export type AttachV0Params = z.infer; +export type ExtAttachParamsV0 = z.infer; +export type AttachParamsV0 = z.infer; diff --git a/shared/api/common/cancelMode.ts b/shared/api/billing/common/cancelAction.ts similarity index 100% rename from shared/api/common/cancelMode.ts rename to shared/api/billing/common/cancelAction.ts diff --git a/shared/api/billing/common/checkoutMode.ts b/shared/api/billing/common/checkoutMode.ts deleted file mode 100644 index 9dc3d5b44..000000000 --- a/shared/api/billing/common/checkoutMode.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod/v4"; - -export const CheckoutModeSchema = z - .enum(["stripe_checkout", "autumn_checkout"]) - .nullable(); - -export type CheckoutMode = z.infer; diff --git a/shared/api/billing/common/featureQuantities.ts b/shared/api/billing/common/featureQuantities.ts index dea249292..c674a9b9b 100644 --- a/shared/api/billing/common/featureQuantities.ts +++ b/shared/api/billing/common/featureQuantities.ts @@ -4,5 +4,3 @@ export const FeatureQuantitySchema = z.object({ feature_id: z.string(), quantity: z.number(), }); - -type FeatureQuantity = z.infer; diff --git a/shared/api/billing/common/planTiming.ts b/shared/api/billing/common/planTiming.ts deleted file mode 100644 index 1a7f03528..000000000 --- a/shared/api/billing/common/planTiming.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { z } from "zod/v4"; - -export const PlanTimingSchema = z.enum(["immediate", "end_of_cycle"]); - -export type PlanTiming = z.infer; diff --git a/shared/api/billing/index.ts b/shared/api/billing/index.ts index a8120b83b..188290387 100644 --- a/shared/api/billing/index.ts +++ b/shared/api/billing/index.ts @@ -3,7 +3,7 @@ export * from "./attach/attachBodyV1.js"; export * from "./attach/prevVersions/attachBodyV0.js"; export * from "./attach/prevVersions/attachResponseV1.js"; // Attach V2 -export * from "./attachV2/attachV0Params.js"; +export * from "./attachV2/attachParamsV0.js"; // Checkout export * from "./checkout/checkoutParamsV1.js"; export * from "./checkout/prevVersions/checkoutParamsV0.js"; @@ -12,8 +12,7 @@ export * from "./checkout/prevVersions/checkoutResponseV0.js"; export * from "./common/billingParamsBase.js"; export * from "./common/billingPreviewResponse.js"; export * from "./common/billingResponse.js"; -export * from "./common/checkoutMode.js"; -export * from "./common/planTiming.js"; +export * from "./common/cancelAction.js"; export * from "./common/refundBehavior.js"; export * from "./updateSubscription/previewUpdateSubscriptionResponse.js"; // Update Subscription diff --git a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts index a54ef33cf..11c03f445 100644 --- a/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts +++ b/shared/api/billing/updateSubscription/updateSubscriptionV0Params.ts @@ -1,11 +1,12 @@ +import { RefundBehaviorSchema } from "@api/billing/common/refundBehavior"; import { CreateFreeTrialSchema } from "@models/productModels/freeTrialModels/freeTrialModels"; import { nullish } from "@utils/utils"; import { z } from "zod/v4"; import { FeatureOptionsSchema } from "../../../models/cusProductModels/cusProductModels"; import { ProductItemSchema } from "../../../models/productV2Models/productItemModels/productItemModels"; -import { CancelActionSchema } from "../../common/cancelMode"; import { BillingBehaviorSchema } from "../common/billingBehavior"; import { BillingParamsBaseSchema } from "../common/billingParamsBase"; +import { CancelActionSchema } from "../common/cancelAction"; export const ExtUpdateSubscriptionV0ParamsSchema = BillingParamsBaseSchema.extend({ @@ -42,6 +43,7 @@ export const ExtUpdateSubscriptionV0ParamsSchema = export const UpdateSubscriptionV0ParamsSchema = ExtUpdateSubscriptionV0ParamsSchema.extend({ customer_product_id: z.string().optional(), + refund_behavior: RefundBehaviorSchema.optional(), }) .refine( (data) => { diff --git a/shared/index.ts b/shared/index.ts index a0ab4be0a..079ea1d51 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -5,6 +5,7 @@ export { schemas }; export * from "./api/apiUtils.js"; // Billing common schemas export * from "./api/billing/common/billingBehavior.js"; +export * from "./api/billing/common/cancelAction.js"; // Cursor pagination utilities export * from "./api/common/cursorPaginationSchemas.js"; // API MODELS diff --git a/vite/src/components/forms/update-subscription-v2/updateSubscriptionFormSchema.ts b/vite/src/components/forms/update-subscription-v2/updateSubscriptionFormSchema.ts index d175c74c5..e63575fc5 100644 --- a/vite/src/components/forms/update-subscription-v2/updateSubscriptionFormSchema.ts +++ b/vite/src/components/forms/update-subscription-v2/updateSubscriptionFormSchema.ts @@ -1,19 +1,12 @@ import { - type BillingBehavior, BillingBehaviorSchema, + CancelActionSchema, FreeTrialDuration, type ProductItem, } from "@autumn/shared"; -import { CancelActionSchema } from "node_modules/@autumn/shared/api/common/cancelMode"; -import { z } from "zod/v4"; -import { - RefundBehaviorSchema, - type RefundBehaviorValue, -} from "@/components/forms/update-subscription-v2/types/refundBehaviourSchema"; -type BillingBehaviorValue = BillingBehavior; -export type { RefundBehaviorValue }; -export type CancelActionValue = z.infer; +import { z } from "zod/v4"; +import { RefundBehaviorSchema } from "@/components/forms/update-subscription-v2/types/refundBehaviourSchema"; export const UpdateSubscriptionFormSchema = z.object({ prepaidOptions: z.record(z.string(), z.number().nonnegative()), diff --git a/vite/src/main.tsx b/vite/src/main.tsx index 9fad829ee..8939abd77 100644 --- a/vite/src/main.tsx +++ b/vite/src/main.tsx @@ -1,8 +1,3 @@ -// Buffer polyfill for @owpz/ksuid browser compatibility -import { Buffer } from "buffer"; - -globalThis.Buffer = Buffer; - import "./index.css"; import "./styles/button.css"; diff --git a/vite/vite.config.ts b/vite/vite.config.ts index 87eefb993..58ce419b9 100644 --- a/vite/vite.config.ts +++ b/vite/vite.config.ts @@ -24,8 +24,6 @@ export default defineConfig({ resolve: { alias: { "@": path.resolve(__dirname, "./src"), - // Buffer polyfill for browser compatibility - buffer: "buffer", // Hide Radix UI imports with cleaner aliases "@radix/accordion": "@radix-ui/react-accordion", @@ -45,8 +43,6 @@ export default defineConfig({ }, optimizeDeps: { - // Force pre-bundle @owpz/ksuid for proper ESM handling - include: ["@owpz/ksuid", "buffer"], // Exclude workspace dependencies from pre-bundling to avoid cache issues exclude: [ "@autumn/shared", From 033d6133c9ed60fe7a32412b5c354c86d85dab9e Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 28 Jan 2026 17:26:58 +0000 Subject: [PATCH 006/110] chore: more clean up, created general attach / update subscription actions --- .claude/skills/write-test/SKILL.md | 2 + .../skills/write-test/references/GOTCHAS.md | 16 +++ server/src/internal/billing/billingRouter.ts | 6 +- .../billing/v2/actions/attach/attach.ts | 99 +++++++++++++++++++ .../computeAttachNewCustomerProduct.ts | 0 .../attach/compute/computeAttachPlan.ts | 0 .../compute/computeAttachTransitionUpdates.ts | 0 .../attach/compute/finalizeAttachPlan.ts | 0 .../attach/errors/handleAttachV2Errors.ts | 0 .../attach/logs/logAttachContext.ts | 0 .../attach/setup/setupAttachBillingContext.ts | 0 .../attach/setup/setupAttachCheckoutMode.ts | 0 .../attach/setup/setupAttachEndOfCycleMs.ts | 0 .../attach/setup/setupAttachProductContext.ts | 2 +- .../setup/setupAttachTransitionContext.ts | 0 .../src/internal/billing/v2/actions/index.ts | 6 ++ .../compute/cancel/applyCancelPlan.ts | 0 .../compute/cancel/applyUncancelToPlan.ts | 2 +- .../compute/cancel/computeCancelFields.ts | 0 .../compute/cancel/computeCancelLineItems.ts | 0 .../compute/cancel/computeCancelPlan.ts | 2 +- .../compute/cancel/computeCancelUpdates.ts | 0 .../cancel/computeCustomerProductToDelete.ts | 0 .../cancel/computeDefaultCustomerProduct.ts | 0 .../compute/cancel/computeEndOfCycleMs.ts | 0 .../compute/computeDeleteCustomerProduct.ts | 0 .../computeUpdateSubscriptionIntent.ts | 0 .../compute/computeUpdateSubscriptionPlan.ts | 10 +- .../compute/customPlan/computeCustomPlan.ts | 4 +- .../computeCustomPlanNewCustomerProduct.ts | 2 +- .../compute/finalizeUpdateSubscriptionPlan.ts | 0 .../calculateUpdateQuantityDifferences.ts | 0 ...alculateUpdateQuantityEntitlementChange.ts | 0 .../computeUpdateQuantityDetails.ts | 0 .../computeUpdateQuantityLineItems.ts | 0 .../computeUpdateQuantityPlan.ts | 0 .../errors/handleBillingBehaviorErrors.ts | 0 .../errors/handleCancelEndOfCycleErrors.ts | 0 .../handleCurrentCustomerProductErrors.ts | 0 .../errors/handleCustomPlanErrors.ts | 0 .../errors/handleFeatureQuantityErrors.ts | 0 .../errors/handleOneOffErrors.ts | 0 .../handleProductTypeTransitionErrors.ts | 0 .../errors/handleUncancelErrors.ts | 0 .../errors/handleUpdateSubscriptionErrors.ts | 2 +- .../logs/logUpdateSubscriptionContext.ts | 0 .../setup/findTargetCustomerProduct.ts | 0 .../setup/setupDefaultProductContext.ts | 0 .../setupUpdateSubscriptionBillingContext.ts | 6 +- .../setupUpdateSubscriptionProductContext.ts | 2 +- .../updateSubscription/updateSubscription.ts | 93 +++++++++++++++++ .../billing/v2/attach/handleAttachV2.ts | 97 ------------------ .../billing/v2/handlers/handleAttachV2.ts | 45 +++++++++ .../v2/handlers/handlePreviewAttach.ts | 40 ++++++++ .../handlePreviewUpdateSubscription.ts | 28 ++++++ .../v2/handlers/handleUpdateSubscription.ts | 46 +++++++++ .../handlePreviewUpdateSubscription.ts | 72 -------------- .../handleUpdateSubscription.ts | 82 --------------- .../customers/cancel/handleCancelV2.ts | 8 +- .../integration/billing/attach/attachTests.md | 11 +++ .../attach/new-plan/attach-entities.test.ts | 26 +++-- .../attach/new-plan/attach-free.test.ts | 4 +- .../attach/new-plan/attach-one-time.test.ts | 16 +-- .../attach/new-plan/attach-paid.test.ts | 10 +- ...compute-update-subscription-intent.spec.ts | 2 +- .../get-update-subscription-body.ts | 10 +- 66 files changed, 441 insertions(+), 310 deletions(-) create mode 100644 server/src/internal/billing/v2/actions/attach/attach.ts rename server/src/internal/billing/v2/{ => actions}/attach/compute/computeAttachNewCustomerProduct.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/compute/computeAttachPlan.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/compute/computeAttachTransitionUpdates.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/compute/finalizeAttachPlan.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/errors/handleAttachV2Errors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/logs/logAttachContext.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/setup/setupAttachBillingContext.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/setup/setupAttachCheckoutMode.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/setup/setupAttachEndOfCycleMs.ts (100%) rename server/src/internal/billing/v2/{ => actions}/attach/setup/setupAttachProductContext.ts (91%) rename server/src/internal/billing/v2/{ => actions}/attach/setup/setupAttachTransitionContext.ts (100%) create mode 100644 server/src/internal/billing/v2/actions/index.ts rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/applyCancelPlan.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/applyUncancelToPlan.ts (94%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeCancelFields.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeCancelLineItems.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeCancelPlan.ts (95%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeCancelUpdates.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/cancel/computeEndOfCycleMs.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/computeDeleteCustomerProduct.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/computeUpdateSubscriptionIntent.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/computeUpdateSubscriptionPlan.ts (78%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/customPlan/computeCustomPlan.ts (92%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts (94%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/updateQuantity/calculateUpdateQuantityDifferences.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleBillingBehaviorErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleCancelEndOfCycleErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleCurrentCustomerProductErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleCustomPlanErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleFeatureQuantityErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleOneOffErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleProductTypeTransitionErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleUncancelErrors.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/errors/handleUpdateSubscriptionErrors.ts (97%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/logs/logUpdateSubscriptionContext.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/setup/findTargetCustomerProduct.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/setup/setupDefaultProductContext.ts (100%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts (93%) rename server/src/internal/billing/v2/{ => actions}/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts (94%) create mode 100644 server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts delete mode 100644 server/src/internal/billing/v2/attach/handleAttachV2.ts create mode 100644 server/src/internal/billing/v2/handlers/handleAttachV2.ts create mode 100644 server/src/internal/billing/v2/handlers/handlePreviewAttach.ts create mode 100644 server/src/internal/billing/v2/handlers/handlePreviewUpdateSubscription.ts create mode 100644 server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts delete mode 100644 server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts delete mode 100644 server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts diff --git a/.claude/skills/write-test/SKILL.md b/.claude/skills/write-test/SKILL.md index aa2fc426e..4e875d944 100644 --- a/.claude/skills/write-test/SKILL.md +++ b/.claude/skills/write-test/SKILL.md @@ -28,6 +28,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p - **ALWAYS use `test.concurrent()` for ALL tests** - never use plain `test()`. This enables parallel execution. - Use `initScenario` with `s.*` builders - Use `product.id` in `s.attach()` (never string literals) +- Use `product.id` in expectations too (initScenario already prefixes with customerId) - Use `Decimal.js` for balance calculations in track tests - Unique `customerId` per test - Use generic types with `AutumnInt`: `autumnV1.customers.get()`, `autumnV1.check()` @@ -41,6 +42,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p - Use raw arithmetic for balance calculations (floating point errors) - Use `as unknown as Type` casting - use generic types instead - Write manual assertion loops when a utility function exists +- Use `${product.id}_${customerId}` for productId - just use `product.id` (already prefixed) ## AutumnInt Response Types diff --git a/.claude/skills/write-test/references/GOTCHAS.md b/.claude/skills/write-test/references/GOTCHAS.md index 91605f54a..19c9cc615 100644 --- a/.claude/skills/write-test/references/GOTCHAS.md +++ b/.claude/skills/write-test/references/GOTCHAS.md @@ -28,6 +28,22 @@ s.attach({ productId: pro.id }) ``` Products are prefixed by `initScenario`. Always use `product.id`. +### Product IDs in Expectations - Just Use `product.id` +```typescript +// WRONG - Double prefix (initScenario already adds customerId prefix) +expectProductActive({ + customer, + productId: `${pro.id}_${customerId}`, // Will fail! +}); + +// RIGHT - Just use product.id directly +expectProductActive({ + customer, + productId: pro.id, +}); +``` +`initScenario` already prefixes product IDs with `customerId`. When verifying products, just use `product.id` directly. + ### Multiple Products Need Unique IDs ```typescript // WRONG - Same default ID diff --git a/server/src/internal/billing/billingRouter.ts b/server/src/internal/billing/billingRouter.ts index 2eb3d01dc..82ccc1819 100644 --- a/server/src/internal/billing/billingRouter.ts +++ b/server/src/internal/billing/billingRouter.ts @@ -1,13 +1,13 @@ import { Hono } from "hono"; -import { handlePreviewUpdateSubscription } from "@/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.js"; import { handleAttachPreview } from "@/internal/customers/attach/handleAttachPreview/handleAttachPreview.js"; import { handleCancelV2 } from "@/internal/customers/cancel/handleCancelV2.js"; import type { HonoEnv } from "../../honoUtils/HonoEnv.js"; import { handleAttach } from "./attach/handleAttach.js"; import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js"; import { handleSetupPayment } from "./handlers/handleSetupPayment.js"; -import { handleAttachV2 } from "./v2/attach/handleAttachV2.js"; -import { handleUpdateSubscription } from "./v2/updateSubscription/handleUpdateSubscription.js"; +import { handleAttachV2 } from "./v2/handlers/handleAttachV2.js"; +import { handlePreviewUpdateSubscription } from "./v2/handlers/handlePreviewUpdateSubscription.js"; +import { handleUpdateSubscription } from "./v2/handlers/handleUpdateSubscription.js"; export const billingRouter = new Hono(); diff --git a/server/src/internal/billing/v2/actions/attach/attach.ts b/server/src/internal/billing/v2/actions/attach/attach.ts new file mode 100644 index 000000000..bebf70322 --- /dev/null +++ b/server/src/internal/billing/v2/actions/attach/attach.ts @@ -0,0 +1,99 @@ +import { type AttachParamsV0, RecaseError } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { computeAttachPlan } from "@/internal/billing/v2/actions/attach/compute/computeAttachPlan"; +import { handleAttachV2Errors } from "@/internal/billing/v2/actions/attach/errors/handleAttachV2Errors"; +import { logAttachContext } from "@/internal/billing/v2/actions/attach/logs/logAttachContext"; +import { setupAttachBillingContext } from "@/internal/billing/v2/actions/attach/setup/setupAttachBillingContext"; +import { executeBillingPlan } from "@/internal/billing/v2/execute/executeBillingPlan"; +import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan"; +import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; +import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult"; +import type { + AttachBillingContext, + BillingPlan, + BillingResult, +} from "@/internal/billing/v2/types"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; + +export async function attach({ + ctx, + params, + preview = false, +}: { + ctx: AutumnContext; + params: AttachParamsV0; + preview?: boolean; +}): Promise<{ + billingContext: AttachBillingContext; + billingPlan: BillingPlan; + billingResult: BillingResult | null; +}> { + // 1. Setup + const billingContext = await setupAttachBillingContext({ + ctx, + params, + }); + + logAttachContext({ ctx, billingContext }); + + // 2. Compute + const autumnBillingPlan = computeAttachPlan({ + ctx, + attachBillingContext: billingContext, + }); + + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); + + // 3. Errors + handleAttachV2Errors({ + ctx, + billingContext, + autumnBillingPlan, + params, + }); + + if (billingContext.checkoutMode !== null) { + // 4. Handle checkout mode (redirect to Stripe checkout) + throw new RecaseError({ + message: `Checkout flow not yet implemented for attach v2 (checkoutMode: ${billingContext.checkoutMode}). Please add a payment method to the customer first.`, + statusCode: 400, + }); + } + + // 5. Evaluate Stripe billing plan + const stripeBillingPlan = await evaluateStripeBillingPlan({ + ctx, + billingContext, + autumnBillingPlan, + }); + + logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); + + const billingPlan = { + autumn: autumnBillingPlan, + stripe: stripeBillingPlan, + }; + + if (!preview) { + return { + billingContext, + billingPlan, + billingResult: null, + }; + } + + // 6. Execute billing plan + const billingResult = await executeBillingPlan({ + ctx, + billingContext, + billingPlan, + }); + + logStripeBillingResult({ ctx, result: billingResult.stripe }); + + return { + billingContext, + billingPlan, + billingResult, + }; +} diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts b/server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts similarity index 100% rename from server/src/internal/billing/v2/attach/compute/computeAttachNewCustomerProduct.ts rename to server/src/internal/billing/v2/actions/attach/compute/computeAttachNewCustomerProduct.ts diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts b/server/src/internal/billing/v2/actions/attach/compute/computeAttachPlan.ts similarity index 100% rename from server/src/internal/billing/v2/attach/compute/computeAttachPlan.ts rename to server/src/internal/billing/v2/actions/attach/compute/computeAttachPlan.ts diff --git a/server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts b/server/src/internal/billing/v2/actions/attach/compute/computeAttachTransitionUpdates.ts similarity index 100% rename from server/src/internal/billing/v2/attach/compute/computeAttachTransitionUpdates.ts rename to server/src/internal/billing/v2/actions/attach/compute/computeAttachTransitionUpdates.ts diff --git a/server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts b/server/src/internal/billing/v2/actions/attach/compute/finalizeAttachPlan.ts similarity index 100% rename from server/src/internal/billing/v2/attach/compute/finalizeAttachPlan.ts rename to server/src/internal/billing/v2/actions/attach/compute/finalizeAttachPlan.ts diff --git a/server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts b/server/src/internal/billing/v2/actions/attach/errors/handleAttachV2Errors.ts similarity index 100% rename from server/src/internal/billing/v2/attach/errors/handleAttachV2Errors.ts rename to server/src/internal/billing/v2/actions/attach/errors/handleAttachV2Errors.ts diff --git a/server/src/internal/billing/v2/attach/logs/logAttachContext.ts b/server/src/internal/billing/v2/actions/attach/logs/logAttachContext.ts similarity index 100% rename from server/src/internal/billing/v2/attach/logs/logAttachContext.ts rename to server/src/internal/billing/v2/actions/attach/logs/logAttachContext.ts diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts similarity index 100% rename from server/src/internal/billing/v2/attach/setup/setupAttachBillingContext.ts rename to server/src/internal/billing/v2/actions/attach/setup/setupAttachBillingContext.ts diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachCheckoutMode.ts similarity index 100% rename from server/src/internal/billing/v2/attach/setup/setupAttachCheckoutMode.ts rename to server/src/internal/billing/v2/actions/attach/setup/setupAttachCheckoutMode.ts diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachEndOfCycleMs.ts similarity index 100% rename from server/src/internal/billing/v2/attach/setup/setupAttachEndOfCycleMs.ts rename to server/src/internal/billing/v2/actions/attach/setup/setupAttachEndOfCycleMs.ts diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachProductContext.ts similarity index 91% rename from server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts rename to server/src/internal/billing/v2/actions/attach/setup/setupAttachProductContext.ts index 9bccfd4e4..46bf14dab 100644 --- a/server/src/internal/billing/v2/attach/setup/setupAttachProductContext.ts +++ b/server/src/internal/billing/v2/actions/attach/setup/setupAttachProductContext.ts @@ -1,7 +1,7 @@ import type { AttachParamsV0 } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { ProductService } from "@/internal/products/ProductService"; -import { setupCustomFullProduct } from "../../setup/setupCustomFullProduct"; +import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct"; /** * Loads the product being attached, handling version and custom items params. diff --git a/server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts b/server/src/internal/billing/v2/actions/attach/setup/setupAttachTransitionContext.ts similarity index 100% rename from server/src/internal/billing/v2/attach/setup/setupAttachTransitionContext.ts rename to server/src/internal/billing/v2/actions/attach/setup/setupAttachTransitionContext.ts diff --git a/server/src/internal/billing/v2/actions/index.ts b/server/src/internal/billing/v2/actions/index.ts new file mode 100644 index 000000000..99dc02fc7 --- /dev/null +++ b/server/src/internal/billing/v2/actions/index.ts @@ -0,0 +1,6 @@ +import { attach } from "@/internal/billing/v2/actions/attach/attach"; +import { updateSubscription } from "@/internal/billing/v2/actions/updateSubscription/updateSubscription"; +export const billingActions = { + attach: attach, + updateSubscription: updateSubscription, +} as const; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/applyCancelPlan.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/applyCancelPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/applyCancelPlan.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/applyUncancelToPlan.ts similarity index 94% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/applyUncancelToPlan.ts index 4fb461f18..513446219 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/applyUncancelToPlan.ts @@ -1,6 +1,6 @@ import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; -import { computeCustomerProductToDelete } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete"; +import { computeCustomerProductToDelete } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCustomerProductToDelete"; /** * Applies uncancel updates to an existing billing plan. diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelFields.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelFields.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelLineItems.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelLineItems.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelLineItems.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts similarity index 95% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts index 49a5df32a..b9f8b761c 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan.ts @@ -1,7 +1,7 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; -import { applyUncancelToPlan } from "@/internal/billing/v2/updateSubscription/compute/cancel/applyUncancelToPlan"; +import { applyUncancelToPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/applyUncancelToPlan"; import { applyCancelPlan } from "./applyCancelPlan"; import { computeCancelLineItems } from "./computeCancelLineItems"; import { computeCancelUpdates } from "./computeCancelUpdates"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelUpdates.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCancelUpdates.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelUpdates.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCustomerProductToDelete.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeDefaultCustomerProduct.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeEndOfCycleMs.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/cancel/computeEndOfCycleMs.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/cancel/computeEndOfCycleMs.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/computeDeleteCustomerProduct.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/computeDeleteCustomerProduct.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/computeDeleteCustomerProduct.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/computeDeleteCustomerProduct.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionIntent.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionIntent.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan.ts similarity index 78% rename from server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan.ts index 1ced87dfb..c929f974c 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan.ts @@ -3,15 +3,15 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; -import { computeCancelPlan } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCancelPlan"; +import { computeCancelPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelPlan"; import { computeUpdateSubscriptionIntent, UpdateSubscriptionIntent, -} from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionIntent"; -import { computeCustomPlan } from "@/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan"; -import { finalizeUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan"; -import { computeUpdateQuantityPlan } from "@/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan"; +} from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent"; +import { computeCustomPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlan"; +import { finalizeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/finalizeUpdateSubscriptionPlan"; +import { computeUpdateQuantityPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan"; /** * Compute the subscription update plan diff --git a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlan.ts similarity index 92% rename from server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlan.ts index 65b3b1cbf..59d05c479 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlan.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlan.ts @@ -6,8 +6,8 @@ import type { AutumnContext } from "@server/honoUtils/HonoEnv"; import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; import { buildAutumnLineItems } from "@/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems"; import type { AutumnBillingPlan } from "@/internal/billing/v2/types"; -import { computeDeleteCustomerProduct } from "@/internal/billing/v2/updateSubscription/compute/computeDeleteCustomerProduct"; -import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct"; +import { computeDeleteCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/computeDeleteCustomerProduct"; +import { computeCustomPlanNewCustomerProduct } from "@/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct"; export const computeCustomPlan = async ({ ctx, diff --git a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts similarity index 94% rename from server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts index aa940a3f4..dcc192a8d 100644 --- a/server/src/internal/billing/v2/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/compute/customPlan/computeCustomPlanNewCustomerProduct.ts @@ -1,7 +1,7 @@ import type { FullCusProduct, FullProduct } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import type { UpdateSubscriptionBillingContext } from "@/internal/billing/v2/types"; -import { computeCancelFields } from "@/internal/billing/v2/updateSubscription/compute/cancel/computeCancelFields"; +import { computeCancelFields } from "@/internal/billing/v2/actions/updateSubscription/compute/cancel/computeCancelFields"; import { cusProductToExistingRollovers } from "@/internal/billing/v2/utils/handleExistingRollovers/cusProductToExistingRollovers"; import { cusProductToExistingUsages } from "@/internal/billing/v2/utils/handleExistingUsages/cusProductToExistingUsages"; import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct"; diff --git a/server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/finalizeUpdateSubscriptionPlan.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/calculateUpdateQuantityDifferences.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityDifferences.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/calculateUpdateQuantityDifferences.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityDifferences.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityDetails.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityLineItems.ts diff --git a/server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts rename to server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/computeUpdateQuantityPlan.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleBillingBehaviorErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleBillingBehaviorErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleBillingBehaviorErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCancelEndOfCycleErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleCancelEndOfCycleErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleCurrentCustomerProductErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleCurrentCustomerProductErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleCustomPlanErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleCustomPlanErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleCustomPlanErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleFeatureQuantityErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleFeatureQuantityErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleFeatureQuantityErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleOneOffErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleOneOffErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleOneOffErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleProductTypeTransitionErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleProductTypeTransitionErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleProductTypeTransitionErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUncancelErrors.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/errors/handleUncancelErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleUncancelErrors.ts diff --git a/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts similarity index 97% rename from server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts rename to server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts index 19a901f51..500c374b0 100644 --- a/server/src/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors.ts @@ -10,7 +10,7 @@ import type { AutumnBillingPlan, UpdateSubscriptionBillingContext, } from "@/internal/billing/v2/types"; -import { handleCancelEndOfCycleErrors } from "@/internal/billing/v2/updateSubscription/errors/handleCancelEndOfCycleErrors"; +import { handleCancelEndOfCycleErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleCancelEndOfCycleErrors"; import { handleBillingBehaviorErrors } from "./handleBillingBehaviorErrors"; import { handleCurrentCustomerProductErrors } from "./handleCurrentCustomerProductErrors"; import { handleCustomPlanErrors } from "./handleCustomPlanErrors"; diff --git a/server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts b/server/src/internal/billing/v2/actions/updateSubscription/logs/logUpdateSubscriptionContext.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext.ts rename to server/src/internal/billing/v2/actions/updateSubscription/logs/logUpdateSubscriptionContext.ts diff --git a/server/src/internal/billing/v2/updateSubscription/setup/findTargetCustomerProduct.ts b/server/src/internal/billing/v2/actions/updateSubscription/setup/findTargetCustomerProduct.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/setup/findTargetCustomerProduct.ts rename to server/src/internal/billing/v2/actions/updateSubscription/setup/findTargetCustomerProduct.ts diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupDefaultProductContext.ts similarity index 100% rename from server/src/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext.ts rename to server/src/internal/billing/v2/actions/updateSubscription/setup/setupDefaultProductContext.ts diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts similarity index 93% rename from server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts rename to server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts index 4d1fa883d..3abd0b51b 100644 --- a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionBillingContext.ts @@ -9,9 +9,9 @@ import { setupInvoiceModeContext } from "@/internal/billing/v2/setup/setupInvoic import { setupResetCycleAnchor } from "@/internal/billing/v2/setup/setupResetCycleAnchor"; import { setupTrialContext } from "@/internal/billing/v2/setup/setupTrialContext"; -import { setupDefaultProductContext } from "@/internal/billing/v2/updateSubscription/setup/setupDefaultProductContext"; -import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext"; -import type { UpdateSubscriptionBillingContext } from "../../types"; +import { setupDefaultProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupDefaultProductContext"; +import { setupUpdateSubscriptionProductContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext"; +import type { UpdateSubscriptionBillingContext } from "../../../types"; /** * Fetch the context for updating a subscription diff --git a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts similarity index 94% rename from server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts rename to server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts index 202fe2c5f..c4ab5e715 100644 --- a/server/src/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionProductContext.ts @@ -7,7 +7,7 @@ import { } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { ProductService } from "@/internal/products/ProductService"; -import { setupCustomFullProduct } from "../../setup/setupCustomFullProduct"; +import { setupCustomFullProduct } from "../../../setup/setupCustomFullProduct"; import { findTargetCustomerProduct } from "./findTargetCustomerProduct"; export const setupUpdateSubscriptionProductContext = async ({ diff --git a/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts b/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts new file mode 100644 index 000000000..53bfc6db6 --- /dev/null +++ b/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts @@ -0,0 +1,93 @@ +import type { UpdateSubscriptionV0Params } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan"; +import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors"; +import { logUpdateSubscriptionContext } from "@/internal/billing/v2/actions/updateSubscription/logs/logUpdateSubscriptionContext"; +import { setupUpdateSubscriptionBillingContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionBillingContext"; +import { executeBillingPlan } from "@/internal/billing/v2/execute/executeBillingPlan"; +import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan"; +import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; +import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult"; +import type { + BillingPlan, + BillingResult, + UpdateSubscriptionBillingContext, +} from "@/internal/billing/v2/types"; +import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; + +export async function updateSubscription({ + ctx, + params, + preview = false, +}: { + ctx: AutumnContext; + params: UpdateSubscriptionV0Params; + preview?: boolean; +}): Promise<{ + billingContext: UpdateSubscriptionBillingContext; + billingPlan: BillingPlan; + billingResult: BillingResult | null; +}> { + ctx.logger.info( + `=============== RUNNING UPDATE SUBSCRIPTION FOR ${params.customer_id} ===============`, + ); + + // 1. Setup + const billingContext = await setupUpdateSubscriptionBillingContext({ + ctx, + params, + }); + logUpdateSubscriptionContext({ ctx, billingContext }); + + // 2. Compute + const autumnBillingPlan = await computeUpdateSubscriptionPlan({ + ctx, + billingContext, + params, + }); + logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); + + // 3. Errors + await handleUpdateSubscriptionErrors({ + ctx, + billingContext, + autumnBillingPlan, + params, + }); + + // 4. Evaluate Stripe billing plan + const stripeBillingPlan = await evaluateStripeBillingPlan({ + ctx, + billingContext, + autumnBillingPlan, + }); + logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); + + const billingPlan = { + autumn: autumnBillingPlan, + stripe: stripeBillingPlan, + }; + + if (!preview) { + return { + billingContext, + billingPlan, + billingResult: null, + }; + } + + // 5. Execute billing plan + const billingResult = await executeBillingPlan({ + ctx, + billingContext, + billingPlan, + }); + + logStripeBillingResult({ ctx, result: billingResult.stripe }); + + return { + billingContext, + billingPlan, + billingResult, + }; +} diff --git a/server/src/internal/billing/v2/attach/handleAttachV2.ts b/server/src/internal/billing/v2/attach/handleAttachV2.ts deleted file mode 100644 index 91c0beb35..000000000 --- a/server/src/internal/billing/v2/attach/handleAttachV2.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AttachParamsV0Schema, RecaseError } from "@autumn/shared"; -import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; -import { createRoute } from "../../../../honoMiddlewares/routeHandler"; -import { executeBillingPlan } from "../execute/executeBillingPlan"; -import { evaluateStripeBillingPlan } from "../providers/stripe/actionBuilders/evaluateStripeBillingPlan"; -import { logStripeBillingPlan } from "../providers/stripe/logs/logStripeBillingPlan"; -import { logStripeBillingResult } from "../providers/stripe/logs/logStripeBillingResult"; -import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse"; -import { computeAttachPlan } from "./compute/computeAttachPlan"; -import { handleAttachV2Errors } from "./errors/handleAttachV2Errors"; -import { logAttachContext } from "./logs/logAttachContext"; -import { setupAttachBillingContext } from "./setup/setupAttachBillingContext"; - -export const handleAttachV2 = createRoute({ - body: AttachParamsV0Schema, - lock: - process.env.NODE_ENV !== "development" - ? { - ttlMs: 120000, - errorMessage: - "Attach already in progress for this customer, try again in a few seconds", - getKey: (c) => { - const ctx = c.get("ctx"); - const body = c.req.valid("json"); - return `lock:attach:${ctx.org.id}:${ctx.env}:${body.customer_id}`; - }, - } - : undefined, - handler: async (c) => { - const ctx = c.get("ctx"); - const body = c.req.valid("json"); - - ctx.logger.info( - `=============== RUNNING ATTACH V2 FOR ${body.customer_id} ===============`, - ); - - // 1. Setup - const billingContext = await setupAttachBillingContext({ - ctx, - params: body, - }); - logAttachContext({ ctx, billingContext }); - - // 2. Compute - const autumnBillingPlan = computeAttachPlan({ - ctx, - attachBillingContext: billingContext, - }); - - logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); - - // 3. Errors - handleAttachV2Errors({ - ctx, - billingContext, - autumnBillingPlan, - params: body, - }); - - // 4. Handle checkout mode (redirect to Stripe checkout) - if (billingContext.checkoutMode !== null) { - throw new RecaseError({ - message: `Checkout flow not yet implemented for attach v2 (checkoutMode: ${billingContext.checkoutMode}). Please add a payment method to the customer first.`, - statusCode: 400, - }); - } - - // 5. Evaluate Stripe billing plan - const stripeBillingPlan = await evaluateStripeBillingPlan({ - ctx, - billingContext, - autumnBillingPlan, - }); - - logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); - - // 6. Execute billing plan - const billingResult = await executeBillingPlan({ - ctx, - billingContext, - billingPlan: { - autumn: autumnBillingPlan, - stripe: stripeBillingPlan, - }, - }); - - logStripeBillingResult({ ctx, result: billingResult.stripe }); - - // 7. Format response - const response = billingResultToResponse({ - billingContext, - billingResult, - }); - - return c.json(response, 200); - }, -}); diff --git a/server/src/internal/billing/v2/handlers/handleAttachV2.ts b/server/src/internal/billing/v2/handlers/handleAttachV2.ts new file mode 100644 index 000000000..22d1047a8 --- /dev/null +++ b/server/src/internal/billing/v2/handlers/handleAttachV2.ts @@ -0,0 +1,45 @@ +import { AttachParamsV0Schema, InternalError } from "@autumn/shared"; +import { billingActions } from "@/internal/billing/v2/actions"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; +import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse"; + +export const handleAttachV2 = createRoute({ + body: AttachParamsV0Schema, + lock: + process.env.NODE_ENV !== "development" + ? { + ttlMs: 120000, + errorMessage: + "Attach already in progress for this customer, try again in a few seconds", + getKey: (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + return `lock:attach:${ctx.org.id}:${ctx.env}:${body.customer_id}`; + }, + } + : undefined, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const { billingContext, billingResult } = await billingActions.attach({ + ctx, + params: body, + preview: true, + }); + + if (!billingResult) { + throw new InternalError({ + message: "billingResult not returned from attach action", + }); + } + + // 7. Format response + const response = billingResultToResponse({ + billingContext, + billingResult, + }); + + return c.json(response, 200); + }, +}); diff --git a/server/src/internal/billing/v2/handlers/handlePreviewAttach.ts b/server/src/internal/billing/v2/handlers/handlePreviewAttach.ts new file mode 100644 index 000000000..fb287d862 --- /dev/null +++ b/server/src/internal/billing/v2/handlers/handlePreviewAttach.ts @@ -0,0 +1,40 @@ +import { AttachParamsV0Schema } from "@autumn/shared"; +import { billingActions } from "@/internal/billing/v2/actions"; +import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; + +export const handlePreviewAttach = createRoute({ + body: AttachParamsV0Schema, + lock: + process.env.NODE_ENV !== "development" + ? { + ttlMs: 120000, + errorMessage: + "Attach already in progress for this customer, try again in a few seconds", + getKey: (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + return `lock:attach:${ctx.org.id}:${ctx.env}:${body.customer_id}`; + }, + } + : undefined, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const { billingContext, billingPlan } = await billingActions.attach({ + ctx, + params: body, + preview: true, + }); + + // 7. Format response + const previewResponse = billingPlanToPreviewResponse({ + ctx, + billingContext, + billingPlan, + }); + + return c.json(previewResponse, 200); + }, +}); diff --git a/server/src/internal/billing/v2/handlers/handlePreviewUpdateSubscription.ts b/server/src/internal/billing/v2/handlers/handlePreviewUpdateSubscription.ts new file mode 100644 index 000000000..94a9effc5 --- /dev/null +++ b/server/src/internal/billing/v2/handlers/handlePreviewUpdateSubscription.ts @@ -0,0 +1,28 @@ +import { UpdateSubscriptionV0ParamsSchema } from "@autumn/shared"; +import { billingActions } from "@/internal/billing/v2/actions"; +import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; + +export const handlePreviewUpdateSubscription = createRoute({ + body: UpdateSubscriptionV0ParamsSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const { billingContext, billingPlan } = + await billingActions.updateSubscription({ + ctx, + params: body, + preview: true, + }); + + // 7. Format response + const previewResponse = billingPlanToPreviewResponse({ + ctx, + billingContext, + billingPlan, + }); + + return c.json(previewResponse, 200); + }, +}); diff --git a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts new file mode 100644 index 000000000..206a6d815 --- /dev/null +++ b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts @@ -0,0 +1,46 @@ +import { UpdateSubscriptionV0ParamsSchema, InternalError } from "@autumn/shared"; +import { billingActions } from "@/internal/billing/v2/actions"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; +import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse"; + +export const handleUpdateSubscription = createRoute({ + body: UpdateSubscriptionV0ParamsSchema, + lock: + process.env.NODE_ENV !== "development" + ? { + ttlMs: 120000, + errorMessage: + "Update subscription already in progress for this customer, try again in a few seconds", + getKey: (c) => { + const ctx = c.get("ctx"); + const attachBody = c.req.valid("json"); + return `lock:attach:${ctx.org.id}:${ctx.env}:${attachBody.customer_id}`; + }, + } + : undefined, + handler: async (c) => { + const ctx = c.get("ctx"); + const body = c.req.valid("json"); + + const { billingContext, billingResult } = + await billingActions.updateSubscription({ + ctx, + params: body, + preview: true, + }); + + if (!billingResult) { + throw new InternalError({ + message: "billingResult not returned from updateSubscription action", + }); + } + + // 7. Format response + const response = billingResultToResponse({ + billingContext, + billingResult, + }); + + return c.json(response, 200); + }, +}); diff --git a/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts b/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts deleted file mode 100644 index dcfeefce0..000000000 --- a/server/src/internal/billing/v2/updateSubscription/handlePreviewUpdateSubscription.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { UpdateSubscriptionV0ParamsSchema } from "@autumn/shared"; -import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan"; -import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; -import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; -import { billingPlanToPreviewResponse } from "@/internal/billing/v2/utils/billingPlanToPreviewResponse"; -import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; -import { createRoute } from "../../../../honoMiddlewares/routeHandler"; -import { computeUpdateSubscriptionPlan } from "./compute/computeUpdateSubscriptionPlan"; -import { logUpdateSubscriptionContext } from "./logs/logUpdateSubscriptionContext"; -import { setupUpdateSubscriptionBillingContext } from "./setup/setupUpdateSubscriptionBillingContext"; - -export const handlePreviewUpdateSubscription = createRoute({ - body: UpdateSubscriptionV0ParamsSchema, - handler: async (c) => { - const ctx = c.get("ctx"); - const body = c.req.valid("json"); - - ctx.logger.info( - `=============== RUNNING PREVIEW UPDATE SUBSCRIPTION FOR ${body.customer_id} ===============`, - ); - - const updateSubscriptionBillingContext = - await setupUpdateSubscriptionBillingContext({ - ctx, - params: body, - }); - logUpdateSubscriptionContext({ - ctx, - billingContext: updateSubscriptionBillingContext, - }); - - const autumnBillingPlan = await computeUpdateSubscriptionPlan({ - ctx, - billingContext: updateSubscriptionBillingContext, - params: body, - }); - logAutumnBillingPlan({ - ctx, - plan: autumnBillingPlan, - billingContext: updateSubscriptionBillingContext, - }); - - await handleUpdateSubscriptionErrors({ - ctx, - billingContext: updateSubscriptionBillingContext, - autumnBillingPlan, - params: body, - }); - - const stripeBillingPlan = await evaluateStripeBillingPlan({ - ctx, - billingContext: updateSubscriptionBillingContext, - autumnBillingPlan, - }); - logStripeBillingPlan({ - ctx, - stripeBillingPlan, - billingContext: updateSubscriptionBillingContext, - }); - - const previewResponse = billingPlanToPreviewResponse({ - ctx, - billingContext: updateSubscriptionBillingContext, - billingPlan: { - autumn: autumnBillingPlan, - stripe: stripeBillingPlan, - }, - }); - - return c.json(previewResponse); - }, -}); diff --git a/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts b/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts deleted file mode 100644 index 428cfe4fe..000000000 --- a/server/src/internal/billing/v2/updateSubscription/handleUpdateSubscription.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { UpdateSubscriptionV0ParamsSchema } from "@autumn/shared"; -import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; -import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult"; -import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan"; -import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; -import { logUpdateSubscriptionContext } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext"; -import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; -import { billingResultToResponse } from "@/internal/billing/v2/utils/billingResult/billingResultToResponse"; -import { createRoute } from "../../../../honoMiddlewares/routeHandler"; -import { executeBillingPlan } from "../execute/executeBillingPlan"; -import { evaluateStripeBillingPlan } from "../providers/stripe/actionBuilders/evaluateStripeBillingPlan"; -import { setupUpdateSubscriptionBillingContext } from "./setup/setupUpdateSubscriptionBillingContext"; - -export const handleUpdateSubscription = createRoute({ - body: UpdateSubscriptionV0ParamsSchema, - lock: - process.env.NODE_ENV !== "development" - ? { - ttlMs: 120000, - errorMessage: - "Update subscription already in progress for this customer, try again in a few seconds", - getKey: (c) => { - const ctx = c.get("ctx"); - const attachBody = c.req.valid("json"); - return `lock:attach:${ctx.org.id}:${ctx.env}:${attachBody.customer_id}`; - }, - } - : undefined, - handler: async (c) => { - const ctx = c.get("ctx"); - const body = c.req.valid("json"); - - ctx.logger.info( - `=============== RUNNING UPDATE SUBSCRIPTION FOR ${body.customer_id} ===============`, - ); - - const billingContext = await setupUpdateSubscriptionBillingContext({ - ctx, - params: body, - }); - logUpdateSubscriptionContext({ ctx, billingContext }); - - const autumnBillingPlan = await computeUpdateSubscriptionPlan({ - ctx, - billingContext, - params: body, - }); - logAutumnBillingPlan({ ctx, plan: autumnBillingPlan, billingContext }); - - await handleUpdateSubscriptionErrors({ - ctx, - billingContext, - autumnBillingPlan, - params: body, - }); - - const stripeBillingPlan = await evaluateStripeBillingPlan({ - ctx, - billingContext, - autumnBillingPlan, - }); - logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); - - const billingResult = await executeBillingPlan({ - ctx, - billingContext, - billingPlan: { - autumn: autumnBillingPlan, - stripe: stripeBillingPlan, - }, - }); - - logStripeBillingResult({ ctx, result: billingResult.stripe }); - - const response = billingResultToResponse({ - billingContext, - billingResult, - }); - - return c.json(response, 200); - }, -}); diff --git a/server/src/internal/customers/cancel/handleCancelV2.ts b/server/src/internal/customers/cancel/handleCancelV2.ts index daf9ac587..a9bf507b1 100644 --- a/server/src/internal/customers/cancel/handleCancelV2.ts +++ b/server/src/internal/customers/cancel/handleCancelV2.ts @@ -4,10 +4,10 @@ import { executeBillingPlan } from "@/internal/billing/v2/execute/executeBilling import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan"; import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan"; import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult"; -import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionPlan"; -import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/updateSubscription/errors/handleUpdateSubscriptionErrors"; -import { logUpdateSubscriptionContext } from "@/internal/billing/v2/updateSubscription/logs/logUpdateSubscriptionContext"; -import { setupUpdateSubscriptionBillingContext } from "@/internal/billing/v2/updateSubscription/setup/setupUpdateSubscriptionBillingContext"; +import { computeUpdateSubscriptionPlan } from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionPlan"; +import { handleUpdateSubscriptionErrors } from "@/internal/billing/v2/actions/updateSubscription/errors/handleUpdateSubscriptionErrors"; +import { logUpdateSubscriptionContext } from "@/internal/billing/v2/actions/updateSubscription/logs/logUpdateSubscriptionContext"; +import { setupUpdateSubscriptionBillingContext } from "@/internal/billing/v2/actions/updateSubscription/setup/setupUpdateSubscriptionBillingContext"; import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; export const handleCancelV2 = createRoute({ diff --git a/server/tests/integration/billing/attach/attachTests.md b/server/tests/integration/billing/attach/attachTests.md index 9c0b06236..50621b270 100644 --- a/server/tests/integration/billing/attach/attachTests.md +++ b/server/tests/integration/billing/attach/attachTests.md @@ -92,6 +92,17 @@ s.billing.attach({ productId: pro.id, isAddOn: true }); ``` +13. **Product IDs in expectations - just use `product.id`** + - `initScenario` already prefixes product IDs with `customerId` + - Don't double-prefix in expectations + ```typescript + // ✅ GOOD - just use product.id + expectProductActive({ customer, productId: pro.id }); + + // ❌ BAD - double prefix + expectProductActive({ customer, productId: `${pro.id}_${customerId}` }); + ``` + 13. **Scheduled-switch tests must advance test clock with `advanceToNextInvoice()`** - After scheduling a downgrade, advance the test clock to verify: - A. Next cycle invoice is correct diff --git a/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts index e016df303..867ec4743 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts @@ -67,7 +67,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: create entity, attach pro to en await expectProductActive({ customer: entity, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Verify entity has messages feature @@ -83,9 +83,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: create entity, attach pro to en const customer = await autumnV1.customers.get(customerId); // Customer should not have products array with this product - const customerProduct = customer.products?.find( - (p) => p.id === `${pro.id}_${customerId}`, - ); + const customerProduct = customer.products?.find((p) => p.id === pro.id); expect(customerProduct).toBeUndefined(); }); @@ -137,11 +135,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: create 2 entities, attach pro t // Both entities should have the product await expectProductActive({ customer: entity1, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); await expectProductActive({ customer: entity2, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Both should have independent balances @@ -234,7 +232,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance // Attach pro to entity 2 mid-cycle await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${pro.id}_${customerId}`, + product_id: pro.id, entity_id: entities[1].id, }); @@ -251,11 +249,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance // Both should have the product await expectProductActive({ customer: entity1, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); await expectProductActive({ customer: entity2, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Get customer to check invoices @@ -314,7 +312,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro annual to entity")}` await expectProductActive({ customer: entity, - productId: `${proAnnual.id}_${customerId}`, + productId: proAnnual.id, }); // Verify messages feature @@ -380,11 +378,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to customer, then pr // Both should have the product await expectProductActive({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); await expectProductActive({ customer: entity, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Both should have independent balances @@ -448,11 +446,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free to customer, then f // Both should have the product await expectProductActive({ customer, - productId: `${free.id}_${customerId}`, + productId: free.id, }); await expectProductActive({ customer: entity, - productId: `${free.id}_${customerId}`, + productId: free.id, }); // Both should have independent balances diff --git a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts index 0500dbbef..b279b0017 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts @@ -55,7 +55,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async // Verify product is active await expectProductActive({ customer, - productId: `${free.id}_${customerId}`, + productId: free.id, }); // Verify messages feature has correct balance @@ -111,7 +111,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple featu // Verify product is active await expectProductActive({ customer, - productId: `${free.id}_${customerId}`, + productId: free.id, }); // Verify messages feature diff --git a/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts index 0d72ad3d6..1aa363914 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts @@ -72,7 +72,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase")}`, a // Verify product is active await expectProductActive({ customer, - productId: `${oneOff.id}_${customerId}`, + productId: oneOff.id, }); // Verify messages balance (100 from 1 pack) @@ -134,7 +134,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase twice" // Attach same product again await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${oneOff.id}_${customerId}`, + product_id: oneOff.id, options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); @@ -199,7 +199,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro then one-time as mai // Attach one-time without isAddOn - should replace pro await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${oneOff.id}_${customerId}`, + product_id: oneOff.id, options: [{ feature_id: TestFeature.Messages, quantity: 1 }], // Note: NOT setting is_add_on: true }); @@ -209,13 +209,13 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro then one-time as mai // Verify pro is no longer present (replaced) await expectProductNotPresent({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Verify one-time is active await expectProductActive({ customer, - productId: `${oneOff.id}_${customerId}`, + productId: oneOff.id, }); }); @@ -323,7 +323,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time as add-on to pr // Attach one-time add-on (is_add_on defined at product level, not in attach params) await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${oneOffAddon.id}_${customerId}`, + product_id: oneOffAddon.id, options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); @@ -332,11 +332,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time as add-on to pr // Verify both products are active await expectProductActive({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); await expectProductActive({ customer, - productId: `${oneOffAddon.id}_${customerId}`, + productId: oneOffAddon.id, }); // Verify combined messages balance (100 from pro + 50 from one-off = 150) diff --git a/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts index e8aa3df43..862e38b9d 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts @@ -70,7 +70,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with mixed features" // Verify product is active await expectProductActive({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Verify consumable words feature (50 included, no prepaid) @@ -154,7 +154,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with allocated, crea // Verify product is active await expectProductActive({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Verify users feature: 3 included, 5 used, -2 balance (overage) @@ -214,7 +214,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach base with prepaid messag func: async () => { await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${base.id}_${customerId}`, + product_id: base.id, }); }, }); @@ -260,7 +260,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid message func: async () => { await autumnV1.billing.attach({ customer_id: customerId, - product_id: `${pro.id}_${customerId}`, + product_id: pro.id, }); }, }); @@ -311,7 +311,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid message // Verify product is active await expectProductActive({ customer, - productId: `${pro.id}_${customerId}`, + productId: pro.id, }); // Verify messages feature: 0 prepaid purchased diff --git a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts index 6de741683..63b4e352b 100644 --- a/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts +++ b/server/tests/unit/billing/update-subscription/compute-update-subscription-intent.spec.ts @@ -13,7 +13,7 @@ import chalk from "chalk"; import { computeUpdateSubscriptionIntent, UpdateSubscriptionIntent, -} from "@/internal/billing/v2/updateSubscription/compute/computeUpdateSubscriptionIntent"; +} from "@/internal/billing/v2/actions/updateSubscription/compute/computeUpdateSubscriptionIntent"; const baseParams: UpdateSubscriptionV0Params = { customer_id: "cus_test", diff --git a/vite/src/components/forms/update-subscription/get-update-subscription-body.ts b/vite/src/components/forms/update-subscription/get-update-subscription-body.ts index 7657ac66b..33d1a9874 100644 --- a/vite/src/components/forms/update-subscription/get-update-subscription-body.ts +++ b/vite/src/components/forms/update-subscription/get-update-subscription-body.ts @@ -1,14 +1,12 @@ import type { BillingBehavior, + CancelAction, CreateFreeTrial, FeatureOptions, ProductItem, ProductV2, + RefundBehavior, } from "@autumn/shared"; -import type { - CancelActionValue, - RefundBehaviorValue, -} from "@/components/forms/update-subscription-v2/updateSubscriptionFormSchema"; export const getUpdateSubscriptionBody = ({ customerId, @@ -40,9 +38,9 @@ export const getUpdateSubscriptionBody = ({ // Custom items - separate from isCustom logic for preview support items?: ProductItem[] | null; // Cancel action fields - cancelAction?: CancelActionValue | null; + cancelAction?: CancelAction | null; billingBehavior?: BillingBehavior | null; - refundBehavior?: RefundBehaviorValue | null; + refundBehavior?: RefundBehavior | null; }) => { // For cancel actions, only include cancellation-related fields if (cancelAction) { From baa3b00295b547d19ecc3c671f97fa2ceee89657 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 29 Jan 2026 09:44:34 +0000 Subject: [PATCH 007/110] wip --- .../plans/checkout-session-completed-v2.md | 314 ++++++++++++++++++ .vscode/settings.json | 10 +- .../stripe/handleStripeWebhookEvent.ts | 11 +- .../handleStripeCheckoutSessionCompleted.ts | 35 ++ .../legacy}/getOptionsFromCheckout.ts | 2 +- .../handleCheckoutSessionCompletedLegacy.ts} | 12 +- .../legacy}/handleCheckoutSub.ts | 4 +- .../legacy}/handleRemainingSets.ts | 2 +- .../legacy}/handleSetupCheckout.ts | 4 +- .../setupCheckoutSessionCompletedContext.ts | 55 +++ server/src/internal/billing/billingRouter.ts | 2 + .../billing/v2/actions/attach/attach.ts | 34 +- .../updateSubscription/updateSubscription.ts | 2 +- .../buildAutumnLineItems.ts | 13 +- .../billing/v2/handlers/handleAttachV2.ts | 2 +- .../v2/handlers/handleUpdateSubscription.ts | 7 +- .../buildStripeCheckoutSessionAction.ts | 95 ++++++ .../buildStripeSubscriptionAction.ts | 12 +- .../buildStripeSubscriptionScheduleAction.ts | 6 +- .../evaluateStripeBillingPlan.ts | 25 +- .../execute/executeStripeBillingPlan.ts | 20 +- .../executeStripeCheckoutSessionAction.ts | 94 ++++++ .../execute/executeStripeInvoiceAction.ts | 6 +- .../executeStripeSubscriptionAction.ts | 8 +- ...executeStripeSubscriptionScheduleAction.ts | 6 +- .../buildStripeSubscriptionItemsUpdate.ts | 50 ++- .../buildStripePhasesUpdate.ts | 36 +- .../billing/v2/types/autumnBillingPlan.ts | 5 +- .../internal/billing/v2/types/billingPlan.ts | 6 +- .../billing/v2/types/billingResult.ts | 1 + server/src/internal/billing/v2/types/index.ts | 1 + .../stripeBillingPlan/stripeBillingPlan.ts | 7 + .../stripeCheckoutSessionAction.ts | 11 + .../billingResult/billingResultToResponse.ts | 15 +- .../checkouts/handlers/handleGetCheckout.ts | 0 server/src/internal/checkouts/index.ts | 0 .../src/internal/metadata/MetadataService.ts | 18 + .../utils/insertMetadataFromBillingPlan.ts | 36 +- .../integration/billing/attach/attachTests.md | 26 +- .../autumn-checkout-basic.test.ts | 102 ++++++ .../stripe-checkout-basic.test.ts | 278 ++++++++++++++++ .../attach/new-plan/attach-entities.test.ts | 199 +++++++++-- .../attach/new-plan/attach-free.test.ts | 36 +- .../attach/new-plan/attach-one-time.test.ts | 209 +++++++++--- .../attach/new-plan/attach-paid.test.ts | 81 +++-- .../update-subscription/BILLING_GUIDE.md | 215 ------------ .../update-subscription.test.ts | 1 + shared/api/billing/common/billingResponse.ts | 3 + .../models/billingModels/cusProductActions.ts | 33 -- .../models/billingModels/newProductAction.ts | 16 - .../billingModels/ongoingCusProductAction.ts | 18 - .../scheduledCusProductAction.ts | 12 - shared/models/otherModels/metadataTable.ts | 6 +- shared/utils/orgUtils/convertOrgUtils.ts | 15 + .../forms/attach-v2/attachFormSchema.ts | 11 + .../attach-v2/components/AttachFooter.tsx | 96 ++++++ .../components/AttachPlanSection.tsx | 140 ++++++++ .../components/AttachPreviewSection.tsx | 60 ++++ .../components/AttachProductSelection.tsx | 48 +++ .../attach-v2/context/AttachFormProvider.tsx | 288 ++++++++++++++++ .../forms/attach-v2/hooks/useAttachForm.ts | 25 ++ .../attach-v2/hooks/useAttachMutation.ts | 110 ++++++ .../forms/attach-v2/hooks/useAttachPreview.ts | 90 +++++ .../attach-v2/hooks/useAttachRequestBody.ts | 125 +++++++ vite/src/components/forms/attach-v2/index.ts | 15 + vite/src/hooks/stores/useSheetStore.ts | 1 + .../sheets/AttachProductSheetV2.tsx | 127 +++++++ .../AttachProductSheetTrigger.tsx | 2 +- .../customers2/customer/CustomerSheets.tsx | 3 + 69 files changed, 2847 insertions(+), 511 deletions(-) create mode 100644 .opencode/plans/checkout-session-completed-v2.md create mode 100644 server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/handleStripeCheckoutSessionCompleted.ts rename server/src/external/stripe/webhookHandlers/{handleCheckoutCompleted => handleStripeCheckoutSessionCompleted/legacy}/getOptionsFromCheckout.ts (95%) rename server/src/external/stripe/webhookHandlers/{handleCheckoutCompleted.ts => handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSessionCompletedLegacy.ts} (92%) rename server/src/external/stripe/webhookHandlers/{handleCheckoutCompleted => handleStripeCheckoutSessionCompleted/legacy}/handleCheckoutSub.ts (94%) rename server/src/external/stripe/webhookHandlers/{handleCheckoutCompleted => handleStripeCheckoutSessionCompleted/legacy}/handleRemainingSets.ts (94%) rename server/src/external/stripe/webhookHandlers/{handleCheckoutCompleted => handleStripeCheckoutSessionCompleted/legacy}/handleSetupCheckout.ts (91%) create mode 100644 server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts create mode 100644 server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts create mode 100644 server/src/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction.ts create mode 100644 server/src/internal/billing/v2/types/stripeBillingPlan/stripeCheckoutSessionAction.ts create mode 100644 server/src/internal/checkouts/handlers/handleGetCheckout.ts create mode 100644 server/src/internal/checkouts/index.ts create mode 100644 server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts create mode 100644 server/tests/integration/billing/attach/checkout/stripe-checkout/stripe-checkout-basic.test.ts delete mode 100644 server/tests/integration/billing/update-subscription/BILLING_GUIDE.md create mode 100644 server/tests/integration/billing/update-subscription/update-subscription.test.ts delete mode 100644 shared/models/billingModels/cusProductActions.ts delete mode 100644 shared/models/billingModels/newProductAction.ts delete mode 100644 shared/models/billingModels/ongoingCusProductAction.ts delete mode 100644 shared/models/billingModels/scheduledCusProductAction.ts create mode 100644 vite/src/components/forms/attach-v2/attachFormSchema.ts create mode 100644 vite/src/components/forms/attach-v2/components/AttachFooter.tsx create mode 100644 vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx create mode 100644 vite/src/components/forms/attach-v2/components/AttachPreviewSection.tsx create mode 100644 vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx create mode 100644 vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx create mode 100644 vite/src/components/forms/attach-v2/hooks/useAttachForm.ts create mode 100644 vite/src/components/forms/attach-v2/hooks/useAttachMutation.ts create mode 100644 vite/src/components/forms/attach-v2/hooks/useAttachPreview.ts create mode 100644 vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts create mode 100644 vite/src/components/forms/attach-v2/index.ts create mode 100644 vite/src/views/customers2/components/sheets/AttachProductSheetV2.tsx diff --git a/.opencode/plans/checkout-session-completed-v2.md b/.opencode/plans/checkout-session-completed-v2.md new file mode 100644 index 000000000..b2a805b71 --- /dev/null +++ b/.opencode/plans/checkout-session-completed-v2.md @@ -0,0 +1,314 @@ +# V2 Checkout Session Completed Implementation Plan + +## Overview + +Implement the V2 flow for `checkout.session.completed` webhook handler. The V2 flow uses the new billing plan architecture where: +1. Billing plan is stored in metadata during checkout session creation +2. When checkout completes, we modify the billing plan based on checkout results +3. Execute the deferred billing plan (which now handles invoice/subscription upserts) + +## Current State + +- ✅ Main entry point created: `handleStripeCheckoutSessionCompleted.ts` +- ✅ Context setup created: `setupCheckoutSessionCompletedContext.ts` +- ✅ Legacy files moved to `legacy/` folder +- ⏳ V2 flow returns early with "not yet implemented" log + +## Architecture Changes + +### 1. Extend AutumnBillingPlan Schema + +**File:** `server/src/internal/billing/v2/types/autumnBillingPlan.ts` + +Add two new optional fields: + +```typescript +export const AutumnBillingPlanSchema = z.object({ + // ...existing fields... + + // NEW: Insert operations for subscription and invoice + insertSubscription: SubscriptionSchema.optional(), + upsertInvoice: InvoiceSchema.optional(), +}); +``` + +**Rationale:** By adding these to the billing plan, we can: +- Use the same `executeAutumnBillingPlan` for all flows +- Keep billing operations centralized +- Allow both immediate execution and deferred execution to use the same path + +### 2. Update executeAutumnBillingPlan + +**File:** `server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts` + +Add at the end: + +```typescript +// 6. Insert subscription (if provided) +if (autumnBillingPlan.insertSubscription) { + await SubService.upsert({ + db, + subscription: autumnBillingPlan.insertSubscription, + }); +} + +// 7. Upsert invoice (if provided) +if (autumnBillingPlan.upsertInvoice) { + await InvoiceService.upsert({ + db, + invoice: autumnBillingPlan.upsertInvoice, + }); +} +``` + +### 3. Add Upsert Methods to Services + +**File:** `server/src/internal/subscriptions/SubService.ts` + +```typescript +static async upsert({ + db, + subscription, +}: { + db: DrizzleCli; + subscription: Subscription; +}) { + const updateColumns = buildConflictUpdateColumns(subscriptions, ["id"]); + await db + .insert(subscriptions) + .values(subscription) + .onConflictDoUpdate({ + target: subscriptions.stripe_id, + set: updateColumns, + }); +} +``` + +**File:** `server/src/internal/invoices/InvoiceService.ts` + +```typescript +static async upsert({ + db, + invoice, +}: { + db: DrizzleCli; + invoice: Invoice; +}) { + const updateColumns = buildConflictUpdateColumns(invoices, ["id"]); + await db + .insert(invoices) + .values(invoice as any) + .onConflictDoUpdate({ + target: invoices.stripe_id, + set: updateColumns, + }); +} +``` + +### 4. Modify upsertInvoiceFromBilling and upsertSubscriptionFromBilling + +These functions currently call services directly. Change them to **build** the Autumn objects and add to the billing plan instead. + +**File:** `server/src/internal/billing/v2/utils/upsertFromStripe/upsertSubscriptionFromBilling.ts` + +Change from: +```typescript +export const upsertSubscriptionFromBilling = async ({ + ctx, + stripeSubscription, +}: { + ctx: AutumnContext; + stripeSubscription: Stripe.Subscription; +}) => { + // ... calls SubService directly +} +``` + +To: +```typescript +export const buildSubscriptionFromStripe = ({ + ctx, + stripeSubscription, +}: { + ctx: AutumnContext; + stripeSubscription: Stripe.Subscription; +}): Subscription => { + const earliestPeriodEnd = getEarliestPeriodEnd({ sub: stripeSubscription }); + const currentPeriodStart = getLatestPeriodStart({ sub: stripeSubscription }); + + return { + id: generateId("sub"), + stripe_id: stripeSubscription.id, + stripe_schedule_id: stripeSubscription.schedule as string | null, + created_at: stripeSubscription.created * 1000, + usage_features: [], + org_id: ctx.org.id, + env: ctx.env, + current_period_start: currentPeriodStart, + current_period_end: earliestPeriodEnd, + }; +}; + +// Keep old function for backward compatibility, but call the new one +export const upsertSubscriptionFromBilling = async ({ + ctx, + stripeSubscription, +}: { + ctx: AutumnContext; + stripeSubscription: Stripe.Subscription; +}) => { + const subscription = buildSubscriptionFromStripe({ ctx, stripeSubscription }); + await SubService.upsert({ db: ctx.db, subscription }); +}; +``` + +**File:** `server/src/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling.ts` + +Similar pattern - add `buildInvoiceFromStripe` that returns `Invoice` object. + +--- + +## Checkout Session Completed Tasks + +### Task Structure + +``` +handleStripeCheckoutSessionCompleted/ +├── handleStripeCheckoutSessionCompleted.ts # Main entry +├── setupCheckoutSessionCompletedContext.ts # Already done +├── legacy/ # Already done +└── tasks/ + ├── modifyStripeSubscriptionFromCheckout.ts # Task 1 + ├── updateBillingPlanFromCheckout.ts # Task 2 + ├── queueCheckoutRewardTasks.ts # Task 3 + └── updateCustomerFromCheckout.ts # Task 4 +``` + +### Main Handler Flow + +```typescript +// handleStripeCheckoutSessionCompleted.ts +if (checkoutContext) { + const { metadata, stripeSubscription, stripeInvoice, stripeCheckoutSession } = checkoutContext; + const billingPlanData = metadata.data as DeferredAutumnBillingPlanData; + + // 1. Modify Stripe subscription (swap metered→empty, migrate to flexible) + if (stripeSubscription) { + await modifyStripeSubscriptionFromCheckout({ ctx, checkoutContext }); + } + + // 2. Update billing plan with checkout data (adds insertSubscription, upsertInvoice) + const updatedBillingPlanData = updateBillingPlanFromCheckout({ + ctx, + checkoutContext, + billingPlanData, + }); + + // 3. Execute deferred billing plan with updated data + await executeDeferredBillingPlanFromCheckout({ + ctx, + metadata, + billingPlanData: updatedBillingPlanData, + }); + + // 4. Queue checkout reward tasks + await queueCheckoutRewardTasks({ ctx, checkoutContext }); + + // 5. Update customer name/email + await updateCustomerFromCheckout({ ctx, checkoutContext }); + + return; +} +``` + +### Task 1: modifyStripeSubscriptionFromCheckout + +**Purpose:** Modify the Stripe subscription after checkout creates it. + +**Actions:** +1. Swap metered prices → empty prices (for entity-attached products) +2. Migrate subscription to flexible billing mode + +**Note:** Leave a TODO comment for "Create Autumn Subscription" - will be handled by billing plan now. + +### Task 2: updateBillingPlanFromCheckout + +**Purpose:** Modify the billing plan based on checkout results. + +**Actions:** +1. Extract prepaid quantities from checkout line items → update `insertCustomerProducts` (handle later) +2. Build `insertSubscription` from Stripe subscription using `buildSubscriptionFromStripe` +3. Build `upsertInvoice` from Stripe invoice using `buildInvoiceFromStripe` +4. Return new `DeferredAutumnBillingPlanData` with updated `billingPlan.autumn` + +### Task 3: queueCheckoutRewardTasks + +**Purpose:** Queue reward jobs for each product. + +**Actions:** +- For each product in `billingPlan.autumn.insertCustomerProducts` +- Queue `JobName.TriggerCheckoutReward` with customer/product/subId + +### Task 4: updateCustomerFromCheckout + +**Purpose:** Sync customer name/email from Stripe checkout details. + +**Actions:** +- If customer is missing name in Autumn but has it in checkout → update +- If customer is missing email in Autumn but has it in checkout → update + +--- + +## Implementation Order + +### Phase 1: Schema & Service Updates +1. Add `insertSubscription` and `upsertInvoice` to `AutumnBillingPlanSchema` +2. Add `SubService.upsert()` method +3. Add `InvoiceService.upsert()` method +4. Update `executeAutumnBillingPlan` to handle new fields + +### Phase 2: Build Functions +5. Create `buildSubscriptionFromStripe` in upsertSubscriptionFromBilling.ts +6. Create `buildInvoiceFromStripe` in upsertInvoiceFromBilling.ts +7. Update existing `upsertSubscriptionFromBilling` to use new builder +8. Update existing `upsertInvoiceFromBilling` to use new builder + +### Phase 3: Checkout Tasks +9. Create `modifyStripeSubscriptionFromCheckout.ts` +10. Create `updateBillingPlanFromCheckout.ts` +11. Create `queueCheckoutRewardTasks.ts` +12. Create `updateCustomerFromCheckout.ts` + +### Phase 4: Wire It Up +13. Update `handleStripeCheckoutSessionCompleted.ts` to call tasks +14. Test the full flow + +--- + +## Files to Modify + +| File | Changes | +|------|---------| +| `server/src/internal/billing/v2/types/autumnBillingPlan.ts` | Add `insertSubscription`, `upsertInvoice` fields | +| `server/src/internal/billing/v2/execute/executeAutumnBillingPlan.ts` | Handle new upsert fields | +| `server/src/internal/subscriptions/SubService.ts` | Add `upsert()` method | +| `server/src/internal/invoices/InvoiceService.ts` | Add `upsert()` method | +| `server/src/internal/billing/v2/utils/upsertFromStripe/upsertSubscriptionFromBilling.ts` | Add `buildSubscriptionFromStripe` | +| `server/src/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling.ts` | Add `buildInvoiceFromStripe` | + +## New Files to Create + +| File | Purpose | +|------|---------| +| `handleStripeCheckoutSessionCompleted/tasks/modifyStripeSubscriptionFromCheckout.ts` | Swap metered prices, migrate to flexible | +| `handleStripeCheckoutSessionCompleted/tasks/updateBillingPlanFromCheckout.ts` | Build subscription/invoice, update billing plan | +| `handleStripeCheckoutSessionCompleted/tasks/queueCheckoutRewardTasks.ts` | Queue reward jobs | +| `handleStripeCheckoutSessionCompleted/tasks/updateCustomerFromCheckout.ts` | Sync customer name/email | + +--- + +## Deferred Items + +- **Prepaid quantities extraction:** Will handle later (Task A from original analysis) +- **Allocated prices:** Skip for now, add comment +- **Idempotency check:** Removed per user feedback diff --git a/.vscode/settings.json b/.vscode/settings.json index 45f4ca949..a0721f975 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,5 +24,13 @@ "editor.defaultFormatter": "biomejs.biome" }, "postman.settings.dotenv-detection-notification-visibility": false, - "typescript.preferences.importModuleSpecifier": "non-relative" + "typescript.preferences.importModuleSpecifier": "non-relative", + "files.exclude": { + // "**/.claude": true, + "**/.cursor": true, + "**/.github": true, + "**/.opencode": true, + "**/.superset": true, + "**/.vscode": true + } } diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts index aa89755a4..66c47a0f4 100644 --- a/server/src/external/stripe/handleStripeWebhookEvent.ts +++ b/server/src/external/stripe/handleStripeWebhookEvent.ts @@ -8,10 +8,10 @@ import { unsetOrgStripeKeys } from "@/internal/orgs/orgUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { handleWebhookErrorSkip } from "@/utils/routerUtils/webhookErrorSkip.js"; import { getSentryTags } from "../sentry/sentryUtils.js"; -import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js"; import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js"; import { handleInvoiceFinalized } from "./webhookHandlers/handleInvoiceFinalized.js"; import { handleInvoiceUpdated } from "./webhookHandlers/handleInvoiceUpdated.js"; +import { handleStripeCheckoutSessionCompleted } from "./webhookHandlers/handleStripeCheckoutSessionCompleted/handleStripeCheckoutSessionCompleted.js"; import { handleStripeInvoiceCreated } from "./webhookHandlers/handleStripeInvoiceCreated/handleStripeInvoiceCreated.js"; import { handleStripeSubscriptionDeleted } from "./webhookHandlers/handleStripeSubscriptionDeleted/handleStripeSubscriptionDeleted.js"; import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js"; @@ -82,14 +82,7 @@ export const handleStripeWebhookEvent = async ( break; case "checkout.session.completed": { - const checkoutSession = event.data.object; - await handleCheckoutSessionCompleted({ - ctx, - db, - data: checkoutSession, - org, - env, - }); + await handleStripeCheckoutSessionCompleted({ ctx, event }); break; } } diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/handleStripeCheckoutSessionCompleted.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/handleStripeCheckoutSessionCompleted.ts new file mode 100644 index 000000000..ece3bd8e0 --- /dev/null +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/handleStripeCheckoutSessionCompleted.ts @@ -0,0 +1,35 @@ +import type Stripe from "stripe"; +import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js"; +import { handleCheckoutSessionCompletedLegacy } from "./legacy/handleCheckoutSessionCompletedLegacy.js"; +import { setupCheckoutSessionCompletedContext } from "./setupCheckoutSessionCompletedContext.js"; + +export const handleStripeCheckoutSessionCompleted = async ({ + ctx, + event, +}: { + ctx: StripeWebhookContext; + event: Stripe.CheckoutSessionCompletedEvent; +}) => { + const checkoutContext = await setupCheckoutSessionCompletedContext({ + ctx, + event, + }); + + // V2 flow + if (checkoutContext) { + ctx.logger.info( + "[checkout.session.completed] V2 checkout - not yet implemented", + ); + return; + } + + // Legacy flow - pass original params unchanged + const { db, org, env } = ctx; + await handleCheckoutSessionCompletedLegacy({ + ctx, + db, + org, + data: event.data.object, + env, + }); +}; diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/getOptionsFromCheckout.ts similarity index 95% rename from server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts rename to server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/getOptionsFromCheckout.ts index 3b9d06131..900b8adb9 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/getOptionsFromCheckout.ts @@ -6,7 +6,7 @@ import { getPriceEntitlement, priceIsOneOffAndTiered, } from "@/internal/products/prices/priceUtils.js"; -import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js"; +import { findStripeItemForPrice } from "../../../stripeSubUtils/stripeSubItemUtils.js"; export const getOptionsFromCheckoutSession = async ({ checkoutSession, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSessionCompletedLegacy.ts similarity index 92% rename from server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts rename to server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSessionCompletedLegacy.ts index 52d4418c1..17e8542b2 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSessionCompletedLegacy.ts @@ -18,13 +18,13 @@ import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtil import { attachToInsertParams } from "@/internal/products/productUtils.js"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js"; -import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; -import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js"; -import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js"; -import { handleSetupCheckout } from "./handleCheckoutCompleted/handleSetupCheckout.js"; +import { getEarliestPeriodEnd } from "../../../stripeSubUtils/convertSubUtils.js"; +import { getOptionsFromCheckoutSession } from "./getOptionsFromCheckout.js"; +import { handleCheckoutSub } from "./handleCheckoutSub.js"; +import { handleRemainingSets } from "./handleRemainingSets.js"; +import { handleSetupCheckout } from "./handleSetupCheckout.js"; -export const handleCheckoutSessionCompleted = async ({ +export const handleCheckoutSessionCompletedLegacy = async ({ ctx, db, org, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSub.ts similarity index 94% rename from server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts rename to server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSub.ts index 70d808178..ab36f2857 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleCheckoutSub.ts @@ -8,8 +8,8 @@ import { } from "@/internal/products/prices/priceUtils/findPriceUtils.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { initSubscription } from "@/internal/subscriptions/utils/initSubscription.js"; -import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js"; -import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; +import { getEmptyPriceItem } from "../../../priceToStripeItem/priceToStripeItem.js"; +import { subToPeriodStartEnd } from "../../../stripeSubUtils/convertSubUtils.js"; export const handleCheckoutSub = async ({ stripeCli, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleRemainingSets.ts similarity index 94% rename from server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts rename to server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleRemainingSets.ts index 78325e8cd..e36342d1e 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleRemainingSets.ts @@ -1,7 +1,7 @@ import { ApiVersion, isUsagePrice, type Organization } from "@autumn/shared"; import type Stripe from "stripe"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js"; +import { getEmptyPriceItem } from "../../../priceToStripeItem/priceToStripeItem.js"; export const handleRemainingSets = async ({ stripeCli, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleSetupCheckout.ts similarity index 91% rename from server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts rename to server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleSetupCheckout.ts index da5a0e976..22fe2ab2e 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/legacy/handleSetupCheckout.ts @@ -5,8 +5,8 @@ import { handleOneOffFunction } from "@/internal/customers/attach/attachFunction import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { isOneOff } from "@/internal/products/productUtils.js"; -import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js"; -import { getCusPaymentMethod } from "../../stripeCusUtils.js"; +import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js"; +import { getCusPaymentMethod } from "../../../stripeCusUtils.js"; export const handleSetupCheckout = async ({ ctx, diff --git a/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts new file mode 100644 index 000000000..dd66f7f2c --- /dev/null +++ b/server/src/external/stripe/webhookHandlers/handleStripeCheckoutSessionCompleted/setupCheckoutSessionCompletedContext.ts @@ -0,0 +1,55 @@ +import { type Metadata, MetadataType } from "@autumn/shared"; +import type Stripe from "stripe"; +import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js"; +import type { StripeWebhookContext } from "../../webhookMiddlewares/stripeWebhookContext.js"; + +export interface CheckoutSessionCompletedContext { + stripeCheckoutSession: Stripe.Checkout.Session; + stripeSubscription?: Stripe.Subscription; + stripeInvoice?: Stripe.Invoice; + metadata: Metadata; +} + +export const setupCheckoutSessionCompletedContext = async ({ + ctx, + event, +}: { + ctx: StripeWebhookContext; + event: Stripe.CheckoutSessionCompletedEvent; +}): Promise => { + const { db, stripeCli } = ctx; + const checkoutSessionData = event.data.object; + + // Get metadata from checkout session + const metadata = await getMetadataFromCheckoutSession( + checkoutSessionData, + db, + ); + + // Return null if no metadata or not V2 checkout session type + if (!metadata || metadata.type !== MetadataType.CheckoutSessionV2) { + return null; + } + + // Expand checkout session to get subscription and invoice + const stripeCheckoutSession = await stripeCli.checkout.sessions.retrieve( + checkoutSessionData.id, + { + expand: ["subscription", "invoice"], + }, + ); + + const stripeSubscription = stripeCheckoutSession.subscription as + | Stripe.Subscription + | undefined; + const stripeInvoice = stripeCheckoutSession.invoice as + | Stripe.Invoice + | undefined; + + return { + stripeCheckoutSession, + stripeSubscription: stripeSubscription ?? undefined, + stripeInvoice: stripeInvoice ?? undefined, + metadata, + }; +}; diff --git a/server/src/internal/billing/billingRouter.ts b/server/src/internal/billing/billingRouter.ts index 82ccc1819..36df018f7 100644 --- a/server/src/internal/billing/billingRouter.ts +++ b/server/src/internal/billing/billingRouter.ts @@ -1,4 +1,5 @@ import { Hono } from "hono"; +import { handlePreviewAttach } from "@/internal/billing/v2/handlers/handlePreviewAttach.js"; import { handleAttachPreview } from "@/internal/customers/attach/handleAttachPreview/handleAttachPreview.js"; import { handleCancelV2 } from "@/internal/customers/cancel/handleCancelV2.js"; import type { HonoEnv } from "../../honoUtils/HonoEnv.js"; @@ -26,3 +27,4 @@ billingRouter.post( // V2 Attach billingRouter.post("/billing/attach", ...handleAttachV2); +billingRouter.post("/billing/preview_attach", ...handlePreviewAttach); diff --git a/server/src/internal/billing/v2/actions/attach/attach.ts b/server/src/internal/billing/v2/actions/attach/attach.ts index bebf70322..97c47afae 100644 --- a/server/src/internal/billing/v2/actions/attach/attach.ts +++ b/server/src/internal/billing/v2/actions/attach/attach.ts @@ -1,4 +1,4 @@ -import { type AttachParamsV0, RecaseError } from "@autumn/shared"; +import type { AttachParamsV0 } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { computeAttachPlan } from "@/internal/billing/v2/actions/attach/compute/computeAttachPlan"; import { handleAttachV2Errors } from "@/internal/billing/v2/actions/attach/errors/handleAttachV2Errors"; @@ -15,6 +15,13 @@ import type { } from "@/internal/billing/v2/types"; import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan"; +export interface AttachResult { + billingContext: AttachBillingContext; + billingPlan?: BillingPlan; + billingResult?: BillingResult | null; + checkoutUrl?: string; +} + export async function attach({ ctx, params, @@ -23,11 +30,7 @@ export async function attach({ ctx: AutumnContext; params: AttachParamsV0; preview?: boolean; -}): Promise<{ - billingContext: AttachBillingContext; - billingPlan: BillingPlan; - billingResult: BillingResult | null; -}> { +}): Promise { // 1. Setup const billingContext = await setupAttachBillingContext({ ctx, @@ -52,19 +55,12 @@ export async function attach({ params, }); - if (billingContext.checkoutMode !== null) { - // 4. Handle checkout mode (redirect to Stripe checkout) - throw new RecaseError({ - message: `Checkout flow not yet implemented for attach v2 (checkoutMode: ${billingContext.checkoutMode}). Please add a payment method to the customer first.`, - statusCode: 400, - }); - } - - // 5. Evaluate Stripe billing plan + // 4. Evaluate Stripe billing plan (handles checkout mode internally) const stripeBillingPlan = await evaluateStripeBillingPlan({ ctx, billingContext, autumnBillingPlan, + checkoutMode: billingContext.checkoutMode, }); logStripeBillingPlan({ ctx, stripeBillingPlan, billingContext }); @@ -74,7 +70,7 @@ export async function attach({ stripe: stripeBillingPlan, }; - if (!preview) { + if (preview) { return { billingContext, billingPlan, @@ -82,6 +78,11 @@ export async function attach({ }; } + if (billingContext.checkoutMode === "autumn_checkout") { + // return autumn checkout URL + // return await createAutumnCheckout(); + } + // 6. Execute billing plan const billingResult = await executeBillingPlan({ ctx, @@ -95,5 +96,6 @@ export async function attach({ billingContext, billingPlan, billingResult, + checkoutUrl: billingResult.stripe.stripeCheckoutSession?.url ?? undefined, }; } diff --git a/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts b/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts index 53bfc6db6..41bcdae08 100644 --- a/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts +++ b/server/src/internal/billing/v2/actions/updateSubscription/updateSubscription.ts @@ -68,7 +68,7 @@ export async function updateSubscription({ stripe: stripeBillingPlan, }; - if (!preview) { + if (preview) { return { billingContext, billingPlan, diff --git a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts index 7d9bda21e..7d828a2b5 100644 --- a/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts +++ b/server/src/internal/billing/v2/compute/computeAutumnUtils/buildAutumnLineItems.ts @@ -50,11 +50,14 @@ export const buildAutumnLineItems = ({ // will be handled in finalizeUpdateSubscriptionPlan const allLineItems = [...deletedLineItems, ...newLineItems]; - logBuildAutumnLineItems({ - logger, - deletedLineItems, - newLineItems, - }); + const debugLogs = false; + if (debugLogs) { + logBuildAutumnLineItems({ + logger, + deletedLineItems, + newLineItems, + }); + } return allLineItems; }; diff --git a/server/src/internal/billing/v2/handlers/handleAttachV2.ts b/server/src/internal/billing/v2/handlers/handleAttachV2.ts index 22d1047a8..67056c349 100644 --- a/server/src/internal/billing/v2/handlers/handleAttachV2.ts +++ b/server/src/internal/billing/v2/handlers/handleAttachV2.ts @@ -25,7 +25,7 @@ export const handleAttachV2 = createRoute({ const { billingContext, billingResult } = await billingActions.attach({ ctx, params: body, - preview: true, + preview: false, }); if (!billingResult) { diff --git a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts index 206a6d815..43954cc25 100644 --- a/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts +++ b/server/src/internal/billing/v2/handlers/handleUpdateSubscription.ts @@ -1,4 +1,7 @@ -import { UpdateSubscriptionV0ParamsSchema, InternalError } from "@autumn/shared"; +import { + InternalError, + UpdateSubscriptionV0ParamsSchema, +} from "@autumn/shared"; import { billingActions } from "@/internal/billing/v2/actions"; import { createRoute } from "../../../../honoMiddlewares/routeHandler"; import { billingResultToResponse } from "../utils/billingResult/billingResultToResponse"; @@ -26,7 +29,7 @@ export const handleUpdateSubscription = createRoute({ await billingActions.updateSubscription({ ctx, params: body, - preview: true, + preview: false, }); if (!billingResult) { diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts new file mode 100644 index 000000000..10b424791 --- /dev/null +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeCheckoutSessionAction.ts @@ -0,0 +1,95 @@ +import { + type FullCusProduct, + msToSeconds, + orgToReturnUrl, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import { billingPlanToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs"; +import { buildStripeSubscriptionItemsUpdate } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; +import type { + AutumnBillingPlan, + BillingContext, + StripeCheckoutSessionAction, +} from "@/internal/billing/v2/types"; + +export const buildStripeCheckoutSessionAction = ({ + ctx, + billingContext, + finalCustomerProducts, + autumnBillingPlan, +}: { + ctx: AutumnContext; + billingContext: BillingContext; + finalCustomerProducts: FullCusProduct[]; + autumnBillingPlan: AutumnBillingPlan; +}): StripeCheckoutSessionAction => { + const { org, env } = ctx; + const { trialContext, stripeCustomer } = billingContext; + + // 1. Get subscription items filtered to largest interval (for Stripe Checkout) + const subItemsUpdate = buildStripeSubscriptionItemsUpdate({ + ctx, + billingContext, + finalCustomerProducts, + filterByLargestInterval: true, + }); + + // 2. Get one-off items + const oneOffItemSpecs = billingPlanToOneOffStripeItemSpecs({ + ctx, + autumnBillingPlan, + }); + + // 3. Determine mode: "subscription" or "payment" + const isOneOffOnly = subItemsUpdate.length === 0; + const mode: "subscription" | "payment" = isOneOffOnly + ? "payment" + : "subscription"; + + // 4. Build line_items from sub items and one-off items + const lineItems: Stripe.Checkout.SessionCreateParams.LineItem[] = [ + ...subItemsUpdate + .filter((item) => item.price && !item.deleted) + .map((item) => ({ + price: item.price!, + quantity: item.quantity, + })), + ...oneOffItemSpecs.map((item) => ({ + price: item.stripePriceId, + quantity: item.quantity ?? 1, + })), + ]; + + // 5. Trial handling (only for subscription mode) + const trialEnd = + mode === "subscription" && trialContext?.trialEndsAt + ? msToSeconds(trialContext.trialEndsAt) + : undefined; + + // 6. Build subscription_data (only for subscription mode) + const subscriptionData: + | Stripe.Checkout.SessionCreateParams.SubscriptionData + | undefined = + mode === "subscription" + ? { + trial_end: trialEnd, + ...(trialContext?.cardRequired && { + trial_settings: { + end_behavior: { missing_payment_method: "cancel" }, + }, + }), + } + : undefined; + + // 7. Build params (only variable params - static params added in execute) + const params: Stripe.Checkout.SessionCreateParams = { + customer: stripeCustomer.id, + mode, + line_items: lineItems, + subscription_data: subscriptionData, + return_url: orgToReturnUrl({ org, env }), + }; + + return { type: "create", params }; +}; diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts index ed3a130e9..d05097214 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionAction.ts @@ -1,12 +1,12 @@ import type { FullCusProduct } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { buildStripeSubscriptionItemsUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate"; import { buildStripeSubscriptionCreateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionCreateAction"; import { buildStripeSubscriptionUpdateAction } from "@server/internal/billing/v2/providers/stripe/utils/subscriptions/buildStripeSubscriptionUpdateAction"; import { billingPlanToOneOffStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/stripeItemSpec/billingPlanToOneOffStripeItemSpecs"; import type { AutumnBillingPlan, + BillingContext, StripeSubscriptionAction, StripeSubscriptionScheduleAction, } from "@/internal/billing/v2/types"; @@ -39,6 +39,11 @@ export const buildStripeSubscriptionAction = ({ autumnBillingPlan, }); + const addInvoiceItems = oneOffItemSpecs.map((item) => ({ + price: item.stripePriceId, + quantity: item.quantity, + })); + // Case 1: No subscription and sub items update is empty -> no action if (!stripeSubscription && subItemsUpdate.length === 0) { return undefined; @@ -50,10 +55,7 @@ export const buildStripeSubscriptionAction = ({ ctx, billingContext, subItemsUpdate, - addInvoiceItems: oneOffItemSpecs.map((item) => ({ - price: item.stripePriceId, - quantity: item.quantity, - })), + addInvoiceItems, subscriptionCancelAt, }); } diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts index 390b637ae..c6fa39c91 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction.ts @@ -5,10 +5,12 @@ import { isCustomerProductOnStripeSubscriptionSchedule, } from "@autumn/shared"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { buildStripePhasesUpdate } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate"; import type Stripe from "stripe"; -import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types"; +import type { + BillingContext, + StripeSubscriptionScheduleAction, +} from "@/internal/billing/v2/types"; // ═══════════════════════════════════════════════════════════════════════════════ // TYPES diff --git a/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts index c075054eb..29b7cf8ae 100644 --- a/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan.ts @@ -2,13 +2,16 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; import { buildStripeSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/actionBuilders/buildStripeSubscriptionScheduleAction"; import { shouldCreateManualStripeInvoice } from "@/internal/billing/v2/providers/stripe/utils/invoices/shouldCreateManualStripeInvoice"; import { autumnBillingPlanToFinalFullCustomer } from "@/internal/billing/v2/utils/autumnBillingPlanToFinalFullCustomer"; -import type { BillingContext } from "../../../types"; +import { buildStripeCheckoutSessionAction } from "../../../providers/stripe/actionBuilders/buildStripeCheckoutSessionAction"; import { buildStripeInvoiceAction } from "../../../providers/stripe/actionBuilders/buildStripeInvoiceAction"; import { buildStripeInvoiceItemsAction } from "../../../providers/stripe/actionBuilders/buildStripeInvoiceItemsAction"; import { buildStripeSubscriptionAction } from "../../../providers/stripe/actionBuilders/buildStripeSubscriptionAction"; import type { AutumnBillingPlan, + BillingContext, + CheckoutMode, StripeBillingPlan, + StripeCheckoutSessionAction, StripeInvoiceAction, StripeInvoiceItemsAction, } from "../../../types"; @@ -18,10 +21,12 @@ export const evaluateStripeBillingPlan = async ({ ctx, billingContext, autumnBillingPlan, + checkoutMode, }: { ctx: AutumnContext; billingContext: BillingContext; autumnBillingPlan: AutumnBillingPlan; + checkoutMode?: CheckoutMode; }): Promise => { await initStripeResourcesForBillingPlan({ ctx, @@ -61,6 +66,17 @@ export const evaluateStripeBillingPlan = async ({ stripeSubscriptionAction, }); + // Build checkout session action if checkout mode is stripe_checkout + let stripeCheckoutSessionAction: StripeCheckoutSessionAction | undefined; + if (checkoutMode === "stripe_checkout") { + stripeCheckoutSessionAction = buildStripeCheckoutSessionAction({ + ctx, + billingContext, + finalCustomerProducts: finalFullCustomer.customer_products, + autumnBillingPlan, + }); + } + let stripeInvoiceAction: StripeInvoiceAction | undefined; let stripeInvoiceItemsAction: StripeInvoiceItemsAction | undefined; if (createManualInvoice && lineItems) { @@ -75,9 +91,14 @@ export const evaluateStripeBillingPlan = async ({ } return { - subscriptionAction: stripeSubscriptionAction, + // If checkout session action is present, don't include subscription action + // (checkout will create the subscription) + subscriptionAction: stripeCheckoutSessionAction + ? undefined + : stripeSubscriptionAction, invoiceAction: stripeInvoiceAction, invoiceItemsAction: stripeInvoiceItemsAction, subscriptionScheduleAction: stripeSubscriptionScheduleAction, + checkoutSessionAction: stripeCheckoutSessionAction, }; }; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts index ec80d7bf0..28c68f865 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeBillingPlan.ts @@ -1,13 +1,16 @@ import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { addStripeSubscriptionScheduleIdToBillingPlan } from "@/internal/billing/v2/execute/addStripeSubscriptionScheduleIdToBillingPlan"; +import { executeStripeCheckoutSessionAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction"; import { executeStripeInvoiceAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction"; import { executeStripeSubscriptionAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction"; import { executeStripeSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction"; import { createStripeInvoiceItems } from "@/internal/billing/v2/providers/stripe/utils/invoices/stripeInvoiceOps"; +import type { + BillingContext, + BillingPlan, + StripeBillingPlanResult, +} from "@/internal/billing/v2/types"; import { StripeBillingStage } from "@/internal/billing/v2/types"; -import type { BillingPlan } from "@/internal/billing/v2/types"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; export const executeStripeBillingPlan = async ({ ctx, @@ -25,8 +28,19 @@ export const executeStripeBillingPlan = async ({ invoiceAction: stripeInvoiceAction, invoiceItemsAction: stripeInvoiceItemsAction, subscriptionScheduleAction: stripeSubscriptionScheduleAction, + checkoutSessionAction: stripeCheckoutSessionAction, } = billingPlan.stripe; + // Execute checkout session FIRST if present (returns early with deferred result) + if (stripeCheckoutSessionAction) { + return executeStripeCheckoutSessionAction({ + ctx, + billingPlan, + billingContext, + checkoutSessionAction: stripeCheckoutSessionAction, + }); + } + // Collect results from each stage let invoiceResult: StripeBillingPlanResult | undefined; let subscriptionResult: StripeBillingPlanResult | undefined; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction.ts new file mode 100644 index 000000000..3b83e3742 --- /dev/null +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeCheckoutSessionAction.ts @@ -0,0 +1,94 @@ +import { addDays } from "date-fns"; +import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli"; +import type { AutumnContext } from "@/honoUtils/HonoEnv"; +import type { + BillingContext, + BillingPlan, + StripeBillingPlanResult, + StripeCheckoutSessionAction, +} from "@/internal/billing/v2/types"; +import { + insertMetadataFromBillingPlan, + updateMetadataWithCheckoutSession, +} from "@/internal/metadata/utils/insertMetadataFromBillingPlan"; +import { orgToCurrency } from "@/internal/orgs/orgUtils"; + +export const executeStripeCheckoutSessionAction = async ({ + ctx, + billingPlan, + billingContext, + checkoutSessionAction, +}: { + ctx: AutumnContext; + billingPlan: BillingPlan; + billingContext: BillingContext; + checkoutSessionAction: StripeCheckoutSessionAction; +}): Promise => { + const { org, logger } = ctx; + const { fullCustomer } = billingContext; + + const stripeCli = createStripeCli({ org, env: fullCustomer.env }); + + // 1. Insert metadata FIRST (without checkout session ID) + const metadata = await insertMetadataFromBillingPlan({ + ctx, + billingPlan, + billingContext, + resumeAfter: undefined, + expiresAt: addDays(Date.now(), 10).getTime(), + }); + + // 2. Build full checkout params (merge variable + static params) + const fullParams: Stripe.Checkout.SessionCreateParams = { + ...checkoutSessionAction.params, + + // Static params + currency: orgToCurrency({ org }), + allow_promotion_codes: true, + saved_payment_method_options: { payment_method_save: "enabled" }, + invoice_creation: + checkoutSessionAction.params.mode === "payment" + ? { enabled: true } + : undefined, + + // Link to metadata + metadata: { autumn_metadata_id: metadata.id }, + }; + + // 3. Create checkout session with fallback for payment method types + let stripeCheckoutSession: Stripe.Checkout.Session; + try { + stripeCheckoutSession = + await stripeCli.checkout.sessions.create(fullParams); + logger.info( + `✅ Created checkout session for customer ${fullCustomer.id ?? fullCustomer.internal_id}`, + ); + } catch (error) { + const msg = error instanceof Error ? error.message : undefined; + if (msg?.includes("No valid payment method types")) { + stripeCheckoutSession = await stripeCli.checkout.sessions.create({ + ...fullParams, + payment_method_types: ["card"], + }); + logger.info( + "✅ Created fallback checkout session with card payment method", + ); + } else { + throw error; + } + } + + // 4. Update metadata with checkout session ID + await updateMetadataWithCheckoutSession({ + ctx, + metadataId: metadata.id, + stripeCheckoutSessionId: stripeCheckoutSession.id, + }); + + // 5. Return result with checkout session + return { + deferred: true, + stripeCheckoutSession, + }; +}; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts index 62b67672b..a42030cd8 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeInvoiceAction.ts @@ -1,14 +1,14 @@ import { ms } from "@autumn/shared"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { shouldDeferBillingPlan } from "@/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan"; import { createInvoiceForBilling } from "@/internal/billing/v2/providers/stripe/utils/invoices/createInvoiceForBilling"; -import { StripeBillingStage } from "@/internal/billing/v2/types"; import type { + BillingContext, BillingPlan, + StripeBillingPlanResult, StripeInvoiceMetadata, } from "@/internal/billing/v2/types"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; +import { StripeBillingStage } from "@/internal/billing/v2/types"; import { isDeferredInvoiceMode } from "@/internal/billing/v2/utils/billingContext/isDeferredInvoiceMode"; import { upsertInvoiceFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling"; import { insertMetadataFromBillingPlan } from "@/internal/metadata/utils/insertMetadataFromBillingPlan"; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts index b33a78985..fe5fd6cfd 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionAction.ts @@ -3,7 +3,6 @@ import { createStripeCli } from "@/external/connect/createStripeCli"; import { isStripeSubscriptionCanceled } from "@/external/stripe/subscriptions/utils/classifyStripeSubscriptionUtils"; import { setStripeSubscriptionLock } from "@/external/stripe/subscriptions/utils/lockStripeSubscriptionUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { addStripeSubscriptionIdToBillingPlan } from "@/internal/billing/v2/execute/addStripeSubscriptionIdToBillingPlan"; import { removeStripeSubscriptionIdFromBillingPlan } from "@/internal/billing/v2/execute/removeStripeSubscriptionIdFromBillingPlan"; import { shouldDeferBillingPlan } from "@/internal/billing/v2/providers/stripe/utils/common/shouldDeferBillingPlan"; @@ -11,9 +10,12 @@ import { finalizeStripeInvoice } from "@/internal/billing/v2/providers/stripe/ut import { executeStripeSubscriptionOperation } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/executeStripeSubscriptionOperation"; import { getLatestInvoiceFromSubscriptionAction } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/getLatestInvoiceFromSubscriptionAction"; import { getRequiredActionFromSubscriptionInvoice } from "@/internal/billing/v2/providers/stripe/utils/subscriptions/getRequiredActionFromSubscriptionInvoice"; +import type { + BillingContext, + BillingPlan, + StripeBillingPlanResult, +} from "@/internal/billing/v2/types"; import { StripeBillingStage } from "@/internal/billing/v2/types"; -import type { BillingPlan } from "@/internal/billing/v2/types"; -import type { StripeBillingPlanResult } from "@/internal/billing/v2/types"; import { upsertInvoiceFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertInvoiceFromBilling"; import { upsertSubscriptionFromBilling } from "@/internal/billing/v2/utils/upsertFromStripe/upsertSubscriptionFromBilling"; import { insertMetadataFromBillingPlan } from "@/internal/metadata/utils/insertMetadataFromBillingPlan"; diff --git a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts index bb3454e8b..67ed19e69 100644 --- a/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts +++ b/server/src/internal/billing/v2/providers/stripe/execute/executeStripeSubscriptionScheduleAction.ts @@ -1,9 +1,11 @@ import { createStripeCli } from "@server/external/connect/createStripeCli"; import type { AutumnContext } from "@server/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import type Stripe from "stripe"; import { logSubscriptionScheduleAction } from "@/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/logSubscriptionScheduleAction"; -import type { StripeSubscriptionScheduleAction } from "@/internal/billing/v2/types"; +import type { + BillingContext, + StripeSubscriptionScheduleAction, +} from "@/internal/billing/v2/types"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService"; /** diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts index dc675c4f3..fde37d335 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionItems/buildStripeSubscriptionItemsUpdate.ts @@ -1,6 +1,7 @@ import { filterCustomerProductsByActiveStatuses, filterCustomerProductsByStripeSubscriptionId, + getLargestInterval, } from "@autumn/shared"; import { customerProductToStripeItemSpecs } from "@server/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs"; import type { StripeItemSpec } from "@shared/models/billingModels/stripeAdapterModels/stripeItemSpec"; @@ -134,15 +135,49 @@ const stripeItemSpecsToSubItemsUpdate = ({ return subItemsUpdate; }; +/** + * Filters stripe item specs to only include items from the largest billing interval. + * Used for Stripe Checkout which doesn't support multi-interval subscriptions. + */ +const filterStripeItemSpecsByLargestInterval = ({ + stripeItemSpecs, +}: { + stripeItemSpecs: StripeItemSpec[]; +}): StripeItemSpec[] => { + const prices = stripeItemSpecs + .map((spec) => spec.autumnPrice) + .filter((p): p is NonNullable => !!p); + + if (prices.length === 0) return stripeItemSpecs; + + const largestInterval = getLargestInterval({ prices, excludeOneOff: true }); + if (!largestInterval) return stripeItemSpecs; + + return stripeItemSpecs.filter((spec) => { + const price = spec.autumnPrice; + if (!price) return false; + + const priceInterval = price.config.interval; + const priceIntervalCount = price.config.interval_count ?? 1; + + return ( + priceInterval === largestInterval.interval && + priceIntervalCount === largestInterval.intervalCount + ); + }); +}; + export const buildStripeSubscriptionItemsUpdate = ({ ctx, billingContext, finalCustomerProducts, + filterByLargestInterval = false, }: { ctx: AutumnContext; billingContext: BillingContext; finalCustomerProducts: FullCusProduct[]; -}) => { + filterByLargestInterval?: boolean; +}): Stripe.SubscriptionUpdateParams.Item[] => { // 1. Filter customer products by stripe subscription id const relatedCustomerProducts = filterCustomerProductsByStripeSubscriptionId({ customerProducts: finalCustomerProducts, @@ -155,15 +190,22 @@ export const buildStripeSubscriptionItemsUpdate = ({ }); // 3. Get recurring subscription item array (doesn't include one off items) - const recurringItems = customerProductsToRecurringStripeItemSpecs({ + let recurringStripeItemSpecs = customerProductsToRecurringStripeItemSpecs({ ctx, billingContext, customerProducts: activeCustomerProducts, }); - // 4. Diff it with the current subscription items + // 4. Optionally filter by largest interval (for Stripe Checkout) + if (filterByLargestInterval) { + recurringStripeItemSpecs = filterStripeItemSpecsByLargestInterval({ + stripeItemSpecs: recurringStripeItemSpecs, + }); + } + + // 5. Diff it with the current subscription items return stripeItemSpecsToSubItemsUpdate({ billingContext, - stripeItemSpecs: recurringItems, + stripeItemSpecs: recurringStripeItemSpecs, }); }; diff --git a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts index e034b0e4a..7fb15a7ab 100644 --- a/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts +++ b/server/src/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/buildStripePhasesUpdate.ts @@ -6,9 +6,9 @@ import { import type Stripe from "stripe"; import { logPhase } from "@/external/stripe/subscriptionSchedules/utils/logStripeSchedulePhaseUtils"; import type { AutumnContext } from "@/honoUtils/HonoEnv"; -import type { BillingContext } from "@/internal/billing/v2/types"; import { customerProductToStripeItemSpecs } from "@/internal/billing/v2/providers/stripe/utils/subscriptionItems/customerProductToStripeItemSpecs"; import { isCustomerProductActiveDuringPeriod } from "@/internal/billing/v2/providers/stripe/utils/subscriptionSchedules/isCustomerProductActiveAtEpochMs"; +import type { BillingContext } from "@/internal/billing/v2/types"; import { buildTransitionPoints } from "./buildTransitionPoints"; import { logTransitionPoints } from "./logBuildPhaseHelpers"; @@ -107,13 +107,17 @@ export const buildStripePhasesUpdate = ({ trialEndsAt: normalizedTrialEndsAt, }); + const debugLogs = false; + // Log customer products and transition points - logTransitionPoints({ - ctx, - customerProducts: normalizedCustomerProducts, - transitionPoints, - nowMs, - }); + if (debugLogs) { + logTransitionPoints({ + ctx, + customerProducts: normalizedCustomerProducts, + transitionPoints, + nowMs, + }); + } let startMs = nowMs; @@ -166,14 +170,16 @@ export const buildStripePhasesUpdate = ({ }; // Log phase details - logPhase({ - ctx, - phase, - customerProducts: activeCustomerProducts, - phaseIndex, - logPrefix: "[buildStripePhasesUpdate]", - showCustomerProducts: true, - }); + if (debugLogs) { + logPhase({ + ctx, + phase, + customerProducts: activeCustomerProducts, + phaseIndex, + logPrefix: "[buildStripePhasesUpdate]", + showCustomerProducts: true, + }); + } phases.push(phase); diff --git a/server/src/internal/billing/v2/types/autumnBillingPlan.ts b/server/src/internal/billing/v2/types/autumnBillingPlan.ts index fdcadf58e..bd16b6bef 100644 --- a/server/src/internal/billing/v2/types/autumnBillingPlan.ts +++ b/server/src/internal/billing/v2/types/autumnBillingPlan.ts @@ -11,8 +11,7 @@ import { PriceSchema, } from "@autumn/shared"; import { z } from "zod/v4"; -import type { BillingContext } from "@/internal/billing/v2/types"; -import type { BillingPlan } from "@/internal/billing/v2/types"; +import type { BillingContext, BillingPlan } from "@/internal/billing/v2/types"; export const UpdateCustomerEntitlementSchema = z.object({ customerEntitlement: FullCustomerEntitlementSchema, @@ -85,5 +84,5 @@ export type DeferredAutumnBillingPlanData = { env: AppEnv; billingPlan: BillingPlan; billingContext: BillingContext; - resumeAfter: StripeBillingStage; + resumeAfter?: StripeBillingStage; }; diff --git a/server/src/internal/billing/v2/types/billingPlan.ts b/server/src/internal/billing/v2/types/billingPlan.ts index 45f1e5c58..92d6424ef 100644 --- a/server/src/internal/billing/v2/types/billingPlan.ts +++ b/server/src/internal/billing/v2/types/billingPlan.ts @@ -7,21 +7,19 @@ import { import { type StripeBillingPlan, StripeBillingPlanSchema, + type StripeCheckoutSessionAction, type StripeInvoiceAction, - StripeInvoiceActionSchema, type StripeInvoiceItemsAction, - StripeInvoiceItemsActionSchema, type StripeInvoiceMetadata, type StripeSubscriptionAction, - StripeSubscriptionActionSchema, type StripeSubscriptionScheduleAction, - StripeSubscriptionScheduleActionSchema, } from "./stripeBillingPlan/stripeBillingPlan"; export type { AutumnBillingPlan, DeferredAutumnBillingPlanData, StripeBillingPlan, + StripeCheckoutSessionAction, StripeInvoiceAction, StripeInvoiceItemsAction, StripeInvoiceMetadata, diff --git a/server/src/internal/billing/v2/types/billingResult.ts b/server/src/internal/billing/v2/types/billingResult.ts index f7f75d77e..ffe4f543b 100644 --- a/server/src/internal/billing/v2/types/billingResult.ts +++ b/server/src/internal/billing/v2/types/billingResult.ts @@ -5,6 +5,7 @@ export interface StripeBillingPlanResult { deferred?: boolean; stripeInvoice?: Stripe.Invoice; stripeSubscription?: Stripe.Subscription; + stripeCheckoutSession?: Stripe.Checkout.Session; requiredAction?: { code: PaymentFailureCode; reason: string; diff --git a/server/src/internal/billing/v2/types/index.ts b/server/src/internal/billing/v2/types/index.ts index eb4427131..926c5688f 100644 --- a/server/src/internal/billing/v2/types/index.ts +++ b/server/src/internal/billing/v2/types/index.ts @@ -6,6 +6,7 @@ export * from "./billingResult"; // Stripe billing plan types export * from "./stripeBillingPlan/stripeBillingPlan"; +export * from "./stripeBillingPlan/stripeCheckoutSessionAction"; export * from "./stripeBillingPlan/stripeInvoiceAction"; export * from "./stripeBillingPlan/stripeInvoiceItemsAction"; export * from "./stripeBillingPlan/stripeSubscriptionAction"; diff --git a/server/src/internal/billing/v2/types/stripeBillingPlan/stripeBillingPlan.ts b/server/src/internal/billing/v2/types/stripeBillingPlan/stripeBillingPlan.ts index ef8ce464e..ee5c6fd03 100644 --- a/server/src/internal/billing/v2/types/stripeBillingPlan/stripeBillingPlan.ts +++ b/server/src/internal/billing/v2/types/stripeBillingPlan/stripeBillingPlan.ts @@ -1,4 +1,8 @@ import { z } from "zod/v4"; +import { + type StripeCheckoutSessionAction, + StripeCheckoutSessionActionSchema, +} from "./stripeCheckoutSessionAction"; import { type StripeInvoiceAction, StripeInvoiceActionSchema, @@ -17,10 +21,12 @@ import { } from "./stripeSubscriptionScheduleAction"; export { + StripeCheckoutSessionActionSchema, StripeInvoiceActionSchema, StripeInvoiceItemsActionSchema, StripeSubscriptionActionSchema, StripeSubscriptionScheduleActionSchema, + type StripeCheckoutSessionAction, type StripeInvoiceAction, type StripeInvoiceItemsAction, type StripeSubscriptionAction, @@ -32,6 +38,7 @@ export const StripeBillingPlanSchema = z.object({ subscriptionScheduleAction: StripeSubscriptionScheduleActionSchema.optional(), invoiceAction: StripeInvoiceActionSchema.optional(), invoiceItemsAction: StripeInvoiceItemsActionSchema.optional(), + checkoutSessionAction: StripeCheckoutSessionActionSchema.optional(), }); export type StripeBillingPlan = z.infer; diff --git a/server/src/internal/billing/v2/types/stripeBillingPlan/stripeCheckoutSessionAction.ts b/server/src/internal/billing/v2/types/stripeBillingPlan/stripeCheckoutSessionAction.ts new file mode 100644 index 000000000..a10b02a9a --- /dev/null +++ b/server/src/internal/billing/v2/types/stripeBillingPlan/stripeCheckoutSessionAction.ts @@ -0,0 +1,11 @@ +import type Stripe from "stripe"; +import { z } from "zod/v4"; + +export const StripeCheckoutSessionActionSchema = z.object({ + type: z.literal("create"), + params: z.custom(), +}); + +export type StripeCheckoutSessionAction = z.infer< + typeof StripeCheckoutSessionActionSchema +>; diff --git a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts index e0fdd3b40..749d38250 100644 --- a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts +++ b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts @@ -16,6 +16,14 @@ export const billingResultToResponse = ({ const customerId = fullCustomer.id ?? fullCustomer.internal_id; const stripeInvoice = billingResult.stripe.stripeInvoice; + const stripeCheckoutSession = billingResult.stripe.stripeCheckoutSession; + + // Checkout session URL takes priority, then invoice hosted URL + const paymentUrl = stripeCheckoutSession?.url + ? stripeCheckoutSession.url + : stripeInvoice?.status === "open" && stripeInvoice.hosted_invoice_url + ? stripeInvoice.hosted_invoice_url + : null; return { customer_id: customerId, @@ -32,11 +40,8 @@ export const billingResultToResponse = ({ hosted_invoice_url: stripeInvoice.hosted_invoice_url ?? null, } : undefined, - payment_url: - stripeInvoice?.status === "open" && stripeInvoice.hosted_invoice_url - ? stripeInvoice.hosted_invoice_url - : null, - + payment_url: paymentUrl, + checkout_url: stripeCheckoutSession?.url ?? null, required_action: billingResult.stripe.requiredAction, } satisfies BillingResponse; }; diff --git a/server/src/internal/checkouts/handlers/handleGetCheckout.ts b/server/src/internal/checkouts/handlers/handleGetCheckout.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/internal/checkouts/index.ts b/server/src/internal/checkouts/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/internal/metadata/MetadataService.ts b/server/src/internal/metadata/MetadataService.ts index 72d70fc36..8679fc2f1 100644 --- a/server/src/internal/metadata/MetadataService.ts +++ b/server/src/internal/metadata/MetadataService.ts @@ -57,4 +57,22 @@ export class MetadataService { static async delete({ db, id }: { db: DrizzleCli; id: string }) { await db.delete(metadata).where(eq(metadata.id, id)); } + + static async update({ + db, + id, + updates, + }: { + db: DrizzleCli; + id: string; + updates: Partial; + }) { + const updatedMetadata = await db + .update(metadata) + .set(updates) + .where(eq(metadata.id, id)) + .returning(); + + return updatedMetadata[0] as Metadata | undefined; + } } diff --git a/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts b/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts index 3b8cd17ac..9588cbe70 100644 --- a/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts +++ b/server/src/internal/metadata/utils/insertMetadataFromBillingPlan.ts @@ -13,13 +13,14 @@ import { generateId } from "@/utils/genUtils"; import { MetadataService } from "../MetadataService"; /** - * Creates metadata from a billing plan and optionally links it to a Stripe invoice. + * Creates metadata from a billing plan and optionally links it to a Stripe invoice or checkout session. */ export const insertMetadataFromBillingPlan = async ({ ctx, billingPlan, billingContext, stripeInvoice, + stripeCheckoutSession, expiresAt, resumeAfter, }: { @@ -27,12 +28,18 @@ export const insertMetadataFromBillingPlan = async ({ billingPlan: BillingPlan; billingContext: BillingContext; stripeInvoice?: Stripe.Invoice; - resumeAfter: StripeBillingStage; + stripeCheckoutSession?: Stripe.Checkout.Session; + resumeAfter?: StripeBillingStage; expiresAt: number; }) => { const id = generateId("meta"); - const type = stripeInvoice ? MetadataType.DeferredInvoice : undefined; + let type: MetadataType | undefined; + if (stripeCheckoutSession) { + type = MetadataType.CheckoutSessionV2; + } else if (stripeInvoice) { + type = MetadataType.DeferredInvoice; + } const data = { requestId: ctx.id, @@ -49,6 +56,7 @@ export const insertMetadataFromBillingPlan = async ({ id, type, stripe_invoice_id: stripeInvoice?.id, + stripe_checkout_session_id: stripeCheckoutSession?.id, data, created_at: Date.now(), expires_at: expiresAt ?? addDays(Date.now(), 10).getTime(), @@ -73,3 +81,25 @@ export const insertMetadataFromBillingPlan = async ({ return metadata; }; + +/** + * Updates metadata with checkout session ID after checkout is created. + */ +export const updateMetadataWithCheckoutSession = async ({ + ctx, + metadataId, + stripeCheckoutSessionId, +}: { + ctx: AutumnContext; + metadataId: string; + stripeCheckoutSessionId: string; +}) => { + return MetadataService.update({ + db: ctx.db, + id: metadataId, + updates: { + stripe_checkout_session_id: stripeCheckoutSessionId, + type: MetadataType.CheckoutSessionV2, + }, + }); +}; diff --git a/server/tests/integration/billing/attach/attachTests.md b/server/tests/integration/billing/attach/attachTests.md index 50621b270..89f8470e7 100644 --- a/server/tests/integration/billing/attach/attachTests.md +++ b/server/tests/integration/billing/attach/attachTests.md @@ -63,18 +63,30 @@ }); ``` -11. **Always call attach preview before attach to verify `preview.total`** - - The preview endpoint validates pricing before the actual attach +11. **ALWAYS call `billing.previewAttach` before `billing.attach` and verify** + - Call preview BEFORE every attach to verify pricing + - Assert `preview.due_today.total` matches expected amount EXACTLY (not `toBeCloseTo`) + - After attach, verify invoice total matches preview total ```typescript - const preview = await autumn.attachPreview({ + // 1. Preview first - verify expected charge + const preview = await autumnV1.billing.previewAttach({ customer_id: customerId, - product_id: productId, + product_id: pro.id, entity_id: entityId, // Optional for entity-level + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], // If prepaid }); - expect(preview.total).toBe(expectedTotal); + expect(preview.due_today.total).toBe(30); // EXACT match, not toBeCloseTo - // Then perform the actual attach - await autumn.attach({ ... }); + // 2. Attach + await autumnV1.billing.attach({ ... }); + + // 3. Verify invoice matches preview + const customer = await autumnV1.customers.get(customerId); + expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 30, // Must match preview.due_today.total + }); ``` 12. **Add-on is defined at product level, NOT in attach params** diff --git a/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts b/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts new file mode 100644 index 000000000..c1015abfe --- /dev/null +++ b/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts @@ -0,0 +1,102 @@ +/** + * Autumn Checkout Basic Tests (Attach V2) + * + * Tests for Autumn Checkout flow when customer HAS a payment method + * but redirect_mode is set to "always". + * + * When checkoutMode = "autumn_checkout", attach returns an autumn confirmation + * page URL instead of charging directly, giving the customer a chance to + * review before payment. + * + * Key behaviors: + * - Has payment method + redirect_mode: "always" → autumn_checkout mode + * - Returns confirmation page URL + * - Product is attached after user confirms + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, AttachPreview } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +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"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: With payment method + redirect_mode: "always" → autumn_checkout +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer HAS a payment method + * - Attach pro product with redirect_mode: "always" + * + * Expected Result: + * - Returns autumn checkout/confirmation URL (not stripe checkout) + * - Does NOT charge immediately + * - Product attached after user confirms on autumn page + * + * NOTE: This test defines expected behavior. Implementation pending per ENG-1013. + */ +test.concurrent(`${chalk.yellowBright("autumn-checkout: with PM + redirect_mode always")}`, async () => { + const customerId = "autumn-checkout-redirect-always"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-autumn-checkout", + items: [messagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ paymentMethod: "success" }), // HAS payment method + s.products({ list: [pro] }), + ], + actions: [], + }); + + // 1. Preview attach - should show $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attempt attach with redirect_mode: "always" + // This should return a confirmation URL instead of charging directly + const result = await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + redirect_mode: "always", + }); + + // Should return a checkout/confirmation URL (autumn hosted page) + // Note: The exact URL format depends on implementation + expect(result.checkout_url || result.payment_url).toBeDefined(); + + // At this point, product should NOT be attached yet (waiting for confirmation) + const customerBefore = + await autumnV1.customers.get(customerId); + const productBefore = customerBefore.products?.find((p) => p.id === pro.id); + + // Product should either not exist or be in a pending state + // (Implementation may vary - could be no product, or product with pending status) + // For now, just verify we got a URL and didn't charge immediately + + // Note: Full test would include completing the autumn checkout flow + // and verifying the product is attached afterward. This is left as + // future work pending the autumn checkout implementation. +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// Future tests to implement once autumn checkout is built: +// ═══════════════════════════════════════════════════════════════════════════════ +// +// TEST 2: autumn-checkout: complete flow and verify product attached +// TEST 3: autumn-checkout: cancel flow (user doesn't confirm) +// TEST 4: autumn-checkout: with prepaid options +// TEST 5: autumn-checkout: entity-level attach diff --git a/server/tests/integration/billing/attach/checkout/stripe-checkout/stripe-checkout-basic.test.ts b/server/tests/integration/billing/attach/checkout/stripe-checkout/stripe-checkout-basic.test.ts new file mode 100644 index 000000000..5e87c4e11 --- /dev/null +++ b/server/tests/integration/billing/attach/checkout/stripe-checkout/stripe-checkout-basic.test.ts @@ -0,0 +1,278 @@ +/** + * Stripe Checkout Basic Tests (Attach V2) + * + * Tests for Stripe Checkout flow when customer has NO payment method. + * When checkoutMode = "stripe_checkout", attach returns a checkout_url + * that the customer uses to complete payment. + * + * Key behaviors: + * - No payment method → triggers stripe_checkout mode + * - Returns checkout_url instead of charging directly + * - Product is attached after checkout completion + */ + +import { expect, test } from "bun:test"; +import type { ApiCustomerV3, AttachPreview } from "@autumn/shared"; +import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; +import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; +import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; +import { TestFeature } from "@tests/setup/v2Features"; +import { items } from "@tests/utils/fixtures/items"; +import { products } from "@tests/utils/fixtures/products"; +import { timeout } from "@tests/utils/genUtils"; +import { completeCheckoutForm } from "@tests/utils/stripeUtils"; +import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; +import chalk from "chalk"; + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 1: No product → pro (new customer, no payment method) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - New customer with NO payment method + * - Attach pro product + * + * Expected Result: + * - Returns checkout_url (Stripe Checkout session) + * - After completing checkout: product is attached, invoice paid + */ +test.concurrent(`${chalk.yellowBright("stripe-checkout: no product → pro")}`, async () => { + const customerId = "stripe-checkout-no-pm-pro"; + + const messagesItem = items.monthlyMessages({ includedUsage: 100 }); + const pro = products.pro({ + id: "pro-checkout", + items: [messagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ testClock: true }), // No payment method! + s.products({ list: [pro] }), + ], + actions: [], + }); + + return; + + // 1. Preview attach - should show $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attempt attach - should return checkout_url (not charge directly) + const result = await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + // Verify checkout_url is returned + expect(result.checkout_url).toBeDefined(); + expect(result.checkout_url).toContain("checkout.stripe.com"); + + // 3. Complete checkout form + await completeCheckoutForm(result.checkout_url); + await timeout(12000); // Wait for webhook processing + + // 4. Verify product is now attached + const customer = await autumnV1.customers.get(customerId); + + await expectProductActive({ + customer, + productId: pro.id, + }); + + // Verify messages feature + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 100, + balance: 100, + usage: 0, + }); + + // Verify invoice was paid (matches preview total) + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 2: Free → pro (upgrade via checkout) +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer on free product, NO payment method + * - Attach pro product (upgrade) + * + * Expected Result: + * - Returns checkout_url + * - After checkout: pro replaces free + */ +test.concurrent(`${chalk.yellowBright("stripe-checkout: free → pro")}`, async () => { + const customerId = "stripe-checkout-free-to-pro"; + + const messagesItem = items.monthlyMessages({ includedUsage: 50 }); + const free = products.base({ + id: "free-checkout", + items: [messagesItem], + }); + + const proMessagesItem = items.monthlyMessages({ includedUsage: 200 }); + const pro = products.pro({ + id: "pro-checkout-upgrade", + items: [proMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ testClock: true }), // No payment method! + s.products({ list: [free, pro] }), + ], + actions: [], + }); + + // 1. First attach free product (no checkout needed - it's free) + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: free.id, + }); + + // Verify free is attached + let customer = await autumnV1.customers.get(customerId); + await expectProductActive({ + customer, + productId: free.id, + }); + + // 2. Preview upgrade to pro - should show $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 3. Attempt attach pro - should return checkout_url + const result = await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + expect(result.checkout_url).toBeDefined(); + + // 4. Complete checkout + await completeCheckoutForm(result.checkout_url); + await timeout(12000); + + // 5. Verify pro replaced free + customer = await autumnV1.customers.get(customerId); + + await expectProductActive({ + customer, + productId: pro.id, + }); + + // Verify messages feature from pro (200, not 50) + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + includedUsage: 200, + balance: 200, + usage: 0, + }); + + // Verify invoice + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════════ +// TEST 3: One-off via checkout (mode: "payment") +// ═══════════════════════════════════════════════════════════════════════════════ + +/** + * Scenario: + * - Customer with NO payment method + * - Attach one-off product + * + * Expected Result: + * - Returns checkout_url with mode: "payment" (not subscription) + * - Credits granted after checkout + */ +test.concurrent(`${chalk.yellowBright("stripe-checkout: one-off purchase")}`, async () => { + const customerId = "stripe-checkout-one-off"; + + const oneOffMessagesItem = items.oneOffMessages({ + includedUsage: 0, + billingUnits: 100, + price: 10, + }); + + const oneOff = products.oneOff({ + id: "one-off-checkout", + items: [oneOffMessagesItem], + }); + + const { autumnV1 } = await initScenario({ + customerId, + setup: [ + s.customer({ testClock: true }), // No payment method! + s.products({ list: [oneOff] }), + ], + actions: [], + }); + + // 1. Preview attach - base ($10) + messages ($10) = $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attempt attach - should return checkout_url + const result = await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + + expect(result.checkout_url).toBeDefined(); + + // 3. Complete checkout + await completeCheckoutForm(result.checkout_url); + await timeout(12000); + + // 4. Verify credits were granted + const customer = await autumnV1.customers.get(customerId); + + await expectProductActive({ + customer, + productId: oneOff.id, + }); + + expectCustomerFeatureCorrect({ + customer, + featureId: TestFeature.Messages, + balance: 100, + usage: 0, + }); + + // Verify invoice + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); +}); diff --git a/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts index 867ec4743..25ddbafa5 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-entities.test.ts @@ -11,7 +11,7 @@ */ import { expect, test } from "bun:test"; -import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; +import type { ApiCustomerV3, ApiEntityV0, AttachPreview } from "@autumn/shared"; import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; @@ -51,12 +51,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: create entity, attach pro to en s.products({ list: [pro] }), s.entities({ count: 1, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ - productId: pro.id, - entityIndex: 0, // Attach to first entity - }), - ], + actions: [], + }); + + // 1. Preview attach to entity - $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach to entity + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, }); // Get entity and verify it has the product @@ -85,6 +95,13 @@ test.concurrent(`${chalk.yellowBright("new-plan: create entity, attach pro to en // Customer should not have products array with this product const customerProduct = customer.products?.find((p) => p.id === pro.id); expect(customerProduct).toBeUndefined(); + + // Verify invoice on customer matches preview total: $20 + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -116,10 +133,35 @@ test.concurrent(`${chalk.yellowBright("new-plan: create 2 entities, attach pro t s.products({ list: [pro] }), s.entities({ count: 2, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ productId: pro.id, entityIndex: 0 }), - s.billing.attach({ productId: pro.id, entityIndex: 1 }), - ], + actions: [], + }); + + // 1. Preview and attach to entity 1 - $20 + const preview1 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, + }); + expect((preview1 as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, + }); + + // 2. Preview and attach to entity 2 - $20 + const preview2 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[1].id, + }); + expect((preview2 as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[1].id, }); // Get both entities and verify independent balances @@ -188,6 +230,14 @@ test.concurrent(`${chalk.yellowBright("new-plan: create 2 entities, attach pro t balance: 100, // Unchanged usage: 0, }); + + // Verify 2 invoices, each $20 + const customer = await autumnV1.customers.get(customerId); + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + latestTotal: 20, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -219,7 +269,21 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance s.products({ list: [pro] }), s.entities({ count: 2, featureId: TestFeature.Users }), ], - actions: [s.billing.attach({ productId: pro.id, entityIndex: 0 })], + actions: [], + }); + + // 1. Preview and attach to entity 1 - $20 (full price) + const preview1 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, + }); + expect((preview1 as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, }); // Advance 2 weeks @@ -229,7 +293,15 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance numberOfWeeks: 2, }); - // Attach pro to entity 2 mid-cycle + // 2. Preview attach to entity 2 mid-cycle (prorated) + const preview2 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[1].id, + }); + const entity2Total = (preview2 as AttachPreview).due_today.total; + + // 3. Attach to entity 2 mid-cycle await autumnV1.billing.attach({ customer_id: customerId, product_id: pro.id, @@ -259,14 +331,12 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to entity 1, advance // Get customer to check invoices const customer = await autumnV1.customers.get(customerId); - // Should have 2 invoices: one full price, one prorated + // Should have 2 invoices: one full price ($20), one prorated await expectCustomerInvoiceCorrect({ customer, count: 2, + latestTotal: entity2Total, // Prorated amount matches preview }); - - // Entity 2's invoice should be prorated (roughly half of $20 = ~$10) - // Note: exact amount depends on billing cycle alignment }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -296,12 +366,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro annual to entity")}` s.products({ list: [proAnnual] }), s.entities({ count: 1, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ - productId: proAnnual.id, - entityIndex: 0, - }), - ], + actions: [], + }); + + // 1. Preview attach to entity - $200 (annual) + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: proAnnual.id, + entity_id: entities[0].id, + }); + expect((preview as AttachPreview).due_today.total).toBe(200); + + // 2. Attach to entity + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: proAnnual.id, + entity_id: entities[0].id, }); // Get entity and verify product @@ -324,7 +404,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro annual to entity")}` usage: 0, }); - // Get customer and verify invoice (annual = $200) + // Get customer and verify invoice matches preview total: $200 const customer = await autumnV1.customers.get(customerId); await expectCustomerInvoiceCorrect({ @@ -362,10 +442,33 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to customer, then pr s.products({ list: [pro] }), s.entities({ count: 1, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ productId: pro.id }), // Customer-level - s.billing.attach({ productId: pro.id, entityIndex: 0 }), // Entity-level - ], + actions: [], + }); + + // 1. Preview and attach to customer - $20 + const previewCust = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((previewCust as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + // 2. Preview and attach to entity - $20 + const previewEnt = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, + }); + expect((previewEnt as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + entity_id: entities[0].id, }); // Get customer and entity @@ -400,6 +503,13 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro to customer, then pr balance: 100, usage: 0, }); + + // Verify 2 invoices, each $20 + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + latestTotal: 20, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -430,10 +540,33 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free to customer, then f s.products({ list: [free] }), s.entities({ count: 1, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ productId: free.id }), // Customer-level - s.billing.attach({ productId: free.id, entityIndex: 0 }), // Entity-level - ], + actions: [], + }); + + // 1. Preview and attach to customer - $0 (free) + const previewCust = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: free.id, + }); + expect((previewCust as AttachPreview).due_today.total).toBe(0); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: free.id, + }); + + // 2. Preview and attach to entity - $0 (free) + const previewEnt = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: free.id, + entity_id: entities[0].id, + }); + expect((previewEnt as AttachPreview).due_today.total).toBe(0); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: free.id, + entity_id: entities[0].id, }); // Get customer and entity @@ -469,7 +602,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free to customer, then f usage: 0, }); - // Verify no invoices (both free) + // Verify no invoices (both free) - matches preview total of 0 await expectCustomerInvoiceCorrect({ customer, count: 0, diff --git a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts index b279b0017..68a46c46f 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-free.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-free.test.ts @@ -11,7 +11,7 @@ */ import { expect, test } from "bun:test"; -import type { ApiCustomerV3 } from "@autumn/shared"; +import type { ApiCustomerV3, AttachPreview } from "@autumn/shared"; import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; @@ -47,7 +47,20 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async const { autumnV1 } = await initScenario({ customerId, setup: [s.customer({}), s.products({ list: [free] })], - actions: [s.billing.attach({ productId: free.id })], + actions: [], + }); + + // 1. Preview attach - verify no charge for free product + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: free.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(0); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: free.id, }); const customer = await autumnV1.customers.get(customerId); @@ -67,7 +80,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free product")}`, async usage: 0, }); - // Verify no invoice created (free product) + // Verify no invoice created (free product) - matches preview total of 0 expectCustomerInvoiceCorrect({ customer, count: 0, @@ -103,7 +116,20 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple featu const { autumnV1 } = await initScenario({ customerId, setup: [s.customer({}), s.products({ list: [free] })], - actions: [s.billing.attach({ productId: free.id })], + actions: [], + }); + + // 1. Preview attach - verify no charge for free product + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: free.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(0); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: free.id, }); const customer = await autumnV1.customers.get(customerId); @@ -135,7 +161,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach free with multiple featu // Verify dashboard feature (boolean - just check it exists) expect(customer.features[TestFeature.Dashboard]).toBeDefined(); - // Verify no invoice created (free product) + // Verify no invoice created (free product) - matches preview total of 0 expectCustomerInvoiceCorrect({ customer, count: 0, diff --git a/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts index 1aa363914..da753f4b6 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-one-time.test.ts @@ -12,7 +12,7 @@ */ import { expect, test } from "bun:test"; -import type { ApiCustomerV3, ApiEntityV0 } from "@autumn/shared"; +import type { ApiCustomerV3, ApiEntityV0, AttachPreview } from "@autumn/shared"; import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { @@ -59,12 +59,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase")}`, a s.customer({ paymentMethod: "success" }), s.products({ list: [oneOff] }), ], - actions: [ - s.billing.attach({ - productId: oneOff.id, - options: [{ feature_id: TestFeature.Messages, quantity: 1 }], - }), - ], + actions: [], + }); + + // 1. Preview attach - verify base ($10) + prepaid ($10) = $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); const customer = await autumnV1.customers.get(customerId); @@ -83,11 +93,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase")}`, a usage: 0, }); - // Verify invoice: one-time charge ($10 base + $10 messages = $20) + // Verify invoice matches preview total: $20 await expectCustomerInvoiceCorrect({ customer, count: 1, - latestTotal: 20, // oneOff base ($10) + prepaid ($10) + latestTotal: 20, }); }); @@ -123,15 +133,33 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase twice" s.customer({ paymentMethod: "success" }), s.products({ list: [oneOff] }), ], - actions: [ - s.billing.attach({ - productId: oneOff.id, - options: [{ feature_id: TestFeature.Messages, quantity: 1 }], - }), - ], + actions: [], }); - // Attach same product again + // 1. Preview first attach - $20 + const preview1 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview1 as AttachPreview).due_today.total).toBe(20); + + // 2. First attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + + // 3. Preview second attach - $20 + const preview2 = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview2 as AttachPreview).due_today.total).toBe(20); + + // 4. Second attach await autumnV1.billing.attach({ customer_id: customerId, product_id: oneOff.id, @@ -148,7 +176,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time purchase twice" usage: 0, }); - // Verify two invoices created + // Verify two invoices created, each matching preview total await expectCustomerInvoiceCorrect({ customer, count: 2, @@ -193,15 +221,34 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro then one-time as mai s.customer({ paymentMethod: "success" }), s.products({ list: [pro, oneOff] }), ], - actions: [s.billing.attach({ productId: pro.id })], + actions: [], }); - // Attach one-time without isAddOn - should replace pro + // 1. Preview and attach pro first - $20 + const previewPro = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((previewPro as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + // 2. Preview one-time replacement (includes refund for pro) + const previewOneOff = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + const oneOffTotal = (previewOneOff as AttachPreview).due_today.total; + + // 3. Attach one-time without isAddOn - should replace pro await autumnV1.billing.attach({ customer_id: customerId, product_id: oneOff.id, options: [{ feature_id: TestFeature.Messages, quantity: 1 }], - // Note: NOT setting is_add_on: true }); const customer = await autumnV1.customers.get(customerId); @@ -217,6 +264,13 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro then one-time as mai customer, productId: oneOff.id, }); + + // Verify latest invoice matches preview + await expectCustomerInvoiceCorrect({ + customer, + count: 2, // pro invoice + one-off invoice + latestTotal: oneOffTotal, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -251,12 +305,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with quantity=0 s.customer({ paymentMethod: "success" }), s.products({ list: [oneOff] }), ], - actions: [ - s.billing.attach({ - productId: oneOff.id, - options: [{ feature_id: TestFeature.Messages, quantity: 1 }], - }), - ], + actions: [], + }); + + // 1. Preview attach - base ($10) + messages ($10) = $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); const customer = await autumnV1.customers.get(customerId); @@ -269,11 +333,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with quantity=0 usage: 0, }); - // Verify invoice: only messages charged + // Verify invoice matches preview total: $20 await expectCustomerInvoiceCorrect({ customer, count: 1, - latestTotal: 20, // base ($10) + messages ($10) + latestTotal: 20, }); }); @@ -317,10 +381,30 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time as add-on to pr s.customer({ paymentMethod: "success" }), s.products({ list: [pro, oneOffAddon] }), ], - actions: [s.billing.attach({ productId: pro.id })], + actions: [], }); - // Attach one-time add-on (is_add_on defined at product level, not in attach params) + // 1. Preview and attach pro - $20 + const previewPro = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((previewPro as AttachPreview).due_today.total).toBe(20); + + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + }); + + // 2. Preview add-on - base ($10) + prepaid ($5) = $15 + const previewAddOn = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOffAddon.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((previewAddOn as AttachPreview).due_today.total).toBe(15); + + // 3. Attach add-on await autumnV1.billing.attach({ customer_id: customerId, product_id: oneOffAddon.id, @@ -346,6 +430,13 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time as add-on to pr balance: 150, usage: 0, }); + + // Verify two invoices: pro ($20) + add-on ($15) + await expectCustomerInvoiceCorrect({ + customer, + count: 2, + latestTotal: 15, + }); }); // ═══════════════════════════════════════════════════════════════════════════════ @@ -379,12 +470,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with multiple f s.customer({ paymentMethod: "success" }), s.products({ list: [oneOff] }), ], - actions: [ - s.billing.attach({ - productId: oneOff.id, - options: [{ feature_id: TestFeature.Messages, quantity: 2 }], // 2 packs = 200 messages - }), - ], + actions: [], + }); + + // 1. Preview attach - base ($10) + 2 packs ($20) = $30 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 2 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(30); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + options: [{ feature_id: TestFeature.Messages, quantity: 2 }], }); const customer = await autumnV1.customers.get(customerId); @@ -397,11 +498,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time with multiple f usage: 0, }); - // Verify invoice + // Verify invoice matches preview total: $30 await expectCustomerInvoiceCorrect({ customer, count: 1, - latestTotal: 30, // base ($10) + 2 packs ($20) + latestTotal: 30, }); }); @@ -439,13 +540,24 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time to entity")}`, s.products({ list: [oneOff] }), s.entities({ count: 1, featureId: TestFeature.Users }), ], - actions: [ - s.billing.attach({ - productId: oneOff.id, - entityIndex: 0, // Attach to first entity - options: [{ feature_id: TestFeature.Messages, quantity: 1 }], - }), - ], + actions: [], + }); + + // 1. Preview attach to entity - base ($10) + prepaid ($10) = $20 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: oneOff.id, + entity_id: entities[0].id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach to entity + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: oneOff.id, + entity_id: entities[0].id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); // Get entity to verify balance @@ -467,4 +579,11 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach one-time to entity")}`, // Customer should not have messages feature (it's on the entity) expect(customer.features[TestFeature.Messages]).toBeUndefined(); + + // Verify invoice on customer matches preview total: $20 + await expectCustomerInvoiceCorrect({ + customer, + count: 1, + latestTotal: 20, + }); }); diff --git a/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts index 862e38b9d..53ba3fa5a 100644 --- a/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts +++ b/server/tests/integration/billing/attach/new-plan/attach-paid.test.ts @@ -10,8 +10,12 @@ * - Allocated features track entity usage */ -import { test } from "bun:test"; -import { type ApiCustomerV3, ErrCode } from "@autumn/shared"; +import { expect, test } from "bun:test"; +import { + type ApiCustomerV3, + type AttachPreview, + ErrCode, +} from "@autumn/shared"; import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; @@ -57,12 +61,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with mixed features" s.customer({ paymentMethod: "success" }), s.products({ list: [pro] }), ], - actions: [ - s.billing.attach({ - productId: pro.id, - options: [{ feature_id: TestFeature.Messages, quantity: 1 }], // 1 pack of 100 - }), - ], + actions: [], + }); + + // 1. Preview attach - verify base ($20) + prepaid ($10) = $30 + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(30); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 1 }], }); const customer = await autumnV1.customers.get(customerId); @@ -99,7 +113,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with mixed features" usage: 0, }); - // Verify invoice: base ($20) + prepaid ($10) = $30 + // Verify invoice matches preview total: $30 await expectCustomerInvoiceCorrect({ customer, count: 1, @@ -137,7 +151,20 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with allocated, crea s.products({ list: [pro] }), s.entities({ count: 5, featureId: TestFeature.Users }), ], - actions: [s.billing.attach({ productId: pro.id })], + actions: [], + }); + + // 1. Preview attach - verify base price ($20) + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, }); // Track 5 users (creates overage of 2) @@ -166,7 +193,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with allocated, crea usage: 5, }); - // Verify invoices: initial ($20) + overage (2 users @ $10 = $20) + // Verify invoices: initial ($20 matches preview) + overage (2 users @ $10 = $20) await expectCustomerInvoiceCorrect({ customer, count: 2, @@ -208,9 +235,9 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach base with prepaid messag actions: [], }); - // Attempt to attach without options - should fail + // Attempt to attach without options - should fail (no preview needed for error case) await expectAutumnError({ - errCode: ErrCode.InvalidRequest, + errCode: ErrCode.InvalidOptions, func: async () => { await autumnV1.billing.attach({ customer_id: customerId, @@ -254,9 +281,9 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid message actions: [], }); - // Attempt to attach without options - should fail + // Attempt to attach without options - should fail (no preview needed for error case) await expectAutumnError({ - errCode: ErrCode.InvalidRequest, + errCode: ErrCode.InvalidOptions, func: async () => { await autumnV1.billing.attach({ customer_id: customerId, @@ -298,12 +325,22 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid message s.customer({ paymentMethod: "success" }), s.products({ list: [pro] }), ], - actions: [ - s.billing.attach({ - productId: pro.id, - options: [{ feature_id: TestFeature.Messages, quantity: 0 }], - }), - ], + actions: [], + }); + + // 1. Preview attach - verify only base price ($20), no prepaid + const preview = await autumnV1.billing.previewAttach({ + customer_id: customerId, + product_id: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 0 }], + }); + expect((preview as AttachPreview).due_today.total).toBe(20); + + // 2. Attach + await autumnV1.billing.attach({ + customer_id: customerId, + product_id: pro.id, + options: [{ feature_id: TestFeature.Messages, quantity: 0 }], }); const customer = await autumnV1.customers.get(customerId); @@ -322,7 +359,7 @@ test.concurrent(`${chalk.yellowBright("new-plan: attach pro with prepaid message usage: 0, }); - // Verify invoice: only base price ($20), no prepaid + // Verify invoice matches preview total: $20 await expectCustomerInvoiceCorrect({ customer, count: 1, diff --git a/server/tests/integration/billing/update-subscription/BILLING_GUIDE.md b/server/tests/integration/billing/update-subscription/BILLING_GUIDE.md deleted file mode 100644 index 858016b8c..000000000 --- a/server/tests/integration/billing/update-subscription/BILLING_GUIDE.md +++ /dev/null @@ -1,215 +0,0 @@ -# Subscription Update Billing Guide - -## Proration & Charges - -When updating a subscription via `subscriptions.update` (custom plan), charges/credits are calculated based on the billing model: - -### Billing Models - -| Model | On Update Behavior | -|-------|-------------------| -| **Base Price** | Prorated charge/credit for price difference | -| **Consumable** | No immediate overage charge (billed in arrears at cycle end) | -| **Allocated** | Prorated charge for current overage above new included amount | -| **Prepaid** | Full refund of previous prepaid, full charge for new prepaid | - -### Detailed Behavior - -#### 1. Base Price Changes -- **Increase**: Charge prorated difference for remaining cycle -- **Decrease**: Credit prorated difference for remaining cycle -- **Remove**: Credit full remaining prorated amount - -```typescript -// $20/mo -> $30/mo at start of cycle = charge $10 -expect(preview.total).toBe(10); - -// $30/mo -> $20/mo at start of cycle = credit $10 -expect(preview.total).toBe(-10); - -// Mid-cycle (15 days): $20/mo -> $30/mo = charge ~$5 (prorated) -expect(preview.total).toBe(5); -``` - -#### 2. Consumable Features -- **Never** charge overage on update -- Overage is billed at end of billing cycle -- Even if usage exceeds new included amount, preview.total = 0 for the consumable portion - -```typescript -// 80 used, 50 included = 30 overage, but... -expect(preview.total).toBe(0); // Consumable overage NOT charged on update -``` - -#### 3. Allocated Features (Seat-Based) -- Charge prorated amount for overage seats above new included amount -- Based on current usage vs new included allowance - -```typescript -// Using 5 seats, decrease included from 5 to 3 -// Overage = 5 - 3 = 2 seats @ $10/seat = $20 -expect(preview.total).toBe(20); - -// Using 2 seats, increase included from 2 to 5 -// No overage, no charge -expect(preview.total).toBe(0); -``` - -##### ⚠️ Important: Allocated Features Create Invoices on Track - -For allocated features (seat-based / prorated billing), **tracking usage past the included boundary immediately creates a prorated invoice**. This is handled in `adjustAllowance.ts`. - -This means: -- When `track()` causes usage to exceed included seats, an invoice is created immediately -- This is different from consumable features, which only bill at cycle end - -```typescript -// Example: Product with 3 included seats @ $10/seat overage -// Customer tracks 5 seats (2 over included) - -await autumnV1.track({ - customer_id: customerId, - feature_id: TestFeature.Users, - value: 5, // 2 over the 3 included -}); - -// This immediately creates an invoice for the 2 extra seats (prorated) -// Invoice count is now: 1 (initial) + 1 (track overage) = 2 - -// Later, when updating subscription: -await autumnV1.subscriptions.update(updateParams); - -// Invoice count becomes: 1 (initial) + 1 (track overage) + 1 (update) = 3 -``` - -This affects invoice count expectations in tests: -- Usage within included: No extra invoice from track -- Usage exceeds included: +1 invoice from track - -#### 4. Prepaid Features - -**Prepaid features require `options` with `quantity`** when attaching or updating. The `quantity` is: -- The **total units** you want (NOT multiplied by billing_units) -- **NOT** inclusive of `included_usage` (included_usage is separate free balance) - -Billing logic on update: -1. **Refund** previous prepaid amount: `old_packs * old_price` -2. **Charge** new prepaid amount: `new_packs * new_price` -3. **preview.total** = new charge - old refund - -```typescript -// Setup: $10 per 100 units (1 pack = 100 units at $10) -const prepaidItem = items.prepaidMessages({ - includedUsage: 0, - billingUnits: 100, - price: 10, -}); - -// Attach with 2 packs (200 units) -await initScenario({ - actions: [ - s.attach({ - productId: "pro", - options: [{ feature_id: TestFeature.Messages, quantity: 200 }], // 2 packs - }), - ], -}); - -// Upgrade to 5 packs (500 units) -const preview = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: pro.id, - options: [{ feature_id: TestFeature.Messages, quantity: 500 }], // 5 packs -}); - -// preview.total = (5 - 2) * $10 = $30 -expect(preview.total).toBe(30); - -// Downgrade to 3 packs (300 units) -const preview2 = await autumnV1.subscriptions.previewUpdate({ - customer_id: customerId, - product_id: pro.id, - options: [{ feature_id: TestFeature.Messages, quantity: 300 }], // 3 packs -}); - -// preview.total = (3 - 5) * $10 = -$20 (credit) -expect(preview2.total).toBe(-20); -``` - -##### Prepaid with Price/Billing Unit Changes - -When changing price or billing units via `items`, the calculation uses old and new pack costs: - -```typescript -// Old: 3 packs of 100 @ $10 = $30 -// New: 3 packs of 100 @ $15 = $45 -// preview.total = $45 - $30 = $15 -expect(preview.total).toBe(15); - -// Old: 300 units / 100 = 3 packs @ $10 = $30 -// New: 300 units / 50 = 6 packs @ $10 = $60 -// preview.total = $60 - $30 = $30 -expect(preview.total).toBe(30); -``` - -### Preview vs Invoice Matching - -Always verify that `preview.total` matches the actual invoice: - -```typescript -const updateParams = { - customer_id: customerId, - product_id: pro.id, - items: [newItem, priceItem], -}; - -const preview = await autumnV1.subscriptions.previewUpdate(updateParams); -expect(preview.total).toBe(expectedAmount); - -await autumnV1.subscriptions.update(updateParams); - -const customer = await autumnV1.customers.get(customerId); -await expectCustomerInvoiceCorrect({ - customer, - count: expectedInvoiceCount, - latestTotal: preview.total, -}); -``` - -### Invoice Count Guidelines - -| Transition | Expected Count | -|------------|---------------| -| Free-to-Free | 0 | -| Free-to-Paid | 1 | -| Paid-to-Paid (upgrade/downgrade) | Initial (1) + Update (1) = 2 | -| Paid-to-Paid (allocated to prepaid) | Initial (1) + Arrear Settlement (1) + Prepaid (1) = 3 | - -#### Allocated Feature Invoice Counts - -For allocated features, invoice count depends on whether usage exceeded included at any point: - -| Scenario | Invoice Count | -|----------|--------------| -| Usage stays within included, then update | Initial (1) + Update (1) = 2 | -| Usage exceeds included via track, then update | Initial (1) + Track Overage (1) + Update (1) = 3 | -| Usage exceeds included via track, update increases included to cover usage | Initial (1) + Track Overage (1) + Update Credit (1) = 3 | - -```typescript -// Example: 3 included seats, track 5 seats (2 over), then increase to 10 included -await expectCustomerInvoiceCorrect({ - customer, - count: 3, // 1 (attach) + 1 (track overage) + 1 (update credit) - latestTotal: preview.total, -}); -``` - -### No-Charge Updates - -These updates should have `preview.total = 0`: -- Adding/removing boolean features (no price impact) -- Changing included usage (no billing attached) -- Changing feature intervals (month → week) -- Updating consumable features (overage not charged on update) -- Increasing allocated seats when within included amount - diff --git a/server/tests/integration/billing/update-subscription/update-subscription.test.ts b/server/tests/integration/billing/update-subscription/update-subscription.test.ts new file mode 100644 index 000000000..bad55ec97 --- /dev/null +++ b/server/tests/integration/billing/update-subscription/update-subscription.test.ts @@ -0,0 +1 @@ +// Just an entry file for search diff --git a/shared/api/billing/common/billingResponse.ts b/shared/api/billing/common/billingResponse.ts index 1e8575c04..8270ebd25 100644 --- a/shared/api/billing/common/billingResponse.ts +++ b/shared/api/billing/common/billingResponse.ts @@ -29,6 +29,9 @@ export const BillingResponseSchema = z.object({ payment_url: z.string().nullable(), + // Checkout URL for Stripe Checkout session (when customer has no payment method) + checkout_url: z.string().nullable(), + required_action: BillingResponseRequiredActionSchema.optional(), }); diff --git a/shared/models/billingModels/cusProductActions.ts b/shared/models/billingModels/cusProductActions.ts deleted file mode 100644 index 201a388b0..000000000 --- a/shared/models/billingModels/cusProductActions.ts +++ /dev/null @@ -1,33 +0,0 @@ -import z from "zod/v4"; - -import { - EnrichedNewProductActionSchema, - type NewProductAction, - NewProductActionSchema, -} from "./newProductAction"; -import { - type OngoingCusProductAction, - OngoingCusProductActionSchema, -} from "./ongoingCusProductAction"; -import { - type ScheduledCusProductAction, - ScheduledCusProductActionSchema, -} from "./scheduledCusProductAction"; - -export interface CusProductActions { - ongoingCusProductAction?: OngoingCusProductAction; - scheduledCusProductAction?: ScheduledCusProductAction; - newProductActions: NewProductAction[]; -} - -export const CusProductActionsSchema = z.object({ - ongoingCusProductAction: OngoingCusProductActionSchema, - scheduledCusProductAction: ScheduledCusProductActionSchema, - newProductActions: z.array(NewProductActionSchema), -}); - -export const EnrichedCusProductActionsSchema = CusProductActionsSchema.extend({ - ongoingCusProductAction: OngoingCusProductActionSchema, - scheduledCusProductAction: ScheduledCusProductActionSchema, - newProductActions: z.array(EnrichedNewProductActionSchema), -}); diff --git a/shared/models/billingModels/newProductAction.ts b/shared/models/billingModels/newProductAction.ts deleted file mode 100644 index 1d365f889..000000000 --- a/shared/models/billingModels/newProductAction.ts +++ /dev/null @@ -1,16 +0,0 @@ -import z from "zod/v4"; -import { FullProductSchema } from "../productModels/productModels"; - -export const NewProductActionSchema = z.object({ - timing: z.literal(["scheduled", "immediate"]), - product: FullProductSchema, -}); - -export const EnrichedNewProductActionSchema = NewProductActionSchema.extend({ - startsAt: z.number().default(Date.now()), -}); - -export type NewProductAction = z.infer; -export type EnrichedNewProductAction = z.infer< - typeof EnrichedNewProductActionSchema ->; diff --git a/shared/models/billingModels/ongoingCusProductAction.ts b/shared/models/billingModels/ongoingCusProductAction.ts deleted file mode 100644 index 2b7ebfe6f..000000000 --- a/shared/models/billingModels/ongoingCusProductAction.ts +++ /dev/null @@ -1,18 +0,0 @@ -import z from "zod/v4"; -import { FullCusProductSchema } from "../cusProductModels/cusProductModels"; - -enum OngoingCusProductActionEnum { - Expire = "expire", - Cancel = "cancel", - Uncancel = "uncancel", - Update = "update", -} - -// What happens to the CURRENT active cus product -export const OngoingCusProductActionSchema = z.object({ - action: z.enum(OngoingCusProductActionEnum), - cusProduct: FullCusProductSchema, -}); -export type OngoingCusProductAction = z.infer< - typeof OngoingCusProductActionSchema ->; diff --git a/shared/models/billingModels/scheduledCusProductAction.ts b/shared/models/billingModels/scheduledCusProductAction.ts deleted file mode 100644 index b62a83b14..000000000 --- a/shared/models/billingModels/scheduledCusProductAction.ts +++ /dev/null @@ -1,12 +0,0 @@ -import z from "zod/v4"; -import { FullCusProductSchema } from "../cusProductModels/cusProductModels"; - -// What happens to any SCHEDULED cus product -export const ScheduledCusProductActionSchema = z.object({ - action: z.literal("delete"), - cusProduct: FullCusProductSchema, -}); - -export type ScheduledCusProductAction = z.infer< - typeof ScheduledCusProductActionSchema ->; diff --git a/shared/models/otherModels/metadataTable.ts b/shared/models/otherModels/metadataTable.ts index cb02da3a0..5ba8d1ab0 100644 --- a/shared/models/otherModels/metadataTable.ts +++ b/shared/models/otherModels/metadataTable.ts @@ -8,9 +8,8 @@ export enum MetadataType { CheckoutSessionCompleted = "checkout_session_completed", DeferredInvoice = "deferred_invoice", - - // InvoiceActionRequiredV2 = "invoice_action_required_v2", - // InvoiceCheckoutV2 = "invoice_checkout_v2", + CheckoutSessionV2 = "checkout_session_v2", + CheckoutSessionCompletedV2 = "checkout_session_completed_v2", } export const metadata = pgTable("metadata", { @@ -20,6 +19,7 @@ export const metadata = pgTable("metadata", { data: jsonb(), type: text("type").$type(), stripe_invoice_id: text("stripe_invoice_id"), + stripe_checkout_session_id: text("stripe_checkout_session_id"), }); export type Metadata = InferSelectModel; diff --git a/shared/utils/orgUtils/convertOrgUtils.ts b/shared/utils/orgUtils/convertOrgUtils.ts index 501cdbdcc..cfb25b20a 100644 --- a/shared/utils/orgUtils/convertOrgUtils.ts +++ b/shared/utils/orgUtils/convertOrgUtils.ts @@ -1,3 +1,4 @@ +import { AppEnv } from "../../index.js"; import { CusProductStatus } from "../../models/cusProductModels/cusProductEnums.js"; import type { Organization } from "../../models/orgModels/orgTable.js"; @@ -11,3 +12,17 @@ export const orgToInStatuses = ({ org }: { org: Organization }) => { export const orgToCurrency = ({ org }: { org: Organization }) => { return org.default_currency || "usd"; }; + +export const orgToReturnUrl = ({ + org, + env, +}: { + org: Organization; + env: AppEnv; +}) => { + if (env === AppEnv.Sandbox) { + return org.stripe_config?.sandbox_success_url || "https://useautumn.com"; + } else { + return org.stripe_config?.success_url || "https://useautumn.com"; + } +}; diff --git a/vite/src/components/forms/attach-v2/attachFormSchema.ts b/vite/src/components/forms/attach-v2/attachFormSchema.ts new file mode 100644 index 000000000..52376640a --- /dev/null +++ b/vite/src/components/forms/attach-v2/attachFormSchema.ts @@ -0,0 +1,11 @@ +import type { ProductItem } from "@autumn/shared"; +import { z } from "zod/v4"; + +export const AttachFormSchema = z.object({ + productId: z.string(), + prepaidOptions: z.record(z.string(), z.number().nonnegative()), + items: z.custom().nullable(), + version: z.number().positive().optional(), +}); + +export type AttachForm = z.infer; diff --git a/vite/src/components/forms/attach-v2/components/AttachFooter.tsx b/vite/src/components/forms/attach-v2/components/AttachFooter.tsx new file mode 100644 index 000000000..985003c31 --- /dev/null +++ b/vite/src/components/forms/attach-v2/components/AttachFooter.tsx @@ -0,0 +1,96 @@ +import { motion } from "motion/react"; +import { useEffect, useState } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Button } from "@/components/v2/buttons/Button"; +import { SheetFooter } from "@/components/v2/sheets/SharedSheetComponents"; +import { useAttachFormContext } from "../context/AttachFormProvider"; + +const FOOTER_DELAY_MS = 350; + +export function AttachFooter() { + const { + isPending, + previewQuery, + handleConfirm, + handleInvoiceAttach, + formValues, + } = useAttachFormContext(); + + const hasProductSelected = !!formValues.productId; + const isLoading = previewQuery.isLoading; + const hasError = !!previewQuery.error; + const isReady = hasProductSelected && !isLoading && !hasError; + + const [showFooter, setShowFooter] = useState(false); + + useEffect(() => { + if (isReady) { + const timer = setTimeout(() => setShowFooter(true), FOOTER_DELAY_MS); + return () => clearTimeout(timer); + } + setShowFooter(false); + }, [isReady]); + + if (!showFooter) return null; + + return ( + + + + + + + +
+ + +
+
+
+ +
+
+ ); +} diff --git a/vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx b/vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx new file mode 100644 index 000000000..a421048f3 --- /dev/null +++ b/vite/src/components/forms/attach-v2/components/AttachPlanSection.tsx @@ -0,0 +1,140 @@ +import type { ProductItem } from "@autumn/shared"; +import { buildEditsForItem, UsageModel } from "@autumn/shared"; +import { PencilSimpleIcon } from "@phosphor-icons/react"; +import { LayoutGroup, motion } from "motion/react"; +import { PriceDisplay } from "@/components/forms/update-subscription-v2/components/PriceDisplay"; +import { StatusBadge } from "@/components/forms/update-subscription-v2/components/StatusBadge"; +import { SubscriptionItemRow } from "@/components/forms/update-subscription-v2/components/SubscriptionItemRow"; +import { LAYOUT_TRANSITION } from "@/components/forms/update-subscription-v2/constants/animationConstants"; +import { Button } from "@/components/v2/buttons/Button"; +import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useAttachFormContext } from "../context/AttachFormProvider"; + +function SectionTitle({ hasCustomizations }: { hasCustomizations: boolean }) { + return ( +
+ Plan Configuration + {hasCustomizations && Custom} +
+ ); +} + +export function AttachPlanSection() { + const { + form, + formValues, + originalItems, + productWithFormItems: product, + hasCustomizations, + handleEditPlan, + } = useAttachFormContext(); + + const { prepaidOptions } = formValues; + + const { org } = useOrg(); + const currency = org?.default_currency ?? "USD"; + + const originalItemsMap = new Map( + originalItems?.filter((i) => i.feature_id).map((i) => [i.feature_id, i]) ?? + [], + ); + + const currentFeatureIds = new Set( + product?.items?.map((i) => i.feature_id).filter(Boolean) ?? [], + ); + + const deletedItems = + hasCustomizations && originalItems + ? originalItems.filter( + (i) => i.feature_id && !currentFeatureIds.has(i.feature_id), + ) + : []; + + if (!product) return null; + + return ( + } + withSeparator + > + {(product?.items?.length ?? 0) > 0 || deletedItems.length > 0 ? ( + <> +
+ +
+ +
+ {product?.items?.map((item: ProductItem, index: number) => { + if (!item.feature_id) return null; + + const featureId = item.feature_id; + const isPrepaid = item.usage_model === UsageModel.Prepaid; + const currentPrepaidQuantity = isPrepaid + ? (prepaidOptions[featureId] ?? 0) + : undefined; + + const originalItem = originalItemsMap.get(featureId); + const isCreated = + hasCustomizations && + !originalItem && + originalItems && + originalItems.length > 0; + + const edits = hasCustomizations + ? buildEditsForItem({ + updatedItem: item, + originalItem, + updatedPrepaidQuantity: currentPrepaidQuantity, + originalPrepaidQuantity: undefined, + }) + : []; + + return ( + + + + ); + })} + {deletedItems.map((item: ProductItem, index: number) => ( + + + + ))} + + + +
+
+ + ) : ( + + )} +
+ ); +} diff --git a/vite/src/components/forms/attach-v2/components/AttachPreviewSection.tsx b/vite/src/components/forms/attach-v2/components/AttachPreviewSection.tsx new file mode 100644 index 000000000..282c137be --- /dev/null +++ b/vite/src/components/forms/attach-v2/components/AttachPreviewSection.tsx @@ -0,0 +1,60 @@ +import type { AxiosError } from "axios"; +import { format } from "date-fns"; +import { PreviewErrorDisplay } from "@/components/forms/update-subscription-v2/components/PreviewErrorDisplay"; +import { LineItemsPreview } from "@/components/v2/LineItemsPreview"; +import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents"; +import { getBackendErr } from "@/utils/genUtils"; +import { useAttachFormContext } from "../context/AttachFormProvider"; + +export function AttachPreviewSection() { + const { previewQuery, formValues } = useAttachFormContext(); + + const hasProductSelected = !!formValues.productId; + + const { isLoading, data: previewData, error: queryError } = previewQuery; + const error = queryError + ? getBackendErr(queryError as AxiosError, "Failed to load preview") + : undefined; + + const totals = []; + + if (previewData) { + totals.push({ + label: "Total Due Now", + amount: previewData.total, + variant: "primary" as const, + }); + + if (previewData.next_cycle) { + totals.push({ + label: "Next Cycle", + amount: previewData.next_cycle.total, + variant: "secondary" as const, + badge: previewData.next_cycle.starts_at + ? format(new Date(previewData.next_cycle.starts_at), "MMM d, yyyy") + : undefined, + }); + } + } + + if (!hasProductSelected) return null; + + if (error) { + return ( + + + + ); + } + + return ( + + ); +} diff --git a/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx b/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx new file mode 100644 index 000000000..5e83c69d0 --- /dev/null +++ b/vite/src/components/forms/attach-v2/components/AttachProductSelection.tsx @@ -0,0 +1,48 @@ +import { isProductAlreadyEnabled } from "@autumn/shared"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { useEntity } from "@/hooks/stores/useSubscriptionStore"; +import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; +import { useAttachFormContext } from "../context/AttachFormProvider"; + +export function AttachProductSelection() { + const { form, hasCustomizations } = useAttachFormContext(); + + const { products } = useProductsQuery(); + const availableProducts = products.filter((p) => !p.archived); + const { customer } = useCusQuery(); + const { entityId } = useEntity(); + + const productId = form.state.values.productId; + + return ( +
+ + {(field) => ( + ({ + label: p.name, + value: p.id, + disabledValue: isProductAlreadyEnabled({ + productId: p.id, + customer, + entityId: entityId ?? undefined, + }) + ? "Already Enabled" + : undefined, + }))} + placeholder="Select Product" + hideFieldInfo + selectValueAfter={ + hasCustomizations && productId ? ( + + Custom + + ) : undefined + } + /> + )} + +
+ ); +} diff --git a/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx new file mode 100644 index 000000000..238728629 --- /dev/null +++ b/vite/src/components/forms/attach-v2/context/AttachFormProvider.tsx @@ -0,0 +1,288 @@ +import type { + Feature, + FrontendProduct, + ProductItem, + ProductV2, +} from "@autumn/shared"; +import { productV2ToFrontendProduct, UsageModel } from "@autumn/shared"; +import { useStore } from "@tanstack/react-form"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import type { PrepaidItemWithFeature } from "@/hooks/stores/useProductStore"; +import { usePrepaidItems } from "@/hooks/stores/useProductStore"; +import type { AttachForm } from "../attachFormSchema"; +import { type UseAttachForm, useAttachForm } from "../hooks/useAttachForm"; +import { useAttachMutation } from "../hooks/useAttachMutation"; +import { + type UseAttachPreviewReturn, + useAttachPreview, +} from "../hooks/useAttachPreview"; +import { useAttachRequestBody } from "../hooks/useAttachRequestBody"; + +export interface AttachFormContext { + customerId: string | undefined; + entityId: string | undefined; +} + +interface AttachFormContextValue { + formContext: AttachFormContext; + form: UseAttachForm; + formValues: AttachForm; + features: Feature[]; + + product: ProductV2 | undefined; + prepaidItems: PrepaidItemWithFeature[]; + originalItems: ProductItem[] | undefined; + productWithFormItems: FrontendProduct | undefined; + hasCustomizations: boolean; + + previewQuery: UseAttachPreviewReturn; + + showPlanEditor: boolean; + handleEditPlan: () => void; + handlePlanEditorSave: (items: ProductItem[]) => void; + handlePlanEditorCancel: () => void; + + isPending: boolean; + handleConfirm: () => void; + handleInvoiceAttach: (params: { enableProductImmediately: boolean }) => void; +} + +const AttachFormReactContext = createContext( + null, +); + +interface AttachFormProviderProps { + customerId: string | undefined; + entityId: string | undefined; + initialProductId?: string; + onPlanEditorOpen?: () => void; + onPlanEditorClose?: () => void; + onInvoiceCreated?: (invoiceId: string) => void; + onCheckoutRedirect?: (checkoutUrl: string) => void; + onSuccess?: () => void; + children: ReactNode; +} + +export function AttachFormProvider({ + customerId, + entityId, + initialProductId, + onPlanEditorOpen, + onPlanEditorClose, + onInvoiceCreated, + onCheckoutRedirect, + onSuccess, + children, +}: AttachFormProviderProps) { + const [showPlanEditor, setShowPlanEditor] = useState(false); + + const form = useAttachForm({ initialProductId }); + + const { features } = useFeaturesQuery(); + const { products } = useProductsQuery(); + + const formValues = useStore(form.store, (state) => state.values); + const { productId, prepaidOptions, items, version } = formValues; + + const product = useMemo( + () => products.find((p) => p.id === productId && !p.archived), + [products, productId], + ); + + const { prepaidItems } = usePrepaidItems({ product }); + + // Track product changes and initialize prepaid options + const previousProductIdRef = useRef(); + useEffect(() => { + // Only trigger when productId actually changes (not on initial mount with same value) + if (previousProductIdRef.current === productId) { + return; + } + + const isProductChange = + previousProductIdRef.current !== undefined && + previousProductIdRef.current !== productId; + + previousProductIdRef.current = productId; + + if (isProductChange) { + // Reset items and version when product changes + form.setFieldValue("items", null); + form.setFieldValue("version", undefined); + } + + // Initialize prepaid options for the selected product + if (product) { + const initialPrepaidOptions: Record = {}; + for (const item of product.items) { + if (item.usage_model === UsageModel.Prepaid && item.feature_id) { + initialPrepaidOptions[item.feature_id] = 0; + } + } + form.setFieldValue("prepaidOptions", initialPrepaidOptions); + } + }, [productId, product, form]); + + const originalItems = product?.items as ProductItem[] | undefined; + + const hasCustomizations = items !== null && items.length > 0; + + const productWithFormItems = useMemo((): FrontendProduct | undefined => { + if (!product) return undefined; + + const baseFrontendProduct = productV2ToFrontendProduct({ + product: product as ProductV2, + }); + + if (items) { + return { + ...baseFrontendProduct, + items, + }; + } + + return baseFrontendProduct; + }, [product, items]); + + const previewQuery = useAttachPreview({ + customerId, + entityId, + product, + prepaidOptions, + items, + version, + }); + + const { buildRequestBody } = useAttachRequestBody({ + customerId, + entityId, + product, + prepaidOptions, + items, + version, + }); + + const { handleConfirm, handleInvoiceAttach, isPending } = useAttachMutation({ + customerId, + buildRequestBody, + onInvoiceCreated, + onCheckoutRedirect, + onSuccess, + }); + + const handleEditPlan = useCallback(() => { + if (!productWithFormItems) return; + setShowPlanEditor(true); + onPlanEditorOpen?.(); + }, [productWithFormItems, onPlanEditorOpen]); + + const handlePlanEditorSave = useCallback( + (newItems: ProductItem[]) => { + form.setFieldValue("items", newItems); + + const currentPrepaidOptions = form.store.state.values.prepaidOptions; + const updatedPrepaidOptions = { ...currentPrepaidOptions }; + let hasNewPrepaidItems = false; + + for (const item of newItems) { + if ( + item.usage_model === "prepaid" && + item.feature_id && + updatedPrepaidOptions[item.feature_id] === undefined + ) { + updatedPrepaidOptions[item.feature_id] = 0; + hasNewPrepaidItems = true; + } + } + + if (hasNewPrepaidItems) { + form.setFieldValue("prepaidOptions", updatedPrepaidOptions); + } + + setShowPlanEditor(false); + onPlanEditorClose?.(); + }, + [form, onPlanEditorClose], + ); + + const handlePlanEditorCancel = useCallback(() => { + setShowPlanEditor(false); + onPlanEditorClose?.(); + }, [onPlanEditorClose]); + + const formContext = useMemo( + (): AttachFormContext => ({ + customerId, + entityId, + }), + [customerId, entityId], + ); + + const value = useMemo( + () => ({ + formContext, + form, + formValues, + features, + product, + prepaidItems, + originalItems, + productWithFormItems, + hasCustomizations, + previewQuery, + showPlanEditor, + handleEditPlan, + handlePlanEditorSave, + handlePlanEditorCancel, + isPending, + handleConfirm, + handleInvoiceAttach, + }), + [ + formContext, + form, + formValues, + features, + product, + prepaidItems, + originalItems, + productWithFormItems, + hasCustomizations, + previewQuery, + showPlanEditor, + handleEditPlan, + handlePlanEditorSave, + handlePlanEditorCancel, + isPending, + handleConfirm, + handleInvoiceAttach, + ], + ); + + return ( + + {children} + + ); +} + +export function useAttachFormContext(): AttachFormContextValue { + const context = useContext(AttachFormReactContext); + if (!context) { + throw new Error( + "useAttachFormContext must be used within AttachFormProvider", + ); + } + return context; +} diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts b/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts new file mode 100644 index 000000000..5f10cbab5 --- /dev/null +++ b/vite/src/components/forms/attach-v2/hooks/useAttachForm.ts @@ -0,0 +1,25 @@ +import { useAppForm } from "@/hooks/form/form"; +import { type AttachForm, AttachFormSchema } from "../attachFormSchema"; + +export function useAttachForm({ + initialProductId, + initialPrepaidOptions, +}: { + initialProductId?: string; + initialPrepaidOptions?: Record; +} = {}) { + return useAppForm({ + defaultValues: { + productId: initialProductId || "", + prepaidOptions: initialPrepaidOptions ?? {}, + items: null, + version: undefined, + } as AttachForm, + validators: { + onChange: AttachFormSchema, + onSubmit: AttachFormSchema, + }, + }); +} + +export type UseAttachForm = ReturnType; diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachMutation.ts b/vite/src/components/forms/attach-v2/hooks/useAttachMutation.ts new file mode 100644 index 000000000..ffcf3b56b --- /dev/null +++ b/vite/src/components/forms/attach-v2/hooks/useAttachMutation.ts @@ -0,0 +1,110 @@ +import type { AttachParamsV0 } from "@autumn/shared"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { toast } from "sonner"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; + +interface AttachResponse { + checkout_url?: string; + invoice?: { + stripe_id: string; + }; +} + +export function useAttachMutation({ + customerId, + buildRequestBody, + onInvoiceCreated, + onCheckoutRedirect, + onSuccess, +}: { + customerId: string | undefined; + buildRequestBody: (params?: { + useInvoice?: boolean; + enableProductImmediately?: boolean; + }) => AttachParamsV0 | null; + onInvoiceCreated?: (invoiceId: string) => void; + onCheckoutRedirect?: (checkoutUrl: string) => void; + onSuccess?: () => void; +}) { + const axiosInstance = useAxiosInstance(); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async ({ + useInvoice, + enableProductImmediately, + }: { + useInvoice?: boolean; + enableProductImmediately?: boolean; + }) => { + if (!customerId) { + throw new Error("Customer ID is required"); + } + + const requestBody = buildRequestBody({ + useInvoice, + enableProductImmediately, + }); + + if (!requestBody) { + throw new Error("Failed to build request body"); + } + + const response = await axiosInstance.post( + "/v1/billing/attach", + requestBody, + ); + + return { data: response.data, useInvoice }; + }, + onSuccess: ({ data, useInvoice }) => { + if (data?.checkout_url) { + onCheckoutRedirect?.(data.checkout_url); + toast.success("Redirecting to checkout..."); + return; + } + + if (useInvoice && data?.invoice) { + onInvoiceCreated?.(data.invoice.stripe_id); + toast.success("Invoice created successfully"); + } else { + toast.success("Product attached successfully"); + } + + onSuccess?.(); + + if (customerId) { + queryClient.invalidateQueries({ queryKey: ["customer", customerId] }); + } + }, + onError: (error) => { + toast.error( + (error as AxiosError<{ message: string }>)?.response?.data?.message ?? + "Failed to attach product", + ); + }, + }); + + const handleConfirm = () => { + mutation.mutate({ useInvoice: false }); + }; + + const handleInvoiceAttach = ({ + enableProductImmediately, + }: { + enableProductImmediately: boolean; + }) => { + mutation.mutate({ + useInvoice: true, + enableProductImmediately, + }); + }; + + return { + mutation, + handleConfirm, + handleInvoiceAttach, + isPending: mutation.isPending, + }; +} diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachPreview.ts b/vite/src/components/forms/attach-v2/hooks/useAttachPreview.ts new file mode 100644 index 000000000..ab51d35f0 --- /dev/null +++ b/vite/src/components/forms/attach-v2/hooks/useAttachPreview.ts @@ -0,0 +1,90 @@ +import type { + BillingPreviewResponse, + ProductItem, + ProductV2, +} from "@autumn/shared"; +import { useQuery } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { useEffect, useMemo, useState } from "react"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useAttachRequestBody } from "./useAttachRequestBody"; + +interface UseAttachPreviewParams { + customerId: string | undefined; + entityId: string | undefined; + product: ProductV2 | undefined; + prepaidOptions: Record; + items: ProductItem[] | null; + version: number | undefined; + enabled?: boolean; +} + +export function useAttachPreview({ + customerId, + entityId, + product, + prepaidOptions, + items, + version, + enabled, +}: UseAttachPreviewParams) { + const axiosInstance = useAxiosInstance(); + + const { requestBody } = useAttachRequestBody({ + customerId, + entityId, + product, + prepaidOptions, + items, + version, + }); + + const shouldEnable = + enabled !== undefined ? enabled : !!(customerId && product && requestBody); + + const queryKeyDeps = useMemo( + () => JSON.stringify(requestBody), + [requestBody], + ); + + const [debouncedQueryKey, setDebouncedQueryKey] = useState(queryKeyDeps); + + useEffect(() => { + const timer = setTimeout(() => { + setDebouncedQueryKey(queryKeyDeps); + }, 300); + return () => clearTimeout(timer); + }, [queryKeyDeps]); + + const isDebouncing = queryKeyDeps !== debouncedQueryKey; + + const query = useQuery({ + queryKey: ["attach-preview-v2", debouncedQueryKey], + queryFn: async () => { + if (!requestBody || !customerId) { + return null; + } + + const response = await axiosInstance.post( + "/v1/billing/preview_attach", + requestBody, + ); + + return response.data; + }, + enabled: shouldEnable, + staleTime: 0, + retry: (failureCount, error) => { + const status = (error as AxiosError)?.response?.status; + if (status && status >= 400 && status < 500) return false; + return failureCount < 3; + }, + }); + + return { + ...query, + isLoading: shouldEnable && (query.isLoading || isDebouncing), + }; +} + +export type UseAttachPreviewReturn = ReturnType; diff --git a/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts b/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts new file mode 100644 index 000000000..8b0355d60 --- /dev/null +++ b/vite/src/components/forms/attach-v2/hooks/useAttachRequestBody.ts @@ -0,0 +1,125 @@ +import { + type AttachParamsV0, + type FeatureOptions, + type ProductItem, + type ProductV2, + UsageModel, +} from "@autumn/shared"; +import Decimal from "decimal.js"; +import { useMemo } from "react"; + +interface UseAttachRequestBodyParams { + customerId: string | undefined; + entityId: string | undefined; + product: ProductV2 | undefined; + prepaidOptions: Record; + items: ProductItem[] | null; + version: number | undefined; +} + +function convertPrepaidOptionsToFeatureOptions({ + prepaidOptions, + product, +}: { + prepaidOptions: Record; + product: ProductV2 | undefined; +}): FeatureOptions[] | undefined { + if (!product || Object.keys(prepaidOptions).length === 0) { + return undefined; + } + + const options: FeatureOptions[] = []; + + for (const [featureId, quantity] of Object.entries(prepaidOptions)) { + const prepaidItem = product.items.find( + (item) => + item.feature_id === featureId && + item.usage_model === UsageModel.Prepaid, + ); + + if (prepaidItem) { + options.push({ + feature_id: featureId, + quantity: new Decimal(quantity || 0) + .mul(prepaidItem.billing_units || 1) + .toNumber(), + }); + } else { + options.push({ + feature_id: featureId, + quantity: quantity, + }); + } + } + + return options.length > 0 ? options : undefined; +} + +export function useAttachRequestBody({ + customerId, + entityId, + product, + prepaidOptions, + items, + version, +}: UseAttachRequestBodyParams) { + const requestBody = useMemo((): AttachParamsV0 | null => { + if (!customerId || !product) { + return null; + } + + const options = convertPrepaidOptionsToFeatureOptions({ + prepaidOptions, + product, + }); + + const body: AttachParamsV0 = { + customer_id: customerId, + product_id: product.id, + }; + + if (entityId) { + body.entity_id = entityId; + } + + if (options && options.length > 0) { + body.options = options; + } + + if (items && items.length > 0) { + body.items = items; + } + + if (version !== undefined) { + body.version = version; + } + + return body; + }, [customerId, entityId, product, prepaidOptions, items, version]); + + const buildRequestBody = useMemo( + () => + ({ + useInvoice, + enableProductImmediately, + }: { + useInvoice?: boolean; + enableProductImmediately?: boolean; + } = {}): AttachParamsV0 | null => { + if (!requestBody) return null; + + const body = { ...requestBody }; + + if (useInvoice) { + body.invoice = true; + body.enable_product_immediately = enableProductImmediately; + body.finalize_invoice = false; + } + + return body; + }, + [requestBody], + ); + + return { requestBody, buildRequestBody }; +} diff --git a/vite/src/components/forms/attach-v2/index.ts b/vite/src/components/forms/attach-v2/index.ts new file mode 100644 index 000000000..12ec09ab6 --- /dev/null +++ b/vite/src/components/forms/attach-v2/index.ts @@ -0,0 +1,15 @@ +// Components + +// Types +export * from "./attachFormSchema"; +export * from "./components/AttachFooter"; +export * from "./components/AttachPlanSection"; +export * from "./components/AttachPreviewSection"; +export * from "./components/AttachProductSelection"; +// Context & Provider +export * from "./context/AttachFormProvider"; +// Hooks +export * from "./hooks/useAttachForm"; +export * from "./hooks/useAttachMutation"; +export * from "./hooks/useAttachPreview"; +export * from "./hooks/useAttachRequestBody"; diff --git a/vite/src/hooks/stores/useSheetStore.ts b/vite/src/hooks/stores/useSheetStore.ts index f9e5d0809..3885d5e46 100644 --- a/vite/src/hooks/stores/useSheetStore.ts +++ b/vite/src/hooks/stores/useSheetStore.ts @@ -10,6 +10,7 @@ export type SheetType = | "new-feature" | "select-feature" | "attach-product" + | "attach-product-v2" | "subscription-detail" | "subscription-update" | "subscription-update-v2" diff --git a/vite/src/views/customers2/components/sheets/AttachProductSheetV2.tsx b/vite/src/views/customers2/components/sheets/AttachProductSheetV2.tsx new file mode 100644 index 000000000..143613262 --- /dev/null +++ b/vite/src/views/customers2/components/sheets/AttachProductSheetV2.tsx @@ -0,0 +1,127 @@ +import type { Entity, FullCustomer } from "@autumn/shared"; +import { + AttachFooter, + AttachFormProvider, + AttachPlanSection, + AttachPreviewSection, + AttachProductSelection, + useAttachFormContext, +} from "@/components/forms/attach-v2"; +import { InlinePlanEditor } from "@/components/v2/inline-custom-plan-editor/InlinePlanEditor"; +import { + LayoutGroup, + SheetHeader, + SheetSection, +} from "@/components/v2/sheets/SharedSheetComponents"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; +import { useSheetStore } from "@/hooks/stores/useSheetStore"; +import { useEntity } from "@/hooks/stores/useSubscriptionStore"; +import { useEnv } from "@/utils/envUtils"; +import { getStripeInvoiceLink } from "@/utils/linkUtils"; +import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery"; +import { useCustomerContext } from "@/views/customers2/customer/CustomerContext"; +import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox"; + +function SheetContent() { + const { + formValues, + productWithFormItems, + showPlanEditor, + handlePlanEditorSave, + handlePlanEditorCancel, + } = useAttachFormContext(); + + const hasProductSelected = !!formValues.productId; + + const { entityId } = useEntity(); + const { customer } = useCusQuery(); + const entities = (customer as FullCustomer)?.entities || []; + const fullEntity = entities.find( + (e: Entity) => e.id === entityId || e.internal_id === entityId, + ); + + return ( + +
+ + + +
+ + + {entityId ? ( +
+ + Attaching plan to entity{" "} + + {fullEntity?.name || fullEntity?.id} + + +
+ ) : entities.length > 0 ? ( +
+ + Attaching plan to customer - all entities will get access + +
+ ) : null} +
+
+ + {hasProductSelected && ( + <> + + + + + )} + + {productWithFormItems && ( + + )} +
+
+ ); +} + +export function AttachProductSheetV2() { + const itemId = useSheetStore((s) => s.itemId); + const { closeSheet } = useSheetStore(); + const { customer } = useCusQuery(); + const { stripeAccount } = useOrgStripeQuery(); + const env = useEnv(); + const { setIsInlineEditorOpen } = useCustomerContext(); + const { entityId } = useEntity(); + + return ( + setIsInlineEditorOpen(true)} + onPlanEditorClose={() => setIsInlineEditorOpen(false)} + onInvoiceCreated={(invoiceId) => { + const invoiceLink = getStripeInvoiceLink({ + stripeInvoice: invoiceId, + env, + accountId: stripeAccount?.id, + }); + window.open(invoiceLink, "_blank"); + }} + onCheckoutRedirect={(checkoutUrl) => { + window.location.href = checkoutUrl; + }} + onSuccess={closeSheet} + > + + + ); +} diff --git a/vite/src/views/customers2/components/table/customer-products/AttachProductSheetTrigger.tsx b/vite/src/views/customers2/components/table/customer-products/AttachProductSheetTrigger.tsx index 4cf6cdeb8..8461ced71 100644 --- a/vite/src/views/customers2/components/table/customer-products/AttachProductSheetTrigger.tsx +++ b/vite/src/views/customers2/components/table/customer-products/AttachProductSheetTrigger.tsx @@ -19,7 +19,7 @@ export function AttachProductSheetTrigger() { const feature = features.features.find((f) => f.id === entity?.feature_id); const handleClick = () => { - setSheet({ type: "attach-product" }); + setSheet({ type: "attach-product-v2" }); }; return ( + + + ); +} + +function LoadingState() { + return ( +
+
+
+

Loading checkout...

+
+
+ ); +} + +function ErrorState({ message }: { message: string }) { + return ( +
+
+

Something went wrong

+

{message}

+
+
+ ); +} + +function SuccessState({ result }: { result: ConfirmCheckoutResponse }) { + return ( +
+
+
+

Purchase Complete

+

Your order has been confirmed.

+ {result.invoice_id && ( +

Invoice ID: {result.invoice_id}

+ )} +
+
+ ); +} + +function formatAmount(cents: number, currency: string): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency.toUpperCase(), + }).format(cents / 100); +} + +function formatDate(timestamp: number): string { + return new Date(timestamp * 1000).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +} diff --git a/apps/checkout/tsconfig.app.json b/apps/checkout/tsconfig.app.json new file mode 100644 index 000000000..040e28568 --- /dev/null +++ b/apps/checkout/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/apps/checkout/tsconfig.json b/apps/checkout/tsconfig.json new file mode 100644 index 000000000..9bc6a8b36 --- /dev/null +++ b/apps/checkout/tsconfig.json @@ -0,0 +1,13 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ], + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/apps/checkout/tsconfig.node.json b/apps/checkout/tsconfig.node.json new file mode 100644 index 000000000..8a67f62f4 --- /dev/null +++ b/apps/checkout/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/checkout/vite.config.ts b/apps/checkout/vite.config.ts new file mode 100644 index 000000000..a9e096710 --- /dev/null +++ b/apps/checkout/vite.config.ts @@ -0,0 +1,23 @@ +import path from "node:path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig({ + plugins: [react(), tsconfigPaths()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, + optimizeDeps: { + exclude: ["@autumn/shared", "zod/v4"], + }, + server: { + host: "0.0.0.0", + port: Number.parseInt(process.env.VITE_PORT || "3001", 10), + fs: { + allow: [".."], + }, + }, +}); diff --git a/bun.lock b/bun.lock index 5568f7200..d8d84c1b7 100644 --- a/bun.lock +++ b/bun.lock @@ -15,6 +15,27 @@ "knip": "^5.82.1", }, }, + "apps/checkout": { + "name": "@autumn/checkout", + "version": "0.0.0", + "dependencies": { + "@autumn/shared": "workspace:*", + "@orpc/client": "catalog:", + "@orpc/openapi-client": "catalog:", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^7.13.0", + }, + "devDependencies": { + "@types/node": "^22.13.10", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.7.2", + "vite": "^6.2.0", + "vite-tsconfig-paths": "^5.1.4", + }, + }, "scripts": { "name": "@autumn/scripts", "version": "1.0.0", @@ -123,6 +144,7 @@ "version": "1.0.0", "dependencies": { "@date-fns/utc": "catalog:", + "@orpc/contract": "catalog:", "date-fns": "^4.1.0", "decimal.js": "^10.5.0", "dotenv": "^16.5.0", @@ -243,6 +265,9 @@ "@better-auth/dash": "0.1.6", "@clickhouse/client": "1.11.2", "@date-fns/utc": "2.1.0", + "@orpc/client": "^1.0.0", + "@orpc/contract": "^1.0.0", + "@orpc/openapi-client": "^1.0.0", "@sentry/bun": "10.25.0", "better-auth": "catalog:", "drizzle-kit": "^0.31.1", @@ -270,6 +295,8 @@ "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.3.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.8.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw=="], + "@autumn/checkout": ["@autumn/checkout@workspace:apps/checkout"], + "@autumn/scripts": ["@autumn/scripts@workspace:scripts"], "@autumn/server": ["@autumn/server@workspace:server"], @@ -450,57 +477,57 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], @@ -876,6 +903,20 @@ "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], + "@orpc/client": ["@orpc/client@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4", "@orpc/standard-server-fetch": "1.13.4", "@orpc/standard-server-peer": "1.13.4" } }, "sha512-s13GPMeoooJc5Th2EaYT5HMFtWG8S03DUVytYfJv8pIhP87RYKl94w52A36denH6r/B4LaAgBeC9nTAOslK+Og=="], + + "@orpc/contract": ["@orpc/contract@1.13.4", "", { "dependencies": { "@orpc/client": "1.13.4", "@orpc/shared": "1.13.4", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-TIxyaF67uOlihCRcasjHZxguZpbqfNK7aMrDLnhoufmQBE4OKvguNzmrOFHgsuM0OXoopX0Nuhun1ccaxKP10A=="], + + "@orpc/openapi-client": ["@orpc/openapi-client@1.13.4", "", { "dependencies": { "@orpc/client": "1.13.4", "@orpc/contract": "1.13.4", "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-tRUcY4E6sgpS5bY/9nNES/Q/PMyYyPOsI4TuhwLhfgxOb0GFPwYKJ6Kif7KFNOhx4fkN/jTOfE1nuWuIZU1gyg=="], + + "@orpc/shared": ["@orpc/shared@1.13.4", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.3.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-TYt9rLG/BUkNQBeQ6C1tEiHS/Seb8OojHgj9GlvqyjHJhMZx5qjsIyTW6RqLPZJ4U2vgK6x4Her36+tlFCKJug=="], + + "@orpc/standard-server": ["@orpc/standard-server@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4" } }, "sha512-ZOzgfVp6XUg+wVYw+gqesfRfGPtQbnBIrIiSnFMtZF+6ncmFJeF2Shc4RI2Guqc0Qz25juy8Ogo4tX3YqysOcg=="], + + "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-/zmKwnuxfAXbppJpgr1CMnQX3ptPlYcDzLz1TaVzz9VG/Xg58Ov3YhabS2Oi1utLVhy5t4kaCppUducAvoKN+A=="], + + "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-UfqnTLqevjCKUk4cmImOG8cQUwANpV1dp9e9u2O1ki6BRBsg/zlXFg6G2N6wP0zr9ayIiO1d2qJdH55yl/1BNw=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.16.4", "", { "os": "android", "cpu": "arm" }, "sha512-6XUHilmj8D6Ggus+sTBp64x/DUQ7LgC/dvTDdUOt4iMQnDdSep6N1mnvVLIiG+qM5tRnNHravNzBJnUlYwRQoA=="], "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.16.4", "", { "os": "android", "cpu": "arm64" }, "sha512-5ODwd1F5mdkm6JIg1CNny9yxIrCzrkKpxmqas7Alw23vE0Ot8D4ykqNBW5Z/nIZkXVEo5VDmnm0sMBBIANcpeQ=="], @@ -1992,7 +2033,7 @@ "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], - "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -2416,7 +2457,7 @@ "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="], @@ -2638,6 +2679,8 @@ "openai": ["openai@6.16.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-fZ1uBqjFUjXzbGc35fFtYKEOxd20kd9fDpFeqWtsOZWiubY8CZ1NAlXHW3iathaFvqmNtCWMIsosCuyeI7Joxg=="], + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], @@ -2794,6 +2837,8 @@ "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + "radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -3050,6 +3095,8 @@ "swr": ["swr@2.3.8", "", { "dependencies": { "dequal": "^2.0.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gaCPRVoMq8WGDcWj9p4YWzCMPHzE0WNl6W8ADIx9c3JBEIdMkJGMzW+uzXvxHMltwcYACr9jP+32H8/hgwMR7w=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], "tailwind-scrollbar-hide": ["tailwind-scrollbar-hide@4.0.0", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20" } }, "sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ=="], @@ -3116,7 +3163,7 @@ "typed-query-selector": ["typed-query-selector@2.12.0", "", {}, "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="], @@ -3258,16 +3305,22 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@autumn/checkout/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], + + "@autumn/scripts/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@autumn/server/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], + "@autumn/server/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@autumn/shared/@date-fns/utc": ["@date-fns/utc@2.1.0", "", {}, "sha512-176grgAgU2U303rD2/vcOmNg0kGPbhzckuH1TEP2al7n0AQipZIy9P15usd2TKQCG1g+E1jX/ZVQSzs4sUDwgA=="], + "@autumn/shared/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@autumn/vite/@types/node": ["@types/node@22.19.7", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw=="], "@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - "@autumn/vite/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], - "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], "@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -3600,8 +3653,6 @@ "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], "@better-auth/core/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], @@ -3628,6 +3679,8 @@ "@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.1.0", "", {}, "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA=="], + "@infisical/sdk/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], @@ -3750,6 +3803,8 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="], + "@orpc/shared/type-fest": ["type-fest@5.4.2", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-FLEenlVYf7Zcd34ISMLo3ZzRE1gRjY1nMDTp+bQRBiPsaKyIW8K3Zr99ioHDUgA9OGuGGJPyYpNcffGmBhJfGg=="], + "@posthog/ai/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="], @@ -3938,8 +3993,6 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "engine.io/@types/node": ["@types/node@25.0.10", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg=="], "engine.io/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], @@ -4014,6 +4067,8 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -4036,8 +4091,6 @@ "react-day-picker/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], - "react-email/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "react-email/glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], "react-email/log-symbols": ["log-symbols@7.0.1", "", { "dependencies": { "is-unicode-supported": "^2.0.0", "yoctocolors": "^2.1.1" } }, "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg=="], @@ -4072,14 +4125,14 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tsx/esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "unified/is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4088,6 +4141,8 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "@autumn/checkout/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@autumn/vite/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -4478,58 +4533,6 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -4582,58 +4585,6 @@ "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - "react-email/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "react-email/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "react-email/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "react-email/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "react-email/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "react-email/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "react-email/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "react-email/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "react-email/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "react-email/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "react-email/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "react-email/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "react-email/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "react-email/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "react-email/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "react-email/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "react-email/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "react-email/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "react-email/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "react-email/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "react-email/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "react-email/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "react-email/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "react-email/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "react-email/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "react-email/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "react-email/glob/jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], "react-email/glob/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], @@ -4646,64 +4597,64 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], - "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], - - "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], - - "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], - - "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], - - "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], - - "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], - - "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], - - "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], - - "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], - - "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], - - "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], - - "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], - - "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], - - "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], - - "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], - - "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], - - "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], - - "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], - - "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], - - "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], - - "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], - - "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], - - "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], - - "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], - - "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], - - "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], diff --git a/package.json b/package.json index 47b19e3f7..f29ff1f80 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "server", "shared", "vite", - "scripts" + "scripts", + "apps/checkout" ], "catalog": { "better-auth": "catalog:", @@ -16,7 +17,10 @@ "@sentry/bun": "10.25.0", "@clickhouse/client": "1.11.2", "@date-fns/utc": "2.1.0", - "@better-auth/dash": "0.1.6" + "@better-auth/dash": "0.1.6", + "@orpc/contract": "^1.0.0", + "@orpc/client": "^1.0.0", + "@orpc/openapi-client": "^1.0.0" } }, "overrides": { diff --git a/scripts/dev.ts b/scripts/dev.ts index 93bce74f3..c5168c0d0 100644 --- a/scripts/dev.ts +++ b/scripts/dev.ts @@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url"; const VITE_PORT = 3000; const SERVER_PORT = 8080; +const CHECKOUT_PORT = 3001; /** * Read environment variable from .env file @@ -53,6 +54,18 @@ async function startDev() { rmSync(viteCachePath, { recursive: true, force: true }); } + // Clear checkout Vite cache + const checkoutCachePath = join( + projectRoot, + "apps/checkout", + "node_modules", + ".vite", + ); + if (existsSync(checkoutCachePath)) { + console.log("🧹 Clearing Checkout Vite cache...\n"); + rmSync(checkoutCachePath, { recursive: true, force: true }); + } + console.log("🚀 Starting development servers in watch mode...\n"); // Use cmd on Windows, sh on Unix @@ -63,16 +76,17 @@ async function startDev() { const serverCmd = `cd server && set SERVER_PORT=${SERVER_PORT} && bun dev`; const workersCmd = `cd server && bun workers:dev`; const viteCmd = `cd vite && set VITE_PORT=${VITE_PORT} && bun dev`; + const checkoutCmd = `cd apps/checkout && set VITE_PORT=${CHECKOUT_PORT} && bun dev`; shellArgs = [ "cmd", "/c", - `bunx concurrently -n server,workers,vite -c green,yellow,blue "${serverCmd}" "${workersCmd}" "${viteCmd}"`, + `bunx concurrently -n server,workers,vite,checkout -c green,yellow,blue,magenta "${serverCmd}" "${workersCmd}" "${viteCmd}" "${checkoutCmd}"`, ]; } else { shellArgs = [ "sh", "-c", - `bunx concurrently -n server,workers,vite -c green,yellow,blue "cd server && SERVER_PORT=${SERVER_PORT} bun dev" "cd server && bun workers:dev" "cd vite && VITE_PORT=${VITE_PORT} bun dev"`, + `bunx concurrently -n server,workers,vite,checkout -c green,yellow,blue,magenta "cd server && SERVER_PORT=${SERVER_PORT} bun dev" "cd server && bun workers:dev" "cd vite && VITE_PORT=${VITE_PORT} bun dev" "cd apps/checkout && VITE_PORT=${CHECKOUT_PORT} bun dev"`, ]; } @@ -82,6 +96,7 @@ async function startDev() { ...process.env, VITE_PORT: VITE_PORT.toString(), SERVER_PORT: SERVER_PORT.toString(), + CHECKOUT_PORT: CHECKOUT_PORT.toString(), }, stdout: "inherit", stderr: "inherit", diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 41e105fb3..6f5cf1047 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -11,6 +11,7 @@ import { type AttachBodyV0, type AttachParamsV0, type BalancesUpdateParams, + type BillingPreviewResponse, type BillingResponse, type CheckQuery, type CreateBalanceParams, @@ -830,8 +831,10 @@ export class AutumnInt { return data; }, - previewAttach: async (params: AttachParamsV0) => { - const data = await this.post(`/billing/attach/preview`, params); + previewAttach: async ( + params: AttachParamsV0, + ): Promise => { + const data = await this.post(`/billing/preview_attach`, params); return data; }, }; diff --git a/server/src/initHono.ts b/server/src/initHono.ts index 88646ddfd..27e035548 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -26,6 +26,7 @@ import { auth } from "./utils/auth.js"; const ALLOWED_ORIGINS = [ "http://localhost:3000", + "http://localhost:3001", "http://localhost:5173", "http://localhost:5174", "https://app.useautumn.com", diff --git a/server/src/internal/billing/v2/actions/attach/attach.ts b/server/src/internal/billing/v2/actions/attach/attach.ts index 0bff9df20..39e56235a 100644 --- a/server/src/internal/billing/v2/actions/attach/attach.ts +++ b/server/src/internal/billing/v2/actions/attach/attach.ts @@ -27,10 +27,12 @@ export async function attach({ ctx, params, preview = false, + skipAutumnCheckout = false, }: { ctx: AutumnContext; params: AttachParamsV0; preview?: boolean; + skipAutumnCheckout?: boolean; }): Promise { // 1. Setup const billingContext = await setupAttachBillingContext({ @@ -79,13 +81,18 @@ export async function attach({ }; } - if (billingContext.checkoutMode === "autumn_checkout") { - return await createAutumnCheckout({ + if ( + billingContext.checkoutMode === "autumn_checkout" && + !skipAutumnCheckout + ) { + const checkoutResult = await createAutumnCheckout({ ctx, params, billingContext, billingPlan, }); + + return checkoutResult; } // 6. Execute billing plan diff --git a/server/src/internal/billing/v2/actions/attach/createAutumnCheckout.ts b/server/src/internal/billing/v2/actions/attach/createAutumnCheckout.ts index e6e290dc5..74b7234b3 100644 --- a/server/src/internal/billing/v2/actions/attach/createAutumnCheckout.ts +++ b/server/src/internal/billing/v2/actions/attach/createAutumnCheckout.ts @@ -24,7 +24,7 @@ export async function createAutumnCheckout({ billingContext: AttachBillingContext; billingPlan: BillingPlan; }): Promise { - const { checkoutUrl } = await billingPlanToAutumnCheckout({ + const { checkout, checkoutUrl } = await billingPlanToAutumnCheckout({ ctx, params, billingContext, @@ -34,7 +34,10 @@ export async function createAutumnCheckout({ return { billingContext, billingPlan, - billingResult: null, + billingResult: { + stripe: {}, + autumn: { checkout }, + }, checkoutUrl, }; } diff --git a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts index 47b6901d9..2e0356c83 100644 --- a/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts +++ b/server/src/internal/billing/v2/utils/billingResult/billingResultToResponse.ts @@ -1,7 +1,8 @@ -import { type BillingResponse, stripeToAtmnAmount } from "@autumn/shared"; -import type { - BillingContext, - BillingResult, +import type { BillingContext, BillingResult } from "@autumn/shared"; +import { + type BillingResponse, + checkoutToUrl, + stripeToAtmnAmount, } from "@autumn/shared"; export const billingResultToResponse = ({ @@ -17,13 +18,16 @@ export const billingResultToResponse = ({ const stripeInvoice = billingResult.stripe.stripeInvoice; const stripeCheckoutSession = billingResult.stripe.stripeCheckoutSession; + const autumnCheckout = billingResult.autumn?.checkout; - // Checkout session URL takes priority, then invoice hosted URL - const paymentUrl = stripeCheckoutSession?.url - ? stripeCheckoutSession.url - : stripeInvoice?.status === "open" && stripeInvoice.hosted_invoice_url - ? stripeInvoice.hosted_invoice_url - : null; + // Autumn checkout URL takes priority, then Stripe checkout session, then invoice hosted URL + const paymentUrl = autumnCheckout + ? checkoutToUrl({ checkoutId: autumnCheckout.id }) + : stripeCheckoutSession?.url + ? stripeCheckoutSession.url + : stripeInvoice?.status === "open" && stripeInvoice.hosted_invoice_url + ? stripeInvoice.hosted_invoice_url + : null; return { customer_id: customerId, diff --git a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts index 5fe26228d..c119dc8a4 100644 --- a/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts +++ b/server/src/internal/checkouts/handlers/handleConfirmCheckout.ts @@ -3,6 +3,7 @@ import { type Checkout, CheckoutAction, CheckoutStatus, + type ConfirmCheckoutResponse, ErrCode, RecaseError, } from "@autumn/shared"; @@ -34,6 +35,14 @@ export const handleConfirmCheckout = createRoute({ }); } + if (checkout.status !== CheckoutStatus.Pending) { + throw new RecaseError({ + message: "Checkout is not pending", + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } + const params = checkout.params as AttachParamsV0; try { @@ -42,6 +51,7 @@ export const handleConfirmCheckout = createRoute({ ctx, params, preview: false, + skipAutumnCheckout: true, }); // Delete from cache (one-time use) @@ -63,7 +73,7 @@ export const handleConfirmCheckout = createRoute({ customer_id: checkout.customer_id, product_id: billingContext.attachProduct.id, invoice_id: billingResult?.stripe?.stripeInvoice?.id ?? null, - }); + } satisfies ConfirmCheckoutResponse); } catch (error) { // Don't delete from cache on error - allow retry if (error instanceof RecaseError) { diff --git a/server/src/internal/checkouts/handlers/handleGetCheckout.ts b/server/src/internal/checkouts/handlers/handleGetCheckout.ts index 466888425..e527c7cd2 100644 --- a/server/src/internal/checkouts/handlers/handleGetCheckout.ts +++ b/server/src/internal/checkouts/handlers/handleGetCheckout.ts @@ -3,6 +3,7 @@ import { type Checkout, CheckoutAction, ErrCode, + type GetCheckoutResponse, RecaseError, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; @@ -52,6 +53,6 @@ export const handleGetCheckout = createRoute({ billingPlan, }); - return c.json({ preview }); + return c.json({ preview } satisfies GetCheckoutResponse); }, }); diff --git a/server/src/internal/checkouts/middleware/checkoutMiddleware.ts b/server/src/internal/checkouts/middleware/checkoutMiddleware.ts index 8ed84e812..3c9d32949 100644 --- a/server/src/internal/checkouts/middleware/checkoutMiddleware.ts +++ b/server/src/internal/checkouts/middleware/checkoutMiddleware.ts @@ -76,6 +76,7 @@ export const checkoutMiddleware = async (c: Context, next: Next) => { env, features: orgWithFeatures.features, isPublic: true, + customerId: checkout.customer_id, }); // Attach checkout to context for handlers diff --git a/server/src/routers/publicRouter.ts b/server/src/routers/publicRouter.ts index c66702f0e..1cb5354e2 100644 --- a/server/src/routers/publicRouter.ts +++ b/server/src/routers/publicRouter.ts @@ -1,4 +1,5 @@ import { Hono } from "hono"; +import { analyticsMiddleware } from "@/honoMiddlewares/analyticsMiddleware.js"; import { publicCheckoutRouter } from "@/internal/checkouts/checkoutRouter.js"; import { publicDevRouter } from "@/internal/dev/devRouter.js"; import { publicTrmnlRouter } from "@/internal/misc/trmnl/trmnlRouter.js"; @@ -6,6 +7,7 @@ import type { HonoEnv } from "../honoUtils/HonoEnv.js"; import { publicInvoiceRouter } from "../internal/invoices/invoiceRouter.js"; export const publicRouter = new Hono(); +publicRouter.use(analyticsMiddleware); publicRouter.route("/checkouts", publicCheckoutRouter); publicRouter.route("/invoices", publicInvoiceRouter); publicRouter.route("/dev", publicDevRouter); diff --git a/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts b/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts index c1015abfe..f6c557f5d 100644 --- a/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts +++ b/server/tests/integration/billing/attach/checkout/autumn-checkout/autumn-checkout-basic.test.ts @@ -15,11 +15,6 @@ */ import { expect, test } from "bun:test"; -import type { ApiCustomerV3, AttachPreview } from "@autumn/shared"; -import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect"; -import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect"; -import { expectProductActive } from "@tests/integration/billing/utils/expectCustomerProductCorrect"; -import { TestFeature } from "@tests/setup/v2Features"; import { items } from "@tests/utils/fixtures/items"; import { products } from "@tests/utils/fixtures/products"; import { initScenario, s } from "@tests/utils/testInitUtils/initScenario"; @@ -64,7 +59,7 @@ test.concurrent(`${chalk.yellowBright("autumn-checkout: with PM + redirect_mode customer_id: customerId, product_id: pro.id, }); - expect((preview as AttachPreview).due_today.total).toBe(20); + expect(preview.total).toBe(20); // 2. Attempt attach with redirect_mode: "always" // This should return a confirmation URL instead of charging directly @@ -74,22 +69,7 @@ test.concurrent(`${chalk.yellowBright("autumn-checkout: with PM + redirect_mode redirect_mode: "always", }); - // Should return a checkout/confirmation URL (autumn hosted page) - // Note: The exact URL format depends on implementation - expect(result.checkout_url || result.payment_url).toBeDefined(); - - // At this point, product should NOT be attached yet (waiting for confirmation) - const customerBefore = - await autumnV1.customers.get(customerId); - const productBefore = customerBefore.products?.find((p) => p.id === pro.id); - - // Product should either not exist or be in a pending state - // (Implementation may vary - could be no product, or product with pending status) - // For now, just verify we got a URL and didn't charge immediately - - // Note: Full test would include completing the autumn checkout flow - // and verifying the product is attached afterward. This is left as - // future work pending the autumn checkout implementation. + console.log("result:", result); }); // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/shared/db/schema.ts b/shared/db/schema.ts index 45ef435ad..5fd394637 100644 --- a/shared/db/schema.ts +++ b/shared/db/schema.ts @@ -3,6 +3,7 @@ // Analytics Tables import { actions } from "../models/analyticsModels/actionTable.js"; import { chatResults } from "../models/chatResultModels/chatResultTable.js"; +import { checkoutsRelations } from "../models/checkouts/checkoutRelations.js"; import { checkouts } from "../models/checkouts/checkoutTable.js"; // Customer Relations import { customersRelations } from "../models/cusModels/cusRelations.js"; @@ -145,6 +146,7 @@ export { replaceableRelations, invoiceRelations, rolloverRelations, + checkoutsRelations, // Auth Relations userRelations, memberRelations, diff --git a/shared/index.ts b/shared/index.ts index f90a91a09..e22e5108f 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -10,20 +10,19 @@ export * from "./api/billing/common/cancelAction.js"; export * from "./api/common/cursorPaginationSchemas.js"; // API MODELS export * from "./api/models.js"; - // API VERSIONING SYSTEM export * from "./api/versionUtils/versionUtils.js"; - // Auth Models export * from "./db/auth-schema.js"; export * from "./enums/APIVersion.js"; export * from "./enums/AttachErrCode.js"; export * from "./enums/ErrCode.js"; export * from "./enums/LoggerAction.js"; - // ENUMS export * from "./enums/SuccessCode.js"; export * from "./enums/WebhookEventType.js"; +// Internal API (checkout app, dashboard) +export * from "./internal/index.js"; // ANALYTICS MODELS export * from "./models/analyticsModels/actionEnums.js"; diff --git a/shared/internal/checkout/checkoutResponses.ts b/shared/internal/checkout/checkoutResponses.ts new file mode 100644 index 000000000..d04af2db2 --- /dev/null +++ b/shared/internal/checkout/checkoutResponses.ts @@ -0,0 +1,25 @@ +import { z } from "zod/v4"; +import { BillingPreviewResponseSchema } from "../../api/billing/common/billingPreviewResponse.js"; + +/** + * GET /checkouts/:checkout_id response + */ +export const GetCheckoutResponseSchema = z.object({ + preview: BillingPreviewResponseSchema, +}); + +/** + * POST /checkouts/:checkout_id/confirm response + */ +export const ConfirmCheckoutResponseSchema = z.object({ + success: z.boolean(), + checkout_id: z.string(), + customer_id: z.string(), + product_id: z.string(), + invoice_id: z.string().nullable(), +}); + +export type GetCheckoutResponse = z.infer; +export type ConfirmCheckoutResponse = z.infer< + typeof ConfirmCheckoutResponseSchema +>; diff --git a/shared/internal/checkout/index.ts b/shared/internal/checkout/index.ts new file mode 100644 index 000000000..ed35f00e7 --- /dev/null +++ b/shared/internal/checkout/index.ts @@ -0,0 +1 @@ +export * from "./checkoutResponses.js"; diff --git a/shared/internal/contracts/checkout.ts b/shared/internal/contracts/checkout.ts new file mode 100644 index 000000000..3a37dbb1b --- /dev/null +++ b/shared/internal/contracts/checkout.ts @@ -0,0 +1,29 @@ +import { oc } from "@orpc/contract"; +import { z } from "zod/v4"; +import { + ConfirmCheckoutResponseSchema, + GetCheckoutResponseSchema, +} from "../checkout/checkoutResponses.js"; + +export const getCheckoutContract = oc + .route({ + method: "GET", + path: "/checkouts/{checkout_id}", + tags: ["internal"], + }) + .input(z.object({ checkout_id: z.string() })) + .output(GetCheckoutResponseSchema); + +export const confirmCheckoutContract = oc + .route({ + method: "POST", + path: "/checkouts/{checkout_id}/confirm", + tags: ["internal"], + }) + .input(z.object({ checkout_id: z.string() })) + .output(ConfirmCheckoutResponseSchema); + +export const checkoutContract = { + getCheckout: getCheckoutContract, + confirmCheckout: confirmCheckoutContract, +}; diff --git a/shared/internal/contracts/index.ts b/shared/internal/contracts/index.ts new file mode 100644 index 000000000..6a5e463e0 --- /dev/null +++ b/shared/internal/contracts/index.ts @@ -0,0 +1 @@ +export * from "./checkout.js"; diff --git a/shared/internal/index.ts b/shared/internal/index.ts new file mode 100644 index 000000000..9d9228327 --- /dev/null +++ b/shared/internal/index.ts @@ -0,0 +1,2 @@ +export * from "./checkout/index.js"; +export * from "./contracts/index.js"; diff --git a/shared/models/billingModels/plan/billingResult.ts b/shared/models/billingModels/plan/billingResult.ts index ffe4f543b..447037512 100644 --- a/shared/models/billingModels/plan/billingResult.ts +++ b/shared/models/billingModels/plan/billingResult.ts @@ -1,4 +1,4 @@ -import type { PaymentFailureCode } from "@autumn/shared"; +import type { Checkout, PaymentFailureCode } from "@autumn/shared"; import type Stripe from "stripe"; export interface StripeBillingPlanResult { @@ -12,6 +12,11 @@ export interface StripeBillingPlanResult { }; } +export interface AutumnBillingResult { + checkout?: Checkout; +} + export interface BillingResult { stripe: StripeBillingPlanResult; + autumn?: AutumnBillingResult; } diff --git a/shared/models/checkouts/checkoutRelations.ts b/shared/models/checkouts/checkoutRelations.ts new file mode 100644 index 000000000..42df1a5ca --- /dev/null +++ b/shared/models/checkouts/checkoutRelations.ts @@ -0,0 +1,15 @@ +import { relations } from "drizzle-orm"; +import { customers } from "../cusModels/cusTable.js"; +import { organizations } from "../orgModels/orgTable.js"; +import { checkouts } from "./checkoutTable.js"; + +export const checkoutsRelations = relations(checkouts, ({ one }) => ({ + org: one(organizations, { + fields: [checkouts.org_id], + references: [organizations.id], + }), + customer: one(customers, { + fields: [checkouts.internal_customer_id], + references: [customers.internal_id], + }), +})); diff --git a/shared/package.json b/shared/package.json index 7ddb95f35..ee8c54a68 100644 --- a/shared/package.json +++ b/shared/package.json @@ -22,6 +22,7 @@ "db:studio": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit studio --config drizzle.config.ts" }, "dependencies": { + "@orpc/contract": "catalog:", "@date-fns/utc": "catalog:", "date-fns": "^4.1.0", "decimal.js": "^10.5.0", diff --git a/shared/utils/checkoutUtils/checkoutToUrl.ts b/shared/utils/checkoutUtils/checkoutToUrl.ts index da80450e5..714bd9501 100644 --- a/shared/utils/checkoutUtils/checkoutToUrl.ts +++ b/shared/utils/checkoutUtils/checkoutToUrl.ts @@ -1,6 +1,7 @@ -// Checkout URL base - defaults to localhost for dev -const AUTUMN_CHECKOUT_BASE_URL = - process.env.AUTUMN_CHECKOUT_BASE_URL || "http://localhost:3001"; - -export const checkoutToUrl = ({ checkoutId }: { checkoutId: string }): string => - `${AUTUMN_CHECKOUT_BASE_URL}/c/${checkoutId}`; +export const checkoutToUrl = ({ + checkoutBaseUrl = "http://localhost:3001", + checkoutId, +}: { + checkoutBaseUrl?: string; + checkoutId: string; +}): string => `${checkoutBaseUrl}/c/${checkoutId}`; From a69dcb91bdcacf55a9573ca890711418fa149bd2 Mon Sep 17 00:00:00 2001 From: Charlie Lamb Date: Thu, 29 Jan 2026 17:41:41 +0000 Subject: [PATCH 012/110] chore: init checkout ui --- apps/checkout/.claude/skills/d3k/SKILL.md | 52 + apps/checkout/README.md | 74 +- apps/checkout/bun.lock | 1205 +++++++++++++++++ apps/checkout/components.json | 24 + apps/checkout/eslint.config.js | 23 - apps/checkout/index.html | 2 +- apps/checkout/package.json | 74 +- .../checkout/CheckoutErrorState.tsx | 18 + .../checkout/CheckoutLoadingState.tsx | 47 + .../checkout/CheckoutSuccessState.tsx | 44 + .../src/components/ui/alert-dialog.tsx | 175 +++ apps/checkout/src/components/ui/badge.tsx | 48 + apps/checkout/src/components/ui/button.tsx | 51 + apps/checkout/src/components/ui/card.tsx | 94 ++ apps/checkout/src/components/ui/combobox.tsx | 290 ++++ .../src/components/ui/dropdown-menu.tsx | 254 ++++ apps/checkout/src/components/ui/field.tsx | 227 ++++ .../src/components/ui/input-group.tsx | 147 ++ apps/checkout/src/components/ui/input.tsx | 20 + apps/checkout/src/components/ui/item.tsx | 200 +++ apps/checkout/src/components/ui/label.tsx | 18 + apps/checkout/src/components/ui/select.tsx | 191 +++ apps/checkout/src/components/ui/separator.tsx | 23 + apps/checkout/src/components/ui/skeleton.tsx | 15 + apps/checkout/src/components/ui/textarea.tsx | 18 + apps/checkout/src/hooks/useCheckout.ts | 28 + apps/checkout/src/index.css | 332 ++--- apps/checkout/src/lib/utils.ts | 6 + apps/checkout/src/main.tsx | 41 +- apps/checkout/src/pages/CheckoutPage.tsx | 227 ++-- apps/checkout/src/utils/formatUtils.ts | 12 + apps/checkout/tsconfig.app.json | 54 +- apps/checkout/tsconfig.json | 22 +- apps/checkout/vite.config.ts | 3 +- bun.lock | 637 +++++++-- .../src/internal/billing/v2/actions/index.ts | 1 + .../v2/utils/billingPlanToPreviewResponse.ts | 14 +- .../checkouts/handlers/handleGetCheckout.ts | 4 +- .../attach/complex-attach-scenario.test.ts | 74 + .../billing/common/billingPreviewResponse.ts | 1 + .../fixedPriceToLineDescription.ts | 2 +- .../usagePriceToLineDescription.ts | 3 +- 42 files changed, 4150 insertions(+), 645 deletions(-) create mode 100644 apps/checkout/.claude/skills/d3k/SKILL.md create mode 100644 apps/checkout/bun.lock create mode 100644 apps/checkout/components.json delete mode 100644 apps/checkout/eslint.config.js create mode 100644 apps/checkout/src/components/checkout/CheckoutErrorState.tsx create mode 100644 apps/checkout/src/components/checkout/CheckoutLoadingState.tsx create mode 100644 apps/checkout/src/components/checkout/CheckoutSuccessState.tsx create mode 100644 apps/checkout/src/components/ui/alert-dialog.tsx create mode 100644 apps/checkout/src/components/ui/badge.tsx create mode 100644 apps/checkout/src/components/ui/button.tsx create mode 100644 apps/checkout/src/components/ui/card.tsx create mode 100644 apps/checkout/src/components/ui/combobox.tsx create mode 100644 apps/checkout/src/components/ui/dropdown-menu.tsx create mode 100644 apps/checkout/src/components/ui/field.tsx create mode 100644 apps/checkout/src/components/ui/input-group.tsx create mode 100644 apps/checkout/src/components/ui/input.tsx create mode 100644 apps/checkout/src/components/ui/item.tsx create mode 100644 apps/checkout/src/components/ui/label.tsx create mode 100644 apps/checkout/src/components/ui/select.tsx create mode 100644 apps/checkout/src/components/ui/separator.tsx create mode 100644 apps/checkout/src/components/ui/skeleton.tsx create mode 100644 apps/checkout/src/components/ui/textarea.tsx create mode 100644 apps/checkout/src/hooks/useCheckout.ts create mode 100644 apps/checkout/src/lib/utils.ts create mode 100644 apps/checkout/src/utils/formatUtils.ts create mode 100644 server/tests/scenarios/attach/complex-attach-scenario.test.ts diff --git a/apps/checkout/.claude/skills/d3k/SKILL.md b/apps/checkout/.claude/skills/d3k/SKILL.md new file mode 100644 index 000000000..a528c0099 --- /dev/null +++ b/apps/checkout/.claude/skills/d3k/SKILL.md @@ -0,0 +1,52 @@ +--- +description: "d3k assistant for debugging web apps" +--- + +# d3k Commands + +d3k captures browser and server logs in a unified log file. Use these commands: + +## Viewing Errors and Logs + +```bash +d3k errors # Show recent errors (browser + server combined) +d3k errors --context # Show errors + user actions that preceded them +d3k errors -n 20 # Show last 20 errors + +d3k logs # Show recent logs (browser + server combined) +d3k logs --type browser # Browser logs only +d3k logs --type server # Server logs only +``` + +## Other Commands + +```bash +d3k fix # Deep analysis of application errors +d3k fix --focus build # Focus on build errors + +d3k crawl # Discover app URLs +d3k crawl --depth all # Exhaustive crawl + +d3k find-component "nav" # Find React component source + +d3k restart # Restart dev server (rarely needed) +``` + +## Browser Interaction + +To click elements, navigate, or take screenshots, use `d3k agent-browser --cdp $(d3k cdp-port)`: + +```bash +d3k agent-browser --cdp $(d3k cdp-port) open http://localhost:3000/page +d3k agent-browser --cdp $(d3k cdp-port) snapshot -i # Get element refs (@e1, @e2) +d3k agent-browser --cdp $(d3k cdp-port) click @e2 +d3k agent-browser --cdp $(d3k cdp-port) fill @e3 "text" +d3k agent-browser --cdp $(d3k cdp-port) screenshot /tmp/shot.png +``` + +## Fix Workflow + +1. `d3k errors --context` - See errors and what triggered them +2. Fix the code +3. `d3k agent-browser --cdp $(d3k cdp-port) open ` then `click @e1` to replay +4. `d3k errors` - Verify fix worked diff --git a/apps/checkout/README.md b/apps/checkout/README.md index d2e77611f..d4b9dd478 100644 --- a/apps/checkout/README.md +++ b/apps/checkout/README.md @@ -1,73 +1,3 @@ -# React + TypeScript + Vite +# React + TypeScript + Vite + shadcn/ui -This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh - -## React Compiler - -The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). - -## Expanding the ESLint configuration - -If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: - -```js -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - - // Remove tseslint.configs.recommended and replace with this - tseslint.configs.recommendedTypeChecked, - // Alternatively, use this for stricter rules - tseslint.configs.strictTypeChecked, - // Optionally, add this for stylistic rules - tseslint.configs.stylisticTypeChecked, - - // Other configs... - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` - -You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: - -```js -// eslint.config.js -import reactX from 'eslint-plugin-react-x' -import reactDom from 'eslint-plugin-react-dom' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - // Other configs... - // Enable lint rules for React - reactX.configs['recommended-typescript'], - // Enable lint rules for React DOM - reactDom.configs.recommended, - ], - languageOptions: { - parserOptions: { - project: ['./tsconfig.node.json', './tsconfig.app.json'], - tsconfigRootDir: import.meta.dirname, - }, - // other options... - }, - }, -]) -``` +This is a template for a new Vite project with React, TypeScript, and shadcn/ui. diff --git a/apps/checkout/bun.lock b/apps/checkout/bun.lock new file mode 100644 index 000000000..09735f569 --- /dev/null +++ b/apps/checkout/bun.lock @@ -0,0 +1,1205 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "vite-app", + "dependencies": { + "@base-ui/react": "^1.1.0", + "@fontsource-variable/inter": "^5.2.8", + "@orpc/client": "^1.13.4", + "@orpc/contract": "^1.13.4", + "@orpc/openapi-client": "^1.13.4", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.1.17", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "shadcn": "^3.7.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.17", + "tw-animate-css": "^1.4.0", + "vite-tsconfig-paths": "^6.0.5", + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4", + }, + }, + }, + "packages": { + "@antfu/ni": ["@antfu/ni@25.0.0", "", { "dependencies": { "ansis": "^4.0.0", "fzf": "^0.5.2", "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" }, "bin": { "na": "bin/na.mjs", "ni": "bin/ni.mjs", "nr": "bin/nr.mjs", "nci": "bin/nci.mjs", "nlx": "bin/nlx.mjs", "nun": "bin/nun.mjs", "nup": "bin/nup.mjs" } }, "sha512-9q/yCljni37pkMr4sPrI3G4jqdIk074+iukc5aFJl7kmDCCsiJrbZ6zKxnES1Gwg+i9RcDZwvktl23puGslmvA=="], + + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], + + "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], + + "@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], + + "@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], + + "@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], + + "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.28.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.28.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], + + "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + + "@base-ui/react": ["@base-ui/react@1.1.0", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@base-ui/utils": "0.2.4", "@floating-ui/react-dom": "^2.1.6", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-ikcJRNj1mOiF2HZ5jQHrXoVoHcNHdBU5ejJljcBl+VTLoYXR6FidjTN86GjO6hyshi6TZFuNvv0dEOgaOFv6Lw=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.4", "", { "dependencies": { "@babel/runtime": "^7.28.4", "@floating-ui/utils": "^0.2.10", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-smZwpMhjO29v+jrZusBSc5T+IJ3vBb9cjIiBjtKcvWmRj9Z4DWGVR3efr1eHR56/bqY5a4qyY9ElkOY5ljo3ng=="], + + "@dotenvx/dotenvx": ["@dotenvx/dotenvx@1.52.0", "", { "dependencies": { "commander": "^11.1.0", "dotenv": "^17.2.1", "eciesjs": "^0.4.10", "execa": "^5.1.1", "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", "picomatch": "^4.0.2", "which": "^4.0.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" } }, "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w=="], + + "@ecies/ciphers": ["@ecies/ciphers@0.2.5", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.2", "", { "os": "android", "cpu": "arm64" }, "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.2", "", { "os": "android", "cpu": "x64" }, "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.2", "", { "os": "linux", "cpu": "arm" }, "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.2", "", { "os": "linux", "cpu": "none" }, "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.2", "", { "os": "linux", "cpu": "x64" }, "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.2", "", { "os": "none", "cpu": "x64" }, "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.2", "", { "os": "none", "cpu": "arm64" }, "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.2", "", {}, "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + + "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + + "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], + + "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.25.3", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "jose": "^6.1.1", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.0" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ=="], + + "@mswjs/interceptors": ["@mswjs/interceptors@0.40.0", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + + "@orpc/client": ["@orpc/client@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4", "@orpc/standard-server-fetch": "1.13.4", "@orpc/standard-server-peer": "1.13.4" } }, "sha512-s13GPMeoooJc5Th2EaYT5HMFtWG8S03DUVytYfJv8pIhP87RYKl94w52A36denH6r/B4LaAgBeC9nTAOslK+Og=="], + + "@orpc/contract": ["@orpc/contract@1.13.4", "", { "dependencies": { "@orpc/client": "1.13.4", "@orpc/shared": "1.13.4", "@standard-schema/spec": "^1.1.0", "openapi-types": "^12.1.3" } }, "sha512-TIxyaF67uOlihCRcasjHZxguZpbqfNK7aMrDLnhoufmQBE4OKvguNzmrOFHgsuM0OXoopX0Nuhun1ccaxKP10A=="], + + "@orpc/openapi-client": ["@orpc/openapi-client@1.13.4", "", { "dependencies": { "@orpc/client": "1.13.4", "@orpc/contract": "1.13.4", "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-tRUcY4E6sgpS5bY/9nNES/Q/PMyYyPOsI4TuhwLhfgxOb0GFPwYKJ6Kif7KFNOhx4fkN/jTOfE1nuWuIZU1gyg=="], + + "@orpc/shared": ["@orpc/shared@1.13.4", "", { "dependencies": { "radash": "^12.1.1", "type-fest": "^5.3.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0" }, "optionalPeers": ["@opentelemetry/api"] }, "sha512-TYt9rLG/BUkNQBeQ6C1tEiHS/Seb8OojHgj9GlvqyjHJhMZx5qjsIyTW6RqLPZJ4U2vgK6x4Her36+tlFCKJug=="], + + "@orpc/standard-server": ["@orpc/standard-server@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4" } }, "sha512-ZOzgfVp6XUg+wVYw+gqesfRfGPtQbnBIrIiSnFMtZF+6ncmFJeF2Shc4RI2Guqc0Qz25juy8Ogo4tX3YqysOcg=="], + + "@orpc/standard-server-fetch": ["@orpc/standard-server-fetch@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-/zmKwnuxfAXbppJpgr1CMnQX3ptPlYcDzLz1TaVzz9VG/Xg58Ov3YhabS2Oi1utLVhy5t4kaCppUducAvoKN+A=="], + + "@orpc/standard-server-peer": ["@orpc/standard-server-peer@1.13.4", "", { "dependencies": { "@orpc/shared": "1.13.4", "@orpc/standard-server": "1.13.4" } }, "sha512-UfqnTLqevjCKUk4cmImOG8cQUwANpV1dp9e9u2O1ki6BRBsg/zlXFg6G2N6wP0zr9ayIiO1d2qJdH55yl/1BNw=="], + + "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.0", "", { "os": "android", "cpu": "arm" }, "sha512-tPgXB6cDTndIe1ah7u6amCI1T0SsnlOuKgg10Xh3uizJk4e5M1JGaUMk7J4ciuAUcFpbOiNhm2XIjP9ON0dUqA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.0", "", { "os": "android", "cpu": "arm64" }, "sha512-sa4LyseLLXr1onr97StkU1Nb7fWcg6niokTwEVNOO7awaKaoRObQ54+V/hrF/BP1noMEaaAW6Fg2d/CfLiq3Mg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/NNIj9A7yLjKdmkx5dC2XQ9DmjIECpGpwHoGmA5E1AhU0fuICSqSWScPhN1yLCkEdkCwJIDu2xIeLPs60MNIVg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-xoh8abqgPrPYPr7pTYipqnUi1V3em56JzE/HgDgitTqZBZ3yKCWI+7KUkceM6tNweyUKYru1UMi7FC060RyKwA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-PCkMh7fNahWSbA0OTUQ2OpYHpjZZr0hPr8lId8twD7a7SeWrvT3xJVyza+dQwXSSq4yEQTMoXgNOfMCsn8584g=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1j3stGx+qbhXql4OCDZhnK7b01s6rBKNybfsX+TNrEe9JNq4DLi1yGiR1xW+nL+FNVvI4D02PUnl6gJ/2y6WJA=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-eyrr5W08Ms9uM0mLcKfM/Uzx7hjhz2bcjv8P2uynfj0yU8GGPdz8iYrBPhiLOZqahoAMB8ZiolRZPbbU2MAi6Q=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Xds90ITXJCNyX9pDhqf85MKWUI4lqjiPAipJ8OLp8xqI2Ehk+TCVhF9rvOoN8xTbcafow3QOThkNnrM33uCFQA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Xws2KA4CLvZmXjy46SQaXSejuKPhwVdaNinldoYfqruZBaJHqVo6hnRa8SDo9z7PBW5x84SH64+izmldCgbezw=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hrKXKbX5FdaRJj7lTMusmvKbhMJSGWJ+w++4KmjiDhpTgNlhYobMvKfDoIWecy4O60K6yA4SnztGuNTQF+Lplw=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-6A+nccfSDGKsPm00d3xKcrsBcbqzCTAukjwWK6rbuAnB2bHaL3r9720HBVZ/no7+FhZLz/U3GwwZZEh6tOSI8Q=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-4P1VyYUe6XAJtQH1Hh99THxr0GKMMwIXsRNOceLrJnaHTDgk1FTcTimDgneRJPvB3LqDQxUmroBclQ1S0cIJwQ=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Vv6pLuIZCMcgXre6c3nOPhE0gjz1+nZP6T+hwWjr7sVH8k0jRkH+XnfjjOTglyMBdSKBPPz54/y1gToSKwrSQ=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-r1te1M0Sm2TBVD/RxBPC6RZVwNqUTwJTA7w+C/IW5v9Ssu6xmxWEi+iJQlpBhtUiT1raJ5b48pI8tBvEjEFnFA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-say0uMU/RaPm3CDQLxUUTF2oNWL8ysvHkAjcCzV2znxBr23kFfaxocS9qJm+NdkRhF8wtdEEAJuYcLPhSPbjuQ=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.0", "", { "os": "linux", "cpu": "none" }, "sha512-/MU7/HizQGsnBREtRpcSbSV1zfkoxSTR7wLsRmBPQ8FwUj5sykrP1MyJTvsxP5KBq9SyE6kH8UQQQwa0ASeoQQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-Q9eh+gUGILIHEaJf66aF6a414jQbDnn29zeu0eX3dHMuysnhTvsUvZTCAyZ6tJhUjnvzBKE4FtuaYxutxRZpOg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-OR5p5yG5OKSxHReWmwvM0P+VTPMwoBS45PXTMYaskKQqybkS3Kmugq1W+YbNWArF8/s7jQScgzXUhArzEQ7x0A=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.0", "", { "os": "linux", "cpu": "x64" }, "sha512-XeatKzo4lHDsVEbm1XDHZlhYZZSQYym6dg2X/Ko0kSFgio+KXLsxwJQprnR48GvdIKDOpqWqssC3iBCjoMcMpw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Lu71y78F5qOfYmubYLHPcJm74GZLU6UJ4THkf/a1K7Tz2ycwC2VUbsqbJAXaR6Bx70SRdlVrt2+n5l7F0agTUw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.0", "", { "os": "none", "cpu": "arm64" }, "sha512-v5xwKDWcu7qhAEcsUubiav7r+48Uk/ENWdr82MBZZRIm7zThSxCIVDfb3ZeRRq9yqk+oIzMdDo6fCcA5DHfMyA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-XnaaaSMGSI6Wk8F4KK3QP7GfuuhjGchElsVerCplUuxRIzdvZ7hRBpLR0omCmw+kI2RFJB80nenhOoGXlJ5TfQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-3K1lP+3BXY4t4VihLw5MEg6IZD3ojSYzqzBG571W3kNQe4G4CcFpSUQVgurYgib5d+YaCjeFow8QivWp8vuSvA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-MDk610P/vJGc5L5ImE4k5s+GZT3en0KoK1MKPXCRgzmksAMk79j4h3k1IerxTNqwDLxsGxStEZVBqG0gIqZqoA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Zv7v6q6aV+VslnpwzqKAmrk5JdVkLUzok2208ZXGipjb+msxBr/fJPZyeEXiFgH7k62Ak0SLIfxQRZQvTuf7rQ=="], + + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], + + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], + + "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.18", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.18", "@tailwindcss/oxide-darwin-arm64": "4.1.18", "@tailwindcss/oxide-darwin-x64": "4.1.18", "@tailwindcss/oxide-freebsd-x64": "4.1.18", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", "@tailwindcss/oxide-linux-x64-musl": "4.1.18", "@tailwindcss/oxide-wasm32-wasi": "4.1.18", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A=="], + + "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.18", "", { "os": "android", "cpu": "arm64" }, "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q=="], + + "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A=="], + + "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw=="], + + "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.18", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA=="], + + "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18", "", { "os": "linux", "cpu": "arm" }, "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA=="], + + "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw=="], + + "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.18", "", { "os": "linux", "cpu": "arm64" }, "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg=="], + + "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g=="], + + "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.18", "", { "os": "linux", "cpu": "x64" }, "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ=="], + + "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.18", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA=="], + + "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.18", "", { "os": "win32", "cpu": "arm64" }, "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA=="], + + "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.18", "", { "os": "win32", "cpu": "x64" }, "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q=="], + + "@tailwindcss/vite": ["@tailwindcss/vite@4.1.18", "", { "dependencies": { "@tailwindcss/node": "4.1.18", "@tailwindcss/oxide": "4.1.18", "tailwindcss": "4.1.18" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA=="], + + "@ts-morph/common": ["@ts-morph/common@0.27.0", "", { "dependencies": { "fast-glob": "^3.3.3", "minimatch": "^10.0.1", "path-browserify": "^1.0.1" } }, "sha512-Wf29UqxWDpc+i61k3oIOzcUfQt79PIT9y/MWfAGlrkjg6lBC1hwDECLXPVJAhWjiGbfBCxZd65F/LIZF3+jeJQ=="], + + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], + + "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], + + "@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="], + + "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + + "@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + + "@types/validate-npm-package-name": ["@types/validate-npm-package-name@4.0.2", "", {}, "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.54.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/type-utils": "8.54.0", "@typescript-eslint/utils": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.54.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.54.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.54.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.54.0", "@typescript-eslint/types": "^8.54.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0" } }, "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.54.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.54.0", "", {}, "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.54.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.54.0", "@typescript-eslint/tsconfig-utils": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/visitor-keys": "8.54.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.54.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.54.0", "", { "dependencies": { "@typescript-eslint/types": "8.54.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "code-block-writer": ["code-block-writer@13.0.3", "", {}, "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cosmiconfig": ["cosmiconfig@9.0.0", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "dedent": ["dedent@1.7.1", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "default-browser": ["default-browser@5.4.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg=="], + + "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + + "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eciesjs": ["eciesjs@0.4.17", "", { "dependencies": { "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" } }, "sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="], + + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "enhanced-resolve": ["enhanced-resolve@5.18.4", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "fuzzysort": ["fuzzysort@3.1.0", "", {}, "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ=="], + + "fzf": ["fzf@0.5.2", "", {}, "sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-own-enumerable-keys": ["get-own-enumerable-keys@1.0.0", "", {}, "sha512-PKsK2FSrQCyxcGHsGrLDcK0lx+0Ke+6e8KFFozA9/fIQLhQzPaRvJFdcz7+Axg3jUH/Mq+NI4xa5u/UT2tQskA=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + + "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphql": ["graphql@16.12.0", "", {}, "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "headers-polyfill": ["headers-polyfill@4.0.3", "", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + + "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], + + "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + + "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + + "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], + + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@3.0.0", "", {}, "sha512-IlsXEHOjtKhpN8r/tRFj2nDyTmHvcfNeu/nrRIcXE17ROeatXchkojffa1SpdqW4cr/Fj6QkEf/Gn4zf6KKvEQ=="], + + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "is-regexp": ["is-regexp@3.1.0", "", {}, "sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA=="], + + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], + + "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], + + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.2", "", { "os": "linux", "cpu": "arm" }, "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "msw": ["msw@2.12.7", "", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.40.0", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.7.0", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg=="], + + "mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-treeify": ["object-treeify@1.1.33", "", {}, "sha512-EFVjAYfzWqWsBMRHPMAXLCDIJnpMhdWAqR7xG6M6a2cs6PMFpl/+Z20w9zDW4vkxOFfddegBKq9Rehd0bxWE7A=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + + "open": ["open@11.0.0", "", { "dependencies": { "default-browser": "^5.4.0", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.1.0", "wsl-utils": "^0.3.0" } }, "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], + + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-manager-detector": ["package-manager-detector@1.6.0", "", {}, "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "postcss-selector-parser": ["postcss-selector-parser@7.1.1", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg=="], + + "powershell-utils": ["powershell-utils@0.1.0", "", {}, "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "radash": ["radash@12.1.1", "", {}, "sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="], + + "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="], + + "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + + "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], + + "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rettime": ["rettime@0.7.0", "", {}, "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shadcn": ["shadcn@3.7.0", "", { "dependencies": { "@antfu/ni": "^25.0.0", "@babel/core": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/plugin-transform-typescript": "^7.28.0", "@babel/preset-typescript": "^7.27.1", "@dotenvx/dotenvx": "^1.48.4", "@modelcontextprotocol/sdk": "^1.17.2", "@types/validate-npm-package-name": "^4.0.2", "browserslist": "^4.26.2", "commander": "^14.0.0", "cosmiconfig": "^9.0.0", "dedent": "^1.6.0", "deepmerge": "^4.3.1", "diff": "^8.0.2", "execa": "^9.6.0", "fast-glob": "^3.3.3", "fs-extra": "^11.3.1", "fuzzysort": "^3.1.0", "https-proxy-agent": "^7.0.6", "kleur": "^4.1.5", "msw": "^2.10.4", "node-fetch": "^3.3.2", "open": "^11.0.0", "ora": "^8.2.0", "postcss": "^8.5.6", "postcss-selector-parser": "^7.1.0", "prompts": "^2.4.2", "recast": "^0.23.11", "stringify-object": "^5.0.0", "ts-morph": "^26.0.0", "tsconfig-paths": "^4.2.0", "validate-npm-package-name": "^7.0.1", "zod": "^3.24.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "shadcn": "dist/index.js" } }, "sha512-zOXNAIFclguSYmmoibyXyKiYA6qjEJtXDSvloAMziSREW9Q0R/dLqBUYdb81lOejmZkDYuZApGabbMLH7G8qvQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + + "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "stringify-object": ["stringify-object@5.0.0", "", { "dependencies": { "get-own-enumerable-keys": "^1.0.0", "is-obj": "^3.0.0", "is-regexp": "^3.1.0" } }, "sha512-zaJYxz2FtcMb4f+g60KsRNFOpVMUyuJgA51Zi5Z1DOTC3S59+OQiVOzE9GZt0x72uBGWKsQIuBKeF9iusmKFsg=="], + + "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "tabbable": ["tabbable@6.4.0", "", {}, "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg=="], + + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tldts": ["tldts@7.0.19", "", { "dependencies": { "tldts-core": "^7.0.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA=="], + + "tldts-core": ["tldts-core@7.0.19", "", {}, "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tough-cookie": ["tough-cookie@6.0.0", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "ts-morph": ["ts-morph@26.0.0", "", { "dependencies": { "@ts-morph/common": "~0.27.0", "code-block-writer": "^13.0.3" } }, "sha512-ztMO++owQnz8c/gIENcM9XfCEzgoGphTv+nKpYNM1bgsdOVC/jRZuEBf6N+mLLDNg68Kl+GgUZfOySaRiG1/Ug=="], + + "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="], + + "tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@5.4.2", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-FLEenlVYf7Zcd34ISMLo3ZzRE1gRjY1nMDTp+bQRBiPsaKyIW8K3Zr99ioHDUgA9OGuGGJPyYpNcffGmBhJfGg=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.54.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.54.0", "@typescript-eslint/parser": "8.54.0", "@typescript-eslint/typescript-estree": "8.54.0", "@typescript-eslint/utils": "8.54.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-tsconfig-paths": ["vite-tsconfig-paths@6.0.5", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-f/WvY6ekHykUF1rWJUAbCU7iS/5QYDIugwpqJA+ttwKbxSbzNlqlE8vZSrsnxNQciUW+z6lvhlXMaEyZn9MSig=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "wsl-utils": ["wsl-utils@0.3.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.1.0" } }, "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + + "@dotenvx/dotenvx/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "@dotenvx/dotenvx/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], + + "@dotenvx/dotenvx/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], + + "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], + + "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@ts-morph/common/minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "log-symbols/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "ora/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "router/path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "shadcn/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], + + "@dotenvx/dotenvx/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], + + "@dotenvx/dotenvx/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "@dotenvx/dotenvx/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], + + "@dotenvx/dotenvx/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "@dotenvx/dotenvx/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], + + "@dotenvx/dotenvx/which/isexe": ["isexe@3.1.1", "", {}, "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ=="], + + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + } +} diff --git a/apps/checkout/components.json b/apps/checkout/components.json new file mode 100644 index 000000000..2af2772bd --- /dev/null +++ b/apps/checkout/components.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "base-nova", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "phosphor", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "menuColor": "default", + "menuAccent": "subtle", + "registries": {} +} diff --git a/apps/checkout/eslint.config.js b/apps/checkout/eslint.config.js deleted file mode 100644 index 5e6b472f5..000000000 --- a/apps/checkout/eslint.config.js +++ /dev/null @@ -1,23 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' -import { defineConfig, globalIgnores } from 'eslint/config' - -export default defineConfig([ - globalIgnores(['dist']), - { - files: ['**/*.{ts,tsx}'], - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - ], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - }, -]) diff --git a/apps/checkout/index.html b/apps/checkout/index.html index 1440f4257..1f738fe7b 100644 --- a/apps/checkout/index.html +++ b/apps/checkout/index.html @@ -4,7 +4,7 @@ - vite-project + vite-app
diff --git a/apps/checkout/package.json b/apps/checkout/package.json index 3bea96479..c6ea49cb4 100644 --- a/apps/checkout/package.json +++ b/apps/checkout/package.json @@ -1,28 +1,48 @@ { - "name": "@autumn/checkout", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview" - }, - "dependencies": { - "@autumn/shared": "workspace:*", - "@orpc/client": "catalog:", - "@orpc/openapi-client": "catalog:", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-router-dom": "^7.13.0" - }, - "devDependencies": { - "@types/node": "^22.13.10", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "@vitejs/plugin-react": "^4.3.4", - "typescript": "~5.7.2", - "vite": "^6.2.0", - "vite-tsconfig-paths": "^5.1.4" - } -} + "name": "checkout-2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@autumn/shared": "workspace:*", + "@base-ui/react": "^1.1.0", + "@fontsource-variable/inter": "^5.2.8", + "@orpc/client": "catalog:", + "@orpc/contract": "catalog:", + "@orpc/openapi-client": "catalog:", + "@phosphor-icons/react": "^2.1.10", + "@tailwindcss/vite": "^4.1.17", + "@tanstack/react-query": "^5.90.20", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "date-fns": "^4.1.0", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", + "shadcn": "^3.7.0", + "tailwind-merge": "^3.4.0", + "tailwindcss": "^4.1.17", + "tw-animate-css": "^1.4.0", + "vite-tsconfig-paths": "^6.0.5" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} \ No newline at end of file diff --git a/apps/checkout/src/components/checkout/CheckoutErrorState.tsx b/apps/checkout/src/components/checkout/CheckoutErrorState.tsx new file mode 100644 index 000000000..bca830e38 --- /dev/null +++ b/apps/checkout/src/components/checkout/CheckoutErrorState.tsx @@ -0,0 +1,18 @@ +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export function CheckoutErrorState({ message }: { message: string }) { + return ( +
+ + + + Something went wrong + + + +

{message}

+
+
+
+ ); +} diff --git a/apps/checkout/src/components/checkout/CheckoutLoadingState.tsx b/apps/checkout/src/components/checkout/CheckoutLoadingState.tsx new file mode 100644 index 000000000..7317b340a --- /dev/null +++ b/apps/checkout/src/components/checkout/CheckoutLoadingState.tsx @@ -0,0 +1,47 @@ +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; + +export function CheckoutLoadingState() { + return ( +
+
+ {/* Header */} + + + {/* Line items card */} +
+
+
+ + +
+ +
+
+
+ + +
+ +
+
+ + + + {/* Amount due today */} +
+
+ + +
+ +
+ + {/* Button */} +
+ +
+
+
+ ); +} diff --git a/apps/checkout/src/components/checkout/CheckoutSuccessState.tsx b/apps/checkout/src/components/checkout/CheckoutSuccessState.tsx new file mode 100644 index 000000000..c6d9b03c0 --- /dev/null +++ b/apps/checkout/src/components/checkout/CheckoutSuccessState.tsx @@ -0,0 +1,44 @@ +import type { ConfirmCheckoutResponse } from "@autumn/shared"; +import { Card, CardContent } from "@/components/ui/card"; + +export function CheckoutSuccessState({ + result, +}: { + result: ConfirmCheckoutResponse; +}) { + return ( +
+ + +
+ +
+
+

Purchase Complete

+

+ Your order has been confirmed. +

+
+ {result.invoice_id && ( +

+ Invoice ID: {result.invoice_id} +

+ )} +
+
+
+ ); +} diff --git a/apps/checkout/src/components/ui/alert-dialog.tsx b/apps/checkout/src/components/ui/alert-dialog.tsx new file mode 100644 index 000000000..ee099f844 --- /dev/null +++ b/apps/checkout/src/components/ui/alert-dialog.tsx @@ -0,0 +1,175 @@ +"use client" + +import * as React from "react" +import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) { + return +} + +function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) { + return ( + + ) +} + +function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: AlertDialogPrimitive.Backdrop.Props) { + return ( + + ) +} + +function AlertDialogContent({ + className, + size = "default", + ...props +}: AlertDialogPrimitive.Popup.Props & { + size?: "default" | "sm" +}) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogMedia({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( +