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/.husky/pre-commit b/.husky/pre-commit
index 6dbfe7a0a..da223edad 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -1,5 +1,5 @@
bun knip
-cd server && bun ts
+(cd server && bun ts)
changed_files="$(git diff --cached --name-only)"
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/knip.json b/knip.json
index 918e2fb0b..b2efc285b 100644
--- a/knip.json
+++ b/knip.json
@@ -16,18 +16,25 @@
"entry": ["apps/scope-picker/**/*"]
},
"server": {
- "entry": ["src/**/*.ts", "tests/**/*.ts"],
+ "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": ["**/*.ts", "!utils/**"],
"project": ["**/*.ts", "!utils/**"],
"includeEntryExports": false
},
"vite": {
- "entry": ["src/**/*.{ts,tsx}"],
- "project": ["src/**/*.{ts,tsx}"],
+ "entry": ["tests/**/*.{ts,tsx}"],
+ "project": ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"],
"ignore": ["src/components/ai-elements/**"],
"ignoreDependencies": [
"tailwindcss",
@@ -53,32 +60,30 @@
"project": ["**/*.{ts,tsx,mdx,mjs}"]
},
"apps/checkout": {
- "entry": ["src/**/*.{ts,tsx}"],
"project": ["src/**/*.{ts,tsx}"],
"ignoreDependencies": ["shadcn", "tailwindcss", "tw-animate-css"]
},
"packages/atmn": {
- "entry": ["src/**/*.{ts,tsx}"],
+ "entry": ["src/cli.tsx", "src/compose/index.ts"],
"project": ["src/**/*.{ts,tsx}", "*.ts"]
},
"packages/autumn-js": {
- "entry": ["src/**/*.ts", "*.ts"],
+ "entry": ["src/{sdk,react,backend,better-auth}/index.ts", "src/backend/adapters/*.ts"],
"project": ["src/**/*.ts", "*.ts"]
},
"packages/ksuid": {
- "entry": ["src/**/*.ts"],
"project": ["src/**/*.ts"]
},
"packages/openapi": {
- "entry": ["**/*.ts"],
+ "entry": ["scripts/**/*.ts", "v2.1/contracts/index.ts"],
"project": ["**/*.ts"]
},
"packages/sdk": {
- "entry": ["src/**/*.ts", "examples/**/*.ts"],
+ "entry": ["src/*.ts", "examples/**/*.ts"],
"project": ["src/**/*.ts", "examples/**/*.ts"]
},
"packages/stripe-sync": {
- "entry": ["src/**/*.ts", "scripts/**/*.ts"],
+ "entry": ["scripts/**/*.ts"],
"project": ["src/**/*.ts", "scripts/**/*.ts"]
}
},
diff --git a/packages/atmn/src/commands/events-aggregate-test/command.ts b/packages/atmn/src/commands/events-aggregate-test/command.ts
deleted file mode 100644
index 049815b9a..000000000
--- a/packages/atmn/src/commands/events-aggregate-test/command.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-import chalk from "chalk";
-// @ts-expect-error - ervy doesn't have types
-import ervy from "ervy";
-import { fetchEvents } from "../../lib/api/endpoints/events.js";
-import { AppEnv } from "../../lib/env/detect.js";
-import { getKey } from "../../lib/env/keys.js";
-
-const { bar, bg } = ervy;
-
-interface AggregateOptions {
- prod: boolean;
- limit: number;
-}
-
-/**
- * Headless test command to inspect aggregate data and test ervy charts
- */
-export async function eventsAggregateTestCommand(options: AggregateOptions) {
- const environment = options.prod ? AppEnv.Live : AppEnv.Sandbox;
- const secretKey = getKey(environment);
-
- console.log(chalk.cyan(`\n=== Events Aggregate Test ===`));
- console.log(chalk.gray(`Environment: ${environment}`));
- console.log(chalk.gray(`Limit: ${options.limit}`));
-
- // Fetch events
- console.log(chalk.yellow(`\nFetching events...`));
- const response = await fetchEvents({
- secretKey,
- limit: options.limit,
- });
-
- const events = response.list;
- console.log(chalk.green(`Fetched ${events.length} events`));
-
- // === RAW DATA ===
- console.log(chalk.cyan(`\n=== Raw Events Sample (first 5) ===`));
- for (const event of events.slice(0, 5)) {
- console.log(
- JSON.stringify(
- {
- id: event.id.slice(0, 12) + "...",
- feature_id: event.feature_id,
- customer_id: event.customer_id.slice(0, 12) + "...",
- value: event.value,
- timestamp: new Date(event.timestamp).toISOString(),
- },
- null,
- 2,
- ),
- );
- }
-
- // === AGGREGATE BY FEATURE ===
- console.log(chalk.cyan(`\n=== Aggregate by Feature ===`));
- const featureMap = new Map
();
- for (const event of events) {
- const existing = featureMap.get(event.feature_id) ?? {
- count: 0,
- totalValue: 0,
- };
- featureMap.set(event.feature_id, {
- count: existing.count + 1,
- totalValue: existing.totalValue + event.value,
- });
- }
-
- const byFeature = Array.from(featureMap.entries())
- .map(([featureId, data]) => ({
- featureId,
- count: data.count,
- totalValue: data.totalValue,
- }))
- .sort((a, b) => b.count - a.count);
-
- console.log(chalk.gray("Feature aggregates:"));
- for (const f of byFeature) {
- console.log(
- ` ${f.featureId.padEnd(30)} count=${f.count} total=${f.totalValue}`,
- );
- }
-
- // === AGGREGATE BY TIME (day) ===
- console.log(chalk.cyan(`\n=== Aggregate by Day ===`));
- const dayMap = new Map();
- for (const event of events) {
- const date = new Date(event.timestamp);
- const dayKey = `${date.getMonth() + 1}/${date.getDate()}`;
- dayMap.set(dayKey, (dayMap.get(dayKey) ?? 0) + 1);
- }
-
- const byDay = Array.from(dayMap.entries())
- .map(([day, count]) => ({ day, count }))
- .sort((a, b) => a.day.localeCompare(b.day));
-
- console.log(chalk.gray("Daily counts:"));
- for (const d of byDay) {
- console.log(` ${d.day.padEnd(10)} count=${d.count}`);
- }
-
- // === ERVY BAR CHART DATA FORMAT ===
- console.log(chalk.cyan(`\n=== ervy Bar Chart Data Format ===`));
-
- // Prepare data for ervy - CORRECT format with bg() function
- const colors = ["cyan", "green", "yellow", "magenta", "blue", "red", "white"];
- const chartData = byFeature.slice(0, 6).map((f, idx) => ({
- key: f.featureId.length > 10 ? f.featureId.slice(0, 8) + ".." : f.featureId,
- value: f.count,
- style: bg(colors[idx % colors.length], 1),
- }));
-
- console.log(chalk.gray("Chart data:"));
- console.log(JSON.stringify(chartData, null, 2));
-
- // === RENDER ERVY BAR CHART ===
- console.log(chalk.cyan(`\n=== ervy Bar Chart Output ===`));
- try {
- const chartOutput = bar(chartData, {
- barWidth: 3,
- height: 6,
- padding: 2,
- });
- console.log(chartOutput);
- } catch (err) {
- console.log(chalk.red(`Bar chart error: ${err}`));
- }
-
- // === SIMPLE ASCII BAR (fallback) ===
- console.log(chalk.cyan(`\n=== Simple ASCII Bar Chart ===`));
- const maxCount = Math.max(...byFeature.map((f) => f.count));
- const barWidth = 40;
-
- for (const f of byFeature.slice(0, 8)) {
- const width = Math.round((f.count / maxCount) * barWidth);
- const barStr = "█".repeat(width);
- const label = f.featureId.length > 20 ? f.featureId.slice(0, 18) + ".." : f.featureId.padEnd(20);
- console.log(`${label} ${chalk.cyan(barStr)} ${f.count}`);
- }
-
- // === SUMMARY ===
- console.log(chalk.cyan(`\n=== Summary ===`));
- console.log(`Total events: ${events.length}`);
- console.log(`Total value: ${events.reduce((sum, e) => sum + e.value, 0)}`);
- console.log(`Unique features: ${featureMap.size}`);
- console.log(
- `Unique customers: ${new Set(events.map((e) => e.customer_id)).size}`,
- );
-
- console.log(chalk.green(`\n=== Done ===\n`));
-}
diff --git a/packages/atmn/src/commands/events-aggregate-test/index.ts b/packages/atmn/src/commands/events-aggregate-test/index.ts
deleted file mode 100644
index f5348a1be..000000000
--- a/packages/atmn/src/commands/events-aggregate-test/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { eventsAggregateTestCommand } from "./command.js";
diff --git a/packages/atmn/src/commands/pull/index.ts b/packages/atmn/src/commands/pull/index.ts
deleted file mode 100644
index 1d30ee85f..000000000
--- a/packages/atmn/src/commands/pull/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { pull } from "./pull.js";
-export type { EnvironmentData, PullOptions, PullResult } from "./types.js";
diff --git a/packages/atmn/src/commands/test-template/command.tsx b/packages/atmn/src/commands/test-template/command.tsx
deleted file mode 100644
index 504c3427b..000000000
--- a/packages/atmn/src/commands/test-template/command.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { render } from "ink";
-import { TemplateSelector } from "../../views/react/template/TemplateSelector.js";
-
-export function testTemplateCommand() {
- render();
-}
diff --git a/packages/atmn/src/lib/constants/templates.ts b/packages/atmn/src/lib/constants/templates.ts
deleted file mode 100644
index 199161bd1..000000000
--- a/packages/atmn/src/lib/constants/templates.ts
+++ /dev/null
@@ -1,120 +0,0 @@
-/**
- * Template data for the template selector
- * Each template has 3 pricing plans with features
- */
-
-export interface PlanData {
- name: string;
- features: string[];
- price: string;
- badge?: string;
-}
-
-export const templateData: Record = {
- Railway: [
- {
- name: "Free",
- features: [
- "500 credits one-time",
- "Memory: 0.039cr/GB-hr",
- "CPU: 0.078cr/vCPU-hr",
- "Egress: 5cr/GB",
- ],
- price: "$0",
- },
- {
- name: "Hobby",
- badge: "most popular",
- features: [
- "500 credits/month",
- "Pay-per-use overage",
- "Memory: 0.039cr/GB-hr",
- "CPU: 0.078cr/vCPU-hr",
- "Egress: 5cr/GB",
- "Storage: 1.5cr/GB-mo",
- ],
- price: "$5/month",
- },
- {
- name: "Pro",
- features: [
- "2000 credits/month",
- "Pay-per-use overage",
- "All resource types",
- "Team features",
- "Priority support",
- ],
- price: "$20/month",
- },
- ],
- Linear: [
- {
- name: "Free",
- features: [
- "2 teams",
- "250 issues limit",
- "Basic integrations",
- "Community support",
- ],
- price: "$0",
- },
- {
- name: "Basic",
- badge: "most popular",
- features: [
- "5 teams",
- "Unlimited issues",
- "All integrations",
- "Cycles & roadmaps",
- "Email support",
- "Guest access",
- ],
- price: "$12/user/mo",
- },
- {
- name: "Business",
- features: [
- "Unlimited teams",
- "Unlimited issues",
- "SAML SSO",
- "Audit logs",
- "Priority support",
- ],
- price: "$18/user/mo",
- },
- ],
- "T3 Chat": [
- {
- name: "Free",
- features: ["100 messages", "/month", "Basic models", "Web access"],
- price: "$0",
- },
- {
- name: "Pro",
- badge: "most popular",
- features: [
- "1500 messages/month",
- "100 premium/month",
- "All models",
- "Priority access",
- "Faster responses",
- "File uploads",
- ],
- price: "$8/month",
- },
- {
- name: "Credits",
- features: [
- "100 premium",
- "messages",
- "One-time purchase",
- "Never expires",
- ],
- price: "$8 add-on",
- },
- ],
-};
-
-export const templates = ["Railway", "Linear", "T3 Chat"] as const;
-
-export type TemplateName = (typeof templates)[number];
diff --git a/packages/atmn/src/lib/utils/index.ts b/packages/atmn/src/lib/utils/index.ts
deleted file mode 100644
index ac75f0d22..000000000
--- a/packages/atmn/src/lib/utils/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from "./monorepo.js";
diff --git a/packages/atmn/src/views/App.tsx b/packages/atmn/src/views/App.tsx
deleted file mode 100644
index 060eae060..000000000
--- a/packages/atmn/src/views/App.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { Box, Text } from "ink";
-
-export default function App() {
- return (
-
-
- atmn
-
- Autumn CLI - Interactive Mode
-
- );
-}
diff --git a/packages/atmn/src/views/react/components/providers/index.ts b/packages/atmn/src/views/react/components/providers/index.ts
deleted file mode 100644
index e105c7df6..000000000
--- a/packages/atmn/src/views/react/components/providers/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { QueryProvider } from "./QueryProvider.js";
diff --git a/packages/atmn/src/views/react/customers/components/CustomerRow.tsx b/packages/atmn/src/views/react/customers/components/CustomerRow.tsx
deleted file mode 100644
index d2e2339e9..000000000
--- a/packages/atmn/src/views/react/customers/components/CustomerRow.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Box, Text } from "ink";
-import type { ColumnWidths, CustomerRowProps } from "../types.js";
-import { formatDate, truncate } from "../types.js";
-
-/**
- * Single customer row in the table
- */
-export function CustomerRow({
- customer,
- isSelected,
- isFocused,
- columnWidths,
-}: CustomerRowProps) {
- const marker = isSelected ? "▸ " : " ";
- const markerColor = isSelected && isFocused ? "magenta" : "gray";
-
- const { colId, colName, colEmail, colCreated, shouldTruncate } = columnWidths;
-
- return (
-
- {marker}
-
-
- {shouldTruncate
- ? truncate(customer.id, colId - 1)
- : customer.id || "-"}
-
-
-
-
- {shouldTruncate
- ? truncate(customer.name, colName - 1)
- : customer.name || "-"}
-
-
-
-
- {shouldTruncate
- ? truncate(customer.email, colEmail - 1)
- : customer.email || "-"}
-
-
-
-
- {formatDate(customer.created_at)}
-
-
-
- );
-}
-
-export interface CustomerTableHeaderProps {
- columnWidths: ColumnWidths;
-}
-
-/**
- * Table header row
- */
-export function CustomerTableHeader({
- columnWidths,
-}: CustomerTableHeaderProps) {
- const { colId, colName, colEmail, colCreated } = columnWidths;
-
- return (
-
- {" "}
-
-
- ID
-
-
-
-
- Name
-
-
-
-
- Email
-
-
-
-
- Created
-
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/CustomersTable.tsx b/packages/atmn/src/views/react/customers/components/CustomersTable.tsx
deleted file mode 100644
index 743a8ba83..000000000
--- a/packages/atmn/src/views/react/customers/components/CustomersTable.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { Box } from "ink";
-import { ScrollList, type ScrollListRef } from "ink-scroll-list";
-import { useEffect, useRef } from "react";
-import type { ColumnWidths, CustomersTableProps } from "../types.js";
-import { CustomerRow, CustomerTableHeader } from "./CustomerRow.js";
-
-export interface CustomersTableComponentProps extends CustomersTableProps {
- columnWidths: ColumnWidths;
-}
-
-/**
- * Scrollable customer table using ink-scroll-list
- */
-export function CustomersTable({
- customers,
- selectedIndex,
- onSelect,
- isFocused,
- columnWidths,
-}: CustomersTableComponentProps) {
- const listRef = useRef(null);
-
- // Handle terminal resize
- useEffect(() => {
- const handleResize = () => {
- listRef.current?.remeasure();
- };
-
- process.stdout.on("resize", handleResize);
- return () => {
- process.stdout.off("resize", handleResize);
- };
- }, []);
-
- // Update selected customer when index changes
- useEffect(() => {
- if (customers[selectedIndex]) {
- onSelect(customers[selectedIndex], selectedIndex);
- }
- }, [selectedIndex, customers, onSelect]);
-
- return (
-
-
-
-
- {customers.map((customer, index) => (
-
- ))}
-
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/EmptyState.tsx b/packages/atmn/src/views/react/customers/components/EmptyState.tsx
deleted file mode 100644
index a521f52ea..000000000
--- a/packages/atmn/src/views/react/customers/components/EmptyState.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { Box, Text } from "ink";
-import { AppEnv } from "../../../../lib/env/detect.js";
-import type { EmptyStateProps } from "../types.js";
-
-/**
- * Empty state when no customers exist
- */
-export function EmptyState({ environment, searchQuery }: EmptyStateProps) {
- const envLabel = environment === AppEnv.Sandbox ? "sandbox" : "live";
-
- // Different message when search has no results
- if (searchQuery) {
- return (
-
- 🔍
-
- No results for "{searchQuery}"
-
-
-
- Try a different search term or press x{" "}
- to clear the search.
-
-
-
- );
- }
-
- return (
-
- 📭
-
- No customers found
-
-
-
- There are no customers in your {envLabel} environment yet.
-
-
-
-
- Create customers via the API or dashboard to see them here.
-
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/ErrorState.tsx b/packages/atmn/src/views/react/customers/components/ErrorState.tsx
deleted file mode 100644
index 9d198ef82..000000000
--- a/packages/atmn/src/views/react/customers/components/ErrorState.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { Box, Text } from "ink";
-import type { ErrorStateProps } from "../types.js";
-
-/**
- * Error state with retry option
- */
-export function ErrorState({ error, onRetry: _onRetry }: ErrorStateProps) {
- return (
-
-
-
- ✗ Error loading customers
-
-
-
- {error.message}
-
-
-
- Press r to retry or{" "}
- q to quit
-
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/KeybindHints.tsx b/packages/atmn/src/views/react/customers/components/KeybindHints.tsx
deleted file mode 100644
index 96fde0be3..000000000
--- a/packages/atmn/src/views/react/customers/components/KeybindHints.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { Box, Text } from "ink";
-import type { KeybindHintsProps } from "../types.js";
-
-/**
- * Context-aware keyboard shortcut hints
- */
-export function KeybindHints({
- focusTarget,
- sheetOpen,
- canGoPrev,
- canGoNext,
-}: KeybindHintsProps) {
- if (focusTarget === "sheet" && sheetOpen) {
- return (
-
-
- Tab
- focus table
-
-
- Esc
- close
-
-
- c
- copy ID
-
-
- o
- open
-
-
- q
- quit
-
-
- );
- }
-
- // Table focused hints
- return (
-
-
- ↑↓
- navigate
-
- {canGoPrev && (
-
- ←
- prev page
-
- )}
- {canGoNext && (
-
- →
- next page
-
- )}
-
- Enter
- inspect
-
-
- /
- search
-
-
- r
- refresh
-
-
- q
- quit
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/LoadingState.tsx b/packages/atmn/src/views/react/customers/components/LoadingState.tsx
deleted file mode 100644
index 312b51c82..000000000
--- a/packages/atmn/src/views/react/customers/components/LoadingState.tsx
+++ /dev/null
@@ -1,28 +0,0 @@
-import { Box, Text } from "ink";
-import Spinner from "ink-spinner";
-import { AppEnv } from "../../../../lib/env/detect.js";
-import type { LoadingStateProps } from "../types.js";
-
-/**
- * Loading state with spinner
- */
-export function LoadingState({ environment }: LoadingStateProps) {
- const envLabel = environment === AppEnv.Sandbox ? "sandbox" : "live";
-
- return (
-
-
-
-
-
- Loading customers from {envLabel}...
-
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/SearchInput.tsx b/packages/atmn/src/views/react/customers/components/SearchInput.tsx
deleted file mode 100644
index 9ffe51ddf..000000000
--- a/packages/atmn/src/views/react/customers/components/SearchInput.tsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { Box, Text, useInput } from "ink";
-import TextInput from "ink-text-input";
-import { useState } from "react";
-
-export interface SearchInputProps {
- /** Current search value */
- initialValue: string;
- /** Called when search is submitted */
- onSubmit: (query: string) => void;
- /** Called when search is cancelled */
- onCancel: () => void;
-}
-
-/**
- * Inline search input that appears below the title bar
- */
-export function SearchInput({
- initialValue,
- onSubmit,
- onCancel,
-}: SearchInputProps) {
- const [value, setValue] = useState(initialValue);
-
- useInput((_input, key) => {
- if (key.escape) {
- onCancel();
- return;
- }
- if (key.return) {
- onSubmit(value);
- return;
- }
- });
-
- return (
-
- Search:
-
- (Enter to search, Esc to cancel)
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/TitleBar.tsx b/packages/atmn/src/views/react/customers/components/TitleBar.tsx
deleted file mode 100644
index bdc93518b..000000000
--- a/packages/atmn/src/views/react/customers/components/TitleBar.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { Box, Text } from "ink";
-import { APP_VERSION } from "../../../../lib/version.js";
-import type { TitleBarProps } from "../types.js";
-
-/**
- * Title bar showing version, command name, pagination info, and search query
- */
-export function TitleBar({
- environment,
- pagination,
- searchQuery,
-}: TitleBarProps) {
- return (
-
- {APP_VERSION}
- │
-
- atmn customers
-
- │
- {pagination.display}
- {searchQuery && (
- <>
- │
- search:
- {searchQuery}
- (x to clear)
- >
- )}
-
- );
-}
diff --git a/packages/atmn/src/views/react/customers/components/index.ts b/packages/atmn/src/views/react/customers/components/index.ts
deleted file mode 100644
index 239bcc697..000000000
--- a/packages/atmn/src/views/react/customers/components/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export { CustomerRow, CustomerTableHeader } from "./CustomerRow.js";
-export { CustomerSheet } from "./CustomerSheet.js";
-export { CustomersTable } from "./CustomersTable.js";
-export { EmptyState } from "./EmptyState.js";
-export { ErrorState } from "./ErrorState.js";
-export { KeybindHints } from "./KeybindHints.js";
-export { LoadingState } from "./LoadingState.js";
-export { SearchInput } from "./SearchInput.js";
-export { TitleBar } from "./TitleBar.js";
diff --git a/packages/atmn/src/views/react/features/components/index.ts b/packages/atmn/src/views/react/features/components/index.ts
deleted file mode 100644
index 682d0d550..000000000
--- a/packages/atmn/src/views/react/features/components/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { FeatureSheet } from "./FeatureSheet.js";
-export type { FeatureSheetProps } from "./FeatureSheet.js";
diff --git a/packages/atmn/src/views/react/features/index.ts b/packages/atmn/src/views/react/features/index.ts
deleted file mode 100644
index a883750be..000000000
--- a/packages/atmn/src/views/react/features/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { FeaturesView } from "./FeaturesView.js";
-export type { FeaturesViewProps } from "./FeaturesView.js";
-export { FeatureSheet } from "./components/index.js";
-export type { FeatureSheetProps } from "./components/index.js";
diff --git a/packages/atmn/src/views/react/init/steps/AgentStep.tsx b/packages/atmn/src/views/react/init/steps/AgentStep.tsx
deleted file mode 100644
index 26e230e72..000000000
--- a/packages/atmn/src/views/react/init/steps/AgentStep.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-import { Box, Text } from "ink";
-import { useEffect, useState } from "react";
-import {
- type AgentIdentifier,
- type FileOption,
- useAgentSetup,
-} from "../../../../lib/hooks/index.js";
-import {
- MultiSelect,
- StatusLine,
- StepHeader,
-} from "../../components/index.js";
-
-interface AgentStepProps {
- step: number;
- totalSteps: number;
- onComplete: () => void;
-}
-
-type AgentState =
- | "selecting"
- | "mcp-agents"
- | "installing"
- | "creating"
- | "complete"
- | "error";
-
-/**
- * Agent setup step - allows user to configure agent files
- */
-export function AgentStep({ step, totalSteps, onComplete }: AgentStepProps) {
- const [state, setState] = useState("selecting");
- const [selectedOptions, setSelectedOptions] = useState([]);
- const [selectedAgents, setSelectedAgents] = useState([]);
- const [error, setError] = useState(null);
-
- const { installMcp, createAgentFiles } = useAgentSetup();
-
- const options = [
- {
- label: "MCP Server Config (for Claude Code, OpenCode, etc.)",
- value: "mcp",
- },
- {
- label: "CLAUDE.md",
- value: "claude-md",
- },
- {
- label: "AGENTS.md",
- value: "agents-md",
- },
- {
- label: ".cursorrules",
- value: "cursor-rules",
- },
- ];
-
- const agentOptions = [
- {
- label: "Claude Code (auto-install via command)",
- value: "claude-code",
- },
- {
- label: "OpenCode, Codex and others (copy URL to clipboard)",
- value: "other",
- },
- ];
-
- const handleSubmit = (values: string[]) => {
- setSelectedOptions(values);
-
- // If MCP is selected, go to agent selection
- if (values.includes("mcp")) {
- setState("mcp-agents");
- } else {
- // Otherwise, create the other files
- setState("creating");
- const fileOptions = values.filter((v) => v !== "mcp") as FileOption[];
- createAgentFiles.mutate(fileOptions);
- }
- };
-
- const handleAgentSubmit = (agents: string[]) => {
- setSelectedAgents(agents);
- setState("installing");
-
- // Install MCP for selected agents
- installMcp.mutate(agents as AgentIdentifier[]);
- };
-
- // Handle MCP installation completion
- useEffect(() => {
- if (installMcp.isSuccess && state === "installing") {
- // Now create the other selected files
- const nonMcpOptions = selectedOptions.filter(
- (opt) => opt !== "mcp",
- ) as FileOption[];
- if (nonMcpOptions.length > 0) {
- setState("creating");
- createAgentFiles.mutate(nonMcpOptions);
- } else {
- setState("complete");
- setTimeout(() => {
- onComplete();
- }, 1000);
- }
- }
- }, [
- installMcp.isSuccess,
- state,
- selectedOptions,
- onComplete,
- createAgentFiles,
- ]);
-
- // Handle MCP installation error
- useEffect(() => {
- if (installMcp.isError) {
- setError(
- installMcp.error instanceof Error
- ? installMcp.error.message
- : "Failed to install MCP",
- );
- setState("error");
- }
- }, [installMcp.isError, installMcp.error]);
-
- // Handle file creation completion
- useEffect(() => {
- if (createAgentFiles.isSuccess && state === "creating") {
- setState("complete");
- setTimeout(() => {
- onComplete();
- }, 1000);
- }
- }, [createAgentFiles.isSuccess, state, onComplete]);
-
- // Handle file creation error
- useEffect(() => {
- if (createAgentFiles.isError) {
- setError(
- createAgentFiles.error instanceof Error
- ? createAgentFiles.error.message
- : "Failed to create files",
- );
- setState("error");
- }
- }, [createAgentFiles.isError, createAgentFiles.error]);
-
- const handleChange = (values: string[]) => {
- setSelectedOptions(values);
- };
-
- const handleAgentChange = (values: string[]) => {
- setSelectedAgents(values);
- };
-
- if (state === "selecting") {
- return (
-
-
-
- Select agent configuration files to create/update:
- (Space to select, Enter to confirm)
-
-
-
-
-
- );
- }
-
- if (state === "mcp-agents") {
- return (
-
-
-
- Which agent(s) are you using?
- (Space to select, Enter to confirm)
-
-
-
-
-
- );
- }
-
- if (state === "installing") {
- const installMessages: string[] = [];
-
- if (selectedAgents.includes("claude-code")) {
- installMessages.push("Installing MCP for Claude Code...");
- }
-
- if (
- selectedAgents.includes("opencode") ||
- selectedAgents.includes("other")
- ) {
- installMessages.push("Copied MCP URL to clipboard!");
- }
-
- return (
-
-
-
- {installMessages.map((msg) => (
-
- ))}
-
-
- );
- }
-
- if (state === "creating") {
- return (
-
-
- o !== "mcp").length} file${selectedOptions.filter((o) => o !== "mcp").length !== 1 ? "s" : ""}...`}
- />
-
- );
- }
-
- if (state === "complete") {
- const createdFiles: string[] = [];
-
- if (selectedAgents.length > 0) {
- createdFiles.push("MCP server config");
- }
- if (selectedOptions.includes("claude-md")) {
- createdFiles.push("CLAUDE.md");
- }
- if (selectedOptions.includes("agents-md")) {
- createdFiles.push("AGENTS.md");
- }
- if (selectedOptions.includes("cursor-rules")) {
- createdFiles.push(".cursorrules");
- }
-
- return (
-
-
- 0
- ? `Created ${createdFiles.join(", ")}`
- : "Setup complete"
- }
- />
-
- );
- }
-
- if (state === "error") {
- return (
-
-
-
-
- );
- }
-
- return null;
-}
diff --git a/packages/atmn/src/views/react/init/steps/StripeStep.tsx b/packages/atmn/src/views/react/init/steps/StripeStep.tsx
deleted file mode 100644
index aa0badf6b..000000000
--- a/packages/atmn/src/views/react/init/steps/StripeStep.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import { Box, Text } from "ink";
-import open from "open";
-import { useCallback, useEffect, useRef, useState } from "react";
-import { FRONTEND_URL } from "../../../../constants.js";
-import { fetchOrganization } from "../../../../lib/api/endpoints/organization.js";
-import { readFromEnv } from "../../../../lib/utils.js";
-import { StatusLine, StepHeader } from "../../components/index.js";
-
-type StripeState =
- | "pending"
- | "checking"
- | "not_connected"
- | "connecting"
- | "connected"
- | "error";
-
-interface StripeStepProps {
- step: number;
- totalSteps: number;
- onComplete: () => void;
-}
-
-export function StripeStep({ step, totalSteps, onComplete }: StripeStepProps) {
- const [stripeState, setStripeState] = useState("pending");
- const [stripeError, setStripeError] = useState(null);
- const pollIntervalRef = useRef(null);
-
- const connectStripe = useCallback(async () => {
- setStripeState("connecting");
-
- // Open dashboard to Stripe connect page
- const stripeConnectUrl = `${FRONTEND_URL}/dev?tab=stripe`;
- await open(stripeConnectUrl);
-
- // Poll for Stripe connection
- const maxAttempts = 60; // 5 minutes with 5 second intervals
- let attempts = 0;
-
- const pollInterval = setInterval(async () => {
- attempts++;
-
- try {
- const secretKey = readFromEnv({ bypass: true });
- if (!secretKey) {
- return; // Continue polling, key may not be ready yet
- }
-
- const orgDetails = await fetchOrganization({ secretKey });
-
- if (
- orgDetails.stripe_connection &&
- orgDetails.stripe_connection !== "none"
- ) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current);
- pollIntervalRef.current = null;
- }
- setStripeState("connected");
- onComplete();
- } else if (attempts >= maxAttempts) {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current);
- pollIntervalRef.current = null;
- }
- setStripeError("Timed out waiting for Stripe connection");
- setStripeState("error");
- }
- } catch {
- // Continue polling on error
- }
- }, 5000);
-
- pollIntervalRef.current = pollInterval;
- }, [onComplete]);
-
- const checkStripe = useCallback(async () => {
- setStripeState("checking");
-
- try {
- const secretKey = readFromEnv({ bypass: true });
- if (!secretKey) {
- setStripeError("No API key found. Please run 'atmn login' first.");
- setStripeState("error");
- return;
- }
-
- // Fetch org details to check Stripe connection
- const orgDetails = await fetchOrganization({ secretKey });
-
- if (
- orgDetails.stripe_connection &&
- orgDetails.stripe_connection !== "none"
- ) {
- setStripeState("connected");
- onComplete();
- } else {
- setStripeState("not_connected");
- // Auto-open Stripe connect page
- await connectStripe();
- }
- } catch (error) {
- setStripeError(
- error instanceof Error
- ? error.message
- : "Failed to check Stripe status",
- );
- setStripeState("error");
- }
- }, [connectStripe, onComplete]);
-
- useEffect(() => {
- // Only start checking when this step is active
- if (stripeState === "pending") {
- checkStripe();
- }
- }, [checkStripe, stripeState]);
-
- // Cleanup: clear poll interval on unmount
- useEffect(() => {
- return () => {
- if (pollIntervalRef.current) {
- clearInterval(pollIntervalRef.current);
- pollIntervalRef.current = null;
- }
- };
- }, []);
-
- return (
-
-
- {stripeState === "checking" && (
-
- )}
- {stripeState === "not_connected" && (
-
- )}
- {stripeState === "connecting" && (
-
-
-
- {" "}
- Complete the setup in your browser, then return here.
-
-
- )}
-
- {stripeState === "connected" && (
-
- )}
-
- {stripeState === "error" && (
-
- )}
-
- );
-}
diff --git a/packages/atmn/src/views/react/products/components/index.ts b/packages/atmn/src/views/react/products/components/index.ts
deleted file mode 100644
index dfee379b7..000000000
--- a/packages/atmn/src/views/react/products/components/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ProductSheet } from "./ProductSheet.js";
-export type { ProductSheetProps } from "./ProductSheet.js";
diff --git a/packages/atmn/src/views/react/products/index.ts b/packages/atmn/src/views/react/products/index.ts
deleted file mode 100644
index ae29158dc..000000000
--- a/packages/atmn/src/views/react/products/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export { ProductsView } from "./ProductsView.js";
-export type { ProductsViewProps } from "./ProductsView.js";
-export { ProductSheet } from "./components/index.js";
-export type { ProductSheetProps } from "./components/index.js";
diff --git a/packages/atmn/src/views/react/push/index.ts b/packages/atmn/src/views/react/push/index.ts
deleted file mode 100644
index d8147a42f..000000000
--- a/packages/atmn/src/views/react/push/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { PushView } from "./Push.js";
diff --git a/packages/atmn/src/views/react/template/TemplateSelector.tsx b/packages/atmn/src/views/react/template/TemplateSelector.tsx
deleted file mode 100644
index a42e02bf9..000000000
--- a/packages/atmn/src/views/react/template/TemplateSelector.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import { Box, Text, useInput } from "ink";
-import React, { useState } from "react";
-import { templateData, templates } from "../../../lib/constants/templates.js";
-
-interface TemplateSelectorProps {
- onSelect?: (template: string) => void;
- onCancel?: () => void;
-}
-
-// Total width of all 3 cards + gaps
-const CARDS_TOTAL_WIDTH = 84;
-
-export const TemplateSelector: React.FC = ({
- onSelect,
- onCancel,
-}) => {
- const [activeIndex, setActiveIndex] = useState(1); // Start on RatGPT (middle)
-
- const activeTemplate = templates[activeIndex];
-
- useInput((_input, key) => {
- if (key.tab || key.rightArrow) {
- // Next template
- setActiveIndex((prev) => (prev + 1) % templates.length);
- } else if (key.leftArrow) {
- // Previous template
- setActiveIndex(
- (prev) => (prev - 1 + templates.length) % templates.length,
- );
- } else if (key.return) {
- // Confirm selection
- if (activeTemplate) {
- onSelect?.(activeTemplate);
- }
- } else if (key.escape) {
- // Cancel
- onCancel?.();
- }
- });
-
- if (!activeTemplate) {
- return No template selected;
- }
-
- const plans = templateData[activeTemplate];
- if (!plans) {
- return Invalid template data;
- }
-
- const [plan0, plan1, plan2] = plans;
- if (!plan0 || !plan1 || !plan2) {
- return Incomplete plan data;
- }
-
- return (
-
- {/* Template Tabs - single box spanning all cards */}
-
- ←
- {templates.map((template: string, idx: number) => (
-
- {idx > 0 && │}
-
- {" "}
- {template}{" "}
-
-
- ))}
- →
-
-
- {/* Plan Cards - 3 columns, centered within fixed width */}
-
-
- {/* Left Card */}
-
-
- {plan0.name}
-
-
- {plan0.features.map((feature: string) => (
-
- • {feature}
-
- ))}
-
-
- {plan0.price}
-
-
-
-
- {/* Center Card */}
-
-
- {plan1.name}
-
- {plan1.badge && (
-
- {plan1.badge}
-
- )}
-
- {plan1.features.map((feature: string) => (
-
- • {feature}
-
- ))}
-
-
- {plan1.price}
-
-
-
-
- {/* Right Card */}
-
-
- {plan2.name}
-
-
- {plan2.features.map((feature: string) => (
-
- • {feature}
-
- ))}
-
-
- {plan2.price}
-
-
-
-
-
-
- {/* Hint for controls */}
-
-
- ← → switch templates • Enter to confirm • Esc to cancel
-
-
-
- );
-};
diff --git a/packages/atmn/src/views/react/template2/index.ts b/packages/atmn/src/views/react/template2/index.ts
deleted file mode 100644
index 34f3d1340..000000000
--- a/packages/atmn/src/views/react/template2/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export { Badge } from "./Badge.js";
-export {
- type Template,
- type TemplateBadge,
- type TemplateCreditCost,
- type TemplateTier,
- templates,
-} from "./data.js";
-export { PlanCard } from "./PlanCard.js";
-export { TemplateRow } from "./TemplateRow.js";
-export { TemplateSelector2 } from "./TemplateSelector2.js";
diff --git a/packages/atmn/src/views/react/test-agent.tsx b/packages/atmn/src/views/react/test-agent.tsx
deleted file mode 100644
index 1b47d135f..000000000
--- a/packages/atmn/src/views/react/test-agent.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/usr/bin/env node
-import { render } from "ink";
-import { AgentStep } from "./init/steps/AgentStep.js";
-
-// Test the AgentStep component standalone
-function TestAgentStep() {
- return (
- {
- console.log("Agent setup complete!");
- process.exit(0);
- }}
- />
- );
-}
-
-render();
diff --git a/packages/autumn-js/src/utils/ErrorResponse.ts b/packages/autumn-js/src/utils/ErrorResponse.ts
deleted file mode 100644
index 63cf61ff8..000000000
--- a/packages/autumn-js/src/utils/ErrorResponse.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// export interface ErrorResponse {
-// message: string;
-// code: string;
-// }
-
-export class ErrorResponse extends Error {
- public readonly message: string;
- public readonly code: string;
-
- constructor(response: { message: string; code: string }) {
- super(response.message);
- this.message = response.message;
- this.code = response.code;
- }
-
- static fromError(error: any) {
- return new ErrorResponse({
- message: error.message || "Unknown error",
- code: error.code || "unknown_error",
- });
- }
-
- toString() {
- return `${this.message} (code: ${this.code})`;
- }
-
- toJSON() {
- return {
- message: this.message,
- code: this.code,
- };
- }
-}
diff --git a/packages/autumn-js/src/utils/handleFetchResult.ts b/packages/autumn-js/src/utils/handleFetchResult.ts
deleted file mode 100644
index cc493b24b..000000000
--- a/packages/autumn-js/src/utils/handleFetchResult.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-export const handleFetchResult = async ({
- response,
- logger,
- logError = true,
-}: {
- response: Response;
- logger: Console;
- logError?: boolean;
-}): Promise => {
- if (response.status === 204) {
- return null;
- }
-
- if (response.status < 200 || response.status >= 300) {
- let error: any;
- try {
- error = await response.json();
- if (logError) {
- logger.error(`[Autumn] ${error.message}`);
- }
-
- throw error;
- } catch (error) {
- // biome-ignore lint/complexity/noUselessCatch: idk
- throw error;
- }
-
- // return {
- // data: null,
- // error: new AutumnError({
- // message: error.message,
- // code: error.code,
- // }),
- // statusCode: response.status,
- // };
- }
-
- try {
- const data = await response.json();
-
- return data;
- // return {
- // data: data,
- // error: null,
- // statusCode: response?.status,
- // };
- } catch (error) {
- return null;
- }
-};
diff --git a/packages/autumn-js/src/utils/logger.ts b/packages/autumn-js/src/utils/logger.ts
deleted file mode 100644
index 5098bb194..000000000
--- a/packages/autumn-js/src/utils/logger.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-// Alternative: Use pino without worker thread transport
-const getTime = () => {
- const timeString = new Date().toISOString();
- return `[${timeString.split("T")[1].split(".")[0]}]`;
-};
-
-const greaterThanLevel = (level: string) => {
- return levels.indexOf(level) >= levels.indexOf(logger.level);
-};
-
-const levels = ["debug", "info", "warn", "error", "fatal"];
-
-export const logger = {
- ...console,
- level: "info",
- debug: (...args: any[]) => {
- if (greaterThanLevel("debug")) {
- console.log(getTime(), "DEBUG", ...args);
- }
- },
- log: (...args: any[]) => {
- console.log(getTime(), "INFO", ...args);
- },
- info: (...args: any[]) => {
- if (greaterThanLevel("info")) {
- console.log(getTime(), "INFO", ...args);
- }
- },
- warn: (...args: any[]) => {
- if (greaterThanLevel("warn")) {
- console.log(getTime(), "WARN", ...args);
- }
- },
- error: (...args: any[]) => {
- if (greaterThanLevel("error")) {
- console.log(getTime(), "ERROR", ...args);
- }
- },
-};
-// export const logger =
-// typeof window !== "undefined"
-// ? console
-// : pino(
-// {
-// level: process.env.LOG_LEVEL || "info",
-// formatters: {
-// level: (label: string) => {
-// return { level: label };
-// },
-// },
-// },
-// pretty({
-// customColors: {
-// default: "white",
-// 60: "bgRed",
-// 50: "red",
-// 40: "yellow",
-// 30: "green",
-// 20: "blue",
-// 10: "gray",
-// message: "reset",
-// greyMessage: "gray",
-// time: "darkGray",
-// },
-// ignore: "pid,hostname",
-// })
-// );
diff --git a/packages/autumn-js/src/utils/toSnakeCase.ts b/packages/autumn-js/src/utils/toSnakeCase.ts
deleted file mode 100644
index 25c3a6d9c..000000000
--- a/packages/autumn-js/src/utils/toSnakeCase.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-function stringToSnakeCase(str: string): string {
- return str
- .replace(/([a-z])([A-Z])/g, "$1_$2")
- .replace(/[-\s]+/g, "_")
- .toLowerCase();
-}
-
-export const toSnakeCase = ({
- obj,
- excludeKeys,
- excludeChildrenOf,
-}: {
- obj: T;
- excludeKeys?: string[];
- excludeChildrenOf?: string[];
-}): T => {
- if (Array.isArray(obj)) {
- return obj.map((item) =>
- toSnakeCase({
- obj: item as unknown as T,
- excludeKeys,
- excludeChildrenOf,
- }),
- ) as T;
- } else if (obj !== null && typeof obj === "object") {
- return Object.fromEntries(
- Object.entries(obj).map(([key, value]) => {
- const snakeKey = stringToSnakeCase(key);
-
- // If this key is in excludeKeys, leave key and value untouched
- if (excludeKeys?.includes(key)) {
- return [key, value];
- }
-
- // If this key is in excludeChildrenOf, convert key but do not recurse into value
- if (excludeChildrenOf?.includes(key)) {
- return [snakeKey, value];
- }
-
- // Otherwise, convert key and recursively process value
- return [
- snakeKey,
- toSnakeCase({
- obj: value as unknown as T,
- excludeKeys,
- excludeChildrenOf,
- }),
- ];
- }),
- ) as T;
- }
- return obj as T;
-};
diff --git a/packages/autumn-js/tsup.dev.config.ts b/packages/autumn-js/tsup.dev.config.ts
deleted file mode 100644
index bcb60aa15..000000000
--- a/packages/autumn-js/tsup.dev.config.ts
+++ /dev/null
@@ -1,83 +0,0 @@
-import alias from "esbuild-plugin-path-alias";
-import * as path from "path";
-import { defineConfig, type Options } from "tsup";
-
-// Path aliases that match tsconfig.json
-const pathAliases = {
- "@": path.resolve("./src/libraries/react"),
- "@sdk": path.resolve("./src/sdk"),
-};
-
-const reactConfigs: Options[] = [
- // Backend
- {
- entry: ["src/libraries/backend/**/*.{ts,tsx}"],
- format: ["cjs", "esm"],
- dts: true,
- clean: false, // Don't clean on subsequent builds
- outDir: "./dist/libraries/backend",
- external: ["react", "react/jsx-runtime", "react-dom"],
- bundle: true,
- esbuildOptions(options) {
- options.plugins = options.plugins || [];
- options.plugins.push(alias(pathAliases));
- },
- },
-
- // React
- {
- entry: ["src/libraries/react/**/*.{ts,tsx}"],
- format: ["cjs", "esm"],
- dts: false,
- clean: false,
- outDir: "./dist/libraries/react",
- external: ["react", "react/jsx-runtime", "react-dom"],
- bundle: true,
- banner: {
- js: '"use client";',
- },
- esbuildOptions(options) {
- options.plugins = options.plugins || [];
- options.plugins.push(alias(pathAliases));
- },
- },
-];
-
-export default defineConfig([
- {
- format: ["cjs", "esm"],
- entry: ["./src/sdk/index.ts"],
- skipNodeModulesBundle: true,
- dts: false,
- shims: true,
- clean: false,
- outDir: "./dist/sdk",
- splitting: false,
-
- treeshake: true,
- target: "es2020",
-
- esbuildOptions(options) {
- options.plugins = options.plugins || [];
- options.plugins.push(alias(pathAliases));
- options.mainFields = ["module", "main"];
- },
- },
-
- // GLOBAL
- {
- entry: ["src/utils/*.{ts,tsx}"],
- format: ["cjs", "esm"],
- dts: false,
- clean: true,
- bundle: true,
- outDir: "./dist/utils", // Fixed wildcard path to specific directory
- external: ["react", "react/jsx-runtime", "react-dom"],
- esbuildOptions(options) {
- options.plugins = options.plugins || [];
- options.plugins.push(alias(pathAliases));
- },
- },
-
- ...reactConfigs,
-]);
diff --git a/packages/openapi/prevVersions/openapi1.2/balancesOpenApi1.2.0.ts b/packages/openapi/prevVersions/openapi1.2/balancesOpenApi1.2.0.ts
deleted file mode 100644
index 973eae779..000000000
--- a/packages/openapi/prevVersions/openapi1.2/balancesOpenApi1.2.0.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import {
- CreateBalanceParamsV0Schema,
- SuccessResponseSchema,
- xCodeSamplesLegacy,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const balancesOpenApi: ZodOpenApiPathsObject = {
- "/balances/create": {
- post: {
- summary: "Create Balance",
- description:
- "Create a new balance for a specific feature for a customer.",
- tags: ["balances"],
- requestBody: {
- content: {
- "application/json": {
- schema: CreateBalanceParamsV0Schema,
- },
- },
- },
- responses: {
- "200": {
- description: "Balance created successfully",
- content: {
- "application/json": { schema: SuccessResponseSchema },
- },
- },
- },
-
- "x-codeSamples": xCodeSamplesLegacy({
- methodPath: "balances.create",
- example: {
- customer_id: "cus_123",
- feature_id: "api_tokens",
- granted_balance: 100,
- reset: {
- interval: "month",
- },
- },
- }),
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/coreOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/coreOpenApi.ts
deleted file mode 100644
index 49606a7a9..000000000
--- a/packages/openapi/prevVersions/openapi1.2/coreOpenApi.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import {
- CheckResponseV1Schema,
- ExtCheckParamsSchema,
- setUsageJsDoc,
- SetUsageParamsSchema,
- SuccessResponseSchema,
- TrackParamsSchema,
- TrackResponseV1Schema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const coreOpenApi: ZodOpenApiPathsObject = {
- "/track": {
- post: {
- summary: "Track Event",
- // description: trackJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: TrackParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: TrackResponseV1Schema } },
- },
- },
- },
- },
- "/check": {
- post: {
- summary: "Check Feature Access",
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: ExtCheckParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: CheckResponseV1Schema } },
- },
- },
- },
- },
- "/usage": {
- post: {
- summary: "Set Usage",
- description: setUsageJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: SetUsageParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: SuccessResponseSchema } },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/customersOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/customersOpenApi.ts
deleted file mode 100644
index 8ba6acf83..000000000
--- a/packages/openapi/prevVersions/openapi1.2/customersOpenApi.ts
+++ /dev/null
@@ -1,204 +0,0 @@
-import { UpdateBalancesParamsSchema } from "@api/balances/prevVersions/legacyUpdateBalanceModels.js";
-import { SuccessResponseSchema } from "@api/common/commonResponses.js";
-import { queryStringArray } from "@api/common/queryHelpers.js";
-import {
- BillingPortalParamsSchema,
- BillingPortalResultSchema,
-} from "@api/core/coreOpModels.js";
-import { CustomerExpandEnum } from "@api/customers/components/customerExpand/customerExpand.js";
-import { CreateCustomerParamsV0Schema } from "@api/customers/crud/createCustomerParams.js";
-import { UpdateCustomerParamsV0Schema } from "@api/customers/crud/updateCustomerParams.js";
-import {
- ListCustomersQuerySchema,
- ListCustomersResponseSchema,
-} from "@api/customers/customerOpModels.js";
-import {
- API_CUSTOMER_V3_EXAMPLE,
- ApiCustomerV3Schema,
-} from "@api/customers/previousVersions/apiCustomerV3.js";
-import { z } from "zod/v4";
-
-export const ApiCustomerWithMeta = ApiCustomerV3Schema.meta({
- id: "Customer",
- example: API_CUSTOMER_V3_EXAMPLE,
-});
-
-export const customersOpenApi = {
- "/customers": {
- get: {
- summary: "List Customers",
- tags: ["customers"],
- requestParams: {
- query: ListCustomersQuerySchema,
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: ListCustomersResponseSchema.extend({
- list: z.array(
- ApiCustomerWithMeta.omit({
- entities: true,
- invoices: true,
- trials_used: true,
- referrals: true,
- payment_method: true,
- }),
- ),
- }).meta({
- examples: [
- {
- list: [API_CUSTOMER_V3_EXAMPLE],
- total: 1,
- total_count: 100,
- total_filtered_count: 100,
- limit: 10,
- offset: 0,
- },
- ],
- }),
- },
- },
- },
- },
- },
- post: {
- summary: "Create Customer",
- tags: ["customers"],
- requestParams: {
- query: z.object({
- expand: queryStringArray(CustomerExpandEnum).optional(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: CreateCustomerParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiCustomerWithMeta } },
- },
- },
- },
- },
-
- "/customers/{customer_id}": {
- get: {
- summary: "Get Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string().meta({
- description: "The ID of the customer.",
- }),
- }),
- query: z.object({
- expand: queryStringArray(CustomerExpandEnum).optional(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiCustomerWithMeta } },
- },
- },
- },
- post: {
- summary: "Update Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdateCustomerParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiCustomerWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- query: z.object({
- delete_in_stripe: z.boolean().default(false).meta({
- description:
- "Whether to delete the customer and cancel all existing subscriptions in Stripe.",
- }),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
- "/customers/{customer_id}/billing_portal": {
- post: {
- summary: "Get Billing Portal URL",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: BillingPortalParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": { schema: BillingPortalResultSchema },
- },
- },
- },
- },
- },
- "/customers/{customer_id}/balances": {
- post: {
- summary: "Set Feature Balances",
- description: "Set the balance of a feature for a specific customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdateBalancesParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": { schema: SuccessResponseSchema },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/entitiesOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/entitiesOpenApi.ts
deleted file mode 100644
index e0826f935..000000000
--- a/packages/openapi/prevVersions/openapi1.2/entitiesOpenApi.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { SuccessResponseSchema } from "@api/common";
-import { CreateEntityParamsV0Schema } from "@api/entities/crud/createEntityParams";
-import {
- API_ENTITY_V0_EXAMPLE,
- ApiEntityV0Schema,
- queryStringArray,
-} from "@autumn/shared";
-import { EntityExpandV0 } from "@models/cusModels/entityModels/entityExpand.js";
-import { z } from "zod/v4";
-
-// Note: The meta with id is added in openapi.ts to avoid duplicate registration
-// This schema is exported through the main index and should not have an id here
-export const ApiEntityWithMeta = ApiEntityV0Schema.meta({
- id: "Entity",
- example: API_ENTITY_V0_EXAMPLE,
-});
-
-export const entitiesOpenApi = {
- "/customers/{customer_id}/entities": {
- post: {
- summary: "Create Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: CreateEntityParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiEntityWithMeta } },
- },
- },
- },
- },
- "/customers/{customer_id}/entities/{entity_id}": {
- get: {
- summary: "Get Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- entity_id: z.string(),
- }),
- query: z.object({
- expand: queryStringArray(z.enum(EntityExpandV0)).optional(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiEntityWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- entity_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/eventsOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/eventsOpenApi.ts
deleted file mode 100644
index e5985d652..000000000
--- a/packages/openapi/prevVersions/openapi1.2/eventsOpenApi.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import {
- ApiEventsListParamsSchema,
- ApiEventsListResponseSchema,
- EVENTS_AGGREGATE_EXAMPLE_V0,
- EVENTS_LIST_EXAMPLE,
- EventsAggregateResponseV0Schema,
- ExtEventsAggregateParamsSchema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const eventsOpenApi: ZodOpenApiPathsObject = {
- "/events/list": {
- post: {
- summary: "List Events",
- tags: ["events"],
- requestBody: {
- content: {
- "application/json": {
- schema: ApiEventsListParamsSchema,
- },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: ApiEventsListResponseSchema.meta({
- example: EVENTS_LIST_EXAMPLE,
- }),
- },
- },
- },
- },
- },
- },
- "/events/aggregate": {
- post: {
- summary: "Aggregate Events",
- tags: ["events"],
- requestBody: {
- content: {
- "application/json": {
- schema: ExtEventsAggregateParamsSchema,
- },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: EventsAggregateResponseV0Schema.meta({
- example: EVENTS_AGGREGATE_EXAMPLE_V0,
- }),
- },
- },
- },
- },
- },
- },
-
- // LEGACY
- "/query": {
- post: {
- summary: "Query Analytics Aggregation",
- tags: ["analytics"],
- requestBody: {
- content: {
- "application/json": {
- schema: ExtEventsAggregateParamsSchema,
- },
- },
- },
- responses: {
- "200": {
- description: "Analytics aggregation results",
- content: {
- "application/json": {
- schema: EventsAggregateResponseV0Schema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/featuresOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/featuresOpenApi.ts
deleted file mode 100644
index ef59fffb9..000000000
--- a/packages/openapi/prevVersions/openapi1.2/featuresOpenApi.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- ApiFeatureV0Schema,
- CreateFeatureV0ParamsSchema,
- FEATURE_EXAMPLE,
- getListResponseSchema,
- SuccessResponseSchema,
- UpdateFeatureV0ParamsSchema,
-} from "@autumn/shared";
-import { z } from "zod/v4";
-
-export const ApiFeatureWithMeta = ApiFeatureV0Schema.extend({
- type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"]),
-}).meta({
- id: "Feature",
- examples: [FEATURE_EXAMPLE],
-});
-
-export const featuresOpenApi = {
- "/features": {
- get: {
- summary: "List Features",
- tags: ["features"],
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: getListResponseSchema({ schema: ApiFeatureWithMeta }),
- },
- },
- },
- },
- },
- post: {
- summary: "Create Feature",
- tags: ["features"],
- requestBody: {
- content: {
- "application/json": { schema: CreateFeatureV0ParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiFeatureWithMeta } },
- },
- },
- },
- },
- "/features/{feature_id}": {
- get: {
- summary: "Get Feature",
- tags: ["features"],
- requestParams: {
- path: z.object({
- feature_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiFeatureWithMeta } },
- },
- },
- },
- post: {
- summary: "Update Feature",
- tags: ["features"],
- requestParams: {
- path: z.object({
- feature_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdateFeatureV0ParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiFeatureWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Feature",
- tags: ["features"],
- requestParams: {
- path: z.object({
- feature_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/openapi1.2.0.ts b/packages/openapi/prevVersions/openapi1.2/openapi1.2.0.ts
deleted file mode 100644
index f2e981d66..000000000
--- a/packages/openapi/prevVersions/openapi1.2/openapi1.2.0.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-import { writeFileSync } from "node:fs";
-import {
- ApiCusFeatureV3Schema,
- ApiCusProductV3Schema,
- ApiProductItemV0Schema,
- CustomerDataSchema,
- EntityDataSchema,
-} from "@autumn/shared";
-import yaml from "yaml";
-import { createDocument } from "zod-openapi";
-import { balancesOpenApi } from "./balancesOpenApi1.2.0.js";
-import { coreOpenApi } from "./coreOpenApi.js";
-import { ApiCustomerWithMeta, customersOpenApi } from "./customersOpenApi.js";
-import { ApiEntityWithMeta, entitiesOpenApi } from "./entitiesOpenApi.js";
-import { eventsOpenApi } from "./eventsOpenApi.js";
-import { ApiFeatureWithMeta, featuresOpenApi } from "./featuresOpenApi.js";
-import { ApiProductWithMeta, productsOpenApi } from "./productsOpenApi.js";
-import { referralsOpenApi } from "./referralsOpenApi.js";
-
-// Register schema with .meta() for OpenAPI spec generation
-
-const OPENAPI_1_2_0 = createDocument(
- {
- openapi: "3.1.0",
- info: {
- title: "Autumn API",
- version: "1.2.0",
- },
-
- servers: [
- {
- url: "https://api.useautumn.com/v1",
- description: "Production server",
- },
- ],
-
- security: [
- {
- secretKey: [],
- },
- ],
- components: {
- schemas: {
- CustomerData: CustomerDataSchema,
- EntityData: EntityDataSchema.meta({
- id: "EntityData",
- description: "Entity data for creating an entity",
- }),
- Customer: ApiCustomerWithMeta,
- CustomerProduct: ApiCusProductV3Schema,
- CustomerFeature: ApiCusFeatureV3Schema.meta({
- id: "CustomerFeature",
- description: "Customer feature object returned by the API",
- }),
- Product: ApiProductWithMeta,
- ProductItem: ApiProductItemV0Schema,
- Feature: ApiFeatureWithMeta,
- Entity: ApiEntityWithMeta,
- },
- parameters: {
- XApiVersion: {
- name: "x-api-version",
- in: "header",
- required: true,
- schema: {
- type: "string",
- enum: ["2.0"],
- },
- },
- },
- securitySchemes: {
- secretKey: {
- type: "http",
- scheme: "bearer",
- bearerFormat: "JWT",
- },
- },
- },
- "x-speakeasy-globals": {
- parameters: [
- {
- $ref: "#/components/parameters/XApiVersion",
- "x-speakeasy-globals-hidden": true,
- },
- ],
- },
-
- paths: {
- ...productsOpenApi,
- ...featuresOpenApi,
- ...coreOpenApi,
- ...customersOpenApi,
- ...entitiesOpenApi,
- ...eventsOpenApi,
- ...balancesOpenApi,
- ...referralsOpenApi,
- },
- },
- {
- // Disable the "Output" suffix that zod-openapi adds to response schemas
- outputIdSuffix: "",
- },
-);
-
-export const writeOpenApi_1_2_0 = ({
- outputFilePath,
-}: {
- outputFilePath: string;
-}) => {
- const yamlContent = yaml.stringify(
- JSON.parse(JSON.stringify(OPENAPI_1_2_0, null, 2)),
- );
- writeFileSync(outputFilePath, yamlContent, "utf8");
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/productsOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/productsOpenApi.ts
deleted file mode 100644
index 5b814f9dc..000000000
--- a/packages/openapi/prevVersions/openapi1.2/productsOpenApi.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- ApiProductSchema,
- CreateProductV2ParamsSchema,
- PRODUCT_EXAMPLE,
- SuccessResponseSchema,
- UpdateProductV2ParamsSchema,
-} from "@autumn/shared";
-import { z } from "zod/v4";
-
-// Register schema with .meta() for OpenAPI spec generation
-export const ApiProductWithMeta = ApiProductSchema.meta({
- id: "Product",
- examples: [PRODUCT_EXAMPLE],
-});
-
-export const productsOpenApi = {
- "/products": {
- get: {
- summary: "List Products",
- tags: ["products"],
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: z.object({
- list: z.array(ApiProductWithMeta),
- }),
- },
- },
- },
- },
- },
- post: {
- summary: "Create Product",
- tags: ["products"],
- requestBody: {
- content: {
- "application/json": { schema: CreateProductV2ParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiProductWithMeta } },
- },
- },
- },
- },
- "/products/{product_id}": {
- get: {
- summary: "Get Product",
- tags: ["products"],
- requestParams: {
- path: z.object({
- product_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiProductWithMeta } },
- },
- },
- },
- post: {
- summary: "Update Product",
- tags: ["products"],
- requestParams: {
- path: z.object({
- product_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdateProductV2ParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiProductWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Product",
- tags: ["products"],
- requestParams: {
- path: z.object({
- product_id: z.string(),
- }),
- query: z.object({
- all_versions: z.boolean().optional(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/prevVersions/openapi1.2/referralsOpenApi.ts b/packages/openapi/prevVersions/openapi1.2/referralsOpenApi.ts
deleted file mode 100644
index f2c8c37e5..000000000
--- a/packages/openapi/prevVersions/openapi1.2/referralsOpenApi.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import {
- CreateReferralCodeParamsSchema,
- CreateReferralCodeResponseSchema,
- RedeemReferralCodeParamsSchema,
- RedeemReferralCodeResponseSchema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-const ReferralCodeSchema = CreateReferralCodeResponseSchema.meta({
- id: "ReferralCode",
- description: "Referral code object returned by the API",
-});
-
-const RedeemReferralCodeResponseSchemaWithMeta =
- RedeemReferralCodeResponseSchema.meta({
- id: "RedeemReferralCodeResponse",
- description: "Redemption response object returned by the API",
- });
-
-export const referralsOpenApi: ZodOpenApiPathsObject = {
- "/referrals/code": {
- post: {
- summary: "Create a referral code",
- tags: ["referrals"],
- requestBody: {
- content: {
- "application/json": { schema: CreateReferralCodeParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "Referral code generated successfully",
- content: {
- "application/json": {
- schema: ReferralCodeSchema,
- },
- },
- },
- },
- },
- },
- "/referrals/redeem": {
- post: {
- summary: "Redeem a referral code",
- tags: ["referrals"],
- requestBody: {
- content: {
- "application/json": { schema: RedeemReferralCodeParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "Referral code redeemed successfully",
- content: {
- "application/json": {
- schema: RedeemReferralCodeResponseSchemaWithMeta,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/balancesOpenApi.ts b/packages/openapi/v2.0/balancesOpenApi.ts
deleted file mode 100644
index cce79d450..000000000
--- a/packages/openapi/v2.0/balancesOpenApi.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import {
- CreateBalanceParamsV0Schema,
- ExtUpdateBalanceParamsV0Schema,
- SuccessResponseSchema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const balancesOpenApi: ZodOpenApiPathsObject = {
- "/balances/update": {
- post: {
- summary: "Update Balance",
- description:
- "Update or set the balance or usage for a specific feature for a customer. Either current_balance or usage must be provided, but not both.",
- tags: ["balances"],
- requestBody: {
- content: {
- "application/json": { schema: ExtUpdateBalanceParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "Balance updated successfully",
- content: {
- "application/json": { schema: SuccessResponseSchema },
- },
- },
- },
- },
- },
- "/balances/create": {
- post: {
- summary: "Create Balance",
- description:
- "Create a new balance for a specific feature for a customer.",
- tags: ["balances"],
- requestBody: {
- content: {
- "application/json": { schema: CreateBalanceParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "Balance created successfully",
- content: {
- "application/json": { schema: SuccessResponseSchema },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/coreOpenApi.ts b/packages/openapi/v2.0/coreOpenApi.ts
deleted file mode 100644
index 24a365394..000000000
--- a/packages/openapi/v2.0/coreOpenApi.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import {
- attachJsDoc,
- billingPortalJsDoc,
- cancelJsDoc,
- checkoutJsDoc,
- queryJsDoc,
- setupPaymentJsDoc,
-} from "@api/common/jsDocs.js";
-import {
- GetBillingPortalBodySchema,
- GetBillingPortalResponseSchema,
-} from "@api/customers/customerOpModels.js";
-import {
- AttachBodyV0Schema,
- AttachResponseV1Schema,
- CancelBodySchema,
- CancelResultSchema,
- CheckoutParamsV0Schema,
- CheckoutResponseV0Schema,
- CheckResponseV2Schema,
- ExtCheckParamsSchema,
- QueryParamsSchema,
- QueryResultSchema,
- SetupPaymentParamsV0Schema,
- SetupPaymentResponseV0Schema,
- TrackParamsSchema,
- TrackResponseV2Schema,
-} from "@api/models.js";
-import { z } from "zod/v4";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const coreOps: ZodOpenApiPathsObject = {
- "/attach": {
- post: {
- summary: "Attach Product",
- description: attachJsDoc,
-
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": {
- schema: AttachBodyV0Schema,
- examples: {
- basic: {
- summary: "Attach a product immediately",
- description:
- "Enable a product for a customer with immediate activation",
- value: {
- customer_id: "cus_123",
- product_id: "pro_plan",
- },
- },
- },
- },
- },
- },
- responses: {
- "200": {
- description: "Product attached successfully",
- content: {
- "application/json": {
- schema: AttachResponseV1Schema,
- },
- },
- },
- },
- },
- },
- "/checkout": {
- post: {
- summary: "Checkout",
- description: checkoutJsDoc,
- tags: ["core"],
- requestBody: {
- content: { "application/json": { schema: CheckoutParamsV0Schema } },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: CheckoutResponseV0Schema } },
- },
- },
- },
- },
- "/cancel": {
- post: {
- summary: "Cancel Product",
- description: cancelJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: CancelBodySchema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: CancelResultSchema } },
- },
- },
- },
- },
- "/track": {
- post: {
- summary: "Track Event",
- // description: trackJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: TrackParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: TrackResponseV2Schema } },
- },
- },
- },
- },
- "/query": {
- post: {
- summary: "Query Analytics",
- description: queryJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: QueryParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: QueryResultSchema } },
- },
- },
- },
- },
- "/check": {
- post: {
- summary: "Check Feature Access",
- description: checkoutJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: ExtCheckParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: CheckResponseV2Schema } },
- },
- },
- },
- },
- "/setup_payment": {
- post: {
- summary: "Setup Payment Method",
- description: setupPaymentJsDoc,
- tags: ["core"],
- requestBody: {
- content: {
- "application/json": { schema: SetupPaymentParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: SetupPaymentResponseV0Schema } },
- },
- },
- },
- },
-
- "/customers/{customer_id}/billing_portal": {
- post: {
- summary: "Create Billing Portal Session",
- description: billingPortalJsDoc,
- tags: ["core"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- // query: GetBillingPortalQuerySchema,
- },
- requestBody: {
- content: {
- "application/json": {
- schema: GetBillingPortalBodySchema,
- },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: {
- "application/json": {
- schema: GetBillingPortalResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/customersOpenApi.ts b/packages/openapi/v2.0/customersOpenApi.ts
deleted file mode 100644
index 9c05fc85d..000000000
--- a/packages/openapi/v2.0/customersOpenApi.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import {
- ApiCustomerSchema,
- BaseApiCustomerSchema,
- CreateCustomerParamsV0Schema,
- CreateCustomerQuerySchema,
- createPagePaginatedResponseSchema,
- GetCustomerQuerySchema,
- ListCustomersV2ParamsSchema,
- SuccessResponseSchema,
- UpdateCustomerParamsV0Schema,
-} from "@autumn/shared";
-import { z } from "zod/v4";
-
-export const customersOpenApi = {
- "/customers": {
- post: {
- summary: "Create Customer",
- tags: ["customers"],
- requestParams: {
- query: CreateCustomerQuerySchema,
- },
- requestBody: {
- content: {
- "application/json": { schema: CreateCustomerParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: ApiCustomerSchema } },
- },
- },
- },
- },
- "/customers/list": {
- post: {
- summary: "List Customers",
- tags: ["customers"],
- requestBody: {
- content: {
- "application/json": { schema: ListCustomersV2ParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: {
- "application/json": {
- schema: createPagePaginatedResponseSchema(
- BaseApiCustomerSchema,
- true,
- ),
- },
- },
- },
- },
- },
- },
- "/customers/{customer_id}": {
- get: {
- summary: "Get Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- query: GetCustomerQuerySchema,
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: ApiCustomerSchema } },
- },
- },
- },
- post: {
- summary: "Update Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- query: z.object({
- expand: z.string().optional(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdateCustomerParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: ApiCustomerSchema } },
- },
- },
- },
- delete: {
- summary: "Delete Customer",
- tags: ["customers"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "200 OK",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/entitiesOpenApi.ts b/packages/openapi/v2.0/entitiesOpenApi.ts
deleted file mode 100644
index 301b7f07f..000000000
--- a/packages/openapi/v2.0/entitiesOpenApi.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { SuccessResponseSchema } from "@api/common";
-import { CreateEntityParamsV0Schema } from "@api/entities/crud/createEntityParams";
-import { ApiEntitySchema, queryStringArray } from "@autumn/shared";
-import { EntityExpand } from "@models/cusModels/entityModels/entityExpand.js";
-import { z } from "zod/v4";
-
-// Note: The meta with id is added in openapi.ts to avoid duplicate registration
-// This schema is exported through the main index and should not have an id here
-export const ApiEntityWithMeta = ApiEntitySchema.meta({
- id: "Entity",
- // examples: [ENTITY_EXAMPLE],
-});
-
-export const entitiesOpenApi = {
- "/customers/{customer_id}/entities": {
- post: {
- summary: "Create Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: CreateEntityParamsV0Schema },
- },
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: ApiEntityWithMeta } },
- },
- },
- },
- },
- "/customers/{customer_id}/entities/{entity_id}": {
- get: {
- summary: "Get Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- entity_id: z.string(),
- }),
- query: z.object({
- expand: queryStringArray(z.enum(EntityExpand)).optional(),
- }),
- },
- responses: {
- "200": {
- description: "200 OK",
- content: { "application/json": { schema: ApiEntityWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Entity",
- tags: ["entities"],
- requestParams: {
- path: z.object({
- customer_id: z.string(),
- entity_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "200 OK",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/eventsOpenApi.ts b/packages/openapi/v2.0/eventsOpenApi.ts
deleted file mode 100644
index af69b5c9a..000000000
--- a/packages/openapi/v2.0/eventsOpenApi.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import {
- ApiEventsListParamsSchema,
- ApiEventsListResponseSchema,
- EVENTS_AGGREGATE_EXAMPLE_V0,
- EVENTS_LIST_EXAMPLE,
- EventsAggregateResponseV0Schema,
- ExtEventsAggregateParamsSchema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-export const eventsOpenApi: ZodOpenApiPathsObject = {
- "/events/list": {
- post: {
- summary: "List Events",
- tags: ["events"],
- requestBody: {
- content: {
- "application/json": {
- schema: ApiEventsListParamsSchema,
- },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: ApiEventsListResponseSchema.meta({
- example: EVENTS_LIST_EXAMPLE,
- }),
- },
- },
- },
- },
- },
- },
- "/events/aggregate": {
- post: {
- summary: "Aggregate Events",
- tags: ["events"],
- requestBody: {
- content: {
- "application/json": {
- schema: ExtEventsAggregateParamsSchema,
- },
- },
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: EventsAggregateResponseV0Schema.meta({
- example: EVENTS_AGGREGATE_EXAMPLE_V0,
- }),
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/openapi2.0.ts b/packages/openapi/v2.0/openapi2.0.ts
deleted file mode 100644
index e474cc5cb..000000000
--- a/packages/openapi/v2.0/openapi2.0.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import { writeFileSync } from "node:fs";
-import {
- ApiCustomerSchema,
- ApiPlanItemV0WithMeta,
- BaseApiCustomerSchema,
- CustomerDataSchema,
- EntityDataSchema,
-} from "@autumn/shared";
-import yaml from "yaml";
-import { createDocument } from "zod-openapi";
-import { balancesOpenApi } from "./balancesOpenApi.js";
-import { coreOps } from "./coreOpenApi.js";
-import { customersOpenApi } from "./customersOpenApi.js";
-import { ApiEntityWithMeta, entitiesOpenApi } from "./entitiesOpenApi.js";
-import { eventsOpenApi } from "./eventsOpenApi.js";
-import { ApiPlanWithMeta, plansOpenApi } from "./plansOpenApi.js";
-import { referralOps } from "./referralsOpenApi.js";
-
-const openapi2_0 = createDocument(
- {
- openapi: "3.1.0",
- info: {
- title: "Autumn API",
- version: "2.0.0",
- },
-
- servers: [
- {
- url: "https://api.useautumn.com/v1",
- description: "Production server",
- },
- ],
-
- security: [
- {
- secretKey: [],
- },
- ],
- components: {
- schemas: {
- CustomerData: CustomerDataSchema,
- EntityData: EntityDataSchema,
- Plan: ApiPlanWithMeta,
- PlanFeature: ApiPlanItemV0WithMeta,
- Customer: ApiCustomerSchema,
- BaseCustomer: BaseApiCustomerSchema,
- Entity: ApiEntityWithMeta,
- },
- parameters: {
- XApiVersion: {
- name: "x-api-version",
- in: "header",
- required: true,
- schema: {
- type: "string",
- default: "2.0",
- },
- },
- },
- securitySchemes: {
- secretKey: {
- type: "http",
- scheme: "bearer",
- bearerFormat: "JWT",
- },
- },
- },
- "x-speakeasy-globals": {
- parameters: [
- {
- $ref: "#/components/parameters/XApiVersion",
- "x-speakeasy-globals-hidden": true,
- },
- ],
- },
-
- paths: {
- ...plansOpenApi,
- ...customersOpenApi,
- ...entitiesOpenApi,
- ...referralOps,
- ...coreOps,
- ...balancesOpenApi,
- ...eventsOpenApi,
- },
- },
- {
- // Disable the "Output" suffix that zod-openapi adds to response schemas
- outputIdSuffix: "",
- },
-);
-
-const injectGlobalHeaderParameters = ({
- openApiDocument,
-}: {
- openApiDocument: Record;
-}) => {
- const methods = [
- "get",
- "put",
- "post",
- "delete",
- "patch",
- "head",
- "options",
- "trace",
- ];
- const headerParamRef = "#/components/parameters/XApiVersion";
-
- const paths = (openApiDocument.paths ?? {}) as Record;
- for (const pathItem of Object.values(paths)) {
- if (!pathItem || typeof pathItem !== "object") continue;
- for (const method of methods) {
- const operation = (pathItem as Record)[method];
- if (!operation || typeof operation !== "object") continue;
- if (!operation) continue;
-
- const operationRecord = operation as Record;
- const parameters = Array.isArray(operationRecord.parameters)
- ? [...operationRecord.parameters]
- : [];
-
- const hasXApiVersionParam = parameters.some((parameter) => {
- if (!parameter || typeof parameter !== "object") return false;
- const parameterRecord = parameter as Record;
- if (parameterRecord.$ref === headerParamRef) return true;
- return (
- parameterRecord.in === "header" &&
- parameterRecord.name === "x-api-version"
- );
- });
-
- if (!hasXApiVersionParam) {
- parameters.unshift({ $ref: headerParamRef });
- }
-
- operationRecord.parameters = parameters;
- }
- }
-};
-
-export const writeOpenApi_2_0_0 = ({
- outputFilePath,
-}: {
- outputFilePath: string;
-}) => {
- const openApiDocument = JSON.parse(JSON.stringify(openapi2_0, null, 2));
- injectGlobalHeaderParameters({ openApiDocument });
- const yamlContent = yaml.stringify(openApiDocument);
- writeFileSync(outputFilePath, yamlContent, "utf8");
-};
diff --git a/packages/openapi/v2.0/plansOpenApi.ts b/packages/openapi/v2.0/plansOpenApi.ts
deleted file mode 100644
index 4fa494f6d..000000000
--- a/packages/openapi/v2.0/plansOpenApi.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-import {
- ApiPlanV0Schema,
- CreatePlanParamsV1Schema,
- ListPlansQuerySchema,
- SuccessResponseSchema,
- UpdatePlanParamsV1Schema,
-} from "@autumn/shared";
-import { z } from "zod/v4";
-
-export const ApiPlanWithMeta = ApiPlanV0Schema.meta({
- id: "Plan",
- // examples: [PLAN_EXAMPLE],
-});
-
-export const plansOpenApi = {
- "/plans": {
- get: {
- summary: "List Plans",
- tags: ["plans"],
- requestParams: {
- query: ListPlansQuerySchema,
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: z.object({
- list: z.array(ApiPlanWithMeta),
- }),
- },
- },
- },
- },
- },
- post: {
- summary: "Create Product",
- tags: ["products"],
- requestBody: {
- content: {
- "application/json": { schema: CreatePlanParamsV1Schema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiPlanWithMeta } },
- },
- },
- },
- },
- "/plans/{plan_id}": {
- get: {
- summary: "Get Plan",
- tags: ["plans"],
- requestParams: {
- path: z.object({
- plan_id: z.string(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiPlanWithMeta } },
- },
- },
- },
- post: {
- summary: "Update Plan",
- tags: ["plans"],
- requestParams: {
- path: z.object({
- plan_id: z.string(),
- }),
- },
- requestBody: {
- content: {
- "application/json": { schema: UpdatePlanParamsV1Schema },
- },
- },
- responses: {
- "200": {
- description: "",
- content: { "application/json": { schema: ApiPlanWithMeta } },
- },
- },
- },
- delete: {
- summary: "Delete Plan",
- tags: ["plans"],
- requestParams: {
- path: z.object({
- plan_id: z.string(),
- }),
- query: z.object({
- all_versions: z.boolean().optional(),
- }),
- },
- responses: {
- "200": {
- description: "",
- content: {
- "application/json": {
- schema: SuccessResponseSchema,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.0/referralsOpenApi.ts b/packages/openapi/v2.0/referralsOpenApi.ts
deleted file mode 100644
index 80d3a1eaa..000000000
--- a/packages/openapi/v2.0/referralsOpenApi.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import {
- CreateReferralCodeParamsSchema,
- CreateReferralCodeResponseSchema,
- RedeemReferralCodeParamsSchema,
- RedeemReferralCodeResponseSchema,
-} from "@autumn/shared";
-import type { ZodOpenApiPathsObject } from "zod-openapi";
-
-const ReferralCodeSchema = CreateReferralCodeResponseSchema.meta({
- id: "ReferralCode",
- description: "Referral code object returned by the API",
-});
-
-const RedeemReferralCodeResponseSchemaWithMeta =
- RedeemReferralCodeResponseSchema.meta({
- id: "RedeemReferralCodeResponse",
- description: "Redemption response object returned by the API",
- });
-
-export const referralOps: ZodOpenApiPathsObject = {
- "/referrals/code": {
- post: {
- summary: "Create a referral code",
- tags: ["referrals"],
- requestBody: {
- content: {
- "application/json": { schema: CreateReferralCodeParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "Referral code generated successfully",
- content: {
- "application/json": {
- schema: ReferralCodeSchema,
- },
- },
- },
- },
- },
- },
- "/referrals/redeem": {
- post: {
- summary: "Redeem a referral code",
- tags: ["referrals"],
- requestBody: {
- content: {
- "application/json": { schema: RedeemReferralCodeParamsSchema },
- },
- },
- responses: {
- "200": {
- description: "Referral code redeemed successfully",
- content: {
- "application/json": {
- schema: RedeemReferralCodeResponseSchemaWithMeta,
- },
- },
- },
- },
- },
- },
-};
diff --git a/packages/openapi/v2.1/contracts/attachContract.ts b/packages/openapi/v2.1/contracts/attachContract.ts
deleted file mode 100644
index 9f623011c..000000000
--- a/packages/openapi/v2.1/contracts/attachContract.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { AttachParamsV0Schema } from "@api/billing/attachV2/attachParamsV0.js";
-import { BillingResponseSchema } from "@api/billing/common/billingResponse.js";
-import { oc } from "@orpc/contract";
-
-export const attachContract = oc
- .route({
- method: "POST",
- path: "/v1/attach",
- operationId: "attach",
- tags: ["billing"],
- })
- .input(AttachParamsV0Schema.omit({ customer_id: true, customer_data: true }))
- .output(BillingResponseSchema);
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/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/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",
- },
-];