diff --git a/.github/workflows/knip.yml b/.github/workflows/knip.yml
index 0141ef708..056dd9c68 100644
--- a/.github/workflows/knip.yml
+++ b/.github/workflows/knip.yml
@@ -25,4 +25,4 @@ jobs:
run: bun install
- name: Run Knip
- run: bun knip --no-exit-code
+ run: bun knip
diff --git a/.github/workflows/server-typecheck.yml b/.github/workflows/server-typecheck.yml
index 9fb4283f2..a081ea20f 100644
--- a/.github/workflows/server-typecheck.yml
+++ b/.github/workflows/server-typecheck.yml
@@ -3,9 +3,33 @@ name: Server Type Check
on:
pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+
jobs:
+ changes:
+ name: Check changed files
+ runs-on: ubuntu-latest
+ outputs:
+ server: ${{ steps.filter.outputs.server }}
+ steps:
+ - name: Check changed files
+ id: filter
+ uses: dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050
+ with:
+ filters: |
+ server:
+ - "server/**"
+ - "shared/**"
+ - "package.json"
+ - "bun.lock"
+ - ".github/workflows/server-typecheck.yml"
+
typecheck:
name: Type Check
+ needs: changes
+ if: needs.changes.outputs.server == 'true'
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/server-unit-tests.yml b/.github/workflows/server-unit-tests.yml
index bdb743a1d..2c99faaec 100644
--- a/.github/workflows/server-unit-tests.yml
+++ b/.github/workflows/server-unit-tests.yml
@@ -2,10 +2,36 @@ name: Server Unit Tests
on:
pull_request:
+ paths:
+ - "server/**"
+
+permissions:
+ contents: read
+ pull-requests: read
jobs:
+ changes:
+ name: Check changed files
+ runs-on: ubuntu-latest
+ outputs:
+ server: ${{ steps.filter.outputs.server }}
+ steps:
+ - name: Check changed files
+ id: filter
+ uses: dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050
+ with:
+ filters: |
+ server:
+ - "server/**"
+ - "shared/**"
+ - "package.json"
+ - "bun.lock"
+ - ".github/workflows/server-unit-tests.yml"
+
unit-tests:
name: Unit Tests
+ needs: changes
+ if: needs.changes.outputs.server == 'true'
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/validate-schema.yml b/.github/workflows/validate-schema.yml
new file mode 100644
index 000000000..0a087cae0
--- /dev/null
+++ b/.github/workflows/validate-schema.yml
@@ -0,0 +1,32 @@
+name: Validate Production Schema
+
+on:
+ pull_request:
+ branches:
+ - dev
+ push:
+ branches:
+ - dev
+
+jobs:
+ validate-schema:
+ name: Validate Schema
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Bun
+ uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: 1.3.10
+
+ - name: Install dependencies
+ run: bun install
+
+ - name: Validate schema
+ env:
+ DATABASE_URL: ${{ secrets.DATABASE_READ_ONLY_URL }}
+ DB_MAX_CONNECTIONS: 1
+ run: bun scripts/migrations/validate-schema.ts
diff --git a/.github/workflows/vite-build.yml b/.github/workflows/vite-build.yml
index 71214f9c0..35917b017 100644
--- a/.github/workflows/vite-build.yml
+++ b/.github/workflows/vite-build.yml
@@ -3,9 +3,35 @@ name: Vite Build Check
on:
pull_request:
+permissions:
+ contents: read
+ pull-requests: read
+
jobs:
+ changes:
+ name: Check changed files
+ runs-on: ubuntu-latest
+ outputs:
+ vite: ${{ steps.filter.outputs.vite }}
+ steps:
+ - name: Check changed files
+ id: filter
+ uses: dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050
+ with:
+ filters: |
+ vite:
+ - "vite/**"
+ - "shared/**"
+ - "packages/atmn/**"
+ - "packages/autumn-js/**"
+ - "package.json"
+ - "bun.lock"
+ - ".github/workflows/vite-build.yml"
+
typecheck:
name: Type Check
+ needs: changes
+ if: needs.changes.outputs.vite == 'true'
runs-on: ubuntu-latest
steps:
@@ -25,6 +51,8 @@ jobs:
build:
name: Build
+ needs: changes
+ if: needs.changes.outputs.vite == 'true'
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/vite-unit-tests.yml b/.github/workflows/vite-unit-tests.yml
index db4708dc5..cdae25ad9 100644
--- a/.github/workflows/vite-unit-tests.yml
+++ b/.github/workflows/vite-unit-tests.yml
@@ -2,10 +2,38 @@ name: Vite Unit Tests
on:
pull_request:
+ paths:
+ - "vite/**"
+
+permissions:
+ contents: read
+ pull-requests: read
jobs:
+ changes:
+ name: Check changed files
+ runs-on: ubuntu-latest
+ outputs:
+ vite: ${{ steps.filter.outputs.vite }}
+ steps:
+ - name: Check changed files
+ id: filter
+ uses: dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050
+ with:
+ filters: |
+ vite:
+ - "vite/**"
+ - "shared/**"
+ - "packages/atmn/**"
+ - "packages/autumn-js/**"
+ - "package.json"
+ - "bun.lock"
+ - ".github/workflows/vite-unit-tests.yml"
+
unit-tests:
name: Unit Tests
+ needs: changes
+ if: needs.changes.outputs.vite == 'true'
runs-on: ubuntu-latest
steps:
diff --git a/.husky/pre-commit b/.husky/pre-commit
index d71478d5d..abc76b154 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1 +1,14 @@
-cd server && bun ts
+set -e
+
+bun knip
+(cd server && bun ts)
+
+changed_files="$(git diff --cached --name-only)"
+
+if printf '%s\n' "$changed_files" | grep -q '^server/'; then
+ (cd server && bun test tests/unit)
+fi
+
+if printf '%s\n' "$changed_files" | grep -q '^vite/'; then
+ (cd vite && bun test tests/)
+fi
diff --git a/apps/checkout/src/components/checkout/layout/CardBackground.tsx b/apps/checkout/src/components/checkout/layout/CardBackground.tsx
deleted file mode 100644
index 578a3c50b..000000000
--- a/apps/checkout/src/components/checkout/layout/CardBackground.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { ReactNode } from "react";
-import { motion } from "motion/react";
-
-/**
- * Full-screen background wrapper with subtle diagonal gradients from primary color.
- * Includes entrance animation for the content container.
- */
-export function CardBackground({ children }: { children: ReactNode }) {
- return (
-
img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col",
- cardVariants[variant],
- className
- )}
- {...props}
- />
- )
-}
-
-function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardAction({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardContent({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
- return (
-
- )
-}
-
-export {
- Card,
- CardHeader,
- CardFooter,
- CardTitle,
- CardAction,
- CardDescription,
- CardContent,
-}
diff --git a/apps/docs/mintlify/api-reference/billing/billingUpdate.mdx b/apps/docs/mintlify/api-reference/billing/billingUpdate.mdx
index a781761ac..c4320c63f 100644
--- a/apps/docs/mintlify/api-reference/billing/billingUpdate.mdx
+++ b/apps/docs/mintlify/api-reference/billing/billingUpdate.mdx
@@ -8,7 +8,7 @@ import { DynamicResponseField } from "/components/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/components/dynamic-response-example.jsx";
- The update endpoint modifies an existing subscription. Use this to change prepaid quantities, cancel subscriptions, or modify plan configuration. For subscribing to a new plan, use [attach](/api-reference/billing/billingAttach) instead.
+ The update endpoint modifies an existing subscription. Use this to change prepaid quantities, cancel subscriptions, or modify plan configuration. For subscribing to a new plan, use [attach](/api-reference/billing/attach) instead.
### Common Use Cases
diff --git a/apps/docs/mintlify/api-reference/plans/createPlan.mdx b/apps/docs/mintlify/api-reference/plans/createPlan.mdx
index 3d136f99c..1d1561772 100644
--- a/apps/docs/mintlify/api-reference/plans/createPlan.mdx
+++ b/apps/docs/mintlify/api-reference/plans/createPlan.mdx
@@ -7,7 +7,7 @@ import { DynamicParamField } from "/components/dynamic-param-field.jsx";
import { DynamicResponseField } from "/components/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/components/dynamic-response-example.jsx";
-Creates a new plan with optional base price and feature configurations. See [How plans work](/documentation/pricing/plans) for concepts and [Adding features to plans](/documentation/pricing/plan-features) for item configuration.
+Creates a new plan with optional base price and feature configurations. See [How plans work](/documentation/concepts/plans) for concepts and [Adding features to plans](/documentation/concepts/plan-items) for item configuration.
### Plan Configuration
diff --git a/apps/docs/mintlify/api-reference/plans/updatePlan.mdx b/apps/docs/mintlify/api-reference/plans/updatePlan.mdx
index 93974b37f..f28cfe93c 100644
--- a/apps/docs/mintlify/api-reference/plans/updatePlan.mdx
+++ b/apps/docs/mintlify/api-reference/plans/updatePlan.mdx
@@ -7,7 +7,7 @@ import { DynamicParamField } from "/components/dynamic-param-field.jsx";
import { DynamicResponseField } from "/components/dynamic-response-field.jsx";
import { DynamicResponseExample } from "/components/dynamic-response-example.jsx";
-Updates an existing plan. By default, creates a new version of the plan. See [Adding features to plans](/documentation/pricing/plan-features) for item configuration.
+Updates an existing plan. By default, creates a new version of the plan. See [Adding features to plans](/documentation/concepts/plan-items) for item configuration.
Updates create a new plan version by default. Existing customers remain on their current version until their subscription renews or they explicitly upgrade.
diff --git a/apps/docs/mintlify/cli/getting-started.mdx b/apps/docs/mintlify/cli/getting-started.mdx
index 661f98c91..03201c41c 100644
--- a/apps/docs/mintlify/cli/getting-started.mdx
+++ b/apps/docs/mintlify/cli/getting-started.mdx
@@ -61,7 +61,7 @@ You'll be prompted to choose a starter template:
| **Linear** | Per-seat pricing with team limits |
| **OpenAI** | Credit system mapping multiple AI models |
-Pick whichever is closest to your use case, or start from scratch and build your own using the [config reference](/api-reference/cli/config).
+Pick whichever is closest to your use case, or start from scratch and build your own using the [config reference](/cli/config).
## Push and pull
@@ -115,7 +115,7 @@ Learn how to define features, plans, and pricing in your `autumn.config.ts`.
Complete reference for features, plans, and plan features
diff --git a/apps/docs/mintlify/documentation/concepts/subscriptions.mdx b/apps/docs/mintlify/documentation/concepts/subscriptions.mdx
index 3552a2b15..5052e0ddb 100644
--- a/apps/docs/mintlify/documentation/concepts/subscriptions.mdx
+++ b/apps/docs/mintlify/documentation/concepts/subscriptions.mdx
@@ -12,7 +12,7 @@ flowchart LR
S -->|provisions| B[Balances]
```
-When a subscription is created, Autumn provisions [balances](/documentation/customers/balances) for each feature in the plan. Balances determine what the customer can access and track how much they've used. For example, a Pro plan might grant 1,000 API requests per month—this becomes a balance that decrements as the customer uses your product.
+When a subscription is created, Autumn provisions [balances](/documentation/concepts/balances) for each feature in the plan. Balances determine what the customer can access and track how much they've used. For example, a Pro plan might grant 1,000 API requests per month—this becomes a balance that decrements as the customer uses your product.
Balances can also be created for [credit systems](/documentation/modelling-pricing/credit-systems) (eg, $10 credits per month) and boolean toggle features (eg, access to a premium analytics dashboard).
@@ -27,10 +27,10 @@ Balances can also be created for [credit systems](/documentation/modelling-prici
| `expired` | Subscription has ended |
-
+
Learn about the checkout and payment flow
-
+
Handle upgrades, downgrades and cancellations
diff --git a/apps/docs/mintlify/documentation/customers/feature-entities.mdx b/apps/docs/mintlify/documentation/customers/feature-entities.mdx
index 545c34f9d..f5908aa4a 100644
--- a/apps/docs/mintlify/documentation/customers/feature-entities.mdx
+++ b/apps/docs/mintlify/documentation/customers/feature-entities.mdx
@@ -10,7 +10,7 @@ Entities are sub accounts of a customer. For example, you may have a product tha
There are two ways you can handle entities depending on your use case.
1. **Entity-level balances**: this is the simplest way of handling entities, and keeps everything under 1 subscription. This is useful when all entities get the same features and limits.
-2. **Entity-level subscriptions**: if an entity can have its own subscription tiers (ie basic seats, pro seats), then you can attach a product directly at the entity level by passing the [entity ID](/api-reference/billing/billingAttach#body-entity-id) into an attach function. Each will be under its own subscription in Stripe with the billing cycles synced.
+2. **Entity-level subscriptions**: if an entity can have its own subscription tiers (ie basic seats, pro seats), then you can attach a product directly at the entity level by passing the [entity ID](/api-reference/billing/attach#body-entity-id) into an attach function. Each will be under its own subscription in Stripe with the billing cycles synced.
**Example**
diff --git a/apps/docs/mintlify/documentation/customers/managing-balances.mdx b/apps/docs/mintlify/documentation/customers/managing-balances.mdx
index dd7272aaa..2125c28cc 100644
--- a/apps/docs/mintlify/documentation/customers/managing-balances.mdx
+++ b/apps/docs/mintlify/documentation/customers/managing-balances.mdx
@@ -76,7 +76,7 @@ curl -X POST https://api.useautumn.com/v1/balances/create \
-See the [Create Balance API reference](/api-reference/features/create-balance) for all available parameters.
+See the [Create Balance API reference](/api-reference/balances/createBalance) for all available parameters.
## Updating Balances (API)
@@ -116,7 +116,7 @@ curl -X POST https://api.useautumn.com/v1/customers/user_123/balances \
-See the [Set Feature Balance API reference](/api-reference/features/set-feature-balances) for all available parameters.
+See the [Set Feature Balance API reference](/api-reference/balances/updateBalance) for all available parameters.
## Querying Balances
diff --git a/apps/docs/mintlify/documentation/customers/payment-flow.mdx b/apps/docs/mintlify/documentation/customers/payment-flow.mdx
index d12efb96c..e563c6645 100644
--- a/apps/docs/mintlify/documentation/customers/payment-flow.mdx
+++ b/apps/docs/mintlify/documentation/customers/payment-flow.mdx
@@ -196,4 +196,4 @@ The `billing.attach` response has two key fields:
**`payment_url`** — a URL the customer should be redirected to, or `null` if no redirect is needed.
-**`required_action`** — present when payment couldn't be processed automatically. See [Edge Cases](/documentation/customers/billing/edge-cases) for details on handling 3DS, payment failures, and retries.
+**`required_action`** — present when payment couldn't be processed automatically. See [Edge Cases](/documentation/customers/edge-cases) for details on handling 3DS, payment failures, and retries.
diff --git a/apps/docs/mintlify/documentation/customers/updating-subscriptions.mdx b/apps/docs/mintlify/documentation/customers/updating-subscriptions.mdx
index 3d6012d7c..b5555316b 100644
--- a/apps/docs/mintlify/documentation/customers/updating-subscriptions.mdx
+++ b/apps/docs/mintlify/documentation/customers/updating-subscriptions.mdx
@@ -168,7 +168,7 @@ By default, updating a subscription with billing changes will generate an invoic
### Previewing changes before executing
-Similar to [`billing.previewAttach`](/documentation/customers/attaching-plans#step-1-preview-the-change), you can use `billing.previewUpdate` to see exactly what will be charged before making changes. This returns line items and totals that you can display in a confirmation UI.
+Similar to [`billing.previewAttach`](/documentation/customers/payment-flow#step-1-preview-the-charge), you can use `billing.previewUpdate` to see exactly what will be charged before making changes. This returns line items and totals that you can display in a confirmation UI.
diff --git a/apps/docs/mintlify/documentation/modelling-pricing/add-ons.mdx b/apps/docs/mintlify/documentation/modelling-pricing/add-ons.mdx
index 61de922fa..f4024e26e 100644
--- a/apps/docs/mintlify/documentation/modelling-pricing/add-ons.mdx
+++ b/apps/docs/mintlify/documentation/modelling-pricing/add-ons.mdx
@@ -148,7 +148,7 @@ const { data } = await autumn.checkout({
## Cancelling add-ons
-Cancel an add-on using the same [cancel](/documentation/customers/managing-subscriptions#cancellations) flow:
+Cancel an add-on using the same [cancel](/documentation/customers/subscription-lifecycle#cancellations) flow:
diff --git a/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx b/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx
index 0503a6b17..81c0a377d 100644
--- a/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx
+++ b/apps/docs/mintlify/documentation/modelling-pricing/recurring.mdx
@@ -56,7 +56,7 @@ Push changes with `atmn push`.
## Attaching a subscription
-Use [billing.attach](/documentation/customers/attaching-plans) to attach a subscription to a customer. With `redirectMode: "always"`, a checkout URL is always returned for the customer to complete payment or confirm the plan change.
+Use [billing.attach](/documentation/customers/payment-flow) to attach a subscription to a customer. With `redirectMode: "always"`, a checkout URL is always returned for the customer to complete payment or confirm the plan change.
@@ -212,7 +212,7 @@ This is useful when you want to offer an annual discount while still metering us
## Managing subscriptions
-Once a customer has an active subscription, you can manage upgrades, downgrades, and cancellations. See [Managing Subscriptions](/documentation/customers/s) for details on:
+Once a customer has an active subscription, you can manage upgrades, downgrades, and cancellations. See [Managing Subscriptions](/documentation/customers/subscription-lifecycle) for details on:
- **Upgrades** — prorated charges for switching to a higher-priced plan
- **Downgrades** — scheduled at end of billing period
diff --git a/apps/docs/mintlify/documentation/modelling-pricing/sub-entity-plans.mdx b/apps/docs/mintlify/documentation/modelling-pricing/sub-entity-plans.mdx
index 4f3bced93..cc27369f7 100644
--- a/apps/docs/mintlify/documentation/modelling-pricing/sub-entity-plans.mdx
+++ b/apps/docs/mintlify/documentation/modelling-pricing/sub-entity-plans.mdx
@@ -198,7 +198,7 @@ curl -X POST "https://api.useautumn.com/v1/check" \
## Upgrading an entity's plan
-To upgrade or downgrade an entity, attach the new plan with the same `entity_id`. The same [upgrade/downgrade](/documentation/customers/billing/subscription-lifecycle) logic applies:
+To upgrade or downgrade an entity, attach the new plan with the same `entity_id`. The same [upgrade/downgrade](/documentation/customers/subscription-lifecycle) logic applies:
@@ -233,7 +233,7 @@ curl -X POST "https://api.useautumn.com/v1/billing.attach" \
## Cancelling an entity's plan
-Use `billing.update` with `cancelAction` to cancel an entity's plan. The same [cancel/uncancel](/documentation/customers/billing/subscription-lifecycle#cancellations) behavior applies.
+Use `billing.update` with `cancelAction` to cancel an entity's plan. The same [cancel/uncancel](/documentation/customers/subscription-lifecycle#cancellations) behavior applies.
diff --git a/apps/docs/mintlify/react/hooks/useCustomer.mdx b/apps/docs/mintlify/react/hooks/useCustomer.mdx
index f8952d158..6b471c16d 100644
--- a/apps/docs/mintlify/react/hooks/useCustomer.mdx
+++ b/apps/docs/mintlify/react/hooks/useCustomer.mdx
@@ -126,7 +126,7 @@ export default function UpgradeButton() {
Open checkout URL in a new tab instead of redirecting.
-See the [API reference](/api-reference/billing/billingAttach) for all available parameters.
+See the [API reference](/api-reference/billing/attach) for all available parameters.
### `check()`
diff --git a/apps/docs/mintlify/understanding.mdx b/apps/docs/mintlify/understanding.mdx
deleted file mode 100644
index f3e1202e1..000000000
--- a/apps/docs/mintlify/understanding.mdx
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: Understanding how Autumn works
-sidebarTitle: Understand Autumn
-description: "Autumn is built to manage your feature access and your billing together. Learn how it all works."
----
-
-## The Autumn flow
-
-The typical Autumn flow looks like this:
-
-
-
-The dashboard is where you define which of your users get access to what, and how much they're charged for it. Here, you'll [create products](/products/create-product) and [manage your customers](/customers/managing-customers).
-
-
-From your application, when your customer wants to [purchase](/products/enabling-product) one of your price plans, the `attach` endpoint will return a Stripe checkout URL.
-
-Once they've purchased, Autumn grants access to the product's features that were defined in the dashboard.
-
-If the customer is already paying for a plan, this will automatically handle any [upgrades and downgrades](products/enabling-product#upgrades-and-downgrades) too.
-
-
-
-
-From your application, you can [check](/features/check) in real-time whether a customer has access to:
-
-- a product (eg, pro tier)
-- a feature balance (eg, 10 remaining credits)
-- a feature flag (eg, premium AI models)
-
-Autumn will return whether they're `allowed` access, and you can use this to control access to your features. When you want to update your pricing model, you can do so without changing any of your code.
-
-
-
-
-For your usage-based features, if Autumn tells you they're `allowed` access, you can [track their usage](/features/tracking-usage) to update their balance.
-
-This means Autumn can enforce any usage limits you set (ie, by returning `allowed: false` once they hit a balance of 0). It also allows you to charge for usage (eg, $1 per AI tokens used), if you have a usage-based pricing model.
-
-
-
-
-When using usage-based features, you'll typically want to [display the customer's balance](/api-reference/customers/get) to them. You can do this from the `customers` route or hook, which will return customer information, the product they've purchased, and the balance of any usage-based features.
-
-
-
-
-This means all of the logic that you would typically have to build yourself is done for you.
-
-- How do I limit usage of this feature to 10 per month, then reset?
-- How would I bill users who go over the limit?
-- How do I build in-app flows for upgrading, downgrading, cancelling?
-- What if I want add-ons, or credits?
-- My billing is at the org level. How do I track feature limits at the user level?
-- What happens to their usage if they upgrade, or downgrade, or cancel?
-- How can I block access if payment fails?
-- How would I migrate people if I change my pricing?
-- What should I do if I want a custom plan for different customers?
-
-Autumn takes care of all of this, and more.
-
-## Why use Autumn?
-
-Autumn is loved by developers, founders and fast-growing startups because it's easy to integrate and **billing never has to be touched again**.
-
-1. **The simplest integration you'll find** - Just 2 API calls to set up any pricing model. You don't need to deal with webhooks, state syncing, or deal with any of Stripe's APIs.
-2. **Enforce access limits** - Autumn knows who's paying for which product and what features they have access to. Just [check](/features/check.mdx) if a customer has access, then [track](/features/tracking-usage.mdx) any usage if needed.
-3. **Billing flows handled** - Upgrades, downgrades, failed payments, and more. Autumn takes care of it all, with customizable UI components that "just work" out of the box.
-4. **Make changes without touching code** - Make pricing changes, set custom plans, or launch new types of pricing without going through engineering. Versioning, grandfathering and migrations is easy through the Autumn dashboard.
-
-## What Autumn isn't
-
-- A Stripe billing replacement: although you don't need to deal with Stripe's APIs, you are still using Stripe's subscriptions, payments and invoicing.
-- Another metered billing product: Autumn easily handles usage-based billing up to 100 events per second, but there are many providers out there that specialize if you need more.
-- For purely sales-led companies: Autumn is designed to be used by product-led, or hybrid product-sales teams. If your customers only pay via custom invoices, Autumn is probably not for you.
-
-Now that you understand how Autumn works, check out one of the quickstart guides to get started!
diff --git a/bun.lock b/bun.lock
index 07b58e541..e87ea1966 100644
--- a/bun.lock
+++ b/bun.lock
@@ -24,6 +24,7 @@
"dotenv": "^16.6.1",
"husky": "^9.1.7",
"inquirer": "^12.10.0",
+ "knip": "^6.7.0",
"ts-to-zod": "^5.1.0",
},
},
@@ -995,9 +996,9 @@
"@ecies/ciphers": ["@ecies/ciphers@0.2.6", "", { "peerDependencies": { "@noble/ciphers": "^1.0.0" } }, "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g=="],
- "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+ "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
- "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+ "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
@@ -1305,7 +1306,7 @@
"@mswjs/interceptors": ["@mswjs/interceptors@0.41.6", "", { "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-qmDvJIjcNsZ6tXWy2G9yuCgMPTTn35GMA3dPpSLm7QJVpbQzYdw0ALy1bKoivXnEM3U93/OrK+/M719b+fg84Q=="],
- "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@next/env": ["@next/env@16.2.4", "", {}, "sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw=="],
@@ -1505,6 +1506,88 @@
"@oslojs/jwt": ["@oslojs/jwt@0.2.0", "", { "dependencies": { "@oslojs/encoding": "0.4.1" } }, "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg=="],
+ "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.127.0", "", { "os": "android", "cpu": "arm" }, "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ=="],
+
+ "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.127.0", "", { "os": "android", "cpu": "arm64" }, "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg=="],
+
+ "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.127.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg=="],
+
+ "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.127.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw=="],
+
+ "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.127.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA=="],
+
+ "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ=="],
+
+ "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.127.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g=="],
+
+ "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ=="],
+
+ "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.127.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA=="],
+
+ "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.127.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ=="],
+
+ "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ=="],
+
+ "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.127.0", "", { "os": "linux", "cpu": "none" }, "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g=="],
+
+ "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.127.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q=="],
+
+ "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ=="],
+
+ "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.127.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg=="],
+
+ "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.127.0", "", { "os": "none", "cpu": "arm64" }, "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ=="],
+
+ "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.127.0", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ=="],
+
+ "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.127.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw=="],
+
+ "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.127.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw=="],
+
+ "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.127.0", "", { "os": "win32", "cpu": "x64" }, "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.127.0", "", {}, "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ=="],
+
+ "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.19.1", "", { "os": "android", "cpu": "arm" }, "sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg=="],
+
+ "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.19.1", "", { "os": "android", "cpu": "arm64" }, "sha512-oolbkRX+m7Pq2LNjr/kKgYeC7bRDMVTWPgxBGMjSpZi/+UskVo4jsMU3MLheZV55jL6c3rNelPl4oD60ggYmqA=="],
+
+ "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.19.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-nUC6d2i3R5B12sUW4O646qD5cnMXf2oBGPLIIeaRfU9doJRORAbE2SGv4eW6rMqhD+G7nf2Y8TTJTLiiO3Q/dQ=="],
+
+ "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.19.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-cV50vE5+uAgNcFa3QY1JOeKDSkM/9ReIcc/9wn4TavhW/itkDGrXhw9jaKnkQnGbjJ198Yh5nbX/Gr2mr4Z5jQ=="],
+
+ "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.19.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xZOQiYGFxtk48PBKff+Zwoym7ScPAIVp4c14lfLxizO2LTTTJe5sx9vQNGrBymrf/vatSPNMD4FgsaaRigPkqw=="],
+
+ "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-lXZYWAC6kaGe/ky2su94e9jN9t6M0/6c+GrSlCqL//XO1cxi5lpAhnJYdyrKfm0ZEr/c7RNyAx3P7FSBcBd5+A=="],
+
+ "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.19.1", "", { "os": "linux", "cpu": "arm" }, "sha512-veG1kKsuK5+t2IsO9q0DErYVSw2azvCVvWHnfTOS73WE0STdLLB7Q1bB9WR+yHPQM76ASkFyRbogWo1GR1+WbQ=="],
+
+ "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-heV2+jmXyYnUrpUXSPugqWDRpnsQcDm2AX4wzTuvgdlZfoNYO0O3W2AVpJYaDn9AG4JdM6Kxom8+foE7/BcSig=="],
+
+ "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.19.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jvo2Pjs1c9KPxMuMPIeQsgu0mOJF9rEb3y3TdpsrqwxRM+AN6/nDDwv45n5ZrUnQMsdBy5gIabioMKnQfWo9ew=="],
+
+ "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.19.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vLmdNxWCdN7Uo5suays6A/+ywBby2PWBBPXctWPg5V0+eVuzsJxgAn6MMB4mPlshskYbppjpN2Zg83ArHze9gQ=="],
+
+ "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-/b+WgR+VTSBxzgOhDO7TlMXC1ufPIMR6Vj1zN+/x+MnyXGW7prTLzU9eW85Aj7Th7CCEG9ArCbTeqxCzFWdg2w=="],
+
+ "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.19.1", "", { "os": "linux", "cpu": "none" }, "sha512-YlRdeWb9j42p29ROh+h4eg/OQ3dTJlpHSa+84pUM9+p6i3djtPz1q55yLJhgW9XfDch7FN1pQ/Vd6YP+xfRIuw=="],
+
+ "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.19.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-EDpafVOQWF8/MJynsjOGFThcqhRHy417sRyLfQmeiamJ8qVhSKAn2Dn2VVKUGCjVB9C46VGjhNo7nOPUi1x6uA=="],
+
+ "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-NxjZe+rqWhr+RT8/Ik+5ptA3oz7tUw361Wa5RWQXKnfqwSSHdHyrw6IdcTfYuml9dM856AlKWZIUXDmA9kkiBQ=="],
+
+ "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.19.1", "", { "os": "linux", "cpu": "x64" }, "sha512-cM/hQwsO3ReJg5kR+SpI69DMfvNCp+A/eVR4b4YClE5bVZwz8rh2Nh05InhwI5HR/9cArbEkzMjcKgTHS6UaNw=="],
+
+ "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.19.1", "", { "os": "none", "cpu": "arm64" }, "sha512-QF080IowFB0+9Rh6RcD19bdgh49BpQHUW5TajG1qvWHvmrQznTZZjYlgE2ltLXyKY+qs4F/v5xuX1XS7Is+3qA=="],
+
+ "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.19.1", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-w8UCKhX826cP/ZLokXDS6+milN8y4X7zidsAttEdWlVoamTNf6lhBJldaWr3ukTDiye7s4HRcuPEPOXNC432Vg=="],
+
+ "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.19.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-nJ4AsUVZrVKwnU/QRdzPCCrO0TrabBqgJ8pJhXITdZGYOV28TIYystV1VFLbQ7DtAcaBHpocT5/ZJnF78YJPtQ=="],
+
+ "@oxc-resolver/binding-win32-ia32-msvc": ["@oxc-resolver/binding-win32-ia32-msvc@11.19.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-EW+ND5q2Tl+a3pH81l1QbfgbF3HmqgwLfDfVithRFheac8OTcnbXt/JxqD2GbDkb7xYEqy1zNaVFRr3oeG8npA=="],
+
+ "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.19.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6hIU3RQu45B+VNTY4Ru8ppFwjVS/S5qwYyGhBotmjxfEKk41I2DlGtRfGJndZ5+6lneE2pwloqunlOyZuX/XAw=="],
+
"@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="],
"@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="],
@@ -3399,6 +3482,8 @@
"favicons": ["favicons@7.2.0", "", { "dependencies": { "escape-html": "^1.0.3", "sharp": "^0.33.1", "xml2js": "^0.6.1" } }, "sha512-k/2rVBRIRzOeom3wI9jBPaSEvoTSQEW4iM0EveBmBBKFxO8mSyyRWtDlfC3VnEfu0avmjrMzy8/ZFPSe6F71Hw=="],
+ "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="],
+
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
@@ -3453,6 +3538,8 @@
"format": ["format@0.2.2", "", {}, "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww=="],
+ "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="],
+
"formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
@@ -3989,6 +4076,8 @@
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
+ "knip": ["knip@6.7.0", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.6.0", "minimist": "^1.2.8", "oxc-parser": "^0.127.0", "oxc-resolver": "^11.19.1", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.16", "unbash": "^3.0.0", "yaml": "^2.8.2", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-ckL51NDH1YJxnv1kNB0iUdDngB4f/e9Igz8uIqYfmNDoyOFmmk1V0WFv3LQ7/hzC63b2Z9X41gGUE9eOWrZpaA=="],
+
"ksuid": ["ksuid@3.0.0", "", { "dependencies": { "base-convert-int-array": "^1.0.1" } }, "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA=="],
"kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
@@ -4485,6 +4574,10 @@
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
+ "oxc-parser": ["oxc-parser@0.127.0", "", { "dependencies": { "@oxc-project/types": "^0.127.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.127.0", "@oxc-parser/binding-android-arm64": "0.127.0", "@oxc-parser/binding-darwin-arm64": "0.127.0", "@oxc-parser/binding-darwin-x64": "0.127.0", "@oxc-parser/binding-freebsd-x64": "0.127.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", "@oxc-parser/binding-linux-arm64-musl": "0.127.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-gnu": "0.127.0", "@oxc-parser/binding-linux-x64-musl": "0.127.0", "@oxc-parser/binding-openharmony-arm64": "0.127.0", "@oxc-parser/binding-wasm32-wasi": "0.127.0", "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", "@oxc-parser/binding-win32-x64-msvc": "0.127.0" } }, "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA=="],
+
+ "oxc-resolver": ["oxc-resolver@11.19.1", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.19.1", "@oxc-resolver/binding-android-arm64": "11.19.1", "@oxc-resolver/binding-darwin-arm64": "11.19.1", "@oxc-resolver/binding-darwin-x64": "11.19.1", "@oxc-resolver/binding-freebsd-x64": "11.19.1", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.19.1", "@oxc-resolver/binding-linux-arm-musleabihf": "11.19.1", "@oxc-resolver/binding-linux-arm64-gnu": "11.19.1", "@oxc-resolver/binding-linux-arm64-musl": "11.19.1", "@oxc-resolver/binding-linux-ppc64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-gnu": "11.19.1", "@oxc-resolver/binding-linux-riscv64-musl": "11.19.1", "@oxc-resolver/binding-linux-s390x-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-gnu": "11.19.1", "@oxc-resolver/binding-linux-x64-musl": "11.19.1", "@oxc-resolver/binding-openharmony-arm64": "11.19.1", "@oxc-resolver/binding-wasm32-wasi": "11.19.1", "@oxc-resolver/binding-win32-arm64-msvc": "11.19.1", "@oxc-resolver/binding-win32-ia32-msvc": "11.19.1", "@oxc-resolver/binding-win32-x64-msvc": "11.19.1" } }, "sha512-qE/CIg/spwrTBFt5aKmwe3ifeDdLfA2NESN30E42X/lII5ClF8V7Wt6WIJhcGZjp0/Q+nQ+9vgxGk//xZNX2hg=="],
+
"p-any": ["p-any@4.0.0", "", { "dependencies": { "p-cancelable": "^3.0.0", "p-some": "^6.0.0" } }, "sha512-S/B50s+pAVe0wmEZHmBs/9yJXeZ5KhHzOsgKzt0hRdgkoR3DxW9ts46fcsWi/r3VnzsnkKS7q4uimze+zjdryw=="],
"p-cancelable": ["p-cancelable@4.0.1", "", {}, "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg=="],
@@ -5043,6 +5136,8 @@
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
+ "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="],
+
"socket.io": ["socket.io@4.8.3", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A=="],
"socket.io-adapter": ["socket.io-adapter@2.5.6", "", { "dependencies": { "debug": "~4.4.1", "ws": "~8.18.3" } }, "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ=="],
@@ -5143,7 +5238,7 @@
"strip-indent": ["strip-indent@4.1.1", "", {}, "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA=="],
- "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
"stripe": ["stripe@19.3.0-beta.1", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=16" }, "optionalPeers": ["@types/node"] }, "sha512-tZLZYj2LPBt+FvYKQYRcop9IAvkDxIsqealtgG2kJ9zVK0DAy8yqC5aXJZxJ+81I5mRWXXpyLKGu298/owkX8Q=="],
@@ -5381,6 +5476,8 @@
"uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
+ "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="],
+
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
@@ -5913,10 +6010,14 @@
"@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
"@fortawesome/fontawesome-svg-core/@fortawesome/fontawesome-common-types": ["@fortawesome/fontawesome-common-types@7.2.0", "", {}, "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw=="],
"@humanwhocodes/config-array/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+ "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
"@infisical/sdk/@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.600.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.600.0", "@aws-sdk/client-sso": "3.598.0", "@aws-sdk/client-sts": "3.600.0", "@aws-sdk/credential-provider-cognito-identity": "3.600.0", "@aws-sdk/credential-provider-env": "3.598.0", "@aws-sdk/credential-provider-http": "3.598.0", "@aws-sdk/credential-provider-ini": "3.598.0", "@aws-sdk/credential-provider-node": "3.600.0", "@aws-sdk/credential-provider-process": "3.598.0", "@aws-sdk/credential-provider-sso": "3.598.0", "@aws-sdk/credential-provider-web-identity": "3.598.0", "@aws-sdk/types": "3.598.0", "@smithy/credential-provider-imds": "^3.1.1", "@smithy/property-provider": "^3.1.1", "@smithy/types": "^3.1.0", "tslib": "^2.6.2" } }, "sha512-cC9uqmX0rgx1efiJGqeR+i0EXr8RQ5SAzH7M45WNBZpYiLEe6reWgIYJY9hmOxuaoMdWSi8kekuN3IjTIORRjw=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
@@ -6473,6 +6574,8 @@
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+ "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
+
"@useautumn/sdk/typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"@useautumn/sdk/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
@@ -6835,6 +6938,8 @@
"katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
+ "knip/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
"langchain/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"langium/chevrotain": ["chevrotain@12.0.0", "", { "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", "@chevrotain/regexp-to-ast": "12.0.0", "@chevrotain/types": "12.0.0", "@chevrotain/utils": "12.0.0" } }, "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ=="],
@@ -6879,6 +6984,8 @@
"mocha/js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
+ "mocha/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
"module-lookup-amd/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
"monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="],
@@ -6959,8 +7066,6 @@
"pino-pretty/pino-abstract-transport": ["pino-abstract-transport@3.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg=="],
- "pino-pretty/strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="],
-
"pkg-conf/find-up": ["find-up@6.3.0", "", { "dependencies": { "locate-path": "^7.1.0", "path-exists": "^5.0.0" } }, "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw=="],
"playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
@@ -8041,6 +8146,10 @@
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+ "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
+
+ "@unrs/resolver-binding-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"ajv-errors/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
@@ -8711,6 +8820,8 @@
"xo/@eslint/eslintrc/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
+ "xo/@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
"xo/@typescript-eslint/typescript-estree/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="],
"xo/eslint/@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="],
@@ -9017,6 +9128,8 @@
"@mintlify/prebuild/@mintlify/scraping/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=="],
+ "@mintlify/prebuild/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
"@mintlify/previewing/chokidar/readdirp/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
"@mintlify/previewing/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
@@ -9267,6 +9380,8 @@
"eslint-formatter-pretty/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+ "favicons/sharp/@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+
"find-cache-dir/pkg-dir/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="],
"find-cache-dir/pkg-dir/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
@@ -9399,6 +9514,8 @@
"xo/@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
+ "xo/eslint/@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
"xo/eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"xo/eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -9573,6 +9690,8 @@
"artillery/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
+ "atmn/eslint-plugin-react-hooks/eslint/@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
+
"atmn/eslint-plugin-react-hooks/eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"atmn/eslint-plugin-react-hooks/eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
diff --git a/knip.json b/knip.json
index 36422be54..d1163be75 100644
--- a/knip.json
+++ b/knip.json
@@ -1,30 +1,46 @@
{
- "$schema": "https://unpkg.com/knip@5/schema.json",
+ "$schema": "https://unpkg.com/knip@6/schema.json",
+ "exclude": [
+ "dependencies",
+ "unlisted",
+ "unresolved",
+ "binaries",
+ "catalog",
+ "exports",
+ "types",
+ "enumMembers",
+ "duplicates"
+ ],
+ "ignoreWorkspaces": [
+ "packages/atmn",
+ "packages/autumn-js",
+ "packages/openapi",
+ "packages/sdk"
+ ],
"workspaces": {
".": {
- "ignore": [".opencode/**"]
+ "entry": ["apps/scope-picker/**/*"]
},
"server": {
- "entry": ["tests/**/*.ts"],
- "project": ["src/**/*.ts", "tests/**/*.ts"],
- "ignore": [
- "src/_luaScriptsV2/**",
- "src/utils/importUtils/**",
- "src/utils/scriptUtils/**",
- "src/utils/workerUtils/**",
- "src/db/**"
+ "entry": [
+ "src/internal/customers/cusUtils/createNewCustomer.ts",
+ "src/utils/importUtils/addProductFromSubs.ts",
+ "src/utils/scriptUtils/readOnlyStripe.ts",
+ "src/utils/scriptUtils/scriptUtils.ts",
+ "src/utils/scriptUtils/getAll/getAllAutumnCustomers.ts",
+ "src/utils/scriptUtils/getAll/getAllOrgs.ts",
+ "tests/**/*.ts"
],
+ "project": ["src/**/*.ts", "tests/**/*.ts"],
"ignoreDependencies": ["@react-email/components", "@axiomhq/pino"]
},
"shared": {
- "entry": ["index.ts"],
- "project": ["**/*.ts"],
- "includeEntryExports": false,
- "ignore": ["api/balances/track/changes/V0.2_TrackChange.ts"]
+ "project": ["**/*.ts", "!utils/**"],
+ "includeEntryExports": false
},
"vite": {
- "entry": ["src/main.tsx"],
- "project": ["src/**/*.{ts,tsx}"],
+ "entry": ["tests/**/*.{ts,tsx}"],
+ "project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"],
"ignore": ["src/components/ai-elements/**"],
"ignoreDependencies": [
"tailwindcss",
@@ -37,11 +53,28 @@
"entry": ["**/*.{ts,js}"],
"project": ["**/*.{ts,js}"]
},
+ "apps/docs": {
+ "entry": ["**/*"],
+ "project": ["**/*.{ts,js,jsx,css,mdx}"]
+ },
+ "apps/sdk-test": {
+ "entry": ["app/**/*.{ts,tsx}", "lib/**/*.{ts,tsx}", "*.ts"],
+ "project": ["**/*.{ts,tsx}"]
+ },
+ "apps/website": {
+ "entry": ["app/**/*.{ts,tsx,mdx}", "components/**/*.{ts,tsx}", "content/**/*.mdx", "*.mjs"],
+ "project": ["**/*.{ts,tsx,mdx,mjs}"]
+ },
"apps/checkout": {
- "entry": ["src/main.tsx"],
"project": ["src/**/*.{ts,tsx}"],
- "ignore": ["src/components/ui/**", "src/hooks/**"],
"ignoreDependencies": ["shadcn", "tailwindcss", "tw-animate-css"]
+ },
+ "packages/ksuid": {
+ "project": ["src/**/*.ts"]
+ },
+ "packages/stripe-sync": {
+ "entry": ["scripts/**/*.ts"],
+ "project": ["src/**/*.ts", "scripts/**/*.ts"]
}
},
"ignoreBinaries": ["infisical", "lsof", "serve"]
diff --git a/package.json b/package.json
index e83ece9ef..bba7cf07f 100644
--- a/package.json
+++ b/package.json
@@ -142,6 +142,7 @@
"dotenv": "^16.6.1",
"husky": "^9.1.7",
"inquirer": "^12.10.0",
+ "knip": "^6.7.0",
"ts-to-zod": "^5.1.0"
}
}
diff --git a/scripts/migrations/validate-schema.ts b/scripts/migrations/validate-schema.ts
index 4a7d388ae..c32b23935 100644
--- a/scripts/migrations/validate-schema.ts
+++ b/scripts/migrations/validate-schema.ts
@@ -2,7 +2,9 @@ import { initDrizzle } from "@server/db/initDrizzle";
import { validateDbSchema } from "@server/db/validateDbSchema";
import { validateSqlFunctions } from "@server/db/validateSqlFunctions";
-const { db } = initDrizzle({ maxConnections: 5 });
+const { db } = initDrizzle({
+ maxConnections: Number(process.env.DB_MAX_CONNECTIONS ?? 5),
+});
// Check if --validate-content flag is passed
const validateContent = process.argv.includes("--validate-content");
diff --git a/server/.env.example b/server/.env.example
index 3d45661ab..77b5cd4f5 100644
--- a/server/.env.example
+++ b/server/.env.example
@@ -64,4 +64,3 @@ SVIX_API_KEY=
# ANTHROPIC
# This is used to generate singular / plural names for your features, which are used when in the dashboard and when you use Autumn UI components
ANTHROPIC_API_KEY=
-
diff --git a/server/package.json b/server/package.json
index eee69a59b..53f066b80 100644
--- a/server/package.json
+++ b/server/package.json
@@ -2,7 +2,7 @@
"name": "@autumn/server",
"version": "1.0.0",
"description": "",
- "main": "index.js",
+ "main": "src/index.ts",
"type": "module",
"scripts": {
"email": "email dev -p 3001",
diff --git a/server/src/external/autumn/autumnCliV2.ts b/server/src/external/autumn/autumnCliV2.ts
deleted file mode 100644
index 28f64bafa..000000000
--- a/server/src/external/autumn/autumnCliV2.ts
+++ /dev/null
@@ -1,542 +0,0 @@
-/** biome-ignore-all lint/suspicious/noExplicitAny: AutumnCliV2 is used for internal testing & scripts */
-import dotenv from "dotenv";
-
-dotenv.config();
-
-import {
- type ApiCustomerV3,
- type ApiEntityBillingControlsParams,
- type AttachBodyV0,
- type CancelBody,
- type CheckoutParams,
- type CheckoutResponseV0,
- type CheckParams,
- type CheckQuery,
- type CheckResponseV1,
- type CreateEntityParams,
- type CreateRewardProgram,
- CustomerExpand,
- EntityExpand,
- ErrCode,
- type OrgConfig,
- type RewardRedemption,
- type SetUsageParams,
- type TrackParams,
-} from "@autumn/shared";
-
-class AutumnError extends Error {
- message: string;
- code: string;
-
- constructor({ message, code }: { message: string; code: string }) {
- super(message);
- this.message = message;
- this.code = code;
- }
-
- toString(): string {
- return `${this.message} (code: ${this.code})`;
- }
-}
-
-/**
- * Robust Autumn API client (V2) with proper version handling
- *
- * Key improvements over V1:
- * - Properly respects x-api-version header for ALL requests
- * - No legacy v1Schema params
- * - Cleaner error handling
- * - Type-safe version parameter
- */
-export class AutumnCliV2 {
- private apiKey: string;
- public headers: Record;
- public baseUrl: string;
- public version?: string;
-
- constructor({
- apiKey,
- secretKey,
- baseUrl,
- version,
- orgConfig,
- liveUrl = false,
- }: {
- apiKey?: string;
- secretKey?: string;
- baseUrl?: string;
- version?: string;
- orgConfig?: Partial;
- liveUrl?: boolean;
- } = {}) {
- this.apiKey =
- apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || "";
-
- this.headers = {
- Authorization: `Bearer ${this.apiKey}`,
- "Content-Type": "application/json",
- };
-
- this.version = version;
-
- if (version) {
- this.headers["x-api-version"] = version;
- }
-
- if (orgConfig) {
- this.headers["org-config"] = JSON.stringify(orgConfig);
- }
-
- this.baseUrl =
- baseUrl ||
- (liveUrl ? "https://api.useautumn.com/v1" : "http://localhost:8080/v1");
- }
-
- async get(path: string) {
- const response = await fetch(`${this.baseUrl}${path}`, {
- headers: this.headers,
- });
-
- if (response.status !== 200) {
- let error: any;
- try {
- error = await response.json();
- } catch (_e) {
- throw new AutumnError({
- message: `GET ${path} failed with status ${response.status}`,
- code: ErrCode.InternalError,
- });
- }
-
- throw new AutumnError({
- message: error.message || `GET ${path} failed`,
- code: error.code || ErrCode.InternalError,
- });
- }
-
- return response.json();
- }
-
- async post(path: string, body: any) {
- const response = await fetch(`${this.baseUrl}${path}`, {
- method: "POST",
- headers: this.headers,
- body: JSON.stringify(body),
- });
-
- if (response.status !== 200) {
- let error: any;
- try {
- error = await response.json();
- } catch (_e) {
- throw new AutumnError({
- message: `POST ${path} failed with status ${response.status}`,
- code: ErrCode.InternalError,
- });
- }
-
- throw new AutumnError({
- message: error.message || `POST ${path} failed`,
- code: error.code || ErrCode.InternalError,
- });
- }
-
- return response.json();
- }
-
- async patch(path: string, body: any) {
- const response = await fetch(`${this.baseUrl}${path}`, {
- method: "PATCH",
- headers: this.headers,
- body: JSON.stringify(body),
- });
-
- if (response.status !== 200) {
- let error: any;
- try {
- error = await response.json();
- } catch (_e) {
- throw new AutumnError({
- message: `PATCH ${path} failed with status ${response.status}`,
- code: ErrCode.InternalError,
- });
- }
-
- throw new AutumnError({
- message: error.message || `PATCH ${path} failed`,
- code: error.code || ErrCode.InternalError,
- });
- }
-
- return response.json();
- }
-
- async delete(
- path: string,
- {
- deleteInStripe = false,
- }: {
- deleteInStripe?: boolean;
- } = {},
- ) {
- const queryParams = deleteInStripe ? "?delete_in_stripe=true" : "";
- const response = await fetch(`${this.baseUrl}${path}${queryParams}`, {
- method: "DELETE",
- headers: this.headers,
- });
-
- if (response.status !== 200) {
- let error: any;
- try {
- error = await response.json();
- } catch (_e) {
- throw new AutumnError({
- message: `DELETE ${path} failed with status ${response.status}`,
- code: ErrCode.InternalError,
- });
- }
-
- throw new AutumnError({
- message: error.message || `DELETE ${path} failed`,
- code: error.code || ErrCode.InternalError,
- });
- }
-
- return response.json();
- }
-
- async createCustomer({
- id,
- email,
- name,
- fingerprint,
- }: {
- id: string;
- email: string;
- name: string;
- fingerprint?: string;
- }) {
- return await this.post("/customers", {
- id,
- email,
- name,
- fingerprint,
- });
- }
-
- async attach(params: AttachBodyV0) {
- return await this.post(`/attach`, params);
- }
-
- async checkout(
- params: CheckoutParams & { invoice?: boolean; force_checkout?: boolean },
- ) {
- const data = await this.post(`/checkout`, params);
- return data as CheckoutResponseV0;
- }
-
- async transfer(
- customerId: string,
- params: {
- from_entity_id?: string;
- to_entity_id: string;
- product_id: string;
- },
- ) {
- const data = await this.post(`/customers/${customerId}/transfer`, params);
- return data;
- }
-
- async sendEvent({
- customerId,
- eventName,
- properties,
- customer_data,
- idempotency_key,
- }: {
- customerId: string;
- eventName: string;
- properties?: any;
- customer_data?: any;
- idempotency_key?: string;
- }) {
- return await this.post(`/events`, {
- customer_id: customerId,
- event_name: eventName,
- properties,
- customer_data,
- idempotency_key,
- });
- }
-
- async entitled({
- customerId,
- featureId,
- quantity,
- customer_data,
- }: {
- customerId: string;
- featureId: string;
- quantity?: number;
- customer_data?: any;
- }) {
- return await this.post(`/entitled`, {
- customer_id: customerId,
- feature_id: featureId,
- quantity,
- customer_data,
- });
- }
-
- customers = {
- list: async (params?: { limit?: number; offset?: number }) => {
- const queryString = params
- ? `?${new URLSearchParams(params as Record).toString()}`
- : "";
- return await this.get(`/customers${queryString}`);
- },
-
- get: async (
- customerId: string,
- params?: {
- expand?: CustomerExpand[];
- },
- ): Promise<
- ApiCustomerV3 & {
- invoices: any[];
- }
- > => {
- const queryParams = new URLSearchParams();
- const defaultParams = {
- expand: [CustomerExpand.Invoices],
- };
-
- const finalParams = { ...defaultParams, ...params };
- if (finalParams.expand) {
- queryParams.append("expand", finalParams.expand.join(","));
- }
-
- return await this.get(
- `/customers/${customerId}?${queryParams.toString()}`,
- );
- },
-
- create: async (customer: { id: string; email?: string; name?: string }) => {
- return await this.post(`/customers?with_autumn_id=true`, customer);
- },
-
- delete: async (
- customerId: string,
- {
- deleteInStripe = false,
- }: {
- deleteInStripe?: boolean;
- } = {},
- ) => {
- return await this.delete(`/customers/${customerId}`, {
- deleteInStripe,
- });
- },
- };
-
- entities = {
- get: async (customerId: string, entityId: string) => {
- return await this.get(
- `/customers/${customerId}/entities/${entityId}?expand=${EntityExpand.Invoices}`,
- );
- },
-
- create: async (
- customerId: string,
- entity: CreateEntityParams | CreateEntityParams[],
- ) => {
- return await this.post(
- `/customers/${customerId}/entities?with_autumn_id=true`,
- entity,
- );
- },
-
- list: async (customerId: string) => {
- return await this.get(`/customers/${customerId}/entities`);
- },
-
- delete: async (customerId: string, entityId: string) => {
- return await this.delete(`/customers/${customerId}/entities/${entityId}`);
- },
-
- update: async (
- customerId: string,
- entityId: string,
- updates: {
- billing_controls?: ApiEntityBillingControlsParams;
- },
- ) => {
- return await this.post(`/entities.update`, {
- customer_id: customerId,
- entity_id: entityId,
- ...updates,
- });
- },
- };
-
- products = {
- /**
- * Get product - respects x-api-version header set in constructor
- */
- get: async (productId: string) => {
- return await this.get(`/products/${productId}`);
- },
-
- /**
- * Create product - respects x-api-version header
- */
- create: async (product: any) => {
- return await this.post(`/products`, product);
- },
-
- /**
- * Update product - respects x-api-version header
- */
- update: async (productId: string, product: any) => {
- return await this.post(`/products/${productId}`, product);
- },
-
- /**
- * Delete product
- */
- delete: async (productId: string) => {
- return await this.delete(`/products/${productId}`);
- },
-
- /**
- * List products - respects x-api-version header
- */
- list: async (params?: { limit?: number; offset?: number }) => {
- const queryString = params
- ? `?${new URLSearchParams(params as Record).toString()}`
- : "";
- return await this.get(`/products${queryString}`);
- },
- };
-
- rewards = {
- get: async (rewardId: string) => {
- return await this.get(`/rewards/${rewardId}`);
- },
-
- create: async (reward: any) => {
- return await this.post(`/rewards?legacyStripe=true`, reward);
- },
-
- delete: async (rewardId: string) => {
- return await this.delete(`/rewards/${rewardId}`);
- },
- };
-
- rewardPrograms = {
- create: async (rewardProgram: CreateRewardProgram) => {
- return await this.post(`/reward_programs`, rewardProgram);
- },
- };
-
- referrals = {
- createCode: async ({
- customerId,
- referralId,
- }: {
- customerId: string;
- referralId: string;
- }) => {
- return await this.post(`/referrals/code`, {
- customer_id: customerId,
- program_id: referralId,
- });
- },
-
- redeem: async ({
- customerId,
- code,
- }: {
- customerId: string;
- code: string;
- }) => {
- return await this.post(`/referrals/redeem`, {
- customer_id: customerId,
- code,
- });
- },
- };
-
- redemptions = {
- get: async (redemptionId: string) => {
- const data = await this.get(`/redemptions/${redemptionId}`);
- return data as RewardRedemption;
- },
- };
-
- events = {
- send: async ({
- customerId,
- featureId,
- value,
- properties,
- }: {
- customerId: string;
- featureId: string;
- value: number;
- properties?: any;
- }) => {
- return await this.post(`/events`, {
- customer_id: customerId,
- feature_id: featureId,
- value,
- properties,
- });
- },
- };
-
- stripe = {
- connect: async (params: {
- secret_key: string;
- success_url: string;
- default_currency: string;
- }) => {
- return await this.post(`/organization/stripe`, params);
- },
-
- delete: async () => {
- return await this.delete(`/organization/stripe`);
- },
- };
-
- track = async (params: TrackParams) => {
- return await this.post(`/track`, params);
- };
-
- usage = async (params: SetUsageParams) => {
- return await this.post(`/usage`, params);
- };
-
- check = async (
- params: CheckParams & CheckQuery,
- ): Promise => {
- return await this.post(`/check`, params);
- };
-
- attachPreview = async (params: AttachBodyV0) => {
- return await this.post(`/attach/preview`, params);
- };
-
- cancel = async (params: CancelBody) => {
- return await this.post(`/cancel`, params);
- };
-
- migrate = async (params: {
- from_product_id: string;
- to_product_id: string;
- from_version: number;
- to_version: number;
- }) => {
- return await this.post(`/migrations`, params);
- };
-}
diff --git a/server/src/external/redis/loadCaCert.ts b/server/src/external/redis/loadCaCert.ts
deleted file mode 100644
index 40e1d4844..000000000
--- a/server/src/external/redis/loadCaCert.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-export const loadCaCert = async ({
- caPath,
- type,
- caValue,
-}: {
- caPath?: string;
- type: "queue" | "cache";
- caValue?: string;
-}) => {
- try {
- if (caValue) {
- if (caValue.startsWith("-----BEGIN CERTIFICATE-----")) {
- return caValue;
- }
-
- return undefined;
- }
-
- const ca = Bun.file(caPath || `/etc/secrets/${type}-cert.pem`);
- const caText = await ca.text();
-
- return caText;
- } catch (_error) {
- return;
- }
-};
diff --git a/server/src/external/redis/redisFailover.ts b/server/src/external/redis/redisFailover.ts
deleted file mode 100644
index 5fcf9c670..000000000
--- a/server/src/external/redis/redisFailover.ts
+++ /dev/null
@@ -1,283 +0,0 @@
-import type { Redis } from "ioredis";
-import { logger } from "@/external/logtail/logtailUtils.js";
-
-// ── Config ──────────────────────────────────────────────────────────
-/** How long primary must stay down before we switch to failover. */
-const FAILOVER_THRESHOLD_MS = 60_000;
-
-/** How long primary must stay healthy before we switch back. */
-const RECOVERY_THRESHOLD_MS = 5_000;
-
-/** Health-check polling interval. */
-const POLL_INTERVAL_MS = 2_000;
-
-/** Log a warning if blip count exceeds this in the trailing window. */
-const BLIP_WARN_THRESHOLD = 10;
-
-/** Trailing window for blip counting. */
-const BLIP_WINDOW_MS = 60 * 60 * 1_000; // 1 hour
-
-// ── State machine ───────────────────────────────────────────────────
-type FailoverPhase = "NORMAL" | "DEGRADED" | "FAILOVER" | "RECOVERING";
-
-type FailoverState = {
- phase: FailoverPhase;
- active: Redis;
- primary: Redis;
- failover: Redis | null;
- failoverRegion: string | null;
- /** Timestamp when the current phase was entered. */
- phaseEnteredAt: number;
-};
-
-let state: FailoverState;
-let primaryHasBeenReady = false;
-let pollTimer: ReturnType | null = null;
-
-/** Tracks timestamps of recent transient blips (DEGRADED → NORMAL). */
-const blipTimestamps: number[] = [];
-
-// ── Callbacks ───────────────────────────────────────────────────────
-type StateChangeCallback = () => void;
-const onChangeCallbacks: StateChangeCallback[] = [];
-
-/** Register a callback invoked whenever `active` changes. */
-export const onActiveChange = (cb: StateChangeCallback): void => {
- onChangeCallbacks.push(cb);
-};
-
-const notifyChange = (): void => {
- for (const cb of onChangeCallbacks) {
- try {
- cb();
- } catch (err) {
- logger.error("[Redis failover] onActiveChange callback threw", {
- error: err,
- });
- }
- }
-};
-
-// ── Helpers ─────────────────────────────────────────────────────────
-const isPrimaryReady = (): boolean => state.primary.status === "ready";
-const isFailoverReady = (): boolean => state.failover?.status === "ready";
-
-const setPhase = (phase: FailoverPhase): void => {
- state.phase = phase;
- state.phaseEnteredAt = Date.now();
-};
-
-const msInPhase = (): number => Date.now() - state.phaseEnteredAt;
-
-const pruneBlips = (): void => {
- const cutoff = Date.now() - BLIP_WINDOW_MS;
- while (blipTimestamps.length > 0 && blipTimestamps[0] < cutoff) {
- blipTimestamps.shift();
- }
-};
-
-const recordBlip = ({ durationMs }: { durationMs: number }): void => {
- blipTimestamps.push(Date.now());
- pruneBlips();
-
- logger.warn(
- `[Redis failover] Primary blip #${blipTimestamps.length} (recovered in ${durationMs}ms)`,
- {
- type: "redis_failover_blip",
- blipCount: blipTimestamps.length,
- durationMs,
- },
- );
-
- if (blipTimestamps.length >= BLIP_WARN_THRESHOLD) {
- logger.error(
- `[Redis failover] ${blipTimestamps.length} blips in the last hour — check Redis health`,
- {
- type: "redis_failover_blip_alert",
- blipCount: blipTimestamps.length,
- },
- );
- }
-};
-
-// ── Core poll tick ──────────────────────────────────────────────────
-const tick = (): void => {
- const ready = isPrimaryReady();
-
- switch (state.phase) {
- case "NORMAL": {
- if (!ready && primaryHasBeenReady) {
- setPhase("DEGRADED");
- logger.warn("[Redis failover] Primary unhealthy — entering DEGRADED", {
- type: "redis_failover_degraded",
- primaryStatus: state.primary.status,
- });
- }
- break;
- }
-
- case "DEGRADED": {
- if (ready) {
- // Blip — primary recovered before we had to failover
- recordBlip({ durationMs: msInPhase() });
- setPhase("NORMAL");
- break;
- }
-
- if (msInPhase() >= FAILOVER_THRESHOLD_MS) {
- if (!state.failover || !isFailoverReady()) {
- logger.error(
- "[Redis failover] Threshold reached but failover instance not ready",
- {
- type: "redis_failover_switch",
- failoverStatus: state.failover?.status ?? "none",
- },
- );
- break;
- }
-
- state.active = state.failover;
- setPhase("FAILOVER");
- notifyChange();
-
- logger.error(
- `[Redis failover] SWITCHED to failover region (${state.failoverRegion})`,
- {
- type: "redis_failover_switch",
- failoverRegion: state.failoverRegion,
- },
- );
- }
- break;
- }
-
- case "FAILOVER": {
- if (ready) {
- setPhase("RECOVERING");
- logger.info("[Redis failover] Primary back — entering RECOVERING", {
- type: "redis_failover_recovering",
- });
- }
- break;
- }
-
- case "RECOVERING": {
- if (!ready) {
- // Primary dropped again — go back to failover
- setPhase("FAILOVER");
- logger.warn(
- "[Redis failover] Primary dropped during recovery — back to FAILOVER",
- { type: "redis_failover_recovery_failed" },
- );
- break;
- }
-
- if (msInPhase() >= RECOVERY_THRESHOLD_MS) {
- state.active = state.primary;
- setPhase("NORMAL");
- notifyChange();
-
- logger.info("[Redis failover] RECOVERED to primary region", {
- type: "redis_failover_recovered",
- });
- }
- break;
- }
- }
-};
-
-// ── Public API ──────────────────────────────────────────────────────
-
-/** Initialize failover. Call once after creating both Redis instances. */
-export const initFailover = ({
- primary,
- failover,
- failoverRegion,
- currentRegion,
-}: {
- primary: Redis;
- failover: Redis | null;
- failoverRegion: string | null;
- currentRegion: string;
-}): void => {
- state = {
- phase: "NORMAL",
- active: primary,
- primary,
- failover,
- failoverRegion,
- phaseEnteredAt: Date.now(),
- };
-
- if (!failover) {
- logger.info(
- "[Redis failover] No failover region configured — failover disabled",
- { type: "redis_failover_init" },
- );
- return;
- }
-
- logger.info(
- `[Redis failover] Enabled: primary=${currentRegion}, failover=${failoverRegion}`,
- { type: "redis_failover_init", currentRegion, failoverRegion },
- );
-
- // Track when primary first connects so we don't failover during startup
- primary.on("ready", () => {
- primaryHasBeenReady = true;
- });
-
- // Clear any existing state from a previous init
- if (pollTimer) {
- clearInterval(pollTimer);
- pollTimer = null;
- }
- primaryHasBeenReady = false;
- blipTimestamps.length = 0;
-
- // Start the single polling loop
- pollTimer = setInterval(tick, POLL_INTERVAL_MS);
-};
-
-/** Get the currently active Redis instance. */
-export const getActiveRedis = (): Redis => state.active;
-
-/** Get current failover state (for debug/monitoring). */
-export const getFailoverState = (): {
- phase: FailoverPhase;
- isUsingFailover: boolean;
- failoverRegion: string | null;
- primaryStatus: string;
- failoverStatus: string | null;
- msInPhase: number;
- blipsLastHour: number;
-} => {
- pruneBlips();
- return {
- phase: state.phase,
- isUsingFailover: state.phase === "FAILOVER" || state.phase === "RECOVERING",
- failoverRegion: state.failoverRegion,
- primaryStatus: state.primary.status,
- failoverStatus: state.failover?.status ?? null,
- msInPhase: msInPhase(),
- blipsLastHour: blipTimestamps.length,
- };
-};
-
-/** Force disconnect the primary (for testing). */
-export const disconnectPrimary = (): void => {
- state.primary.disconnect();
-};
-
-/** Force reconnect the primary (for testing). */
-export const reconnectPrimary = (): void => {
- state.primary.connect();
-};
-
-/** Stop the polling loop (for testing/cleanup). */
-export const stopFailoverPolling = (): void => {
- if (pollTimer) {
- clearInterval(pollTimer);
- pollTimer = null;
- }
-};
diff --git a/server/src/external/redis/utils/index.ts b/server/src/external/redis/utils/index.ts
deleted file mode 100644
index 471c3ea10..000000000
--- a/server/src/external/redis/utils/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export { RedisUnavailableError } from "./errors.js";
-export {
- runRedisOp,
- tryRedisOp,
- type UnavailableReason,
-} from "./runRedisOp.js";
-export { withRedisFailOpen } from "./withRedisFailOpen.js";
diff --git a/server/src/external/stripe/checkoutSessions/operations/getExpandedStripeCheckoutSession.ts b/server/src/external/stripe/checkoutSessions/operations/getExpandedStripeCheckoutSession.ts
deleted file mode 100644
index 76b3e74b8..000000000
--- a/server/src/external/stripe/checkoutSessions/operations/getExpandedStripeCheckoutSession.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type Stripe from "stripe";
-import { createStripeCli } from "@/external/connect/createStripeCli";
-import type { AutumnContext } from "@/honoUtils/HonoEnv";
-
-export const getStripeCheckoutSession = async ({
- ctx,
- checkoutSessionId,
-}: {
- ctx: AutumnContext;
- checkoutSessionId: string;
-}): Promise => {
- const { org, env } = ctx;
- const stripeCli = createStripeCli({ org, env });
-
- return stripeCli.checkout.sessions.retrieve(checkoutSessionId);
-};
diff --git a/server/src/external/stripe/subscriptions/subscriptionItems/index.ts b/server/src/external/stripe/subscriptions/subscriptionItems/index.ts
deleted file mode 100644
index 9952ae103..000000000
--- a/server/src/external/stripe/subscriptions/subscriptionItems/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { findSubscriptionItemByAutumnPrice } from "@/external/stripe/subscriptions/subscriptionItems/utils/findSubscriptionItemByAutumnPrice";
-
-export const stripeSubscriptionItemUtils = {
- find: {
- byAutumnPrice: findSubscriptionItemByAutumnPrice,
- },
-};
diff --git a/server/src/external/stripe/subscriptions/subscriptionItems/utils/findSubscriptionItemByAutumnPrice.ts b/server/src/external/stripe/subscriptions/subscriptionItems/utils/findSubscriptionItemByAutumnPrice.ts
deleted file mode 100644
index b2c46d828..000000000
--- a/server/src/external/stripe/subscriptions/subscriptionItems/utils/findSubscriptionItemByAutumnPrice.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import {
- InternalError,
- isFixedPrice,
- type Price,
- type Product,
- type UsagePriceConfig,
-} from "@autumn/shared";
-
-import type Stripe from "stripe";
-
-type FindSubscriptionItemParams = {
- stripeSubscriptionItems: Stripe.SubscriptionItem[];
- price: Price;
- product: Product;
-};
-
-// Overload: errorOnNotFound = true → guaranteed SubscriptionItem
-export function findSubscriptionItemByAutumnPrice(
- params: FindSubscriptionItemParams & { errorOnNotFound: true },
-): Stripe.SubscriptionItem;
-
-// Overload: errorOnNotFound = false/undefined → SubscriptionItem | undefined
-export function findSubscriptionItemByAutumnPrice(
- params: FindSubscriptionItemParams & { errorOnNotFound?: false },
-): Stripe.SubscriptionItem | undefined;
-
-// Implementation
-export function findSubscriptionItemByAutumnPrice({
- stripeSubscriptionItems,
- price,
- product,
- errorOnNotFound,
-}: FindSubscriptionItemParams & { errorOnNotFound?: boolean }):
- | Stripe.SubscriptionItem
- | undefined {
- const stripeProductId = product.processor?.id;
-
- let result: Stripe.SubscriptionItem | undefined;
-
- if (isFixedPrice(price)) {
- const config = price.config;
-
- result = stripeSubscriptionItems.find((si) => {
- return (
- config.stripe_price_id === si.price?.id ||
- (stripeProductId && si.price?.product === stripeProductId)
- );
- });
- } else {
- const config = price.config as UsagePriceConfig;
- result = stripeSubscriptionItems.find(
- (si: Stripe.SubscriptionItem | Stripe.LineItem) => {
- return (
- config.stripe_price_id === si.price?.id ||
- config.stripe_product_id === si.price?.product ||
- config.stripe_empty_price_id === si.price?.id ||
- config.stripe_prepaid_price_v2_id === si.price?.id
- );
- },
- );
- }
-
- if (errorOnNotFound && !result) {
- throw new InternalError({
- message: `Stripe subscription item not found for price: ${price.id}`,
- });
- }
-
- return result;
-}
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts
deleted file mode 100644
index f946492be..000000000
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts
+++ /dev/null
@@ -1,145 +0,0 @@
-import { ACTIVE_STATUSES } from "@autumn/shared";
-
-import type Stripe from "stripe";
-import { createStripeCli } from "@/external/connect/createStripeCli.js";
-import {
- submitBillingDataToVercel,
- submitInvoiceToVercel,
-} from "@/external/vercel/misc/vercelInvoicing.js";
-import { logVercelWebhook } from "@/external/vercel/misc/vercelMiddleware.js";
-import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
-import { FeatureService } from "@/internal/features/FeatureService.js";
-import { ProductService } from "@/internal/products/ProductService.js";
-import { stripeInvoiceToStripeSubscriptionId } from "../invoices/utils/convertStripeInvoice";
-import {
- getFullStripeInvoice,
- getStripeExpandedInvoice,
-} from "../stripeInvoiceUtils.js";
-import type { StripeWebhookContext } from "../webhookMiddlewares/stripeWebhookContext.js";
-
-/**
- * Handles invoice.finalized webhook
- *
- * For regular invoices: Creates Autumn invoice records
- * For Vercel custom payment method invoices: Submits invoice to Vercel marketplace for payment processing
- */
-export const handleInvoiceFinalized = async ({
- ctx,
-}: {
- ctx: StripeWebhookContext;
-}) => {
- const { db, org, env, logger, stripeEvent, stripeCli, fullCustomer } = ctx;
-
- const invoiceData = stripeEvent.data.object as Stripe.Invoice;
-
- const invoice = await getFullStripeInvoice({
- stripeCli,
- stripeId: invoiceData.id!,
- });
-
- const features = await FeatureService.list({
- db,
- orgId: org.id,
- env,
- });
-
- const subId = stripeInvoiceToStripeSubscriptionId(invoice);
-
- if (subId) {
- const stripeCli = createStripeCli({ org, env });
- // Handle Vercel custom payment method invoices
- if (subId && invoice.amount_due > 0) {
- const subscription = await stripeCli.subscriptions.retrieve(subId);
-
- const vercelInstallationId =
- subscription.metadata?.vercel_installation_id;
- const vercelBillingPlanId = subscription.metadata?.vercel_billing_plan_id;
-
- if (
- vercelInstallationId &&
- vercelBillingPlanId &&
- subscription.default_payment_method
- ) {
- const paymentMethod = await stripeCli.paymentMethods.retrieve(
- subscription.default_payment_method as string,
- );
-
- // Only process if it's a custom payment method (Vercel)
- if (paymentMethod.type === "custom" && fullCustomer) {
- logVercelWebhook({
- logger,
- org,
- event: {
- type: "marketplace.invoice.finalized",
- id: invoice.id,
- },
- });
-
- try {
- const product = await ProductService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: vercelBillingPlanId,
- });
-
- if (!product) {
- console.error("Product not found for Vercel billing plan", {
- billingPlanId: vercelBillingPlanId,
- });
- return;
- }
-
- // Submit billing data to Vercel (detailed usage breakdown)
- await submitBillingDataToVercel({
- installationId: vercelInstallationId,
- invoice,
- customer: fullCustomer,
- product,
- });
-
- // Submit invoice to Vercel
- await submitInvoiceToVercel({
- installationId: vercelInstallationId,
- invoice,
- customer: fullCustomer,
- product,
- org,
- features,
- });
-
- // Do NOT report payment to Stripe here - we've only submitted the invoice to Vercel
- // Vercel will process payment asynchronously and send marketplace.invoice.paid webhook
- // handleMarketplaceInvoicePaid will then:
- // 1. Create cus_product (user gets access)
- // 2. Report payment as "guaranteed" to Stripe
- // 3. Attach payment record to invoice (marks it as paid)
- } catch (error) {
- logger.error("Failed to process Vercel invoice", {
- data: {
- error: String(error),
- invoiceId: invoice.id,
- },
- });
- }
- }
- }
- }
- const expandedInvoice = await getStripeExpandedInvoice({
- stripeCli,
- stripeInvoiceId: invoice.id!,
- });
-
- const activeProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: subId,
- orgId: org.id,
- env,
- inStatuses: ACTIVE_STATUSES,
- });
-
- if (activeProducts.length === 0) {
- return;
- }
- }
-};
diff --git a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceFinalized/index.ts b/server/src/external/stripe/webhookHandlers/handleStripeInvoiceFinalized/index.ts
deleted file mode 100644
index 2475ba037..000000000
--- a/server/src/external/stripe/webhookHandlers/handleStripeInvoiceFinalized/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { handleStripeInvoiceFinalized } from "./handleStripeInvoiceFinalized";
-export type { InvoiceFinalizedContext } from "./setupInvoiceFinalizedContext";
diff --git a/server/src/internal/billing/attachPreview/attachParamsToChanges.ts b/server/src/internal/billing/attachPreview/attachParamsToChanges.ts
deleted file mode 100644
index 6677c7be6..000000000
--- a/server/src/internal/billing/attachPreview/attachParamsToChanges.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { type FullCusProduct, isPrepaidPrice } from "@autumn/shared";
-
-/**
- * Convert cusProduct.options to feature_quantities with actual quantities
- * (multiplied by billingUnits for prepaid features)
- */
-function cusProductToFeatureQuantities({
- cusProduct,
-}: {
- cusProduct: FullCusProduct;
-}) {
- return cusProduct.options.map((option) => {
- const cusPrice = cusProduct.customer_prices.find((cp) => {
- const cusEnt = cusProduct.customer_entitlements.find(
- (ce) =>
- ce.internal_feature_id === option.internal_feature_id ||
- ce.entitlement.feature_id === option.feature_id,
- );
- return (
- cusEnt &&
- cp.price.config.internal_feature_id ===
- cusEnt.entitlement.internal_feature_id
- );
- });
-
- let quantity = option.quantity;
-
- if (cusPrice && isPrepaidPrice(cusPrice.price)) {
- const billingUnits = cusPrice.price.config.billing_units ?? 1;
- quantity = option.quantity * billingUnits;
- }
-
- return {
- feature_id: option.feature_id,
- quantity,
- };
- });
-}
diff --git a/server/src/internal/billing/v2/actions/createSchedule/compute/buildCreateScheduleExecutionPlan.ts b/server/src/internal/billing/v2/actions/createSchedule/compute/buildCreateScheduleExecutionPlan.ts
deleted file mode 100644
index 2d909f8cd..000000000
--- a/server/src/internal/billing/v2/actions/createSchedule/compute/buildCreateScheduleExecutionPlan.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { AutumnBillingPlan } from "@autumn/shared";
-import type { MaterializedScheduledPhase } from "../utils/materializeScheduledPhases";
-
-/** Merge immediate billing changes with future scheduled rows for Autumn execution. */
-export const buildCreateScheduleExecutionPlan = ({
- immediateAutumnBillingPlan,
- futureScheduledPhases,
-}: {
- immediateAutumnBillingPlan: AutumnBillingPlan;
- futureScheduledPhases: MaterializedScheduledPhase[];
-}): AutumnBillingPlan => ({
- ...immediateAutumnBillingPlan,
- insertCustomerProducts: [
- ...immediateAutumnBillingPlan.insertCustomerProducts,
- ...futureScheduledPhases.flatMap((phase) => phase.customerProducts),
- ],
- customPrices: [
- ...(immediateAutumnBillingPlan.customPrices ?? []),
- ...futureScheduledPhases.flatMap((phase) => phase.customPrices),
- ],
- customEntitlements: [
- ...(immediateAutumnBillingPlan.customEntitlements ?? []),
- ...futureScheduledPhases.flatMap((phase) => phase.customEntitlements),
- ],
-});
diff --git a/server/src/internal/billing/v2/actions/createSchedule/utils/materializeScheduledPhases.ts b/server/src/internal/billing/v2/actions/createSchedule/utils/materializeScheduledPhases.ts
deleted file mode 100644
index 38ca6fcbf..000000000
--- a/server/src/internal/billing/v2/actions/createSchedule/utils/materializeScheduledPhases.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-import {
- addDuration,
- BillingVersion,
- type CreateScheduleParamsV0,
- CusProductStatus,
- type FullCustomer,
-} from "@autumn/shared";
-import type { AutumnContext } from "@/honoUtils/HonoEnv";
-import { setupFeatureQuantitiesContext } from "@/internal/billing/v2/setup/setupFeatureQuantitiesContext";
-import { initFullCustomerProduct } from "@/internal/billing/v2/utils/initFullCustomerProduct/initFullCustomerProduct";
-import { setupAttachProductContext } from "../../attach/setup/setupAttachProductContext";
-import { validateCreateSchedulePhasePlans } from "../errors/validateCreateSchedulePhasePlans";
-
-export type MaterializedScheduledPhase = {
- starts_at: number;
- customerProducts: Awaited>[];
- customPrices: NonNullable<
- Awaited>["customPrices"]
- >;
- customEntitlements: NonNullable<
- Awaited>["customEnts"]
- >;
-};
-
-/** Build scheduled customer products for future phases. */
-export const materializeScheduledPhases = async ({
- ctx,
- currentEpochMs,
- fullCustomer,
- phases,
-}: {
- ctx: AutumnContext;
- currentEpochMs: number;
- fullCustomer: FullCustomer;
- phases: CreateScheduleParamsV0["phases"][number][];
-}): Promise => {
- return await Promise.all(
- phases.map(async (phase, index) => {
- const nextPhaseStartsAt = phases[index + 1]?.starts_at;
- const materializedProducts = await Promise.all(
- phase.plans.map(async (plan) => {
- const {
- fullProduct,
- customPrices = [],
- customEnts: customEntitlements = [],
- } = await setupAttachProductContext({
- ctx,
- params: plan,
- });
- const trialEndsAt = fullProduct.free_trial
- ? addDuration({
- now: phase.starts_at,
- durationType: fullProduct.free_trial.duration,
- durationLength: fullProduct.free_trial.length,
- })
- : undefined;
- const featureQuantities = setupFeatureQuantitiesContext({
- ctx,
- featureQuantitiesParams: {
- feature_quantities: plan.feature_quantities,
- },
- fullProduct,
- initializeUndefinedQuantities: true,
- });
-
- return {
- fullProduct,
- customerProduct: initFullCustomerProduct({
- ctx,
- initContext: {
- fullCustomer,
- fullProduct,
- featureQuantities,
- resetCycleAnchor: phase.starts_at,
- freeTrial: fullProduct.free_trial ?? null,
- trialEndsAt,
- now: currentEpochMs,
- billingVersion: BillingVersion.V2,
- },
- initOptions: {
- startsAt: phase.starts_at,
- endedAt: nextPhaseStartsAt,
- status: CusProductStatus.Scheduled,
- isCustom:
- customPrices.length > 0 || customEntitlements.length > 0,
- },
- }),
- customPrices,
- customEntitlements,
- };
- }),
- );
- validateCreateSchedulePhasePlans({
- fullProducts: materializedProducts.map(
- ({ fullProduct }) => fullProduct,
- ),
- });
-
- return {
- starts_at: phase.starts_at,
- customerProducts: materializedProducts.map(
- ({ customerProduct }) => customerProduct,
- ),
- customPrices: materializedProducts.flatMap(
- ({ customPrices }) => customPrices,
- ),
- customEntitlements: materializedProducts.flatMap(
- ({ customEntitlements }) => customEntitlements,
- ),
- };
- }),
- );
-};
diff --git a/server/src/internal/billing/v2/actions/createSchedule/utils/resolveCurrentEpochMs.ts b/server/src/internal/billing/v2/actions/createSchedule/utils/resolveCurrentEpochMs.ts
deleted file mode 100644
index fa4587a49..000000000
--- a/server/src/internal/billing/v2/actions/createSchedule/utils/resolveCurrentEpochMs.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { getTestClockFrozenTimeMs } from "@/external/stripe/testClocks/utils/convertStripeTestClock";
-import type { AutumnContext } from "@/honoUtils/HonoEnv";
-import { CusService } from "@/internal/customers/CusService";
-
-/** Resolves "now" for schedule operations, respecting Stripe test clocks in sandbox. */
-export const resolveCurrentEpochMs = async ({
- ctx,
- customerId,
-}: {
- ctx: AutumnContext;
- customerId: string;
-}): Promise => {
- const customer = await CusService.get({
- db: ctx.db,
- idOrInternalId: customerId,
- orgId: ctx.org.id,
- env: ctx.env,
- });
- const testClockMs = await getTestClockFrozenTimeMs({
- ctx,
- stripeCustomerId: customer?.processor?.id,
- });
- return testClockMs ?? Date.now();
-};
diff --git a/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts b/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts
deleted file mode 100644
index 6dc53c1c7..000000000
--- a/server/src/internal/billing/v2/actions/updateSubscription/compute/updateQuantity/calculateUpdateQuantityEntitlementChange.ts
+++ /dev/null
@@ -1,59 +0,0 @@
-import {
- customerPriceToBillingUnits,
- type FullCustomerEntitlement,
- type FullCustomerPrice,
- priceToProrationConfig,
-} from "@autumn/shared";
-import { Decimal } from "decimal.js";
-
-/**
- * Calculates the entitlement balance change resulting from a quantity update.
- *
- * Computes balance change as: quantity_difference × billing_units_per_quantity.
- * Returns entitlement ID from price association, or undefined if price has no entitlement.
- *
- * @param quantityDifferenceForEntitlements - Change in quantity (can be negative)
- * @param billingUnitsPerQuantity - Multiplier for converting quantities to usage units
- * @param customerPrice - Customer's price configuration
- * @param customerEntitlements - Array of all entitlements for this customer product
- * @returns Entitlement ID and balance change to apply
- */
-export const calculateUpdateQuantityEntitlementChange = ({
- quantityDifferenceForEntitlements,
- customerPrice,
- customerEntitlement,
-}: {
- quantityDifferenceForEntitlements: number;
- customerPrice: FullCustomerPrice;
- customerEntitlement: FullCustomerEntitlement;
-}): {
- customerEntitlementId: string;
- customerEntitlementBalanceChange: number;
-} => {
- const isUpgrade = quantityDifferenceForEntitlements > 0;
-
- const { shouldApplyProration } = priceToProrationConfig({
- price: customerPrice.price,
- isUpgrade,
- });
-
- // If downgrade and no proration, don't change entitlement balance THIS cycle
- if (!isUpgrade && !shouldApplyProration) {
- return {
- customerEntitlementId: customerEntitlement?.id,
- customerEntitlementBalanceChange: 0,
- };
- }
-
- const billingUnits = customerPriceToBillingUnits({ customerPrice });
- const customerEntitlementBalanceChange = new Decimal(
- quantityDifferenceForEntitlements,
- )
- .mul(billingUnits)
- .toNumber();
-
- return {
- customerEntitlementId: customerEntitlement?.id,
- customerEntitlementBalanceChange,
- };
-};
diff --git a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleFeatureQuantityErrors.ts b/server/src/internal/billing/v2/actions/updateSubscription/errors/handleFeatureQuantityErrors.ts
deleted file mode 100644
index 07b4f5618..000000000
--- a/server/src/internal/billing/v2/actions/updateSubscription/errors/handleFeatureQuantityErrors.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { AutumnBillingPlan } from "@autumn/shared";
-import {
- cusProductToPrices,
- ErrCode,
- isOneOffPrice,
- isPrepaidPrice,
- RecaseError,
- type UsagePriceConfig,
-} from "@autumn/shared";
-
-export const handleFeatureQuantityErrors = ({
- autumnBillingPlan,
-}: {
- autumnBillingPlan: AutumnBillingPlan;
-}) => {
- const newCustomerProduct = autumnBillingPlan.insertCustomerProducts?.[0];
- if (!newCustomerProduct) return;
-
- const newPrices = cusProductToPrices({ cusProduct: newCustomerProduct });
- const prepaidPrices = newPrices.filter(
- (p) => isPrepaidPrice(p) && !isOneOffPrice(p),
- );
-
- 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;
-
- // Check if there's an option for this prepaid price
- const hasOption = options.some(
- (opt) => opt.internal_feature_id === internalFeatureId,
- );
-
- if (!hasOption) {
- // Try to find the feature_id from customer_entitlements
- 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,
- });
- }
-};
diff --git a/server/src/internal/billing/v2/utils/billingContext/buildMinimalBillingContext.ts b/server/src/internal/billing/v2/utils/billingContext/buildMinimalBillingContext.ts
deleted file mode 100644
index 7d4de5633..000000000
--- a/server/src/internal/billing/v2/utils/billingContext/buildMinimalBillingContext.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { BillingVersion, type FullCustomer } from "@autumn/shared";
-import type Stripe from "stripe";
-
-/** Build a minimal BillingContext with just the fields createInvoiceForBilling needs. */
-export const buildMinimalBillingContext = ({
- fullCustomer,
- stripeCustomerId,
- paymentMethod,
-}: {
- fullCustomer: FullCustomer;
- stripeCustomerId: string;
- paymentMethod: Stripe.PaymentMethod;
-}) => ({
- fullCustomer,
- fullProducts: [],
- featureQuantities: [],
- currentEpochMs: Date.now(),
- billingCycleAnchorMs: "now" as const,
- resetCycleAnchorMs: "now" as const,
- stripeCustomer: { id: stripeCustomerId } as Stripe.Customer,
- paymentMethod,
- billingVersion: BillingVersion.V2,
-});
diff --git a/server/src/internal/billing/v2/utils/billingContextToCheckoutResponse.ts b/server/src/internal/billing/v2/utils/billingContextToCheckoutResponse.ts
deleted file mode 100644
index 034405faf..000000000
--- a/server/src/internal/billing/v2/utils/billingContextToCheckoutResponse.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-import type { BillingContext, BillingPlan } from "@autumn/shared";
-import {
- type CheckoutLineV0,
- type CheckoutResponseV0,
- CheckoutResponseV0Schema,
- orgToCurrency,
- toProductItem,
-} from "@autumn/shared";
-import { Decimal } from "decimal.js";
-import type { AutumnContext } from "@/honoUtils/HonoEnv";
-import { getPriceEntitlement } from "@/internal/products/prices/priceUtils";
-import {
- getProductItemResponse,
- getProductResponse,
-} from "@/internal/products/productUtils/productResponseUtils/getProductResponse";
-import { notNullish } from "@/utils/genUtils";
-import { billingPlanToNextCyclePreview } from "./billingPlan/toNextCyclePreview/billingPlanToNextCyclePreview";
-
-export const billingContextToCheckoutResponse = async ({
- ctx,
- billingContext,
- billingPlan,
-}: {
- ctx: AutumnContext;
- billingContext: BillingContext;
- billingPlan: BillingPlan;
-}): Promise => {
- const { fullCustomer, fullProducts, featureQuantities } = billingContext;
- const { features, org } = ctx;
- const currency = orgToCurrency({ org });
-
- // 1. Get primary product (first non-add-on or first product)
- const mainProduct = fullProducts.find((p) => !p.is_add_on) ?? fullProducts[0];
-
- const product = mainProduct
- ? await getProductResponse({
- product: mainProduct,
- features,
- fullCus: fullCustomer,
- currency,
- db: ctx.db,
- options: featureQuantities,
- })
- : null;
-
- // 2. Build line items from billing plan
- const planLineItems = billingPlan.autumn.lineItems ?? [];
-
- // Collect all prices and entitlements from products for lookup
- const allPrices = fullProducts.flatMap((p) => p.prices);
- const allEnts = fullProducts.flatMap((p) => p.entitlements);
-
- const lines: CheckoutLineV0[] = planLineItems
- .filter((line) => line.chargeImmediately)
- .map((line) => {
- const { price } = line.context;
-
- // Find entitlement for this price
- const ent = getPriceEntitlement(price, allEnts);
-
- // Build product item from price + entitlement
- const productItem = toProductItem({ ent, price });
-
- return {
- description: line.description,
- amount: line.amountAfterDiscounts,
- item: getProductItemResponse({
- item: productItem,
- features,
- currency,
- withDisplay: true,
- options: featureQuantities,
- }),
- };
- })
- .filter(notNullish);
-
- // 3. Calculate total
- const total = new Decimal(lines.reduce((acc, line) => acc + line.amount, 0))
- .toDecimalPlaces(2)
- .toNumber();
-
- // 4. Get next cycle preview
- const { nextCycle } = billingPlanToNextCyclePreview({
- ctx,
- billingContext,
- billingPlan,
- });
-
- // 5. Build options from feature quantities
- const options = featureQuantities
- .map((fq) => {
- const price = allPrices.find(
- (p) =>
- p.config &&
- "feature_id" in p.config &&
- (p.config.feature_id === fq.feature_id ||
- p.config.internal_feature_id === fq.internal_feature_id),
- );
-
- if (!price) return undefined;
-
- const billingUnits =
- price.config && "billing_units" in price.config
- ? price.config.billing_units || 1
- : 1;
-
- return {
- feature_id: fq.feature_id,
- quantity: fq.quantity * billingUnits,
- };
- })
- .filter(notNullish);
-
- return CheckoutResponseV0Schema.parse({
- customer_id: fullCustomer.id || fullCustomer.internal_id,
- product,
- current_product: null,
- lines,
- options,
- total,
- currency,
- next_cycle: nextCycle,
- });
-};
diff --git a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts b/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts
deleted file mode 100644
index b7e1ab4dc..000000000
--- a/server/src/internal/billing/v2/utils/billingPlan/billingPlanToNewActiveCustomerProduct.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { cp } from "@autumn/shared";
-import type { AutumnBillingPlan } from "@autumn/shared";
-
-export const billingPlanToNewActiveCustomerProduct = ({
- autumnBillingPlan,
-}: {
- autumnBillingPlan: AutumnBillingPlan;
-}) => {
- return autumnBillingPlan.insertCustomerProducts?.find(
- (customerProduct) => cp(customerProduct).hasActiveStatus().valid,
- );
-};
diff --git a/server/src/internal/billing/v2/utils/lineItems/billingLineItemToDbLineItem.ts b/server/src/internal/billing/v2/utils/lineItems/billingLineItemToDbLineItem.ts
deleted file mode 100644
index 82f0eed45..000000000
--- a/server/src/internal/billing/v2/utils/lineItems/billingLineItemToDbLineItem.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type {
- InsertDbInvoiceLineItem,
- InvoiceLineItemDiscount,
- LineItem,
-} from "@autumn/shared";
-
-/**
- * Helper for full match case - converts an Autumn LineItem to InsertInvoiceLineItem.
- */
-export const billingLineItemToInsertDbLineItem = ({
- lineItem,
- invoiceId,
- stripeInvoiceId,
- stripeLineItemId,
-}: {
- lineItem: LineItem;
- invoiceId: string;
- stripeInvoiceId: string;
- stripeLineItemId?: string;
-}): InsertDbInvoiceLineItem => {
- const { context } = lineItem;
-
- return {
- id: lineItem.id,
- invoice_id: invoiceId,
- stripe_id: stripeLineItemId ?? null,
- stripe_invoice_id: stripeInvoiceId,
- stripe_product_id: lineItem.stripeProductId ?? null,
- stripe_price_id: lineItem.stripePriceId ?? null,
- stripe_discountable: context.discountable ?? true,
-
- amount: lineItem.amount,
- amount_after_discounts: lineItem.amountAfterDiscounts,
- currency: context.currency,
-
- total_quantity: lineItem.totalQuantity ?? null,
- paid_quantity: lineItem.paidQuantity ?? null,
-
- description: lineItem.description,
- direction: context.direction,
- billing_timing: context.billingTiming,
- prorated: lineItem.prorated,
-
- price_id: context.price.id,
- customer_product_ids: context.customerProduct?.id
- ? [context.customerProduct.id]
- : [],
- customer_price_ids: context.customerPrice?.id
- ? [context.customerPrice.id]
- : [],
- customer_entitlement_ids: context.customerEntitlement?.id
- ? [context.customerEntitlement.id]
- : [],
- internal_product_id: context.product.internal_id,
- product_id: context.product.id,
- internal_feature_id: context.feature?.internal_id ?? null,
- feature_id: context.feature?.id ?? null,
-
- effective_period_start: context.effectivePeriod?.start ?? null,
- effective_period_end: context.effectivePeriod?.end ?? null,
-
- discounts: lineItem.discounts.map(
- (d): InvoiceLineItemDiscount => ({
- amount_off: d.amountOff,
- percent_off: d.percentOff,
- stripe_coupon_id: d.stripeCouponId,
- }),
- ),
- };
-};
diff --git a/server/src/internal/billing/v2/utils/logs/logNextCyclePreview.ts b/server/src/internal/billing/v2/utils/logs/logNextCyclePreview.ts
deleted file mode 100644
index 55ba0e96d..000000000
--- a/server/src/internal/billing/v2/utils/logs/logNextCyclePreview.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import type {
- BillingPreviewResponse,
- FullCusProduct,
- LineItem,
-} from "@autumn/shared";
-import { formatMs } from "@autumn/shared";
-import type { AutumnContext } from "@/honoUtils/HonoEnv";
-import { addToExtraLogs } from "@/utils/logging/addToExtraLogs";
-import type { NextCyclePreviewDebug } from "../billingPlan/toNextCyclePreview/billingPlanToNextCyclePreview";
-
-const formatCustomerProduct = (customerProduct: FullCusProduct) =>
- `${customerProduct.product.name} (${customerProduct.product_id}) [${customerProduct.status}]`;
-
-const formatLineItem = (item: LineItem) =>
- `${item.description}: ${item.amountAfterDiscounts} (charge: ${item.chargeImmediately})`;
-
-export const logBillingPreview = ({
- ctx,
- allLineItems,
- immediateLineItems,
- total,
- currency,
- nextCycleDebug,
- nextCycle,
-}: {
- ctx: AutumnContext;
- allLineItems: LineItem[];
- immediateLineItems: LineItem[];
- total: number;
- currency: string;
- nextCycleDebug: NextCyclePreviewDebug;
- nextCycle: BillingPreviewResponse["next_cycle"];
-}) => {
- const {
- allCustomerProducts,
- currentCustomerProducts,
- smallestInterval,
- anchorMs,
- nextCycleStart,
- filteredCustomerProducts,
- } = nextCycleDebug;
-
- addToExtraLogs({
- ctx,
- extras: {
- billingPreview: {
- // Immediate charge breakdown
- total: `${total} ${currency}`,
- allLineItems:
- allLineItems.length > 0 ? allLineItems.map(formatLineItem) : "none",
- immediateLineItems:
- immediateLineItems.length > 0
- ? immediateLineItems.map(formatLineItem)
- : "none",
-
- // Next cycle calculation
- nextCycle: {
- allCustomerProducts:
- allCustomerProducts.map(formatCustomerProduct).join(", ") || "none",
- currentCustomerProducts:
- currentCustomerProducts.map(formatCustomerProduct).join(", ") ||
- "none",
- smallestInterval: smallestInterval
- ? `${smallestInterval.intervalCount} ${smallestInterval.interval}`
- : "none (not a subscription)",
- anchor: formatMs(anchorMs),
- nextCycleStart: nextCycleStart ? formatMs(nextCycleStart) : "n/a",
- filteredCustomerProducts:
- filteredCustomerProducts.map(formatCustomerProduct).join(", ") ||
- "none",
- result: nextCycle
- ? `starts: ${formatMs(nextCycle.starts_at)} | total: ${nextCycle.total} | items: ${nextCycle.line_items.length}`
- : "undefined",
- },
- },
- },
- });
-};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts
deleted file mode 100644
index 049abb6ed..000000000
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import {
- type AttachConfig,
- calculateProrationAmount,
- cusProductToProduct,
- customerPriceToCustomerEntitlement,
- type Feature,
- type FeatureOptions,
- type FullCusProduct,
- findCusPriceByFeature,
- getFeatureInvoiceDescription,
- OnDecrease,
- priceToInvoiceAmount,
- shouldBillNow,
- shouldProrate,
- type UsagePriceConfig,
-} from "@autumn/shared";
-import { Decimal } from "decimal.js";
-import type { Stripe } from "stripe";
-import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
-import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
-import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
-import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
-import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
-import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
-import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
-import { notNullish } from "@/utils/genUtils.js";
-import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
-
-export const handleQuantityDowngrade = async ({
- ctx,
- attachParams,
- attachConfig,
- cusProduct,
- stripeSub,
- oldOptions,
- newOptions,
- subItem,
-}: {
- ctx: AutumnContext;
- attachParams: AttachParams;
- attachConfig: AttachConfig;
- cusProduct: FullCusProduct;
- stripeSub: Stripe.Subscription;
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
- subItem: Stripe.SubscriptionItem;
-}) => {
- const { db, logger, org, features } = ctx;
- const { stripeCli, paymentMethod } = attachParams;
-
- const cusPrice = findCusPriceByFeature({
- internalFeatureId: newOptions.internal_feature_id!,
- cusPrices: cusProduct.customer_prices,
- })!;
-
- const onDecrease =
- cusPrice.price.proration_config?.on_decrease ||
- OnDecrease.ProrateImmediately;
-
- const subItemDifference = new Decimal(newOptions.quantity)
- .minus(
- notNullish(oldOptions.upcoming_quantity)
- ? oldOptions.upcoming_quantity!
- : oldOptions.quantity,
- )
- .toNumber();
-
- const billingUnits =
- (cusPrice.price.config as UsagePriceConfig).billing_units || 1;
-
- const newSubItemQuantity = new Decimal(subItem.quantity || 0)
- .plus(subItemDifference)
- .toNumber();
-
- let invoice = null;
- const createDowngradeInvoice = async () => {
- const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
-
- const prevAmount = priceToInvoiceAmount({
- price: cusPrice.price,
- quantity: new Decimal(oldOptions.quantity).mul(billingUnits!).toNumber(),
- });
-
- const newAmount = priceToInvoiceAmount({
- price: cusPrice.price,
- quantity: new Decimal(newOptions.quantity).mul(billingUnits!).toNumber(),
- });
-
- let amount = new Decimal(newAmount).minus(prevAmount).toNumber();
-
- amount = calculateProrationAmount({
- periodEnd: end * 1000,
- periodStart: start * 1000,
- now: attachParams.now || Date.now(),
- amount,
- allowNegative: true,
- });
-
- const product = cusProductToProduct({ cusProduct });
- const feature = features.find(
- (f: Feature) => f.internal_id === newOptions.internal_feature_id,
- )!;
- const invoiceItem = constructStripeInvoiceItem({
- ctx,
- product,
- amount: amount,
- price: cusPrice.price,
- description: getFeatureInvoiceDescription({
- feature: feature,
- usage: newOptions.quantity,
- billingUnits: (cusPrice.price.config as UsagePriceConfig).billing_units,
- prodName: product.name,
- isPrepaid: true,
- fromUnix: attachParams.now,
- }),
- stripeSubId: stripeSub.id,
- stripeCustomerId: stripeSub.customer as string,
- periodStart: Math.floor(
- attachParams.now ? attachParams.now / 1000 : Date.now(),
- ),
- periodEnd: Math.floor(end * 1000),
- });
-
- logger.info(
- `🔥 Creating downgrade prepaid invoice item: ${invoiceItem.description} - ${amount}`,
- );
-
- await stripeCli.invoiceItems.create(invoiceItem);
-
- if (shouldBillNow(onDecrease)) {
- const { invoice: finalInvoice } = await createAndFinalizeInvoice({
- stripeCli,
- stripeCusId: stripeSub.customer as string,
- stripeSubId: stripeSub.id,
- paymentMethod: paymentMethod || null,
- chargeAutomatically: !attachConfig.invoiceOnly,
- logger,
- });
-
- invoice = finalInvoice;
-
- try {
- const invoiceItems = await getInvoiceItems({
- stripeInvoice: finalInvoice,
- prices: [cusPrice.price],
- logger,
- });
-
- await InvoiceService.createInvoiceFromStripe({
- db,
- stripeInvoice: finalInvoice,
- internalCustomerId: cusProduct.internal_customer_id!,
- internalEntityId: cusProduct.internal_entity_id,
- productIds: [cusProduct.product_id],
- internalProductIds: [cusProduct.internal_product_id],
- org,
- sendRevenueEvent: true,
- items: invoiceItems,
- });
- } catch (error) {
- logger.error(`Failed to create invoice from stripe: ${error}`);
- }
- }
- };
-
- await stripeCli.subscriptionItems.update(subItem.id, {
- quantity: Math.max(newSubItemQuantity, 0),
- // proration_behavior: stripeProration,
- proration_behavior: "none",
- });
-
- if (!shouldProrate(onDecrease)) {
- newOptions.upcoming_quantity = newOptions.quantity;
- newOptions.quantity = oldOptions.quantity;
- return;
- }
-
- await createDowngradeInvoice();
-
- const cusEnt = customerPriceToCustomerEntitlement({
- customerPrice: cusPrice,
- customerEntitlements: cusProduct.customer_entitlements,
- });
-
- if (cusEnt) {
- const decrementBy = new Decimal(oldOptions.quantity)
- .minus(new Decimal(newOptions.quantity))
- .mul(billingUnits)
- .toNumber();
-
- await CusEntService.decrement({
- ctx,
- id: cusEnt.id,
- amount: decrementBy,
- });
- }
-};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts
deleted file mode 100644
index 236a8f184..000000000
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import {
- type AttachConfig,
- calculateProrationAmount,
- cusProductToProduct,
- customerPriceToCustomerEntitlement,
- type Feature,
- type FeatureOptions,
- type FullCusProduct,
- type FullCustomerPrice,
- getFeatureInvoiceDescription,
- OnIncrease,
- priceToInvoiceAmount,
- shouldBillNow,
- shouldProrate,
- type UsagePriceConfig,
-} from "@autumn/shared";
-import { Decimal } from "decimal.js";
-import type { Stripe } from "stripe";
-import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
-import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
-import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
-import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
-import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
-import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
-import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
-import { notNullish } from "@/utils/genUtils.js";
-import type { AutumnContext } from "../../../../../honoUtils/HonoEnv";
-
-export const handleQuantityUpgrade = async ({
- ctx,
- attachParams,
- cusProduct,
- stripeSubs,
- attachConfig,
- oldOptions,
- newOptions,
- cusPrice,
- stripeSub,
- subItem,
-}: {
- ctx: AutumnContext;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- attachConfig: AttachConfig;
- stripeSubs: Stripe.Subscription[];
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
- cusPrice: FullCustomerPrice;
- stripeSub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
-}) => {
- // Manually calculate prorations...
- const { features, org, logger, db } = ctx;
- const { stripeCli, now, paymentMethod } = attachParams;
-
- const difference = new Decimal(newOptions.quantity)
- .minus(oldOptions.quantity)
- .toNumber();
-
- const subItemDifference = new Decimal(newOptions.quantity)
- .minus(
- notNullish(oldOptions.upcoming_quantity)
- ? oldOptions.upcoming_quantity!
- : oldOptions.quantity,
- )
- .toNumber();
-
- const onIncrease =
- cusPrice.price.proration_config?.on_increase ||
- OnIncrease.ProrateImmediately;
-
- const prorate = shouldProrate(onIncrease);
- const config = cusPrice.price.config as UsagePriceConfig;
- const billingUnits = config.billing_units || 1;
-
- let invoice = null;
- if (prorate && stripeSub?.status !== "trialing") {
- const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
-
- const prevAmount = priceToInvoiceAmount({
- price: cusPrice.price,
- quantity: new Decimal(oldOptions.quantity).mul(billingUnits!).toNumber(),
- });
-
- const newAmount = priceToInvoiceAmount({
- price: cusPrice.price,
- quantity: new Decimal(newOptions.quantity).mul(billingUnits!).toNumber(),
- });
-
- let amount = new Decimal(newAmount).minus(prevAmount).toNumber();
- if (prorate) {
- amount = calculateProrationAmount({
- periodEnd: end * 1000,
- periodStart: start * 1000,
- now: now || Date.now(),
- amount,
- });
- }
-
- const feature = features.find(
- (f: Feature) => f.internal_id === newOptions.internal_feature_id,
- )!;
-
- const product = cusProductToProduct({ cusProduct });
- const invoiceItem = constructStripeInvoiceItem({
- ctx,
- product,
- amount: amount,
- price: cusPrice.price,
- description: getFeatureInvoiceDescription({
- feature: feature,
- usage: newOptions.quantity,
- billingUnits,
- prodName: product.name,
- isPrepaid: true,
- fromUnix: now,
- }),
- stripeSubId: stripeSub.id,
- stripeCustomerId: stripeSub.customer as string,
- periodStart: Math.floor((now || Date.now()) / 1000),
- periodEnd: Math.floor(end * 1000),
- });
-
- logger.info(
- `🔥 Creating prepaid invoice item: ${invoiceItem.description} - ${amount}`,
- );
-
- await stripeCli.invoiceItems.create(invoiceItem);
-
- if (shouldBillNow(onIncrease)) {
- const { invoice: finalInvoice } = await createAndFinalizeInvoice({
- stripeCli,
- stripeCusId: stripeSub.customer as string,
- stripeSubId: stripeSub.id,
- paymentMethod: paymentMethod || null,
- chargeAutomatically: !attachConfig.invoiceOnly,
- logger,
- });
-
- try {
- const invoiceItems = await getInvoiceItems({
- stripeInvoice: finalInvoice,
- prices: [cusPrice.price],
- logger,
- });
-
- await InvoiceService.createInvoiceFromStripe({
- db,
- stripeInvoice: finalInvoice,
- internalCustomerId: cusProduct.internal_customer_id!,
- internalEntityId: cusProduct.internal_entity_id,
- productIds: [cusProduct.product_id],
- internalProductIds: [cusProduct.internal_product_id],
- org,
- sendRevenueEvent: true,
- items: invoiceItems,
- });
- } catch (error) {
- logger.error(`Failed to create invoice from stripe: ${error}`);
- }
- invoice = finalInvoice;
- }
- }
-
- // 1. If no sub item, add
- if (!subItem) {
- if (!cusPrice.price.config.stripe_price_id) {
- throw new Error(
- "Trying to add new sub item for upgrade quantity flow, but no Stripe price ID found",
- );
- }
-
- await stripeCli.subscriptionItems.create({
- subscription: stripeSub.id,
- price: cusPrice.price.config.stripe_price_id,
- quantity: subItemDifference,
- proration_behavior: "none",
- });
- } else {
- await stripeCli.subscriptionItems.update(subItem.id, {
- // quantity: newOptions.quantity,
- quantity: (subItem.quantity || 0) + subItemDifference,
- proration_behavior: "none",
- });
- }
-
- // Update cus ent
-
- const cusEnt = customerPriceToCustomerEntitlement({
- customerPrice: cusPrice,
- customerEntitlements: cusProduct.customer_entitlements,
- });
-
- if (cusEnt) {
- const incrementBy = new Decimal(difference).mul(billingUnits).toNumber();
- logger.info(
- `🔥 Incrementing feature ${cusEnt.entitlement.feature.id} balance by ${incrementBy}`,
- );
- await CusEntService.increment({
- ctx,
- id: cusEnt.id,
- amount: incrementBy,
- });
- }
- return { invoice };
-};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts
deleted file mode 100644
index 1caaf542a..000000000
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import {
- type AttachConfig,
- ErrCode,
- type FeatureOptions,
- type FullCusProduct,
- findCusPriceByFeature,
-} from "@autumn/shared";
-import type { Stripe } from "stripe";
-import { stripeSubscriptionItemUtils } from "@/external/stripe/subscriptions/subscriptionItems/index.js";
-import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
-import RecaseError from "@/utils/errorUtils.js";
-import type { AutumnContext } from "../../../../../honoUtils/HonoEnv.js";
-import { handleQuantityDowngrade } from "./handleQuantityDowngrade.js";
-import { handleQuantityUpgrade } from "./handleQuantityUpgrade.js";
-
-export const handleUpdateFeatureQuantity = async ({
- ctx,
- attachParams,
- attachConfig,
- cusProduct,
- stripeSubs,
- oldOptions,
- newOptions,
-}: {
- ctx: AutumnContext;
- attachParams: AttachParams;
- attachConfig: AttachConfig;
- cusProduct: FullCusProduct;
- stripeSubs: Stripe.Subscription[];
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
-}) => {
- const subToUpdate = stripeSubs?.[0];
-
- const cusPrice = findCusPriceByFeature({
- internalFeatureId: newOptions.internal_feature_id!,
- cusPrices: cusProduct.customer_prices,
- })!;
-
- const price = cusPrice.price;
-
- if (!subToUpdate) {
- throw new RecaseError({
- message: `Failed to update prepaid quantity for ${newOptions.feature_id} because no subscription found`,
- code: ErrCode.InternalError,
- statusCode: 500,
- });
- }
-
- const subItem = stripeSubscriptionItemUtils.find.byAutumnPrice({
- stripeSubscriptionItems: subToUpdate.items.data,
- price,
- product: cusProduct.product,
- errorOnNotFound: true,
- });
-
- if (newOptions.quantity < oldOptions.quantity) {
- return await handleQuantityDowngrade({
- ctx,
- attachParams,
- attachConfig,
- cusProduct,
- stripeSub: subToUpdate,
- oldOptions,
- newOptions,
- subItem,
- });
- } else {
- return await handleQuantityUpgrade({
- ctx,
- attachParams,
- attachConfig,
- cusProduct,
- stripeSubs,
- oldOptions,
- newOptions,
- cusPrice,
- stripeSub: subToUpdate,
- subItem,
- });
- }
-};
diff --git a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts b/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts
deleted file mode 100644
index bb8ceab72..000000000
--- a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts
+++ /dev/null
@@ -1,147 +0,0 @@
-import type { AttachConfig, FullCusProduct } from "@autumn/shared";
-import type Stripe from "stripe";
-import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
-import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
-import type { AttachParams } from "../../cusProducts/AttachParams.js";
-import { CusProductService } from "../../cusProducts/CusProductService.js";
-import { paramsToScheduleItems } from "./paramsToScheduleItems.js";
-import { getCusProductsToRemove } from "./paramsToSubItems.js";
-
-export const subToNewSchedule = async ({
- ctx,
- sub,
- attachParams,
- config,
- endOfBillingPeriod,
- removeCusProducts,
-}: {
- ctx: AutumnContext;
- sub: Stripe.Subscription;
- attachParams: AttachParams;
- config: AttachConfig;
- endOfBillingPeriod: number;
- removeCusProducts?: FullCusProduct[];
-}) => {
- const itemSet = await getStripeSubItems2({
- attachParams,
- config,
- });
-
- let cusProductsToRemove: FullCusProduct[] = [];
- cusProductsToRemove = getCusProductsToRemove({
- attachParams,
- includeCanceled: true,
- });
-
- console.log(
- `REMOVING CUS PRODUCTS: ${cusProductsToRemove.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`).join(", ")}`,
- );
-
- const res = await paramsToScheduleItems({
- ctx,
- sub,
- attachParams,
- config,
- removeCusProducts: removeCusProducts || cusProductsToRemove,
- billingPeriodEnd: endOfBillingPeriod,
- });
-
- const { stripeCli } = attachParams;
- let newSchedule: Stripe.SubscriptionSchedule | undefined;
-
- // if (sub.cancel_at) {
- // logger.info(`UNCANCELING SUB ${sub.id}`);
- // await stripeCli.subscriptions.update(sub.id, {
- // cancel_at: null,
- // });
- // }
-
- // console.log("New phase");
- // await logPhases({
- // phases: res.phases,
- // db: req.db,
- // });
- // throw new Error("test");
-
- if (res.phases[0].items.length > 0) {
- itemSet.subItems = res.phases[0].items;
-
- // Create schedule from existing subscription
- newSchedule = await stripeCli.subscriptionSchedules.create({
- from_subscription: sub.id,
- });
-
- // console.log("SChedule ID: ", newSchedule.id);
-
- // const newScheduleId = "sub_sched_1RxyM89mx3u0jkgOgbbsAbDS";
- const newScheduleId = newSchedule.id;
- await stripeCli.subscriptionSchedules.update(newScheduleId, {
- phases: [
- {
- items: newSchedule.phases[0].items.map((item) => {
- const priceId = item.price as string;
-
- // Re-apply metadata from subscription items since
- // Stripe's from_subscription doesn't copy item metadata
- const subItem = sub.items.data.find(
- (si) => si.price.id === priceId,
- );
- const metadata =
- subItem?.metadata && Object.keys(subItem.metadata).length > 0
- ? subItem.metadata
- : item.metadata && Object.keys(item.metadata).length > 0
- ? item.metadata
- : undefined;
-
- return {
- price: priceId,
- quantity: item.quantity,
- ...(metadata && { metadata }),
- };
- }),
- start_date: newSchedule.phases[0].start_date,
- end_date: endOfBillingPeriod,
- trial_end: sub?.trial_end || undefined,
- },
- {
- items: res.phases[0].items,
- start_date: endOfBillingPeriod,
- },
- ],
- end_behavior: "release",
- });
-
- await CusProductService.updateByStripeSubId({
- db: ctx.db,
- stripeSubId: sub.id!,
- updates: {
- scheduled_ids: [newSchedule!.id],
- },
- });
- }
-
- return newSchedule as Stripe.SubscriptionSchedule;
-};
-
-// phases: ([
-// {
-// items: scheduleItems.items,
-// // Set proration behavior for this phase transition
-// proration_behavior: "create_prorations", // Options: 'create_prorations', 'none', 'always_invoice'
-// // Optional: Set how long this phase should last
-// iterations: 1, // Number of billing cycles for this phase
-// // Optional: Add metadata for this phase
-// metadata: {
-// phase_type: "scheduled_update",
-// created_by: "attach_flow",
-// },
-// },
-// ],
-
-// Option 2: Use your existing createSubSchedule function
-// newSchedule = await createSubSchedule({
-// db: req.db,
-// attachParams,
-// itemSet,
-// endOfBillingPeriod,
-// });
diff --git a/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts b/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts
deleted file mode 100644
index 8cfc9b574..000000000
--- a/server/src/internal/customers/cusProducts/cusEnts/repos/customerEntitlementRepo.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const customerEntitlementRepo = {};
diff --git a/server/src/internal/customers/repos/getFullSubject/getEntityAggregateFragments copy.ts b/server/src/internal/customers/repos/getFullSubject/getEntityAggregateFragments copy.ts
deleted file mode 100644
index 0e4062e24..000000000
--- a/server/src/internal/customers/repos/getFullSubject/getEntityAggregateFragments copy.ts
+++ /dev/null
@@ -1,495 +0,0 @@
-import { type SQL, sql } from "drizzle-orm";
-import { getEntityOptionsAggregateFragments } from "./getEntityOptionsAggregateFragments.js";
-
-/**
- * Per-entity rollover rows sourced from the `rollovers` table, mirroring the
- * four branches in `entity_balance_rows` (product-attached / loose, per-entity
- * jsonb / top-level). Top-level rollovers are attributed to the owning entity
- * via `cp.internal_entity_id` (product-attached) or `ce.internal_entity_id`
- * (loose), matching main-balance behaviour. Also exposes per-entity and
- * per-feature rollups used by the outer aggregate CTEs.
- */
-const buildEntityRolloverCtes = ({
- statusFilter,
-}: {
- statusFilter: SQL;
-}) => sql`
- entity_rollover_rows AS (
- -- Product-attached cusEnt, per-entity rollover (rollovers.entities jsonb)
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- kv.entity_key AS entity_key,
- COALESCE((kv.entity_value->>'balance')::numeric, 0) AS rollover_balance,
- COALESCE((kv.entity_value->>'usage')::numeric, 0) AS rollover_usage
- FROM rollovers r
- JOIN customer_entitlements ce ON r.cus_ent_id = ce.id
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- CROSS JOIN LATERAL jsonb_each(r.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND cp.internal_entity_id IS NOT NULL
- AND jsonb_typeof(r.entities) = 'object'
- AND (r.expires_at IS NULL OR r.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- ${statusFilter}
-
- UNION ALL
-
- -- Product-attached cusEnt, top-level rollover (attributed to cp.internal_entity_id)
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- cp.internal_entity_id AS entity_key,
- r.balance::numeric AS rollover_balance,
- COALESCE(r.usage, 0)::numeric AS rollover_usage
- FROM rollovers r
- JOIN customer_entitlements ce ON r.cus_ent_id = ce.id
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND cp.internal_entity_id IS NOT NULL
- AND (r.expires_at IS NULL OR r.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- ${statusFilter}
-
- UNION ALL
-
- -- Loose cusEnt (no customer_product), per-entity rollover
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- kv.entity_key AS entity_key,
- COALESCE((kv.entity_value->>'balance')::numeric, 0) AS rollover_balance,
- COALESCE((kv.entity_value->>'usage')::numeric, 0) AS rollover_usage
- FROM rollovers r
- JOIN customer_entitlements ce ON r.cus_ent_id = ce.id
- CROSS JOIN LATERAL jsonb_each(r.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND jsonb_typeof(r.entities) = 'object'
- AND (r.expires_at IS NULL OR r.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
-
- UNION ALL
-
- -- Loose cusEnt, top-level rollover (attributed to ce.internal_entity_id)
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- ce.internal_entity_id AS entity_key,
- r.balance::numeric AS rollover_balance,
- COALESCE(r.usage, 0)::numeric AS rollover_usage
- FROM rollovers r
- JOIN customer_entitlements ce ON r.cus_ent_id = ce.id
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND (r.expires_at IS NULL OR r.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- ),
-
- entity_rollover_keys AS (
- SELECT
- internal_feature_id,
- internal_customer_id,
- entity_key,
- SUM(rollover_balance) AS rollover_balance,
- SUM(rollover_usage) AS rollover_usage
- FROM entity_rollover_rows
- WHERE entity_key IS NOT NULL
- GROUP BY internal_feature_id, internal_customer_id, entity_key
- ),
-
- entity_rollover_feature AS (
- SELECT
- internal_feature_id,
- internal_customer_id,
- SUM(rollover_balance) AS rollover_balance,
- SUM(rollover_usage) AS rollover_usage
- FROM entity_rollover_rows
- GROUP BY internal_feature_id, internal_customer_id
- )
-`;
-
-/**
- * Per-feature/per-entity aggregates sourced strictly from `ce.entities` JSON.
- * This powers the `entities` map in customer-level aggregates and intentionally
- * excludes top-level entity attribution paths.
- */
-const buildEntityEntitiesCtes = ({
- statusFilter,
-}: {
- statusFilter: SQL;
-}) => sql`
- entity_entities_rows AS (
- -- Product-attached entitlements: aggregate directly from ce.entities keys
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- kv.entity_key AS entity_key,
- COALESCE((kv.entity_value->>'balance')::numeric, 0) AS entity_balance,
- COALESCE((kv.entity_value->>'adjustment')::numeric, 0) AS entity_adjustment,
- COALESCE((kv.entity_value->>'additional_balance')::numeric, 0) AS entity_additional_balance
- FROM customer_entitlements ce
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- CROSS JOIN LATERAL jsonb_each(ce.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND cp.internal_entity_id IS NOT NULL
- AND jsonb_typeof(ce.entities) = 'object'
- ${statusFilter}
-
- UNION ALL
-
- -- Loose entitlements: aggregate directly from ce.entities keys
- SELECT
- ce.internal_feature_id,
- ce.internal_customer_id,
- kv.entity_key AS entity_key,
- COALESCE((kv.entity_value->>'balance')::numeric, 0) AS entity_balance,
- COALESCE((kv.entity_value->>'adjustment')::numeric, 0) AS entity_adjustment,
- COALESCE((kv.entity_value->>'additional_balance')::numeric, 0) AS entity_additional_balance
- FROM customer_entitlements ce
- CROSS JOIN LATERAL jsonb_each(ce.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND jsonb_typeof(ce.entities) = 'object'
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- ),
-
- entity_entities_aggregate_keys AS (
- SELECT
- internal_feature_id,
- internal_customer_id,
- entity_key,
- SUM(entity_balance) AS balance,
- SUM(entity_adjustment) AS adjustment,
- SUM(entity_additional_balance) AS additional_balance
- FROM entity_entities_rows
- WHERE entity_key IS NOT NULL
- GROUP BY internal_feature_id, internal_customer_id, entity_key
- )
-`;
-
-export const getEntityAggregateFragments = ({
- entityId,
- statusFilter,
-}: {
- entityId?: string;
- statusFilter: SQL;
-}) => {
- if (entityId) {
- return {
- ctes: sql``,
- productRefsUnion: sql``,
- entitlementRefsUnion: sql``,
- priceRefsUnion: sql``,
- freeTrialRefsUnion: sql``,
- selectColumns: sql``,
- };
- }
-
- const entityOptionsAggregateFragments = getEntityOptionsAggregateFragments();
-
- const ctes = sql`,
-
- entity_distinct_product_ids AS (
- SELECT DISTINCT cp.internal_product_id, cp.internal_customer_id
- FROM customer_products cp
- JOIN subject_customer_records scr
- ON cp.internal_customer_id = scr.internal_id
- WHERE cp.internal_entity_id IS NOT NULL
- ${statusFilter}
- ),
-
- entity_distinct_cus_products AS (
- SELECT sub.*
- FROM entity_distinct_product_ids edpi
- JOIN LATERAL (
- SELECT cp.*
- FROM customer_products cp
- WHERE cp.internal_customer_id = edpi.internal_customer_id
- AND cp.internal_product_id = edpi.internal_product_id
- AND cp.internal_entity_id IS NOT NULL
- ${statusFilter}
- ORDER BY cp.created_at DESC
- LIMIT 1
- ) sub ON true
- ),
-
- entity_cus_products_for_options AS (
- SELECT cp.*
- FROM customer_products cp
- JOIN subject_customer_records scr
- ON cp.internal_customer_id = scr.internal_id
- WHERE cp.internal_entity_id IS NOT NULL
- ${statusFilter}
- ),
-
- entity_cus_prices AS (
- SELECT cpr.*
- FROM customer_prices cpr
- WHERE cpr.customer_product_id IN (SELECT id FROM entity_distinct_cus_products)
- ),
-
- ${entityOptionsAggregateFragments.ctes},
-
- entity_balance_rows AS (
- SELECT
- COALESCE(ce.external_id, ce.id) AS api_id,
- ce.internal_feature_id,
- ce.internal_customer_id,
- ce.feature_id,
- COALESCE(ent.allowance, 0)::numeric AS allowance,
- ce.balance::numeric AS balance,
- ce.adjustment::numeric AS adjustment,
- COALESCE(ce.additional_balance, 0)::numeric AS additional_balance,
- ce.unlimited,
- ce.usage_allowed,
- cp.internal_entity_id AS entity_key,
- ce.balance::numeric AS entity_balance,
- COALESCE(ce.adjustment, 0)::numeric AS entity_adjustment,
- COALESCE(ce.additional_balance, 0)::numeric AS entity_additional_balance
- FROM customer_entitlements ce
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- JOIN entitlements ent ON ce.entitlement_id = ent.id
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND cp.internal_entity_id IS NOT NULL
- ${statusFilter}
-
- UNION ALL
-
- SELECT
- COALESCE(ce.external_id, ce.id) AS api_id,
- ce.internal_feature_id,
- ce.internal_customer_id,
- ce.feature_id,
- COALESCE(ent.allowance, 0)::numeric AS allowance,
- 0::numeric AS balance,
- 0::numeric AS adjustment,
- 0::numeric AS additional_balance,
- ce.unlimited,
- ce.usage_allowed,
- kv.entity_key AS entity_key,
- (kv.entity_value->>'balance')::numeric AS entity_balance,
- COALESCE((kv.entity_value->>'adjustment')::numeric, 0) AS entity_adjustment,
- COALESCE((kv.entity_value->>'additional_balance')::numeric, 0) AS entity_additional_balance
- FROM customer_entitlements ce
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- JOIN entitlements ent ON ce.entitlement_id = ent.id
- CROSS JOIN LATERAL jsonb_each(ce.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND cp.internal_entity_id IS NOT NULL
- AND jsonb_typeof(ce.entities) = 'object'
- ${statusFilter}
-
- UNION ALL
-
- SELECT
- COALESCE(ce.external_id, ce.id) AS api_id,
- ce.internal_feature_id,
- ce.internal_customer_id,
- ce.feature_id,
- COALESCE(ent.allowance, 0)::numeric AS allowance,
- 0::numeric AS balance,
- 0::numeric AS adjustment,
- 0::numeric AS additional_balance,
- ce.unlimited,
- ce.usage_allowed,
- kv.entity_key AS entity_key,
- (kv.entity_value->>'balance')::numeric AS entity_balance,
- COALESCE((kv.entity_value->>'adjustment')::numeric, 0) AS entity_adjustment,
- COALESCE((kv.entity_value->>'additional_balance')::numeric, 0) AS entity_additional_balance
- FROM customer_entitlements ce
- JOIN entitlements ent ON ce.entitlement_id = ent.id
- CROSS JOIN LATERAL jsonb_each(ce.entities) AS kv(entity_key, entity_value)
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND jsonb_typeof(ce.entities) = 'object'
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
-
- UNION ALL
-
- SELECT
- COALESCE(ce.external_id, ce.id) AS api_id,
- ce.internal_feature_id,
- ce.internal_customer_id,
- ce.feature_id,
- COALESCE(ent.allowance, 0)::numeric AS allowance,
- ce.balance::numeric AS balance,
- COALESCE(ce.adjustment, 0)::numeric AS adjustment,
- COALESCE(ce.additional_balance, 0)::numeric AS additional_balance,
- ce.unlimited,
- ce.usage_allowed,
- ce.internal_entity_id AS entity_key,
- ce.balance::numeric AS entity_balance,
- COALESCE(ce.adjustment, 0)::numeric AS entity_adjustment,
- COALESCE(ce.additional_balance, 0)::numeric AS entity_additional_balance
- FROM customer_entitlements ce
- JOIN entitlements ent ON ce.entitlement_id = ent.id
- WHERE ce.internal_customer_id IN (SELECT internal_id FROM subject_customer_records)
- AND ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- ),
-
- ${buildEntityRolloverCtes({ statusFilter })},
-
- ${buildEntityEntitiesCtes({ statusFilter })},
-
- entity_aggregate_keys AS (
- SELECT
- COALESCE(ebk.internal_feature_id, erk.internal_feature_id) AS internal_feature_id,
- COALESCE(ebk.internal_customer_id, erk.internal_customer_id) AS internal_customer_id,
- COALESCE(ebk.entity_key, erk.entity_key) AS entity_key,
- COALESCE(ebk.balance, 0) AS balance,
- COALESCE(ebk.adjustment, 0) AS adjustment,
- COALESCE(ebk.additional_balance, 0) AS additional_balance,
- COALESCE(erk.rollover_balance, 0) AS rollover_balance,
- COALESCE(erk.rollover_usage, 0) AS rollover_usage
- FROM (
- SELECT
- internal_feature_id,
- internal_customer_id,
- entity_key,
- SUM(entity_balance) AS balance,
- SUM(entity_adjustment) AS adjustment,
- SUM(entity_additional_balance) AS additional_balance
- FROM entity_balance_rows
- WHERE entity_key IS NOT NULL
- GROUP BY internal_feature_id, internal_customer_id, entity_key
- ) ebk
- FULL OUTER JOIN entity_rollover_keys erk
- ON erk.internal_feature_id = ebk.internal_feature_id
- AND erk.internal_customer_id = ebk.internal_customer_id
- AND erk.entity_key = ebk.entity_key
- ),
-
- entity_aggregate_map AS (
- SELECT
- ejk.internal_feature_id,
- ejk.internal_customer_id,
- jsonb_object_agg(
- ejk.entity_key,
- jsonb_build_object(
- 'id', ejk.entity_key,
- 'balance', ejk.balance,
- 'adjustment', ejk.adjustment,
- 'additional_balance', ejk.additional_balance,
- 'rollover_balance', COALESCE(erk.rollover_balance, 0),
- 'rollover_usage', COALESCE(erk.rollover_usage, 0)
- )
- ) AS entities
- FROM entity_entities_aggregate_keys ejk
- LEFT JOIN entity_rollover_keys erk
- ON erk.internal_feature_id = ejk.internal_feature_id
- AND erk.internal_customer_id = ejk.internal_customer_id
- AND erk.entity_key = ejk.entity_key
- GROUP BY ejk.internal_feature_id, ejk.internal_customer_id
- ),
-
- entity_aggregated_cus_entitlements AS (
- SELECT
- MIN(ebr.api_id) AS api_id,
- ebr.internal_feature_id,
- ebr.internal_customer_id,
- MIN(ebr.feature_id) AS feature_id,
- SUM(ebr.allowance) AS allowance_total,
- COALESCE(MAX(epgo.prepaid_grant_from_options), 0) AS prepaid_grant_from_options,
- SUM(ebr.balance) AS balance,
- SUM(ebr.adjustment) AS adjustment,
- SUM(ebr.additional_balance) AS additional_balance,
- COALESCE(MAX(erf.rollover_balance), 0) AS rollover_balance,
- COALESCE(MAX(erf.rollover_usage), 0) AS rollover_usage,
- BOOL_OR(ebr.unlimited) AS unlimited,
- BOOL_OR(ebr.usage_allowed) AS usage_allowed,
- COUNT(DISTINCT ebr.entity_key) FILTER (WHERE ebr.entity_key IS NOT NULL) AS entity_count,
- eam.entities
- FROM entity_balance_rows ebr
- LEFT JOIN entity_aggregate_map eam
- ON eam.internal_feature_id = ebr.internal_feature_id
- AND eam.internal_customer_id = ebr.internal_customer_id
- LEFT JOIN entity_rollover_feature erf
- ON erf.internal_feature_id = ebr.internal_feature_id
- AND erf.internal_customer_id = ebr.internal_customer_id
- LEFT JOIN entity_prepaid_grant_from_options epgo
- ON epgo.internal_feature_id = ebr.internal_feature_id
- AND epgo.internal_customer_id = ebr.internal_customer_id
- GROUP BY
- ebr.internal_feature_id,
- ebr.internal_customer_id,
- eam.entities
- )
- `;
-
- const productRefsUnion = sql`
- UNION ALL
- SELECT ecp.internal_customer_id, ecp.internal_product_id
- FROM entity_distinct_cus_products ecp
- `;
-
- const entitlementRefsUnion = sql`
- UNION
- SELECT DISTINCT
- ce.internal_customer_id,
- ce.entitlement_id
- FROM customer_entitlements ce
- JOIN customer_products cp ON ce.customer_product_id = cp.id
- WHERE cp.internal_entity_id IS NOT NULL
- ${statusFilter}
-
- UNION
- SELECT DISTINCT
- ce.internal_customer_id,
- ce.entitlement_id
- FROM customer_entitlements ce
- WHERE ce.customer_product_id IS NULL
- AND ce.internal_entity_id IS NOT NULL
- AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
- `;
-
- const priceRefsUnion = sql`
- UNION ALL
- SELECT ecpr.price_id, ecp.internal_customer_id
- FROM entity_cus_prices ecpr
- JOIN entity_distinct_cus_products ecp
- ON ecp.id = ecpr.customer_product_id
- `;
-
- const freeTrialRefsUnion = sql`
- UNION ALL
- SELECT ecp.free_trial_id, ecp.internal_customer_id
- FROM entity_distinct_cus_products ecp
- WHERE ecp.free_trial_id IS NOT NULL
- `;
-
- const selectColumns = sql`,
-
- json_build_object(
- 'aggregated_customer_products', COALESCE(
- (
- SELECT json_agg(row_to_json(ecp))
- FROM entity_distinct_cus_products ecp
- WHERE ecp.internal_customer_id = scr.internal_id
- ),
- '[]'::json
- ),
- 'aggregated_customer_entitlements', COALESCE(
- (
- SELECT json_agg(row_to_json(eace))
- FROM entity_aggregated_cus_entitlements eace
- WHERE eace.internal_customer_id = scr.internal_id
- ),
- '[]'::json
- )
- ) AS entity_aggregations
- `;
-
- return {
- ctes,
- productRefsUnion,
- entitlementRefsUnion,
- priceRefsUnion,
- freeTrialRefsUnion,
- selectColumns,
- };
-};
diff --git a/server/src/internal/entities/actions/updateEntityDbAndCache.ts b/server/src/internal/entities/actions/updateEntityDbAndCache.ts
deleted file mode 100644
index a6c5a412e..000000000
--- a/server/src/internal/entities/actions/updateEntityDbAndCache.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { Entity } from "@autumn/shared";
-import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
-import { EntityService } from "@/internal/api/entities/EntityService.js";
-
-export const updateEntityDbAndCache = async ({
- ctx,
- entity,
- updates,
-}: {
- ctx: AutumnContext;
- entity: Entity;
- updates: Partial<
- Pick
- >;
-}) => {
- const filteredUpdates = Object.fromEntries(
- Object.entries(updates).filter(([, value]) => value !== undefined),
- ) as Partial<
- Pick
- >;
-
- if (Object.keys(filteredUpdates).length === 0) {
- return entity;
- }
-
- return EntityService.update({
- db: ctx.db,
- internalId: entity.internal_id,
- update: filteredUpdates,
- });
-};
diff --git a/server/src/internal/misc/debug/debugRouter.ts b/server/src/internal/misc/debug/debugRouter.ts
deleted file mode 100644
index 91a0e0182..000000000
--- a/server/src/internal/misc/debug/debugRouter.ts
+++ /dev/null
@@ -1,249 +0,0 @@
-import { sql } from "drizzle-orm";
-import { Hono } from "hono";
-import { dbCritical, dbGeneral } from "@/db/initDrizzle.js";
-import {
- forceDegraded,
- forceHealthy,
- getPgHealthState,
-} from "@/db/pgHealthMonitor.js";
-import { redis } from "@/external/redis/initRedis.js";
-import {
- disconnectPrimary,
- getFailoverState,
- reconnectPrimary,
-} from "@/external/redis/redisFailover.js";
-import { orgConfigMiddleware } from "@/honoMiddlewares/orgConfigMiddleware.js";
-import { secretKeyMiddleware } from "@/honoMiddlewares/secretKeyMiddleware.js";
-import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
-
-const ALLOWED_ORG_IDS = new Set([
- "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt",
- "org_2rzkkRh7r5dBSaBC101QHG9KDgt",
- "org_2vwdxwTdqxRrLEdUYddcynMv3n3",
-]);
-
-export const debugRouter = new Hono();
-
-debugRouter.use("*", secretKeyMiddleware);
-debugRouter.use("*", orgConfigMiddleware);
-
-debugRouter.get("/memory", async (c) => {
- const ctx = c.get("ctx");
-
- if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
- return c.json({ error: "Forbidden" }, 403);
- }
-
- const mem = process.memoryUsage();
-
- return c.json({
- ok: true,
- pid: process.pid,
- timestamp: new Date().toISOString(),
- memory: {
- rssMB: +(mem.rss / 1024 / 1024).toFixed(1),
- heapUsedMB: +(mem.heapUsed / 1024 / 1024).toFixed(1),
- heapTotalMB: +(mem.heapTotal / 1024 / 1024).toFixed(1),
- externalMB: +(mem.external / 1024 / 1024).toFixed(1),
- arrayBuffersMB: +(mem.arrayBuffers / 1024 / 1024).toFixed(1),
- },
- });
-});
-
-/** Check what statement_timeout the DB connections actually see. */
-debugRouter.get("/statement-timeout", async (c) => {
- if (process.env.NODE_ENV === "production") {
- return c.json({ error: "Not available in production" }, 403);
- }
-
- const ctx = c.get("ctx");
- if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
- return c.json({ error: "Forbidden" }, 403);
- }
-
- const criticalResult = await dbCritical.execute(sql`SHOW statement_timeout`);
- const generalResult = await dbGeneral.execute(sql`SHOW statement_timeout`);
-
- return c.json({
- ok: true,
- critical: criticalResult[0],
- general: generalResult[0],
- });
-});
-
-/**
- * Pool isolation test endpoint. Runs pg_sleep or SELECT 1 on a specific pool.
- * Blocked in production.
- */
-debugRouter.post("/pool-test", async (c) => {
- if (process.env.NODE_ENV === "production") {
- return c.json({ error: "Not available in production" }, 403);
- }
-
- const ctx = c.get("ctx");
- if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
- return c.json({ error: "Forbidden" }, 403);
- }
-
- if (process.env.DATABASE_URL?.includes("us-east-2")) {
- return c.json({ error: "Not available against production database" }, 403);
- }
-
- const body = await c.req.json<{
- action: "sleep" | "ping" | "cpu";
- pool: "general" | "critical";
- seconds?: number;
- /** Row count for CPU burn (default 5_000_000). Higher = more CPU time. */
- rows?: number;
- }>();
-
- const { action, pool, seconds = 5, rows = 5_000_000 } = body;
- const db = pool === "critical" ? dbCritical : dbGeneral;
- const start = Date.now();
-
- try {
- if (action === "sleep") {
- await db.execute(sql`SELECT pg_sleep(${seconds})`);
- } else if (action === "cpu") {
- // CPU-intensive: hash millions of rows. Burns real CPU on the DB.
- await db.execute(
- sql`SELECT count(*) FROM generate_series(1, ${rows}) AS s WHERE md5(s::text) IS NOT NULL`,
- );
- } else {
- await db.execute(sql`SELECT 1`);
- }
-
- return c.json({
- ok: true,
- pool,
- action,
- durationMs: Date.now() - start,
- });
- } catch (error) {
- return c.json({
- ok: false,
- pool,
- action,
- durationMs: Date.now() - start,
- error: error instanceof Error ? error.message : String(error),
- });
- }
-});
-
-/**
- * Redis failover test endpoints. Blocked in production.
- */
-debugRouter.post("/redis-failover", async (c) => {
- if (process.env.NODE_ENV === "production") {
- return c.json({ error: "Not available in production" }, 403);
- }
-
- const ctx = c.get("ctx");
-
- if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
- return c.json({ error: "Forbidden" }, 403);
- }
-
- const body = await c.req.json<{
- action: "status" | "kill-primary" | "recover-primary" | "ping";
- }>();
-
- if (body.action === "status") {
- return c.json({ ok: true, ...getFailoverState() });
- }
-
- if (body.action === "kill-primary") {
- disconnectPrimary();
- return c.json({ ok: true, message: "Primary disconnected" });
- }
-
- if (body.action === "recover-primary") {
- reconnectPrimary();
- return c.json({ ok: true, message: "Primary reconnect triggered" });
- }
-
- if (body.action === "ping") {
- const start = Date.now();
- try {
- await redis.ping();
- return c.json({
- ok: true,
- durationMs: Date.now() - start,
- ...getFailoverState(),
- });
- } catch (error) {
- return c.json({
- ok: false,
- durationMs: Date.now() - start,
- error: error instanceof Error ? error.message : String(error),
- ...getFailoverState(),
- });
- }
- }
-
- return c.json({ error: "Unknown action" }, 400);
-});
-
-/**
- * PG health monitor test endpoints. Blocked in production.
- */
-debugRouter.post("/pg-health", async (c) => {
- if (process.env.NODE_ENV === "production") {
- return c.json({ error: "Not available in production" }, 403);
- }
-
- const ctx = c.get("ctx");
- if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
- return c.json({ error: "Forbidden" }, 403);
- }
-
- const body = await c.req.json<{
- action: "status" | "force-degraded" | "force-healthy";
- }>();
-
- if (body.action === "status") {
- return c.json({ ok: true, ...getPgHealthState() });
- }
-
- if (body.action === "force-degraded") {
- forceDegraded();
- return c.json({
- ok: true,
- message: "Forced DEGRADED",
- ...getPgHealthState(),
- });
- }
-
- if (body.action === "force-healthy") {
- forceHealthy();
- return c.json({
- ok: true,
- message: "Forced HEALTHY",
- ...getPgHealthState(),
- });
- }
-
- return c.json({ error: "Unknown action" }, 400);
-});
-
-/** Write a V8 heap snapshot to disk. Requires secret key auth + dev-only. */
-// debugRouter.get("/heap-snapshot", async (c) => {
-// if (process.env.NODE_ENV === "production") {
-// return c.json({ error: "Not available in production" }, 403);
-// }
-
-// const ctx = c.get("ctx");
-// if (!ALLOWED_ORG_IDS.has(ctx.org?.id)) {
-// return c.json({ error: "Forbidden" }, 403);
-// }
-
-// const snapshotDir = new URL("../../../perf/snapshots/", import.meta.url)
-// .pathname;
-// const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
-// const filename = `heap-${timestamp}-pid${process.pid}.heapsnapshot`;
-// const filepath = `${snapshotDir}${filename}`;
-
-// writeHeapSnapshot(filepath);
-
-// return c.json({ ok: true, file: filename, path: filepath });
-// });
diff --git a/server/src/internal/products/planRouter.ts b/server/src/internal/products/planRouter.ts
deleted file mode 100644
index fc194853c..000000000
--- a/server/src/internal/products/planRouter.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-import { Hono } from "hono";
-import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
-import { handlePlanHasCustomersV2 } from "@/internal/products/handlers/handlePlanHasCustomersV2.js";
-import { handleCopyProductV2 } from "./handlers/handleCopyProduct/handleCopyProductV2.js";
-import { handleCreatePlan } from "./handlers/handleCreateProduct/handleCreatePlan.js";
-import { handleCreatePlanV2 } from "./handlers/handleCreateProduct/handleCreatePlanV2.js";
-import { handleDeletePlanV1 } from "./handlers/handleDeletePlan/handleDeletePlanV1.js";
-import { handleDeletePlanV2 } from "./handlers/handleDeletePlan/handleDeletePlanV2.js";
-import { handleGetPlanV1 } from "./handlers/handleGetPlan/handleGetPlanV1.js";
-import { handleGetPlanV2 } from "./handlers/handleGetPlan/handleGetPlanV2.js";
-import { handleGetPlanDeleteInfo } from "./handlers/handleGetPlanDeleteInfo.js";
-import { handleListPlansV2 } from "./handlers/handleListPlans/handleListPlansV2.js";
-import { handleListPlans } from "./handlers/handleListPlans.js";
-import { handleMigrateProductV2 } from "./handlers/handleMigrateProductV2.js";
-import { handleUpdatePlanV1 } from "./handlers/handleUpdatePlan/handleUpdatePlanV1.js";
-import { handleUpdatePlanV2 } from "./handlers/handleUpdatePlan/handleUpdatePlanV2.js";
-
-export const honoProductBetaRouter = new Hono();
-honoProductBetaRouter.get("", ...handleListPlans);
-
-// Create a Hono app for products
-export const honoProductRouter = new Hono();
-export const migrationRouter = new Hono();
-
-// Migrations
-migrationRouter.post("/migrations", ...handleMigrateProductV2);
-
-// CRUD
-honoProductRouter.get("", ...handleListPlans);
-honoProductRouter.post("", ...handleCreatePlan);
-honoProductRouter.get("/:product_id", ...handleGetPlanV1);
-honoProductRouter.post("/:product_id", ...handleUpdatePlanV1); // will be deprecated
-honoProductRouter.patch("/:product_id", ...handleUpdatePlanV1); // will be deprecated
-honoProductRouter.delete("/:product_id", ...handleDeletePlanV1);
-
-// Others
-honoProductRouter.post("/:product_id/copy", ...handleCopyProductV2);
-
-// Info before deleting plan
-honoProductRouter.get(
- "/:product_id/has_customers",
- ...handlePlanHasCustomersV2,
-);
-honoProductRouter.post(
- "/:product_id/has_customers",
- ...handlePlanHasCustomersV2,
-);
-honoProductRouter.get("/:product_id/deletion_info", ...handleGetPlanDeleteInfo);
-
-// RPC
-export const plansRpcRouter = new Hono();
-plansRpcRouter.post("/plans.get", ...handleGetPlanV2);
-plansRpcRouter.post("/plans.list", ...handleListPlansV2);
-plansRpcRouter.post("/plans.create", ...handleCreatePlanV2);
-plansRpcRouter.post("/plans.update", ...handleUpdatePlanV2);
-plansRpcRouter.post("/plans.delete", ...handleDeletePlanV2);
diff --git a/server/src/utils/importUtils/updateUsages.ts b/server/src/utils/importUtils/updateUsages.ts
deleted file mode 100644
index a5c9bdba4..000000000
--- a/server/src/utils/importUtils/updateUsages.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- type FullCustomer,
- fullCustomerToCustomerEntitlements,
-} from "@autumn/shared";
-import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
-import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
-import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
-
-export const updateUsages = async ({
- ctx,
- featureId,
- usage,
- fullCus,
-}: {
- ctx: AutumnContext;
- featureId: string;
- usage: number;
- fullCus: FullCustomer;
-}) => {
- const cusEnts = fullCustomerToCustomerEntitlements({
- fullCustomer: fullCus,
- inStatuses: RELEVANT_STATUSES,
- featureId,
- });
- if (cusEnts.length === 0) {
- throw new Error(`No cus ent for ${featureId}`);
- }
-
- const cusEnt = cusEnts[0];
- const newBalance = cusEnt.balance! - usage;
-
- await CusEntService.update({
- ctx,
- id: cusEnt.id,
- updates: {
- balance: newBalance,
- },
- });
-};
diff --git a/server/src/utils/scriptUtils/clearOrg.ts b/server/src/utils/scriptUtils/clearOrg.ts
deleted file mode 100644
index 85851a970..000000000
--- a/server/src/utils/scriptUtils/clearOrg.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
- AppEnv,
- customers,
- features,
- type Organization,
- products,
-} from "@autumn/shared";
-import { and, eq, inArray } from "drizzle-orm";
-import type { DrizzleCli } from "@/db/initDrizzle.js";
-
-export const clearCustomersInBatches = async ({
- db,
- org,
- batchSize = 450,
-}: {
- db: DrizzleCli;
- org: Organization;
- batchSize?: number;
-}) => {
- let deletedCount = 0;
-
- while (true) {
- // Get a batch of customer IDs to delete
- const customerBatch = await db
- .select({ internalId: customers.internal_id })
- .from(customers)
- .where(
- and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
- )
- .limit(batchSize);
-
- if (customerBatch.length === 0) {
- break; // No more customers to delete
- }
-
- // Delete the batch
- const customerIds = customerBatch
- .map((c) => c.internalId)
- .filter((id) => id !== null);
-
- console.log("Deleting customers:", customerIds);
-
- await db
- .delete(customers)
- .where(inArray(customers.internal_id, customerIds));
-
- deletedCount += customerBatch.length;
- console.log(
- `Deleted ${customerBatch.length} customers (total: ${deletedCount})`,
- );
- }
-
- return deletedCount;
-};
-
-export const clearOrg = async ({
- db,
- org,
-}: {
- db: DrizzleCli;
- org: Organization;
-}) => {
- const deletedCount = await clearCustomersInBatches({ db, org });
- console.log(`Cleared ${deletedCount} customers`);
-
- await db
- .delete(products)
- .where(and(eq(products.org_id, org.id), eq(products.env, AppEnv.Sandbox)));
-
- console.log("Cleared products");
-
- await db
- .delete(features)
- .where(and(eq(features.org_id, org.id), eq(features.env, AppEnv.Sandbox)));
-
- console.log("Cleared features");
-};
diff --git a/server/src/utils/scriptUtils/genScriptUtils.ts b/server/src/utils/scriptUtils/genScriptUtils.ts
deleted file mode 100644
index b367b5d25..000000000
--- a/server/src/utils/scriptUtils/genScriptUtils.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import csv from "csv-parser";
-import fs from "fs";
-
-export const parseCsv = ({
- path,
- delimiter = ",",
-}: {
- path: string;
- delimiter?: string;
-}) => {
- return new Promise((resolve, reject) => {
- const stream = fs.createReadStream(path);
- const results: any[] = [];
- const headers: string[] = [];
- stream
- .pipe(csv({ separator: delimiter }))
- .on("data", (data) => {
- results.push(data);
- // if (headers.length === 0) {
- // headers = Object.keys(data);
- // } else {
- // results.push(data);
- // }
- })
- .on("end", () => resolve(results))
- .on("error", (error) => reject(error));
- }) as Promise;
-};
diff --git a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts b/server/src/utils/scriptUtils/getAll/getAllCusProds.ts
deleted file mode 100644
index 6c550394a..000000000
--- a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import { AppEnv, type FullCusProduct } from "@autumn/shared";
-import { sql } from "drizzle-orm";
-import type { DrizzleCli } from "@/db/initDrizzle.js";
-
-const cusProductsQuery = ({
- lastProductId,
- internalProductId,
- pageSize = 250,
-}: {
- lastProductId?: string;
- internalProductId?: string;
- pageSize?: number;
-}) => {
- // const withStatusFilter = () => {
- // return inStatuses
- // ? sql`AND cp.status = ANY(ARRAY[${sql.join(
- // inStatuses.map((status) => sql`${status}`),
- // sql`, `,
- // )}])`
- // : sql``;
- // };
-
- return sql`
- SELECT
- cp.*,
- row_to_json(prod) AS product,
-
- -- Spread customer_prices fields + add price field
- COALESCE(
- json_agg(DISTINCT (
- to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
- )) FILTER (WHERE cpr.id IS NOT NULL),
- '[]'::json
- ) AS customer_prices,
-
- -- Spread customer_entitlements fields + add entitlement and replaceables
- COALESCE(
- json_agg(DISTINCT (
- to_jsonb(ce.*) || jsonb_build_object(
- 'entitlement', (
- SELECT row_to_json(ent_with_feature)
- FROM (
- SELECT e.*, row_to_json(f) AS feature
- FROM entitlements e
- JOIN features f ON e.internal_feature_id = f.internal_id
- WHERE e.id = ce.entitlement_id
- ) AS ent_with_feature
- ),
- 'replaceables', (
- SELECT COALESCE(
- json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
- '[]'::json
- )
- FROM replaceables r
- WHERE r.cus_ent_id = ce.id
- )
- )
- )) FILTER (WHERE ce.id IS NOT NULL),
- '[]'::json
- ) AS customer_entitlements,
-
- -- free_trial
- (
- SELECT row_to_json(ft)
- FROM free_trials ft
- WHERE ft.id = cp.free_trial_id
- ) AS free_trial
-
- FROM customer_products cp
- JOIN products prod ON cp.internal_product_id = prod.internal_id
- LEFT JOIN customer_prices cpr ON cpr.customer_product_id = cp.id
- LEFT JOIN prices p ON cpr.price_id = p.id
- LEFT JOIN customer_entitlements ce ON ce.customer_product_id = cp.id
- WHERE cp.internal_product_id = ${internalProductId}
- ${lastProductId ? sql`AND cp.id < ${lastProductId}` : sql``}
- GROUP BY cp.id, prod.*
- ORDER BY cp.id DESC
- LIMIT ${pageSize}
- `;
-};
-
-export const getAllFullCusProducts = async ({
- db,
- internalProductId,
-}: {
- db: DrizzleCli;
- internalProductId: string;
-}) => {
- let lastProductId = "";
- const allData: any[] = [];
- const pageSize = 500;
-
- while (true) {
- const data = await db.execute(
- cusProductsQuery({
- lastProductId,
- pageSize,
- internalProductId,
- }),
- );
-
- if (data.length === 0) break;
-
- console.log(`Fetched ${data.length} customer products`);
- allData.push(...data);
- lastProductId = data[data.length - 1].id as string;
- }
-
- return allData as FullCusProduct[];
-};
diff --git a/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts b/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts
deleted file mode 100644
index 5bd1c2b0e..000000000
--- a/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts
+++ /dev/null
@@ -1,99 +0,0 @@
-import type { Stripe } from "stripe";
-import { timeout } from "@/utils/genUtils.js";
-
-export const getAllStripeSubscriptions = async ({
- numPages,
- limit = 100,
- stripeCli,
- waitForSeconds,
-}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
- waitForSeconds?: number;
-}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allSubscriptions: any[] = [];
-
- let pageCount = 0;
- while (hasMore) {
- const response: any = await stripeCli.subscriptions.list({
- limit,
- starting_after: startingAfter || undefined,
- expand: ["data.discounts.coupon"],
- });
-
- if (response.data.length === 0) {
- break;
- }
-
- allSubscriptions.push(...response.data);
-
- hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
-
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
-
- console.log("Fetched", allSubscriptions.length, "subscriptions");
- if (waitForSeconds) {
- await timeout(1000);
- }
- }
-
- return {
- subscriptions: allSubscriptions,
- total: allSubscriptions.length,
- };
-};
-export const getAllStripeSchedules = async ({
- numPages,
- limit = 100,
- stripeCli,
- waitForSeconds,
-}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
- waitForSeconds?: number;
-}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allSchedules: any[] = [];
-
- let pageCount = 0;
- while (hasMore) {
- const response: any = await stripeCli.subscriptionSchedules.list({
- limit,
- starting_after: startingAfter || undefined,
- expand: ["data.phases.items.price"],
- });
-
- if (response.data.length === 0) {
- break;
- }
-
- allSchedules.push(...response.data);
-
- hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
-
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
-
- console.log("Fetched", allSchedules.length, "schedules");
- if (waitForSeconds) {
- await timeout(1000);
- }
- }
-
- return {
- schedules: allSchedules,
- total: allSchedules.length,
- };
-};
diff --git a/server/src/utils/scriptUtils/getAll/getAllUsers.ts b/server/src/utils/scriptUtils/getAll/getAllUsers.ts
deleted file mode 100644
index 90feab40c..000000000
--- a/server/src/utils/scriptUtils/getAll/getAllUsers.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { user } from "@autumn/shared";
-import { desc } from "drizzle-orm";
-import type { DrizzleCli } from "@/db/initDrizzle.js";
-
-export const getAllUsers = async (db: DrizzleCli) => {
- const users = [];
- let offset = 0;
- const limit = 200;
-
- while (true) {
- const batch = await db
- .select()
- .from(user)
- .limit(limit)
- .offset(offset)
- .orderBy(desc(user.createdAt));
- users.push(...batch);
-
- if (batch.length < limit) {
- break;
- }
-
- offset += limit;
- console.log(`Fetched ${users.length} users`);
- }
- return users;
-};
diff --git a/server/src/utils/scriptUtils/logUtils/logSubItems.ts b/server/src/utils/scriptUtils/logUtils/logSubItems.ts
deleted file mode 100644
index 07dc09a92..000000000
--- a/server/src/utils/scriptUtils/logUtils/logSubItems.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { stripeToAtmnAmount } from "@autumn/shared";
-import { subItemToAutumnInterval } from "@tests/utils/stripeUtils";
-import type Stripe from "stripe";
-import type { Logger } from "../../../external/logtail/logtailUtils";
-
-export const logSubItems = ({
- sub,
- subItems,
- withPriceId = false,
- withItemId = false,
- logger,
-}: {
- sub?: Stripe.Subscription;
- subItems?: Stripe.SubscriptionItem[];
- withPriceId?: boolean;
- withItemId?: boolean;
- logger?: Logger | Console;
-}) => {
- const finalSubItems = subItems || sub!.items.data;
-
- if (!logger) {
- logger = console;
- }
-
- for (const item of finalSubItems) {
- const isMetered = item.price.recurring?.usage_type === "metered";
-
- const atmnPrice = stripeToAtmnAmount({
- amount: item.price.unit_amount || 0,
- currency: item.price.currency,
- });
-
- if (isMetered) {
- logger.info(`Usage price`);
- } else {
- const price = atmnPrice;
- const subInterval = subItemToAutumnInterval(item);
- logger.info(
- `${price} ${item.price.currency}${item.quantity !== 1 ? ` x ${item.quantity}` : ""} / ${subInterval?.intervalCount} ${subInterval?.interval} ${withPriceId ? `(${item.price.id})` : ""} ${withItemId ? `(${item.id})` : ""}`,
- );
- }
- }
-};
diff --git a/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts b/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts
deleted file mode 100644
index a5a93900e..000000000
--- a/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { AppEnv } from "@autumn/shared";
-import type { ExtendedRequest } from "@/utils/models/Request.js";
-
-export interface HandleCustomerCreatedData {
- req: Partial;
- orgId: string;
- env: AppEnv;
- internalCustomerId: string;
-}
diff --git a/server/tests/unit/balances/track-v3/runTrackV3Idempotency.test.ts b/server/tests/unit/balances/track-v3/runTrackV3Idempotency.test.ts
index 7bfcae858..9cd4d1b77 100644
--- a/server/tests/unit/balances/track-v3/runTrackV3Idempotency.test.ts
+++ b/server/tests/unit/balances/track-v3/runTrackV3Idempotency.test.ts
@@ -61,7 +61,10 @@ mock.module("@/internal/balances/track/v3/runRedisTrackV3.js", () => ({
},
}));
-import { runTrackV3 } from "@/internal/balances/track/v3/runTrackV3.js";
+const { runTrackV3 } = await import(
+ // @ts-expect-error - Bun test cache-busting import query isolates module mocks.
+ "@/internal/balances/track/v3/runTrackV3.js?runTrackV3Idempotency"
+);
const ctx = {
apiVersion: new ApiVersionClass(ApiVersion.V2_1),
diff --git a/server/tinybird/pipes/aggregate_groupable.pipe b/server/tinybird/pipes/aggregate_groupable.pipe
index 2202a3263..4e3da59f5 100644
--- a/server/tinybird/pipes/aggregate_groupable.pipe
+++ b/server/tinybird/pipes/aggregate_groupable.pipe
@@ -1,9 +1,8 @@
DESCRIPTION >
Aggregate queries with grouping by a property key, customer_id, or entity_id.
Routes to the smallest viable MV for the query shape:
- - events_hourly_no_properties_two_mv: customer_id/entity_id grouping with no filters
- - events_hourly_promoted_mv: promoted property key (apiKeyId, endpoint, source) with no filters
- - events_hourly_mv: fallback for arbitrary properties or filtered queries
+ - events_hourly_no_properties_two_mv: customer_id/entity_id grouping with no property filters
+ - events_hourly_mv: arbitrary properties or filtered queries
Uses PER-BIN TOP N groups + "AUTUMN_RESERVED" bucket ((N+1) max total per bin).
N is controlled by the max_groups parameter (default 9).
Returns unpivoted data: (period, event_name, group_value, total_value, _truncated)
@@ -15,10 +14,8 @@ DESCRIPTION >
Aggregate raw data by period, event_name, and group_value.
SQL >
%
- {% set no_filters = (not defined(filter_key_0) or String(filter_key_0, '') == '') and (not defined(filter_key_1) or String(filter_key_1, '') == '') and (not defined(filter_key_2) or String(filter_key_2, '') == '') and (not defined(filter_key_3) or String(filter_key_3, '') == '') and (not defined(filter_key_4) or String(filter_key_4, '') == '') %}
- {% set is_promoted_key = String(property_key, '') in ['apiKeyId', 'endpoint', 'source'] %}
- {% set use_no_props = (String(group_column, 'property') in ['customer_id', 'entity_id']) and no_filters %}
- {% set use_promoted = (String(group_column, 'property') == 'property') and is_promoted_key and no_filters %}
+ {% set no_property_filters = (not defined(filter_key_0) or String(filter_key_0, '') == '') and (not defined(filter_key_1) or String(filter_key_1, '') == '') and (not defined(filter_key_2) or String(filter_key_2, '') == '') and (not defined(filter_key_3) or String(filter_key_3, '') == '') and (not defined(filter_key_4) or String(filter_key_4, '') == '') %}
+ {% set use_no_props = (String(group_column, 'property') in ['customer_id', 'entity_id']) and no_property_filters %}
SELECT
{% if String(bin_size, 'day') == 'hour' %}
@@ -33,8 +30,6 @@ SQL >
customer_id as group_value,
{% elif String(group_column, 'property') == 'entity_id' %}
entity_id as group_value,
- {% elif use_promoted %}
- {{ column(String(property_key, '')) }} as group_value,
{% else %}
{{ column('properties.' + String(property_key, '')) }}::String as group_value,
{% end %}
@@ -42,8 +37,6 @@ SQL >
FROM
{% if use_no_props %}
events_hourly_no_properties_two_mv
- {% elif use_promoted %}
- events_hourly_promoted_mv
{% else %}
events_hourly_mv
{% end %}
@@ -78,8 +71,6 @@ SQL >
{# Filter out null/empty grouping values #}
{% if String(group_column, 'property') == 'entity_id' %}
AND entity_id IS NOT NULL AND entity_id != ''
- {% elif use_promoted %}
- AND {{ column(String(property_key, '')) }} != ''
{% elif String(group_column, 'property') == 'property' %}
AND {{ column('properties.' + String(property_key, '')) }}::String IS NOT NULL
AND {{ column('properties.' + String(property_key, '')) }}::String != ''
diff --git a/shared/api/balances/track/changes/V0.2_TrackChange.ts b/shared/api/balances/track/changes/V0.2_TrackChange.ts
deleted file mode 100644
index 2cf3f058c..000000000
--- a/shared/api/balances/track/changes/V0.2_TrackChange.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
-import {
- AffectedResource,
- defineVersionChange,
-} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
-import type { z } from "zod/v4";
-import {
- TrackResponseV0Schema,
- TrackResponseV1Schema,
-} from "../prevVersions/trackResponseV1.js";
-import {
- type TrackLegacyData,
- TrackLegacyDataSchema,
-} from "../trackLegacyData.js";
-
-/**
- * V0_2_CheckChange: Transforms check response TO V0_2 format
- *
- * Applied when: targetVersion <= V0_2
- *
- * Breaking changes introduced in V0.2 (that we reverse here):
- *
- * 1. Structure: Single check result object → balances array format
- * - V0.2+: Single object with allowed, feature_id, balance, unlimited, etc.
- * - V0_2: { allowed, balances: [{ feature_id, required, balance, unlimited, usage_allowed }] }
- *
- * 2. Boolean features: No balance fields → balance: null
- * 3. Unlimited features: Return unlimited: true, usage_allowed based on overage_allowed
- * 4. Metered features: Return required_balance and balance
- *
- * Input: CheckResult (V0.2+ format)
- * Output: CheckResponseV0 (V0_2 balances array format)
- */
-
-export const V0_2_CheckChange = defineVersionChange({
- name: "V0.2 Check Change",
- newVersion: ApiVersion.V1_1, // Breaking change introduced in V1_1
- oldVersion: ApiVersion.V0_2, // Applied when targetVersion <= V0_2
- description: [
- "Check response transformed to balances array format",
- "Single check result object → { allowed, balances: [...] }",
- ],
- affectedResources: [AffectedResource.Check],
- newSchema: TrackResponseV1Schema,
- oldSchema: TrackResponseV0Schema,
- legacyDataSchema: TrackLegacyDataSchema,
- affectsResponse: true,
-
- // Response: V1.1+ (CheckResult) → V0_2 (CheckResponseV0)
- transformResponse: ({
- input,
- legacyData,
- }: {
- input: z.infer;
- legacyData?: TrackLegacyData;
- }): z.infer => {
- return {
- success: true,
- };
- },
-});
diff --git a/shared/api/billing/openCustomerPortalParams.ts b/shared/api/billing/openCustomerPortalParams.ts
deleted file mode 100644
index e69de29bb..000000000
diff --git a/shared/api/customers/cusFeatures/utils/convert/balancesToCheckFeature.ts b/shared/api/customers/cusFeatures/utils/convert/balancesToCheckFeature.ts
deleted file mode 100644
index 9f0f341f0..000000000
--- a/shared/api/customers/cusFeatures/utils/convert/balancesToCheckFeature.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import type { ApiBalanceInput } from "@api/customers/cusFeatures/utils/convert/apiBalanceToAllowed";
-
-export const balancesToCheckFeature = ({
- balances,
-}: {
- balances: Record;
-}) => {
- return balances.map((balance) => {
- return {
- featureId: balance.featureId,
- };
- });
-};
diff --git a/shared/api/customers/cusPlans/changes/V2.0_ApiSubscriptionChange.ts b/shared/api/customers/cusPlans/changes/V2.0_ApiSubscriptionChange.ts
deleted file mode 100644
index bfc269261..000000000
--- a/shared/api/customers/cusPlans/changes/V2.0_ApiSubscriptionChange.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { planV1ToV0 } from "@api/products/mappers/planV1ToV0";
-import type { ApiSubscription } from "../apiSubscription";
-import type { ApiSubscriptionV1 } from "../apiSubscriptionV1";
-
-export function transformApiSubscriptionV1ToV0({
- input,
-}: {
- input: ApiSubscriptionV1;
-}): ApiSubscription {
- return {
- plan: input.plan ? planV1ToV0(input.plan) : undefined,
- plan_id: input.plan_id,
- default: input.auto_enable,
- add_on: input.add_on,
- status: input.status,
- past_due: input.past_due,
- canceled_at: input.canceled_at,
- expires_at: input.expires_at,
- trial_ends_at: input.trial_ends_at,
- started_at: input.started_at,
- current_period_start: input.current_period_start,
- current_period_end: input.current_period_end,
- quantity: input.quantity,
- };
-}
diff --git a/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts b/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts
deleted file mode 100644
index 9395ab0ba..000000000
--- a/shared/api/customers/cusPlans/mappers/apiPurchaseV0ToSubscriptionV0.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { planV1ToV0 } from "@api/products/mappers/planV1ToV0";
-import type { SharedContext } from "../../../../types/sharedContext";
-import type { ApiSubscription } from "../apiSubscription";
-import type { ApiPurchaseV0 } from "../apiSubscriptionV1";
-
-/**
- * Converts an ApiPurchaseV0 to an ApiSubscription (V0) for backwards compatibility.
- * Purchases are represented as subscriptions with sensible defaults for missing fields.
- */
-export function apiPurchaseV0ToSubscriptionV0({
- ctx,
- input,
-}: {
- ctx: SharedContext;
- input: ApiPurchaseV0;
-}): ApiSubscription {
- return {
- plan: input.plan ? planV1ToV0({ ctx, plan: input.plan }) : undefined,
- plan_id: input.plan_id,
- default: false,
- add_on: true,
- status: "active",
- past_due: false,
- canceled_at: null,
- expires_at: input.expires_at,
- trial_ends_at: null,
- started_at: input.started_at,
- current_period_start: null,
- current_period_end: null,
- quantity: input.quantity,
- };
-}
diff --git a/shared/api/features/utils/findCreditSystemsByFeatureId.ts b/shared/api/features/utils/findCreditSystemsByFeatureId.ts
deleted file mode 100644
index b53a89ecf..000000000
--- a/shared/api/features/utils/findCreditSystemsByFeatureId.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { ApiFeatureV1 } from "@api/features/apiFeatureV1";
-
-export const findCreditSystemsByFeatureId = ({
- featureId,
- creditSystems,
-}: {
- featureId: string;
- creditSystems: ApiFeatureV1[];
-}) => {
- return creditSystems.filter((creditSystem) =>
- creditSystem.credit_schema?.some(
- (schema) => schema.metered_feature_id === featureId,
- ),
- );
-};
diff --git a/shared/api/products/items/mappers/planItemParamsV1ToPlanItemV0.ts b/shared/api/products/items/mappers/planItemParamsV1ToPlanItemV0.ts
deleted file mode 100644
index cfebb2ec4..000000000
--- a/shared/api/products/items/mappers/planItemParamsV1ToPlanItemV0.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { FeatureNotFoundError } from "@api/errors/classes/featureErrClasses.js";
-import { billingMethodToUsageModel } from "@api/products/components/mappers/billingMethodTousageModel.js";
-import type { CreatePlanItemParamsV1 } from "@api/products/items/crud/createPlanItemParamsV1.js";
-import type { ApiPlanItemV0 } from "@api/products/items/previousVersions/apiPlanItemV0.js";
-import { featureUtils } from "@utils/index";
-import { subtractIncludedFromTiers } from "@utils/productV2Utils/productItemUtils/tierUtils.js";
-import type { SharedContext } from "../../../../types/sharedContext.js";
-
-/**
- * Converts V1 plan item params (CreatePlanItemParamsV1) to V0 response format (ApiPlanItemV0)
- */
-export function planItemParamsV1ToPlanItemV0({
- ctx,
- item,
-}: {
- ctx: SharedContext;
- item: CreatePlanItemParamsV1;
-}): ApiPlanItemV0 {
- const { features } = ctx;
-
- const feature = features.find((f) => f.id === item.feature_id);
- if (!feature) {
- throw new FeatureNotFoundError({ featureId: item.feature_id });
- }
-
- const isAllocatedFeature = featureUtils.isAllocated(feature);
-
- const included = item.included ?? 0;
-
- // V1 API: tier `to` values INCLUDE included usage.
- // Internal: tier `to` values do NOT include included usage.
- const internalTiers = item.price?.tiers
- ? subtractIncludedFromTiers({ tiers: item.price.tiers, included })
- : undefined;
-
- return {
- feature_id: item.feature_id,
- granted_balance: included,
- unlimited: item.unlimited ?? false,
-
- reset: item.reset
- ? {
- interval: item.reset.interval,
- interval_count: item.reset.interval_count,
- reset_when_enabled: !isAllocatedFeature,
- }
- : null,
-
- price: item.price
- ? {
- amount: item.price.amount,
- tiers: internalTiers,
- tier_behavior: item.price.tier_behavior,
- interval: item.price.interval,
- interval_count: item.price.interval_count,
- billing_units: item.price.billing_units ?? 1,
- usage_model: billingMethodToUsageModel(item.price.billing_method),
- max_purchase: item.price.max_purchase ?? null,
- }
- : null,
-
- rollover: item.rollover
- ? {
- max: item.rollover.max ?? null,
- max_percentage: item.rollover.max_percentage ?? null,
- expiry_duration_type: item.rollover.expiry_duration_type,
- expiry_duration_length: item.rollover.expiry_duration_length,
- }
- : undefined,
-
- proration: item.proration,
- };
-}
diff --git a/shared/api/products/mappers/planV0ToProductV2.ts b/shared/api/products/mappers/planV0ToProductV2.ts
deleted file mode 100644
index 5153b42dd..000000000
--- a/shared/api/products/mappers/planV0ToProductV2.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { planV0ToProductItems } from "@api/products/mappers/planV0ToProductItems";
-import type { ApiPlan } from "@api/products/previousVersions/apiPlanV0";
-import type { ProductV2 } from "@models/productV2Models/productV2Models";
-import type { SharedContext } from "../../../types/sharedContext";
-
-export function planV0ToProductV2({
- ctx,
- plan,
-}: {
- ctx: SharedContext;
- plan: ApiPlan;
-}): ProductV2 {
- // Convert plan to items using shared utility
- const items = planV0ToProductItems({ ctx, plan });
-
- // Check if archived field exists on plan (it's on ApiPlan, not CreatePlanParams)
- const archived =
- "archived" in plan && plan.archived !== undefined
- ? plan.archived
- : undefined;
-
- return {
- id: plan.id,
- name: plan.name,
- description: plan.description ?? null,
- is_add_on: plan.add_on,
- is_default: plan.default,
- group: plan.group ?? "",
- items,
- free_trial: plan.free_trial
- ? {
- duration: plan.free_trial.duration_type,
- length: plan.free_trial.duration_length,
- unique_fingerprint: false,
- card_required: plan.free_trial.card_required,
- }
- : null,
- ...(archived !== undefined && { archived }),
-
- version: plan.version,
- env: plan.env,
- created_at: plan.created_at,
- };
-}
diff --git a/shared/models/cusModels/fullSubjectModel.ts b/shared/models/cusModels/fullSubjectModel.ts
deleted file mode 100644
index 3c5bf4663..000000000
--- a/shared/models/cusModels/fullSubjectModel.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { z } from "zod/v4";
-import { FullAggregatedCustomerEntitlementSchema } from "../cusProductModels/cusEntModels/aggregatedCusEnt.js";
-import {
- type FullCustomerEntitlement,
- FullCustomerEntitlementSchema,
-} from "../cusProductModels/cusEntModels/cusEntModels.js";
-import { CustomerPriceSchema } from "../cusProductModels/cusPriceModels/cusPriceModels.js";
-import {
- type FullCusProduct,
- FullCusProductSchema,
-} from "../cusProductModels/cusProductModels.js";
-import { SubscriptionSchema } from "../subModels/subModels.js";
-import { type Customer, CustomerSchema } from "./cusModels.js";
-import { type Entity, EntitySchema } from "./entityModels/entityModels.js";
-import { InvoiceSchema } from "./invoiceModels/invoiceModels.js";
-
-export const SubjectType = {
- Customer: "customer",
- Entity: "entity",
-} as const;
-export type SubjectType = (typeof SubjectType)[keyof typeof SubjectType];
-
-export const FullSubjectSchema = z.object({
- subjectType: z.enum(["customer", "entity"]),
-
- customerId: z.string(),
- internalCustomerId: z.string(),
- entityId: z.string().optional(),
- internalEntityId: z.string().optional(),
-
- customer: CustomerSchema,
- entity: EntitySchema.optional(),
-
- customer_products: z.array(FullCusProductSchema),
- extra_customer_entitlements: z.array(FullCustomerEntitlementSchema),
-
- subscriptions: z.array(SubscriptionSchema).optional(),
- invoices: z.array(InvoiceSchema),
-
- aggregated_customer_products: z.array(FullCusProductSchema).optional(),
- aggregated_customer_entitlements: z
- .array(FullAggregatedCustomerEntitlementSchema)
- .optional(),
- aggregated_customer_prices: z.array(CustomerPriceSchema).optional(),
-});
-
-export type FullSubject = z.infer;
-
-/** Backward-compat type for entity DB layer files. */
-export type FullEntity = Entity & {
- customer: Customer;
- customer_products: FullCusProduct[];
- extra_customer_entitlements: FullCustomerEntitlement[];
-};
diff --git a/vite/src/components/forms/attach-product/attach-confirmation-info.tsx b/vite/src/components/forms/attach-product/attach-confirmation-info.tsx
deleted file mode 100644
index 06f5adf86..000000000
--- a/vite/src/components/forms/attach-product/attach-confirmation-info.tsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import type { CheckoutResponseV0 } from "@autumn/shared";
-import type { ReactNode } from "react";
-import { useIsLatestVersion } from "@/hooks/stores/useProductStore";
-import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
-import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
-
-export const AttachConfirmationInfo = ({
- previewData,
-}: {
- previewData?: CheckoutResponseV0 | null;
-}) => {
- const isLatestVersion = useIsLatestVersion(previewData?.product);
-
- const renderInfoBoxes = (): ReactNode[] => {
- const boxes: ReactNode[] = [];
-
- if (!previewData) {
- return boxes;
- }
-
- if (!isLatestVersion) {
- boxes.push(
-
- You're enabling a previous version (v{previewData.product.version}) of
- this plan.
- ,
- );
- }
-
- // Payment method required
- if (previewData.url) {
- let secondaryText = "";
- if (previewData.product.free_trial?.card_required === true) {
- secondaryText = "to start this trial";
- } else {
- secondaryText = "as this plan has prices";
- }
-
- boxes.push(
-
- A payment method is required {secondaryText}
- ,
- );
- }
-
- if (previewData.product.free_trial) {
- let secondaryText = "";
- if (previewData.product.free_trial?.card_required === true) {
- secondaryText = " and the customer will be charged";
- } else {
- secondaryText = " and this plan will expire";
- }
-
- boxes.push(
-
- Trial ends {formatUnixToDate(previewData.next_cycle?.starts_at)}
- {secondaryText}
- ,
- );
- }
-
- // Show scenario-based info if switching from another plan and not attaching add-on
- if (
- previewData.current_product &&
- previewData.product &&
- !previewData.product.is_add_on
- ) {
- const scenario = previewData.product.scenario as
- | "upgrade"
- | "downgrade"
- | "cancel"
- | "new"
- | string;
-
- switch (scenario) {
- case "upgrade":
- boxes.push(
-
- This upgrade will immediately replace the customer's current plan:{" "}
- {previewData.current_product.name}
- ,
- );
- break;
- case "downgrade":
- boxes.push(
-
- This downgrade will replace the customer's current plan:{" "}
- {previewData.current_product.name}
- ,
- );
- break;
- case "cancel":
- boxes.push(
-
- This will cancel the customer's current billing subscription:{" "}
- {previewData.current_product.name}
- ,
- );
- break;
- case "new":
- boxes.push(
-
- This will be enabled alongside existing plans{" "}
- ,
- );
- }
- }
-
- if (
- previewData.next_cycle?.starts_at &&
- previewData.product?.scenario === "downgrade"
- ) {
- const startsAtString = formatUnixToDate(previewData.next_cycle.starts_at);
-
- boxes.push(
-
- Plan change will take effect next cycle, on{" "}
- {startsAtString}
- ,
- );
- }
-
- // If switching products, show info about current product
-
- return boxes;
- };
-
- const infoBoxes = renderInfoBoxes();
-
- if (infoBoxes.length === 0) {
- return null;
- }
-
- return (
-
- {infoBoxes.map((box, index) => (
-
{box}
- ))}
-
- );
-};
diff --git a/vite/src/components/forms/attach-product/attach-product-actions.tsx b/vite/src/components/forms/attach-product/attach-product-actions.tsx
deleted file mode 100644
index 587dec17d..000000000
--- a/vite/src/components/forms/attach-product/attach-product-actions.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-import type { CheckoutResponseV0, ProductV2 } from "@autumn/shared";
-import type { LucideIcon } from "lucide-react";
-import { ArrowUpRightFromSquare, CircleCheck } from "lucide-react";
-import { useState } from "react";
-import { toast } from "sonner";
-import { useAttachProductMutation } from "@/components/forms/attach-product/use-attach-product-mutation";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/v2/buttons/Button";
-import { useOrg } from "@/hooks/common/useOrg";
-import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery";
-import { useSheetStore } from "@/hooks/stores/useSheetStore";
-import { useEntity } from "@/hooks/stores/useSubscriptionStore";
-import { useEnv } from "@/utils/envUtils";
-import { openInNewTab } from "@/utils/genUtils";
-import { getStripeInvoiceLink } from "@/utils/linkUtils";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-
-function getAttachButtonConfig(isCheckout: boolean): {
- text: string;
- icon: LucideIcon;
-} {
- return isCheckout
- ? { text: "Checkout", icon: ArrowUpRightFromSquare }
- : { text: "Confirm", icon: CircleCheck };
-}
-
-interface AttachProductActionsProps {
- form: UseAttachProductForm;
- product: ProductV2;
- customerId: string;
- onSuccess?: () => void;
- previewData?: CheckoutResponseV0 | null;
- isPreviewLoading?: boolean;
-}
-
-export function AttachProductActions({
- form,
- product,
- customerId,
- onSuccess,
- previewData,
- isPreviewLoading,
-}: AttachProductActionsProps) {
- const { stripeAccount } = useOrgStripeQuery();
- const env = useEnv();
- const org = useOrg();
- const { entityId } = useEntity();
- const [activeAction, setActiveAction] = useState<"invoice" | "attach" | null>(
- null,
- );
- const { closeSheet } = useSheetStore();
-
- const ownStripeAccount = org.org?.stripe_connection !== "default";
-
- const attachMutation = useAttachProductMutation({
- customerId,
- onSuccess: () => {
- form.reset();
- setActiveAction(null);
- onSuccess?.();
- },
- });
-
- const handleAttach = async ({
- useInvoice,
- enableProductImmediately,
- action,
- }: {
- useInvoice: boolean;
- enableProductImmediately?: boolean;
- action: "invoice" | "attach";
- }) => {
- const { prepaidOptions } = form.state.values;
- setActiveAction(action);
-
- if (previewData?.url && action === "attach") {
- window.open(previewData.url, "_blank");
- setActiveAction(null);
- closeSheet();
- return;
- }
-
- try {
- const result = await attachMutation.mutateAsync({
- product,
- prepaidOptions: prepaidOptions || {},
- useInvoice,
- enableProductImmediately,
- entityId: entityId ?? undefined,
- });
-
- // Handle checkout URLs and invoice links
- if (result.data.checkout_url) {
- openInNewTab({ url: result.data.checkout_url });
- } else if (result.data.invoice) {
- const stripeInvoiceUrl = getStripeInvoiceLink({
- stripeInvoice: result.data.invoice,
- env,
- accountId: stripeAccount?.id,
- });
-
- openInNewTab({ url: stripeInvoiceUrl });
- toast.success("Redirected to Stripe to finalize the invoice");
- }
- } catch (error) {
- setActiveAction(null);
- throw error;
- }
- };
-
- const isLoading = attachMutation.isPending;
- const isInvoiceLoading = isLoading && activeAction === "invoice";
- const isAttachLoading = isLoading && activeAction === "attach";
-
- // Don't show buttons if preview is loading
- if (isPreviewLoading || !product) {
- return null;
- }
-
- const isCheckout = !!previewData?.url;
- const { text: attachText, icon: AttachIcon } =
- getAttachButtonConfig(isCheckout);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-form-schema.ts b/vite/src/components/forms/attach-product/attach-product-form-schema.ts
deleted file mode 100644
index 4eae52b28..000000000
--- a/vite/src/components/forms/attach-product/attach-product-form-schema.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { z } from "zod/v4";
-
-export const AttachProductFormSchema = z.object({
- productId: z.string(),
- prepaidOptions: z.record(z.string(), z.number().optional()),
-});
-
-export type AttachProductForm = z.infer;
diff --git a/vite/src/components/forms/attach-product/attach-product-form.tsx b/vite/src/components/forms/attach-product/attach-product-form.tsx
deleted file mode 100644
index fcdd9c108..000000000
--- a/vite/src/components/forms/attach-product/attach-product-form.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import type {
- Entity,
- FrontendProduct,
- FullCustomer,
- ProductV2,
-} from "@autumn/shared";
-import { useStore } from "@tanstack/react-form";
-import { FormWrapper } from "@/components/general/form/form-wrapper";
-import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
-import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
-import { usePrepaidItems } from "@/hooks/stores/useProductStore";
-import { useSheetStore } from "@/hooks/stores/useSheetStore";
-import { useEntity } from "@/hooks/stores/useSubscriptionStore";
-import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
-import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
-import { AttachProductActions } from "./attach-product-actions";
-import { AttachProductPrepaidOptions } from "./attach-product-prepaid-options";
-import { AttachProductSelection } from "./attach-product-selection";
-import { AttachProductSummary } from "./attach-product-summary";
-import { useAttachPreview } from "./use-attach-preview";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-import { useAttachProductForm } from "./use-attach-product-form";
-
-interface FormContentProps {
- products: ProductV2[];
- customerId: string;
- form: UseAttachProductForm;
- onSuccess?: () => void;
-}
-
-function FormContent({
- products,
- customerId,
- form,
- onSuccess,
-}: FormContentProps) {
- const sheetData = useSheetStore((s) => s.data);
- const productId = useStore(form.store, (state) => state.values.productId);
- const prepaidOptions = useStore(
- form.store,
- (state) => state.values.prepaidOptions,
- );
-
- // Use customized product from sheet data if available, otherwise find from products list
- const customizedProduct = sheetData?.customizedProduct as
- | FrontendProduct
- | undefined;
- const product = customizedProduct?.id
- ? customizedProduct
- : products.find((p) => p.id === productId && !p.archived);
-
- const { prepaidItems } = usePrepaidItems({ product });
-
- const { entityId } = useEntity();
-
- // Call preview once here and pass data down to children
- const previewQuery = useAttachPreview({
- customerId,
- product,
- entityId: entityId ?? undefined,
- prepaidOptions: prepaidOptions ?? undefined,
- version: product?.version,
- });
-
- // Check if there are prepaid items and if any are not set (undefined/null)
- // Note: 0 is a valid quantity value
- if (prepaidItems.length > 0) {
- const hasUnsetPrepaidQuantity = prepaidItems.some((item) => {
- const quantity = prepaidOptions?.[item.feature_id as string];
- return quantity === undefined || quantity === null;
- });
-
- if (hasUnsetPrepaidQuantity) {
- return null;
- }
- }
-
- if (!form.state.values.productId || !product) {
- return null;
- }
-
- return (
- <>
-
-
-
- >
- );
-}
-
-export function AttachProductForm({
- customerId,
- onSuccess,
-}: {
- customerId: string;
- onSuccess?: () => void;
-}) {
- const itemId = useSheetStore((s) => s.itemId);
- const form = useAttachProductForm({ initialProductId: itemId || undefined });
- const { products, isLoading } = useProductsQuery();
-
- const activeProducts = products.filter((p) => !p.archived);
-
- 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,
- );
-
- if (isLoading) {
- return Loading products...
;
- }
-
- return (
-
-
-
-
-
-
- {entityId ? (
-
-
- Attaching plan to entity{" "}
-
- {fullEntity?.name || fullEntity?.id}
-
-
-
- ) : entities.length > 0 ? (
-
-
- Attaching plan to customer - all entities will get access
-
-
- ) : null}
-
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-line-items.tsx b/vite/src/components/forms/attach-product/attach-product-line-items.tsx
deleted file mode 100644
index ce87810a0..000000000
--- a/vite/src/components/forms/attach-product/attach-product-line-items.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { CheckoutResponseV0 } from "@autumn/shared";
-import {
- SheetAccordion,
- SheetAccordionItem,
-} from "@/components/v2/sheets/SheetAccordion";
-
-export function AttachProductLineItems({
- previewData,
-}: {
- previewData?: CheckoutResponseV0 | null;
-}) {
- const lineItems =
- previewData?.lines?.map((line) => {
- return {
- name: line.description || "Unknown",
- total: line.amount,
- };
- }) || [];
-
- if (lineItems.length === 0) {
- return null;
- }
-
- return (
-
-
-
- {lineItems.map((item, index) => (
-
-
- {item.name}
-
-
- ${item.total.toFixed(2)}
-
-
- ))}
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-prepaid-options.tsx b/vite/src/components/forms/attach-product/attach-product-prepaid-options.tsx
deleted file mode 100644
index ea42f3e31..000000000
--- a/vite/src/components/forms/attach-product/attach-product-prepaid-options.tsx
+++ /dev/null
@@ -1,78 +0,0 @@
-import {
- type FrontendProductItem,
- getFeaturePriceItemDisplay,
-} from "@autumn/shared";
-import { useStore } from "@tanstack/react-form";
-import { useOrg } from "@/hooks/common/useOrg";
-import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
-import {
- usePrepaidItems,
- useProductStore,
-} from "@/hooks/stores/useProductStore";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-
-interface PrepaidOptionsFieldProps {
- form: UseAttachProductForm;
-}
-
-export function AttachProductPrepaidOptions({
- form,
-}: PrepaidOptionsFieldProps) {
- const storeProduct = useProductStore((s) => s.product);
- const { products = [] } = useProductsQuery();
- const selectedProductId = useStore(
- form.store,
- (state) => state.values.productId,
- );
- const { org } = useOrg();
- const product = storeProduct?.id
- ? storeProduct
- : products.find((p) => p.id === selectedProductId && !p.archived);
-
- const { prepaidItems } = usePrepaidItems({ product });
-
- if (prepaidItems.length === 0 || !selectedProductId) {
- return null;
- }
-
- return (
-
-
- {prepaidItems.map((item) => {
- const display = getFeaturePriceItemDisplay({
- item: item as FrontendProductItem,
- feature: item.feature,
- currency: org?.default_currency || "USD",
- fullDisplay: true,
- amountFormatOptions: {
- currencyDisplay: "narrowSymbol",
- },
- });
- return (
-
-
- {display.primary_text}
- {display.secondary_text && ` ${display.secondary_text}`}
-
-
-
- {(quantityField) => (
-
- )}
-
-
- );
- })}
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-selection.tsx b/vite/src/components/forms/attach-product/attach-product-selection.tsx
deleted file mode 100644
index 77ae309a1..000000000
--- a/vite/src/components/forms/attach-product/attach-product-selection.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import {
- isProductAlreadyEnabled,
- isProductCurrentlyAttached,
-} from "@autumn/shared";
-import { PencilSimpleIcon } from "@phosphor-icons/react";
-import { useNavigate } from "react-router";
-import { IconButton } from "@/components/v2/buttons/IconButton";
-import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
-import { useHasChanges } from "@/hooks/stores/useProductStore";
-import { useEntity } from "@/hooks/stores/useSubscriptionStore";
-import { pushPage } from "@/utils/genUtils";
-import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
-import {
- type UseAttachProductForm,
- useResetPrepaidOnProductChange,
-} from "./use-attach-product-form";
-
-interface AttachProductSelectionProps {
- form: UseAttachProductForm;
- customerId: string;
-}
-
-export function AttachProductSelection({
- form,
- customerId,
-}: AttachProductSelectionProps) {
- const { products } = useProductsQuery();
- const availableProducts = products.filter((p) => !p.archived);
- const navigate = useNavigate();
- const productId = form.state.values.productId;
- const hasChanges = useHasChanges();
- const { customer } = useCusQuery();
- const { entityId } = useEntity();
-
- useResetPrepaidOnProductChange({ form });
-
- const handleCustomize = ({ productId }: { productId: string }) => {
- if (!productId || !customerId) {
- return;
- }
-
- pushPage({
- path: `/customers/${customerId}/${productId}`,
- navigate,
- });
- };
-
- return (
-
-
-
- {(field) => (
- {
- const entityIdVal = entityId ?? undefined;
- const alreadyEnabled = isProductAlreadyEnabled({
- productId: p.id,
- customer,
- entityId: entityIdVal,
- });
- const currentlyAttached =
- !alreadyEnabled &&
- isProductCurrentlyAttached({
- productId: p.id,
- customer,
- entityId: entityIdVal,
- });
-
- return {
- label: p.name,
- value: p.id,
- disabledValue: alreadyEnabled ? "Already Enabled" : undefined,
- badgeValue: currentlyAttached ? "Already Enabled" : undefined,
- };
- })}
- placeholder="Select Product"
- hideFieldInfo
- selectValueAfter={
- hasChanges && productId ? (
-
- Custom
-
- ) : undefined
- }
- />
- )}
-
-
-
-
state.values.productId}>
- {(productId) => (
- }
- onClick={() => handleCustomize({ productId })}
- disabled={!productId}
- type="button"
- >
- Customize
-
- )}
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-summary.tsx b/vite/src/components/forms/attach-product/attach-product-summary.tsx
deleted file mode 100644
index c964a41c3..000000000
--- a/vite/src/components/forms/attach-product/attach-product-summary.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { CheckoutResponseV0 } from "@autumn/shared";
-import { LoadingShimmerText } from "@/components/v2/LoadingShimmerText";
-import { AttachConfirmationInfo } from "./attach-confirmation-info";
-import { AttachProductLineItems } from "./attach-product-line-items";
-import { AttachProductTotals } from "./attach-product-totals";
-
-export function AttachProductSummary({
- previewData,
- isLoading,
-}: {
- previewData?: CheckoutResponseV0 | null;
- isLoading?: boolean;
-}) {
- if (isLoading) {
- return (
-
- );
- }
-
- return (
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/attach-product-totals.tsx b/vite/src/components/forms/attach-product/attach-product-totals.tsx
deleted file mode 100644
index e6fafc5d4..000000000
--- a/vite/src/components/forms/attach-product/attach-product-totals.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { CheckoutResponseV0 } from "@autumn/shared";
-import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils";
-
-export function AttachProductTotals({
- previewData,
-}: {
- previewData?: CheckoutResponseV0 | null;
-}) {
- const total = previewData?.total || 0;
- const nextCycleTotal = previewData?.next_cycle?.total || 0;
- const nextCycleStartsAt = formatUnixToDate(
- previewData?.next_cycle?.starts_at || 0,
- );
-
- return (
-
-
-
Total
-
${total.toFixed(2)}
-
- {nextCycleStartsAt && (
-
-
- Next Cycle ({nextCycleStartsAt})
-
-
- ${nextCycleTotal.toFixed(2)}
-
-
- )}
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/update-product-actions.tsx b/vite/src/components/forms/attach-product/update-product-actions.tsx
deleted file mode 100644
index e4418fdcc..000000000
--- a/vite/src/components/forms/attach-product/update-product-actions.tsx
+++ /dev/null
@@ -1,176 +0,0 @@
-import type { CheckoutResponseV0, ProductV2 } from "@autumn/shared";
-import type { LucideIcon } from "lucide-react";
-import { ArrowUpRightFromSquare, CircleCheck } from "lucide-react";
-import { useAttachProductMutation } from "@/components/forms/attach-product/use-attach-product-mutation";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Button } from "@/components/v2/buttons/Button";
-import { useOrg } from "@/hooks/common/useOrg";
-import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery";
-import { useEnv } from "@/utils/envUtils";
-import { getStripeInvoiceLink } from "@/utils/linkUtils";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-
-function getUpdateButtonConfig(isCheckout: boolean): {
- text: string;
- icon: LucideIcon;
-} {
- return isCheckout
- ? { text: "Checkout", icon: ArrowUpRightFromSquare }
- : { text: "Confirm Update", icon: CircleCheck };
-}
-
-interface UpdateProductActionsProps {
- product?: ProductV2;
- customerId?: string;
- entityId?: string;
- onSuccess?: () => void;
- previewData?: CheckoutResponseV0 | null;
- isPreviewLoading?: boolean;
- version?: number;
- form: UseAttachProductForm;
-}
-
-export function UpdateProductActions({
- form,
- product,
- customerId,
- entityId,
- onSuccess,
- previewData,
- isPreviewLoading,
- version,
-}: UpdateProductActionsProps) {
- const { stripeAccount } = useOrgStripeQuery();
- const env = useEnv();
- const org = useOrg();
-
- const isOwnStripeAccount = stripeAccount?.id === org.org?.stripe_connection;
- const attachMutation = useAttachProductMutation({
- customerId: customerId ?? "",
- successMessage: "Plan updated successfully",
- onSuccess: () => {
- onSuccess?.();
- },
- });
-
- const handleUpdate = async ({
- useInvoice,
- enableProductImmediately,
- }: {
- useInvoice: boolean;
- enableProductImmediately?: boolean;
- }) => {
- // Only redirect to checkout URL for the "Checkout" button flow (useInvoice: false)
- // When useInvoice is true, we always call the attach mutation to generate an invoice
- if (previewData?.url && !useInvoice) {
- window.open(previewData.url, "_blank");
- return;
- }
-
- // Does the update
- const result = await attachMutation.mutateAsync({
- product,
- entityId,
- useInvoice,
- enableProductImmediately,
- prepaidOptions: form.state.values.prepaidOptions ?? undefined,
- version,
- });
-
- // Handle checkout URLs and invoice links
- if (result.data.invoice) {
- window.open(
- getStripeInvoiceLink({
- stripeInvoice: result.data.invoice,
- env,
- accountId: stripeAccount?.id,
- }),
- "_blank",
- );
- }
- };
-
- const isLoading = attachMutation.isPending;
-
- // Don't show buttons if preview is loading
- if (isPreviewLoading || !product) {
- return null;
- }
-
- const isCheckout = !!previewData?.url;
- const { text: updateText, icon: UpdateIcon } =
- getUpdateButtonConfig(isCheckout);
-
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/update-product-prepaid-options.tsx b/vite/src/components/forms/attach-product/update-product-prepaid-options.tsx
deleted file mode 100644
index ce779bd27..000000000
--- a/vite/src/components/forms/attach-product/update-product-prepaid-options.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import {
- type FrontendProductItem,
- getFeaturePriceItemDisplay,
-} from "@autumn/shared";
-import { useOrg } from "@/hooks/common/useOrg";
-import {
- usePrepaidItems,
- useProductStore,
-} from "@/hooks/stores/useProductStore";
-import { useSheetStore } from "@/hooks/stores/useSheetStore";
-import { useSubscriptionById } from "@/hooks/stores/useSubscriptionStore";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-
-export function UpdateProductPrepaidOptions({
- form,
-}: {
- form: UseAttachProductForm;
-}) {
- const storeProduct = useProductStore((s) => s.product);
- const itemId = useSheetStore((s) => s.itemId);
-
- const { org } = useOrg();
- const { productV2 } = useSubscriptionById({ itemId });
-
- // Use store product if it has a real ID, otherwise use productV2 from subscription
- const product = storeProduct?.id ? storeProduct : (productV2 ?? undefined);
-
- const { prepaidItems } = usePrepaidItems({ product });
-
- if (prepaidItems.length === 0) {
- return null;
- }
-
- console.log("prepaidItems", prepaidItems);
-
- return (
-
-
- {prepaidItems.map((item) => {
- const display = getFeaturePriceItemDisplay({
- item: item as FrontendProductItem,
- feature: item.feature,
- currency: org?.default_currency || "USD",
- fullDisplay: true,
- amountFormatOptions: {
- currencyDisplay: "narrowSymbol",
- },
- });
-
- return (
-
-
- {display.primary_text}
- {display.secondary_text && ` ${display.secondary_text}`}
-
-
-
- {(quantityField) => (
-
- )}
-
-
- );
- })}
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-product/update-product-summary.tsx b/vite/src/components/forms/attach-product/update-product-summary.tsx
deleted file mode 100644
index 7ffb06d20..000000000
--- a/vite/src/components/forms/attach-product/update-product-summary.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import type {
- PreviewUpdateSubscriptionResponse,
- ProductV2,
-} from "@autumn/shared";
-import { LoadingShimmerText } from "@/components/v2/LoadingShimmerText";
-import { UpdateConfirmationInfo } from "../update-subscription/update-confirmation-info";
-import { AttachProductLineItems } from "./attach-product-line-items";
-import { AttachProductTotals } from "./attach-product-totals";
-import type { UseAttachProductForm } from "./use-attach-product-form";
-
-export function UpdateProductSummary({
- product,
- previewData,
- isLoading,
- form,
-}: {
- product?: ProductV2;
- previewData?: PreviewUpdateSubscriptionResponse | null;
- isLoading?: boolean;
- form: UseAttachProductForm;
-}) {
- if (isLoading) {
- return (
-
- );
- }
-
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/vite/src/components/forms/attach-product/use-attach-body-builder.ts b/vite/src/components/forms/attach-product/use-attach-body-builder.ts
deleted file mode 100644
index f5444a0cc..000000000
--- a/vite/src/components/forms/attach-product/use-attach-body-builder.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { AppEnv, type ProductV2 } from "@autumn/shared";
-import { useMemo } from "react";
-import { useOrg } from "@/hooks/common/useOrg";
-import { useProductsQuery } from "@/hooks/queries/useProductsQuery";
-import { useHasChanges, useProductStore } from "@/hooks/stores/useProductStore";
-import { useEntity } from "@/hooks/stores/useSubscriptionStore";
-import { convertPrepaidOptionsToFeatureOptions } from "@/utils/billing/prepaidQuantityUtils";
-import { useEnv } from "@/utils/envUtils";
-import { getRedirectUrl } from "@/utils/genUtils";
-import { getAttachBody } from "@/views/customers/customer/product/components/attachProductUtils";
-
-interface AttachBodyBuilderParams {
- customerId?: string;
- productId?: string;
- product?: ProductV2;
- entityId?: string;
- prepaidOptions?: Record;
- version?: number;
- useInvoice?: boolean;
- enableProductImmediately?: boolean;
- successUrl?: string;
-}
-
-/**
- * Shared hook to build attach body from explicit params
- * Used by both useAttachPreview and useAttachProductMutation to keep logic DRY
- */
-export function useAttachBodyBuilder(params: AttachBodyBuilderParams = {}) {
- const { products } = useProductsQuery();
- const hasChanges = useHasChanges();
- const storeProduct = useProductStore((s) => s.product);
- const { entityId: storeEntityId } = useEntity();
- const env = useEnv();
- const { org, isLoading: isOrgLoading, error: orgError } = useOrg();
-
- // Memoized builder function that can be called with runtime params
- const buildAttachBody = useMemo(
- () => (runtimeParams?: AttachBodyBuilderParams) => {
- const mergedParams = { ...params, ...runtimeParams };
-
- const redirectUrl = getRedirectUrl(
- `/customers/${mergedParams.customerId}`,
- env,
- );
-
- // Resolve the product: use provided product or find by ID
- const product =
- mergedParams.product ||
- products.find((p) => p.id === mergedParams.productId);
-
- if (!product || !mergedParams.customerId) {
- return null;
- }
-
- // Determine if this is a custom product (from store with changes)
- const isCustom =
- hasChanges && !!storeProduct?.id && product === storeProduct
- ? true
- : undefined;
- const version = storeProduct?.id ? storeProduct.version : undefined;
-
- // Convert prepaidOptions to options array
- const options = mergedParams.prepaidOptions
- ? convertPrepaidOptionsToFeatureOptions({
- prepaidOptions: mergedParams.prepaidOptions,
- product,
- })
- : undefined;
-
- // Build the attach body
- return getAttachBody({
- customerId: mergedParams.customerId,
- product,
- entityId: mergedParams.entityId ?? storeEntityId ?? undefined,
- optionsInput: options,
- isCustom,
- version,
- useInvoice: mergedParams.useInvoice,
- enableProductImmediately: mergedParams.enableProductImmediately,
- successUrl:
- // env === AppEnv.Sandbox
- // ? `${import.meta.env.VITE_FRONTEND_URL}${redirectUrl}`
- // : undefined,
- org?.success_url && !isOrgLoading && !orgError
- ? org.success_url
- : env === AppEnv.Sandbox
- ? `${import.meta.env.VITE_FRONTEND_URL}${redirectUrl}`
- : undefined,
- });
- },
- [
- products,
- hasChanges,
- storeProduct,
- storeEntityId,
- params,
- org,
- isOrgLoading,
- orgError,
- ],
- );
-
- // For simple usage, return the built body with current params
- const attachBody = useMemo(() => buildAttachBody(), [buildAttachBody]);
-
- return { attachBody, buildAttachBody };
-}
diff --git a/vite/src/components/forms/attach-product/use-attach-preview.ts b/vite/src/components/forms/attach-product/use-attach-preview.ts
deleted file mode 100644
index 4c2296ff2..000000000
--- a/vite/src/components/forms/attach-product/use-attach-preview.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { CheckoutResponseV0, ProductV2 } from "@autumn/shared";
-import { useQuery } from "@tanstack/react-query";
-import { useEffect, useMemo, useState } from "react";
-import { useQueryKeyFactory } from "@/hooks/common/useQueryKeyFactory";
-import { useAxiosInstance } from "@/services/useAxiosInstance";
-import { useAttachBodyBuilder } from "./use-attach-body-builder";
-
-interface AttachPreviewParams {
- // Required params - no fallbacks
- customerId?: string;
- product?: ProductV2;
- entityId?: string;
- prepaidOptions?: Record;
- version?: number;
-
- // Control behavior
- enabled?: boolean;
-}
-
-export function useAttachPreview(params: AttachPreviewParams = {}) {
- const axiosInstance = useAxiosInstance();
- const buildKey = useQueryKeyFactory();
-
- // Build attach body using shared hook with explicit params
- const { attachBody } = useAttachBodyBuilder({
- customerId: params.customerId,
- product: params.product,
- entityId: params.entityId,
- prepaidOptions: params.prepaidOptions,
- version: params.version,
- });
-
- // Auto-enable if not explicitly set and all required data is present
- const shouldEnable =
- params.enabled !== undefined
- ? params.enabled
- : !!(params.customerId && params.product && attachBody);
-
- // Create a stable serialized key from attachBody (which already captures all dependencies)
- const queryKeyDeps = useMemo(() => JSON.stringify(attachBody), [attachBody]);
-
- // Debounce the query key to delay API calls by 150ms
- const [debouncedQueryKey, setDebouncedQueryKey] = useState(queryKeyDeps);
-
- useEffect(() => {
- const timer = setTimeout(() => {
- setDebouncedQueryKey(queryKeyDeps);
- }, 300);
- return () => clearTimeout(timer);
- }, [queryKeyDeps]);
-
- // Track if we're in a debouncing state (query key has changed but debounce hasn't completed)
- const isDebouncing = queryKeyDeps !== debouncedQueryKey;
-
- const query = useQuery({
- queryKey: buildKey(["attach-checkout", debouncedQueryKey]),
- queryFn: async () => {
- if (!attachBody || !params.customerId) {
- return null;
- }
-
- const response = await axiosInstance.post(
- "/v1/checkout",
- attachBody,
- );
-
- return response.data;
- },
- enabled: shouldEnable,
- staleTime: 0, // Always fetch fresh pricing
- });
-
- // Override isLoading to include debouncing state
- // This prevents showing stale data during the transition between diff plans in the selector
- return {
- ...query,
- isLoading: query.isLoading || isDebouncing,
- };
-}
diff --git a/vite/src/components/forms/attach-product/use-attach-product-form.ts b/vite/src/components/forms/attach-product/use-attach-product-form.ts
deleted file mode 100644
index ccc26e13b..000000000
--- a/vite/src/components/forms/attach-product/use-attach-product-form.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { useEffect, useRef } from "react";
-import { useAppForm } from "@/hooks/form/form";
-import {
- type AttachProductForm,
- AttachProductFormSchema,
-} from "./attach-product-form-schema";
-
-export function useAttachProductForm({
- initialProductId,
- initialPrepaidOptions,
-}: {
- initialProductId?: string;
- initialPrepaidOptions?: Record;
-} = {}) {
- return useAppForm({
- defaultValues: {
- productId: initialProductId || "",
- prepaidOptions: initialPrepaidOptions ?? {},
- } as AttachProductForm,
- validators: {
- onChange: AttachProductFormSchema,
- onSubmit: AttachProductFormSchema,
- },
- });
-}
-
-// Subscribe to form changes and clear prepaid options when productId changes
-// Prevents stale prepaid options from causing "no prepaid price found" in the `checkout` call
-export function useResetPrepaidOnProductChange({
- form,
-}: {
- form: UseAttachProductForm;
-}) {
- const previousProductIdRef = useRef();
-
- useEffect(() => {
- const subscription = form.store.subscribe(() => {
- const currentProductId = form.store.state.values.productId;
-
- if (
- previousProductIdRef.current !== undefined &&
- previousProductIdRef.current !== currentProductId
- ) {
- form.setFieldValue("prepaidOptions", {});
- }
- previousProductIdRef.current = currentProductId;
- });
-
- return () => subscription();
- }, [form.store, form.setFieldValue]);
-}
-
-export type UseAttachProductForm = ReturnType;
diff --git a/vite/src/components/forms/attach-product/use-attach-product-mutation.ts b/vite/src/components/forms/attach-product/use-attach-product-mutation.ts
deleted file mode 100644
index da323c8f2..000000000
--- a/vite/src/components/forms/attach-product/use-attach-product-mutation.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import type { ProductV2 } from "@autumn/shared";
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import type { AxiosError } from "axios";
-import { toast } from "sonner";
-import { useSheetStore } from "@/hooks/stores/useSheetStore";
-import { CusService } from "@/services/customers/CusService";
-import { useAxiosInstance } from "@/services/useAxiosInstance";
-import { useAttachBodyBuilder } from "./use-attach-body-builder";
-
-interface AttachProductParams {
- // Product selection (provide one of these)
- productId?: string;
- product?: ProductV2;
-
- // Optional overrides
- entityId?: string;
- prepaidOptions?: Record;
- version?: number;
-
- // Invoice options
- useInvoice?: boolean;
- enableProductImmediately?: boolean;
-}
-
-export function useAttachProductMutation({
- customerId,
- onSuccess,
- onError,
- successMessage = "Successfully attached product",
-}: {
- customerId: string;
- onSuccess?: (data: unknown) => void | Promise;
- onError?: (error: unknown) => void;
- successMessage?: string;
-}) {
- const axiosInstance = useAxiosInstance();
- const queryClient = useQueryClient();
- const { closeSheet } = useSheetStore();
-
- // Get builder function from shared hook
- const { buildAttachBody } = useAttachBodyBuilder({ customerId });
-
- return useMutation({
- mutationFn: async (params: AttachProductParams) => {
- // Build attach body using shared builder function
- const attachBody = buildAttachBody({
- productId: params.productId,
- product: params.product,
- entityId: params.entityId,
- prepaidOptions: params.prepaidOptions,
- version: params.version,
- useInvoice: params.useInvoice,
- enableProductImmediately: params.enableProductImmediately,
- });
-
- if (!attachBody) {
- throw new Error(
- "Failed to build attach body - product not found or missing data",
- );
- }
-
- return await CusService.attach(axiosInstance, attachBody);
- },
- onSuccess: async (response) => {
- // Don't show success toast if checkout_url is returned - product not attached yet
- if (response.data.checkout_url) {
- toast.success("Redirecting to checkout URL");
- closeSheet();
- return;
- }
-
- toast.success(successMessage);
- closeSheet();
- queryClient.invalidateQueries({ queryKey: ["customer", customerId] });
-
- if (onSuccess) {
- await onSuccess(response.data);
- }
- },
- onError: (error) => {
- if (onError) {
- onError(error);
- } else {
- toast.error(
- (error as AxiosError<{ message: string }>)?.response?.data?.message ??
- "Failed to attach product",
- );
- console.error(error);
- }
- },
- });
-}
diff --git a/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx b/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx
deleted file mode 100644
index 03bd75812..000000000
--- a/vite/src/components/forms/attach-v2/components/AttachPlanSkeleton.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { TimerIcon } from "@phosphor-icons/react";
-import { motion } from "motion/react";
-import {
- STAGGER_CONTAINER,
- STAGGER_ITEM,
-} from "@/components/forms/update-subscription-v2/constants/animationConstants";
-import { Skeleton } from "@/components/ui/skeleton";
-import { IconButton } from "@/components/v2/buttons/IconButton";
-import { SheetSection } from "@/components/v2/sheets/SharedSheetComponents";
-
-export function AttachPlanSkeleton() {
- return (
-
-
- {/* Section title - static content with disabled buttons */}
-
-
-
-
- Plan Configuration
-
- }
- variant="secondary"
- className="h-7 whitespace-nowrap"
- disabled
- >
- Free Trial
-
-
-
-
-
- {/* Price display skeleton */}
-
-
-
-
-
-
-
- {/* Item rows skeleton */}
- {[0, 1].map((i) => (
-
-
-
- ))}
-
- {/* Edit button skeleton */}
-
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/attach-v2/components/AttachSettingsPopover.tsx b/vite/src/components/forms/attach-v2/components/AttachSettingsPopover.tsx
deleted file mode 100644
index cb6c0a2f4..000000000
--- a/vite/src/components/forms/attach-v2/components/AttachSettingsPopover.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { CalendarIcon, GearIcon, LightningIcon } from "@phosphor-icons/react";
-import { useState } from "react";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import { Separator } from "@/components/ui/separator";
-import { IconButton } from "@/components/v2/buttons/IconButton";
-import { IconCheckbox } from "@/components/v2/checkboxes/IconCheckbox";
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/v2/tooltips/Tooltip";
-import { cn } from "@/lib/utils";
-import { usePlanScheduleField } from "../hooks/usePlanScheduleField";
-
-export function AttachSettingsPopover() {
- const [open, setOpen] = useState(false);
-
- const {
- hasActiveSubscription,
- hasOutgoing,
- hasCustomSchedule,
- isImmediateSelected,
- isEndOfCycleSelected,
- handleScheduleChange,
- } = usePlanScheduleField();
-
- if (!hasActiveSubscription) return null;
-
- return (
-
-
-
- }
- variant="secondary"
- className={cn(
- "h-7 whitespace-nowrap",
- hasCustomSchedule &&
- "text-blue-400! border-blue-500/50 bg-blue-500/10",
- )}
- >
- Settings
-
-
- e.preventDefault()}
- onCloseAutoFocus={(e) => e.preventDefault()}
- >
-
-
-
- Advanced Configuration
-
-
Override default billing behavior
-
-
-
-
Plan Schedule
-
- }
- iconOrientation="left"
- variant="secondary"
- size="sm"
- checked={isImmediateSelected}
- onCheckedChange={() => handleScheduleChange("immediate")}
- className={cn(
- "rounded-r-none",
- !isImmediateSelected && "border-r-0",
- )}
- >
- Immediately
-
-
-
-
- }
- iconOrientation="left"
- variant="secondary"
- size="sm"
- checked={isEndOfCycleSelected}
- disabled={!hasOutgoing}
- onCheckedChange={() =>
- handleScheduleChange("end_of_cycle")
- }
- className={cn(
- "rounded-l-none",
- !isEndOfCycleSelected && "border-l-0",
- )}
- >
- End of cycle
-
-
-
- {!hasOutgoing && (
-
- Only available when transitioning from an existing plan
-
- )}
-
-
-
-
-
-
- );
-}
diff --git a/vite/src/components/forms/create-schedule/index.ts b/vite/src/components/forms/create-schedule/index.ts
deleted file mode 100644
index 8b24c9a72..000000000
--- a/vite/src/components/forms/create-schedule/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export * from "./components/CreateScheduleSheetContent";
-export * from "./components/SchedulePhaseCard";
-export * from "./components/SchedulePlanRow";
-export * from "./context/CreateScheduleFormProvider";
-export * from "./createScheduleFormSchema";
-export * from "./hooks/useCreateScheduleForm";
-export * from "./hooks/useCreateScheduleMutation";
-export * from "./hooks/useCreateScheduleRequestBody";
diff --git a/vite/src/components/forms/update-subscription-v2/components/CompactValueChange.tsx b/vite/src/components/forms/update-subscription-v2/components/CompactValueChange.tsx
deleted file mode 100644
index d29c63121..000000000
--- a/vite/src/components/forms/update-subscription-v2/components/CompactValueChange.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { cn } from "@/lib/utils";
-
-interface CompactValueChangeProps {
- oldValue: string | number | null;
- newValue: string | number | null;
- isUpgrade?: boolean;
-}
-
-export function CompactValueChange({
- oldValue,
- newValue,
- isUpgrade = true,
-}: CompactValueChangeProps) {
- return (
-
-
- {oldValue}
-
- →
-
- {newValue}
-
-
- );
-}
diff --git a/vite/src/components/forms/update-subscription-v2/utils/getEditIcon.tsx b/vite/src/components/forms/update-subscription-v2/utils/getEditIcon.tsx
deleted file mode 100644
index b3a949a54..000000000
--- a/vite/src/components/forms/update-subscription-v2/utils/getEditIcon.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { EditIconType } from "@autumn/shared";
-import {
- CurrencyDollarIcon,
- HashIcon,
- PackageIcon,
- StackIcon,
- TagIcon,
-} from "@phosphor-icons/react";
-import { cn } from "@/lib/utils";
-
-export function getEditIcon(iconType: EditIconType, isUpgrade: boolean) {
- const iconProps = {
- size: 14,
- className: cn("shrink-0", isUpgrade ? "text-green-500" : "text-red-500"),
- };
- switch (iconType) {
- case "price":
- return ;
- case "tier":
- return ;
- case "usage":
- return ;
- case "units":
- return ;
- case "prepaid":
- return ;
- default:
- return null;
- }
-}
diff --git a/vite/src/components/forms/update-subscription/update-confirmation-info.tsx b/vite/src/components/forms/update-subscription/update-confirmation-info.tsx
deleted file mode 100644
index 370d353d1..000000000
--- a/vite/src/components/forms/update-subscription/update-confirmation-info.tsx
+++ /dev/null
@@ -1,129 +0,0 @@
-import type {
- PreviewUpdateSubscriptionResponse,
- ProductV2,
-} from "@autumn/shared";
-import type { ReactNode } from "react";
-import { useMemo } from "react";
-import { useHasChanges, usePrepaidItems } from "@/hooks/stores/useProductStore";
-import { InfoBox } from "@/views/onboarding2/integrate/components/InfoBox";
-import type { UseUpdateSubscriptionForm } from "./use-update-subscription-form";
-
-export const UpdateConfirmationInfo = ({
- previewData,
- product,
- form,
-}: {
- previewData?: PreviewUpdateSubscriptionResponse | null;
- product?: ProductV2;
- form: UseUpdateSubscriptionForm;
-}) => {
- const hasChanges = useHasChanges();
- // const hasBillingChanges = useHasBillingChanges({
- // baseProduct: previewData?.current_product,
- // newProduct: previewData?.product,
- // });
-
- const hasPrepaidQuantityChanges = useHasPrepaidQuantityChanges(product, form);
-
- const renderInfoBoxes = (): ReactNode[] => {
- const boxes: ReactNode[] = [];
-
- if (!previewData) {
- return boxes;
- }
-
- // Plan customization notice
- if (hasChanges) {
- boxes.push(
-
- This plan has been customized for this customer
- ,
- );
- }
-
- // Version change notice
- // if (previewData.current_product?.version !== previewData.product.version) {
- // boxes.push(
- //
- // You're switching from v{previewData.current_product?.version} to v
- // {previewData.product.version} of this plan
- // ,
- // );
- // }
-
- // Prepaid quantity changes notice
- if (hasPrepaidQuantityChanges) {
- boxes.push(
-
- Prepaid quantities have been updated
- ,
- );
- }
-
- // No billing changes notice
- // if (!hasBillingChanges && !hasPrepaidQuantityChanges) {
- // boxes.push(
- //
- // No changes to billing will be made
- // ,
- // );
- // }
-
- // Free trial updated
- // if (previewData.product.free_trial) {
- // const trialEndDate = previewData.next_cycle?.starts_at
- // ? formatUnixToDate(previewData.next_cycle.starts_at)
- // : null;
-
- // boxes.push(
- //
- // Free trial updated
- // {trialEndDate && (
- // <>
- // {" "}
- // - trial ends {trialEndDate}
- // >
- // )}
- // ,
- // );
- // }
-
- return boxes;
- };
-
- const infoBoxes = renderInfoBoxes();
-
- if (infoBoxes.length === 0) {
- return null;
- }
-
- return (
-
- {infoBoxes.map((box, index) => (
-
{box}
- ))}
-
- );
-};
-
-const useHasPrepaidQuantityChanges = (
- product: ProductV2 | undefined,
- form: UseUpdateSubscriptionForm,
-) => {
- const { prepaidItems } = usePrepaidItems({ product });
- const currentPrepaidOptions = form.state.values.prepaidOptions;
- const defaultPrepaidOptions = form.options.defaultValues?.prepaidOptions;
-
- return useMemo(() => {
- if (prepaidItems.length === 0 || !currentPrepaidOptions) {
- return false;
- }
-
- return prepaidItems.some((item) => {
- const currentQuantity = currentPrepaidOptions[item.feature_id as string];
- const defaultQuantity =
- defaultPrepaidOptions?.[item.feature_id as string];
- return currentQuantity !== defaultQuantity;
- });
- }, [prepaidItems, currentPrepaidOptions, defaultPrepaidOptions]);
-};
diff --git a/vite/src/components/forms/update-subscription/use-update-subscription-form.ts b/vite/src/components/forms/update-subscription/use-update-subscription-form.ts
deleted file mode 100644
index 9f8c86088..000000000
--- a/vite/src/components/forms/update-subscription/use-update-subscription-form.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { useEffect, useRef } from "react";
-import { useAppForm } from "@/hooks/form/form";
-import {
- type AttachProductForm,
- AttachProductFormSchema,
-} from "../attach-product/attach-product-form-schema";
-
-export function useUpdateSubscriptionForm({
- initialProductId,
- initialPrepaidOptions,
-}: {
- initialProductId?: string;
- initialPrepaidOptions?: Record;
-} = {}) {
- return useAppForm({
- defaultValues: {
- productId: initialProductId || "",
- prepaidOptions: initialPrepaidOptions ?? {},
- } as AttachProductForm,
- validators: {
- onChange: AttachProductFormSchema,
- onSubmit: AttachProductFormSchema,
- },
- });
-}
-
-// Subscribe to form changes and clear prepaid options when productId changes
-// Prevents stale prepaid options from causing "no prepaid price found" in the `checkout` call
-function useResetPrepaidOnProductChange({
- form,
-}: {
- form: UseUpdateSubscriptionForm;
-}) {
- const previousProductIdRef = useRef();
-
- useEffect(() => {
- const subscription = form.store.subscribe(() => {
- const currentProductId = form.store.state.values.productId;
-
- if (
- previousProductIdRef.current !== undefined &&
- previousProductIdRef.current !== currentProductId
- ) {
- form.setFieldValue("prepaidOptions", {});
- }
- previousProductIdRef.current = currentProductId;
- });
-
- return () => subscription();
- }, [form.store, form.setFieldValue]);
-}
-
-export type UseUpdateSubscriptionForm = ReturnType<
- typeof useUpdateSubscriptionForm
->;
diff --git a/vite/src/components/general/ToggleButton.tsx b/vite/src/components/general/ToggleButton.tsx
deleted file mode 100644
index ea33993c7..000000000
--- a/vite/src/components/general/ToggleButton.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-import { Check } from "lucide-react";
-import { cn } from "@/lib/utils";
-import { Button } from "../ui/button";
-import { Switch } from "../ui/switch";
-import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
-import { InfoTooltip } from "./modal-components/InfoTooltip";
-
-export const ToggleButton = ({
- value,
- setValue,
- tooltipContent,
- buttonText,
- className,
- disabled,
- infoContent,
- switchClassName,
-}: {
- value: boolean;
- setValue: (value: boolean) => void;
- tooltipContent?: string;
- buttonText?: string | React.ReactNode;
- className?: string;
- disabled?: boolean;
- infoContent?: string;
- switchClassName?: string;
-}) => {
- const MainButton = (
-
- );
-
- if (tooltipContent) {
- return (
-
- {MainButton}
- {tooltipContent}
-
- );
- }
-
- return MainButton;
-};
diff --git a/vite/src/components/general/form/form-wrapper.tsx b/vite/src/components/general/form/form-wrapper.tsx
deleted file mode 100644
index 233619e05..000000000
--- a/vite/src/components/general/form/form-wrapper.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import type { AnyFormApi } from "@tanstack/react-form";
-import { cn } from "@/lib/utils";
-
-export function FormWrapper({
- form,
- className,
- children,
-}: {
- form: AnyFormApi;
- className?: string;
- children: React.ReactNode;
-}) {
- return (
-
- );
-}
diff --git a/vite/src/components/ui/select.tsx b/vite/src/components/ui/select.tsx
deleted file mode 100644
index 22abb42f7..000000000
--- a/vite/src/components/ui/select.tsx
+++ /dev/null
@@ -1,238 +0,0 @@
-import * as SelectPrimitive from "@radix-ui/react-select";
-import { CheckIcon, ChevronDownIcon, ChevronUpIcon, X } from "lucide-react";
-import type * as React from "react";
-
-import { cn } from "@/lib/utils";
-import { Button } from "./button";
-
-function Select({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectGroup({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectValue({
- ...props
-}: React.ComponentProps) {
- return ;
-}
-
-function SelectTrigger({
- className,
- children,
- iconClassName,
- onClear,
- ...props
-}: React.ComponentProps & {
- iconClassName?: string;
- onClear?: () => void;
-}) {
- if (onClear) {
- return (
-
-
span]:line-clamp-1 dark:border-zinc-800 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300
- h-8
-
- data-[state=open]:border-focus data-[state=open]:shadow-focus
- focus:ring-0
- data-[placeholder]:text-t3
- transition-colors duration-100
- p-2
- `,
- className,
- )}
- {...props}
- >
- {children}
-
-
-
-
- {onClear && (
-
-
-
- )}
-
- );
- }
-
- return (
- span]:line-clamp-1 dark:border-zinc-800 dark:ring-offset-zinc-950 dark:placeholder:text-zinc-400 dark:focus:ring-zinc-300
-h-8
-
-data-[state=open]:border-focus data-[state=open]:shadow-focus
-focus:ring-0
-
-data-[placeholder]:text-t3
-
-transition-colors duration-100
-p-2
-`,
- className,
- )}
- {...props}
- >
- {children}
-
-
-
-
- );
-}
-
-function SelectContent({
- className,
- children,
- position = "popper",
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
-
- {children}
-
-
-
-
- );
-}
-
-function SelectLabel({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SelectItem({
- className,
- children,
- endComponent,
- ...props
-}: React.ComponentProps & {
- endComponent?: React.ReactNode;
-}) {
- return (
-
-
- {endComponent ? (
- endComponent
- ) : (
-
-
-
- )}
-
-
- {children}
-
- );
-}
-
-function SelectSeparator({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
- );
-}
-
-function SelectScrollUpButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-function SelectScrollDownButton({
- className,
- ...props
-}: React.ComponentProps) {
- return (
-
-
-
- );
-}
-
-export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue };
diff --git a/vite/src/hooks/stores/useProductStore.ts b/vite/src/hooks/stores/useProductStore.ts
index 1f595d53e..5724b1793 100644
--- a/vite/src/hooks/stores/useProductStore.ts
+++ b/vite/src/hooks/stores/useProductStore.ts
@@ -81,7 +81,8 @@ export const useHasChanges = () => {
const hasChanges =
!comparison.itemsSame ||
!comparison.detailsSame ||
- !comparison.freeTrialsSame;
+ !comparison.freeTrialsSame ||
+ !comparison.configSame;
return hasChanges;
}, [product, baseProduct, features]);
diff --git a/vite/src/hooks/useMounted.ts b/vite/src/hooks/useMounted.ts
deleted file mode 100644
index 09f269340..000000000
--- a/vite/src/hooks/useMounted.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useEffect, useState } from "react";
-
-/**
- * Hook that returns true after the component has mounted and the browser has completed a paint cycle.
- * Useful for deferring rendering until layout is stable, preventing visual glitches on navigation.
- */
-export function useMounted(): boolean {
- const [isMounted, setIsMounted] = useState(false);
-
- useEffect(() => {
- const frame = requestAnimationFrame(() => {
- setIsMounted(true);
- });
- return () => cancelAnimationFrame(frame);
- }, []);
-
- return isMounted;
-}
diff --git a/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx b/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx
deleted file mode 100644
index 12afdc92c..000000000
--- a/vite/src/views/customers/customer/analytics/hooks/useTopEventNames.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-import { useQueryKeyFactory } from "@/hooks/common/useQueryKeyFactory";
-import { useAxiosInstance } from "@/services/useAxiosInstance";
-
-export const useTopEventNames = () => {
- const axiosInstance = useAxiosInstance();
- const buildKey = useQueryKeyFactory();
-
- const {
- data: eventNamesData,
- isLoading: eventNamesLoading,
- error: eventNamesError,
- } = useQuery({
- queryKey: buildKey(["query-event-names"]),
- queryFn: async () => {
- const { data } = await axiosInstance.get("/query/event_names");
- return data;
- },
- });
-
- return {
- topEvents: {
- featureIds: eventNamesData?.featureIds ?? [],
- eventNames: eventNamesData?.eventNames ?? [],
- },
- isLoading: eventNamesLoading,
- error: eventNamesError,
- };
-};
diff --git a/vite/src/views/customers/customer/analytics/utils/getAllEventNames.ts b/vite/src/views/customers/customer/analytics/utils/getAllEventNames.ts
deleted file mode 100644
index 6299f515c..000000000
--- a/vite/src/views/customers/customer/analytics/utils/getAllEventNames.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { type Feature, FeatureType, FeatureUsageType } from "@autumn/shared";
-
-export const getAllEventNames = ({ features }: { features: Feature[] }) => {
- return features.flatMap((feature: Feature) => {
- if (feature.type !== FeatureType.Metered) return [];
- const eventNames = feature.event_names || [];
-
- return eventNames.filter(
- (name: string) =>
- !features.some(
- (f: Feature) =>
- f.id == name && f.config.usage_type == "continuous_use",
- ),
- );
- });
-};
-
-export const eventNameBelongsToFeature = ({
- eventName,
- features,
-}: {
- eventName: string;
- features: Feature[];
-}) => {
- return features.some(
- (feature: Feature) =>
- feature.type === FeatureType.Metered &&
- feature.config.usage_type === FeatureUsageType.Single &&
- feature.event_names &&
- feature.event_names.includes(eventName),
- );
-};
diff --git a/vite/src/views/customers/customer/product/components/attachProductUtils.ts b/vite/src/views/customers/customer/product/components/attachProductUtils.ts
deleted file mode 100644
index e1ee8f40e..000000000
--- a/vite/src/views/customers/customer/product/components/attachProductUtils.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import type { FeatureOptions, ProductV2 } from "@autumn/shared";
-
-// export type FrontendProduct = ProductV2 & {
-// isActive: boolean;
-// options: FeatureOptions[];
-// isCanceled: boolean;
-// };
-
-export const getAttachBody = ({
- customerId,
- product,
- entityId,
- optionsInput,
- useInvoice,
- enableProductImmediately = true,
- successUrl,
- version,
- isCustom = false,
-}: {
- customerId: string;
- product: ProductV2;
- entityId?: string;
- optionsInput?: FeatureOptions[];
- useInvoice?: boolean;
- enableProductImmediately?: boolean;
- successUrl?: string;
- version?: number;
- isCustom?: boolean;
-}) => {
- const customData = isCustom
- ? {
- items: product.items,
- free_trial: product.free_trial,
- }
- : {};
-
- return {
- customer_id: customerId,
- product_id: product.id,
- entity_id: entityId || undefined,
- options: optionsInput
- ? optionsInput.map((option) => ({
- feature_id: option.feature_id,
- quantity: option.quantity || 0,
- }))
- : undefined,
- is_custom: isCustom,
- ...customData,
- free_trial: isCustom ? product.free_trial || undefined : undefined,
-
- invoice: useInvoice,
- enable_product_immediately: useInvoice
- ? enableProductImmediately
- : undefined,
- finalize_invoice: useInvoice ? false : undefined,
-
- force_checkout:
- useInvoice && enableProductImmediately === false ? true : undefined,
-
- success_url: successUrl,
- version: version ? Number(version) : undefined,
- };
-};
diff --git a/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTable.tsx b/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTable.tsx
deleted file mode 100644
index ac8c849f2..000000000
--- a/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTable.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { FullCusEntWithFullCusProduct } from "@autumn/shared";
-import { useMemo } from "react";
-import { Table } from "@/components/general/table";
-import { useCustomerTable } from "@/views/customers2/hooks/useCustomerTable";
-import { CustomerBooleanBalanceTableColumns } from "./CustomerBooleanBalanceTableColumns";
-
-export function CustomerBooleanBalanceTable({
- allEnts,
- aggregatedMap,
- isLoading,
-}: {
- allEnts: FullCusEntWithFullCusProduct[];
- aggregatedMap: Map;
- isLoading: boolean;
-}) {
- const columns = useMemo(
- () =>
- CustomerBooleanBalanceTableColumns({
- aggregatedMap,
- }),
- [aggregatedMap],
- );
-
- const enableSorting = false;
- const table = useCustomerTable({
- data: allEnts,
- columns,
- options: {},
- });
-
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTableColumns.tsx b/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTableColumns.tsx
deleted file mode 100644
index 023f4522f..000000000
--- a/vite/src/views/customers2/components/table/customer-boolean-balance/CustomerBooleanBalanceTableColumns.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { FullCusEntWithFullCusProduct } from "@autumn/shared";
-import type { Row } from "@tanstack/react-table";
-import { CustomerFeatureConfiguration } from "../customer-feature-usage/CustomerFeatureConfiguration";
-
-export const CustomerBooleanBalanceTableColumns = ({
- aggregatedMap,
-}: {
- aggregatedMap: Map;
-}) => [
- {
- header: "Feature",
- size: 200,
- accessorKey: "feature",
- cell: ({ row }: { row: Row }) => {
- const ent = row.original;
- const featureId = ent.entitlement.feature.id;
- const originalEnts = aggregatedMap.get(featureId);
- const isAggregated = originalEnts && originalEnts.length > 1;
- const balanceCount = originalEnts?.length || 1;
-
- return (
-
-
- {ent.entitlement.feature.name}
-
- {isAggregated && (
-
- {balanceCount}
-
- )}
-
- );
- },
- },
- {
- header: "Type",
- size: 200,
- accessorKey: "type",
- cell: ({ row }: { row: Row }) => {
- const ent = row.original;
-
- return (
-
-
-
- );
- },
- },
-];
diff --git a/vite/src/views/onboarding4/steps/AttachStep.tsx b/vite/src/views/onboarding4/steps/AttachStep.tsx
deleted file mode 100644
index 183b0d76b..000000000
--- a/vite/src/views/onboarding4/steps/AttachStep.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import { useMemo, useState } from "react";
-import { StepBadge } from "@/components/v2/badges/StepBadge";
-import { GroupedTabButton } from "@/components/v2/buttons/GroupedTabButton";
-import { getSnippet, type Snippet } from "@/lib/snippets";
-import { SnippetCodeBlock } from "./SnippetCodeBlock";
-
-interface AttachStepProps {
- snippet: Snippet;
- stepNumber: number;
-}
-
-export function AttachStep({ snippet: _, stepNumber }: AttachStepProps) {
- const [attachMode, setAttachMode] = useState<"pricing-table" | "custom">(
- "pricing-table",
- );
-
- // Get snippets based on mode
- const pricingTableSnippet = useMemo(
- () =>
- getSnippet({
- id: "attach-pricing-table",
- sdk: "react",
- }),
- [],
- );
-
- const billingStateSnippet = useMemo(
- () =>
- getSnippet({
- id: "billing-state",
- sdk: "react",
- }),
- [],
- );
-
- const checkoutSnippet = useMemo(
- () =>
- getSnippet({
- id: "checkout",
- sdk: "react",
- }),
- [],
- );
-
- return (
-
- {/* Mode selector above steps */}
-
- setAttachMode(val as "pricing-table" | "custom")
- }
- options={[
- {
- value: "pricing-table",
- label: "Use ",
- },
- {
- value: "custom",
- label: "Build your own",
- },
- ]}
- />
-
- {attachMode === "pricing-table" ? (
- /* Single step for PricingTable */
-
-
- {stepNumber}
-
- {pricingTableSnippet.title}
-
-
-
- {pricingTableSnippet.description}
-
-
-
-
-
- ) : (
- /* Two steps for Build your own */
- <>
- {/* Step 1: Billing State */}
-
-
- {stepNumber}
-
- {billingStateSnippet.title}
-
-
-
- {billingStateSnippet.description}
-
-
-
-
-
-
- {/* Step 2: Checkout */}
-
-
- {stepNumber + 1}
-
- {checkoutSnippet.title}
-
-
-
- {checkoutSnippet.description}
-
-
-
-
-
- >
- )}
-
- );
-}
diff --git a/vite/src/views/onboarding4/templateConfigs.ts b/vite/src/views/onboarding4/templateConfigs.ts
deleted file mode 100644
index 010e7f9fb..000000000
--- a/vite/src/views/onboarding4/templateConfigs.ts
+++ /dev/null
@@ -1,286 +0,0 @@
-export interface PricingTier {
- name: string;
- price: string;
- interval?: string;
- description?: string;
- features: string[];
- highlighted?: boolean;
-}
-
-interface TemplateConfig {
- id: string;
- name: string;
- company: string;
- tags: string[];
- description: string;
- pricingTiers: PricingTier[];
- websiteUrl: string;
-}
-
-const TEMPLATE_CONFIGS: TemplateConfig[] = [
- {
- id: "cursor",
- name: "Cursor",
- company: "Cursor",
- tags: ["Usage-based", "Trial", "Freemium"],
- description:
- "AI-powered code editor with a generous free tier and usage-based premium features. Users get free completions and can upgrade for more advanced AI capabilities with metered usage.",
- pricingTiers: [
- {
- name: "Hobby",
- price: "Free",
- description: "For casual developers",
- features: [
- "2000 completions",
- "50 slow premium requests",
- "200 cursor-small uses",
- ],
- },
- {
- name: "Pro",
- price: "$20",
- interval: "month",
- description: "For professional developers",
- features: [
- "Unlimited completions",
- "500 fast premium requests",
- "Unlimited slow premium requests",
- "Unlimited cursor-small uses",
- ],
- highlighted: true,
- },
- {
- name: "Business",
- price: "$40",
- interval: "user/month",
- description: "For teams",
- features: [
- "Everything in Pro",
- "Centralized billing",
- "Admin dashboard",
- "Enforce privacy mode",
- "SAML/OIDC SSO",
- ],
- },
- ],
- websiteUrl: "https://cursor.com/pricing",
- },
- {
- id: "railway",
- name: "Railway",
- company: "Railway",
- tags: ["Credits", "Usage-based", "Pay-as-you-go"],
- description:
- "Infrastructure platform with credit-based pricing. Users receive monthly credits and pay for additional usage based on compute, memory, and egress consumption.",
- pricingTiers: [
- {
- name: "Hobby",
- price: "Free",
- description: "For personal projects",
- features: [
- "$5 of usage per month",
- "Limited to 500 execution hours",
- "Community support",
- ],
- },
- {
- name: "Pro",
- price: "$20",
- interval: "user/month",
- description: "For teams and startups",
- features: [
- "Includes $10 of usage",
- "Unlimited execution hours",
- "Team collaboration",
- "Priority support",
- ],
- highlighted: true,
- },
- {
- name: "Enterprise",
- price: "Custom",
- description: "For large organizations",
- features: [
- "Volume discounts",
- "Dedicated support",
- "SLA guarantees",
- "Custom contracts",
- ],
- },
- ],
- websiteUrl: "https://railway.app/pricing",
- },
- {
- id: "t3-chat",
- name: "T3 Chat",
- company: "T3 Chat",
- tags: ["Prepaid", "Add-ons", "Subscription"],
- description:
- "AI chat platform with subscription tiers and prepaid message packs. Users subscribe to a base plan and can purchase additional message credits as needed.",
- pricingTiers: [
- {
- name: "Free",
- price: "Free",
- description: "Try it out",
- features: ["Limited messages", "Basic models", "Web access only"],
- },
- {
- name: "Plus",
- price: "$8",
- interval: "month",
- description: "For regular users",
- features: [
- "1000 messages/month",
- "All models",
- "Mobile app access",
- "Message history",
- ],
- highlighted: true,
- },
- {
- name: "Message Pack",
- price: "$5",
- description: "Add-on",
- features: ["500 additional messages", "Never expires", "Use anytime"],
- },
- ],
- websiteUrl: "https://t3.chat",
- },
- {
- id: "openai",
- name: "OpenAI API",
- company: "OpenAI",
- tags: ["Credits", "Prepaid", "Pay-as-you-go"],
- description:
- "API platform with prepaid credits and pay-as-you-go pricing. Developers purchase credits upfront and consume them based on token usage across different models.",
- pricingTiers: [
- {
- name: "Free Tier",
- price: "Free",
- description: "Get started",
- features: [
- "$5 free credits",
- "Rate limited",
- "Access to GPT-3.5",
- "3 months expiry",
- ],
- },
- {
- name: "Pay as you go",
- price: "Usage-based",
- description: "For developers",
- features: [
- "All models access",
- "Higher rate limits",
- "Pay per token",
- "No monthly commitment",
- ],
- highlighted: true,
- },
- {
- name: "Enterprise",
- price: "Custom",
- description: "For organizations",
- features: [
- "Volume discounts",
- "Dedicated capacity",
- "Custom models",
- "Enterprise support",
- ],
- },
- ],
- websiteUrl: "https://openai.com/pricing",
- },
- {
- id: "notion",
- name: "Notion",
- company: "Notion",
- tags: ["Per-seat", "Add-ons", "Freemium"],
- description:
- "Workspace platform with per-seat pricing and AI add-ons. Teams pay per member with optional AI features available as an additional subscription.",
- pricingTiers: [
- {
- name: "Free",
- price: "Free",
- description: "For individuals",
- features: [
- "Unlimited pages",
- "Share with 10 guests",
- "7 day page history",
- "Basic integrations",
- ],
- },
- {
- name: "Plus",
- price: "$10",
- interval: "user/month",
- description: "For small teams",
- features: [
- "Unlimited team members",
- "Unlimited file uploads",
- "30 day page history",
- "100 guest collaborators",
- ],
- highlighted: true,
- },
- {
- name: "AI Add-on",
- price: "$8",
- interval: "user/month",
- description: "Add-on",
- features: [
- "AI writing assistant",
- "AI autofill",
- "AI summaries",
- "Works on any plan",
- ],
- },
- ],
- websiteUrl: "https://notion.so/pricing",
- },
- {
- id: "lovable",
- name: "Lovable",
- company: "Lovable",
- tags: ["Prepaid", "Credits", "Subscription"],
- description:
- "AI app builder with prepaid credit packs. Users subscribe to plans with included credits and can purchase additional credit packs for more generation capacity.",
- pricingTiers: [
- {
- name: "Free",
- price: "Free",
- description: "Try it out",
- features: [
- "Limited credits",
- "1 project",
- "Community support",
- "Basic features",
- ],
- },
- {
- name: "Starter",
- price: "$20",
- interval: "month",
- description: "For builders",
- features: [
- "100 credits/month",
- "5 projects",
- "Priority support",
- "All features",
- ],
- highlighted: true,
- },
- {
- name: "Credit Pack",
- price: "$10",
- description: "Add-on",
- features: [
- "50 additional credits",
- "Never expires",
- "Use across projects",
- ],
- },
- ],
- websiteUrl: "https://lovable.dev/pricing",
- },
-];