diff --git a/.cursor/environment.json b/.cursor/environment.json
index 0477c801f..f32303afa 100644
--- a/.cursor/environment.json
+++ b/.cursor/environment.json
@@ -1,3 +1,3 @@
{
- "terminals": []
-}
\ No newline at end of file
+ "terminals": []
+}
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index b98de2f44..7d2f6b9d0 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,12 +1,12 @@
{
- "name": "Autumn dev container",
- "image": "mcr.microsoft.com/devcontainers/node:18",
- "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
- "forwardPorts": [8080],
- // "postCreateCommand": "pnpm install",
- "postCreateCommand": "apt update && apt install -y zsh",
- "settings": {
- "terminal.integrated.shell.linux": "/bin/zsh"
- },
- "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
+ "name": "Autumn dev container",
+ "image": "mcr.microsoft.com/devcontainers/node:18",
+ "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
+ "forwardPorts": [8080],
+ // "postCreateCommand": "pnpm install",
+ "postCreateCommand": "apt update && apt install -y zsh",
+ "settings": {
+ "terminal.integrated.shell.linux": "/bin/zsh"
+ },
+ "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}
diff --git a/biome.json b/biome.json
index 3a9e72855..6edad8450 100644
--- a/biome.json
+++ b/biome.json
@@ -34,4 +34,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/example/components.json b/example/components.json
index ffe928f5b..5bcedb314 100644
--- a/example/components.json
+++ b/example/components.json
@@ -1,21 +1,21 @@
{
- "$schema": "https://ui.shadcn.com/schema.json",
- "style": "new-york",
- "rsc": true,
- "tsx": true,
- "tailwind": {
- "config": "",
- "css": "src/app/globals.css",
- "baseColor": "neutral",
- "cssVariables": true,
- "prefix": ""
- },
- "aliases": {
- "components": "@/components",
- "utils": "@/lib/utils",
- "ui": "@/components/ui",
- "lib": "@/lib",
- "hooks": "@/hooks"
- },
- "iconLibrary": "lucide"
-}
\ No newline at end of file
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/example/eslint.config.mjs b/example/eslint.config.mjs
index 436a54845..33fcef556 100644
--- a/example/eslint.config.mjs
+++ b/example/eslint.config.mjs
@@ -6,16 +6,16 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
- baseDirectory: __dirname,
+ baseDirectory: __dirname,
});
const eslintConfig = [
- ...compat.extends("next/core-web-vitals", "next/typescript"),
- {
- rules: {
- "@typescript-eslint/no-explicit-any": "off",
- },
- },
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
+ {
+ rules: {
+ "@typescript-eslint/no-explicit-any": "off",
+ },
+ },
];
export default eslintConfig;
diff --git a/example/next.config.ts b/example/next.config.ts
index e9ffa3083..7921f35d7 100644
--- a/example/next.config.ts
+++ b/example/next.config.ts
@@ -1,7 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ /* config options here */
};
export default nextConfig;
diff --git a/example/package.json b/example/package.json
index 3e7e27537..2441e917a 100644
--- a/example/package.json
+++ b/example/package.json
@@ -1,39 +1,39 @@
{
- "name": "nextjs-autumn-template",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "next lint"
- },
- "dependencies": {
- "@radix-ui/react-dialog": "^1.1.6",
- "@radix-ui/react-separator": "^1.1.2",
- "@radix-ui/react-slot": "^1.1.2",
- "@radix-ui/react-tooltip": "^1.1.8",
- "autumn-js": "^0.0.7",
- "class-variance-authority": "^0.7.1",
- "clsx": "^2.1.1",
- "lucide-react": "^0.483.0",
- "next": "15.2.3",
- "next-themes": "^0.4.6",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "sonner": "^2.0.1",
- "tailwind-merge": "^3.0.2",
- "tw-animate-css": "^1.2.4"
- },
- "devDependencies": {
- "@eslint/eslintrc": "^3",
- "@tailwindcss/postcss": "^4",
- "@types/node": "^20",
- "@types/react": "^18.3.18",
- "@types/react-dom": "^18.3.5",
- "eslint": "^9",
- "eslint-config-next": "15.2.3",
- "tailwindcss": "^4",
- "typescript": "^5"
- }
+ "name": "nextjs-autumn-template",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "@radix-ui/react-dialog": "^1.1.6",
+ "@radix-ui/react-separator": "^1.1.2",
+ "@radix-ui/react-slot": "^1.1.2",
+ "@radix-ui/react-tooltip": "^1.1.8",
+ "autumn-js": "^0.0.7",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.483.0",
+ "next": "15.2.3",
+ "next-themes": "^0.4.6",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "sonner": "^2.0.1",
+ "tailwind-merge": "^3.0.2",
+ "tw-animate-css": "^1.2.4"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3",
+ "@tailwindcss/postcss": "^4",
+ "@types/node": "^20",
+ "@types/react": "^18.3.18",
+ "@types/react-dom": "^18.3.5",
+ "eslint": "^9",
+ "eslint-config-next": "15.2.3",
+ "tailwindcss": "^4",
+ "typescript": "^5"
+ }
}
diff --git a/example/postcss.config.mjs b/example/postcss.config.mjs
index c7bcb4b1e..f50127cda 100644
--- a/example/postcss.config.mjs
+++ b/example/postcss.config.mjs
@@ -1,5 +1,5 @@
const config = {
- plugins: ["@tailwindcss/postcss"],
+ plugins: ["@tailwindcss/postcss"],
};
export default config;
diff --git a/example/src/app/demo.tsx b/example/src/app/demo.tsx
index 4f9d6c5b2..6f514565e 100644
--- a/example/src/app/demo.tsx
+++ b/example/src/app/demo.tsx
@@ -3,14 +3,14 @@ import CustomerDetailsExample from "@/components/billing";
import Intro from "@/components/introduction";
export default function Home() {
- return (
-
+ );
}
diff --git a/example/src/app/globals.css b/example/src/app/globals.css
index 09dfdcc4d..d09c334ab 100644
--- a/example/src/app/globals.css
+++ b/example/src/app/globals.css
@@ -2,142 +2,142 @@
@import "tw-animate-css";
button {
- padding: 5px 10px;
- background-color: #8838ff;
- color: white;
- cursor: pointer;
- border-radius: 5px;
- font-weight: 500;
+ padding: 5px 10px;
+ background-color: #8838ff;
+ color: white;
+ cursor: pointer;
+ border-radius: 5px;
+ font-weight: 500;
}
button:hover {
- background-color: #413652;
+ background-color: #413652;
}
@custom-variant dark (&:is(.dark *));
@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
- /* Custom font sizes */
- --text-xs: 12px; /* Changed from default 11px to 12px */
- --text-sm: 13px;
- --text-md: 15px;
- --text-lg: 17px;
- --text-xl: 20px;
+ /* Custom font sizes */
+ --text-xs: 12px; /* Changed from default 11px to 12px */
+ --text-sm: 13px;
+ --text-md: 15px;
+ --text-lg: 17px;
+ --text-xl: 20px;
- --color-sidebar-ring: var(--sidebar-ring);
- --color-sidebar-border: var(--sidebar-border);
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
- --color-sidebar-accent: var(--sidebar-accent);
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
- --color-sidebar-primary: var(--sidebar-primary);
- --color-sidebar-foreground: var(--sidebar-foreground);
- --color-sidebar: var(--sidebar);
- --color-chart-5: var(--chart-5);
- --color-chart-4: var(--chart-4);
- --color-chart-3: var(--chart-3);
- --color-chart-2: var(--chart-2);
- --color-chart-1: var(--chart-1);
- --color-ring: var(--ring);
- --color-input: var(--input);
- --color-border: var(--border);
- --color-destructive: var(--destructive);
- --color-accent-foreground: var(--accent-foreground);
- --color-accent: var(--accent);
- --color-muted-foreground: var(--muted-foreground);
- --color-muted: var(--muted);
- --color-secondary-foreground: var(--secondary-foreground);
- --color-secondary: var(--secondary);
- --color-primary-foreground: var(--primary-foreground);
- --color-primary: var(--primary);
- --color-popover-foreground: var(--popover-foreground);
- --color-popover: var(--popover);
- --color-card-foreground: var(--card-foreground);
- --color-card: var(--card);
- --radius-sm: calc(var(--radius) - 4px);
- --radius-md: calc(var(--radius) - 2px);
- --radius-lg: var(--radius);
- --radius-xl: calc(var(--radius) + 4px);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
}
:root {
- --radius: 0.625rem;
- --card: oklch(1 0 0);
- --card-foreground: oklch(0.145 0 0);
- --popover: oklch(1 0 0);
- --popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
- --primary-foreground: oklch(0.985 0 0);
- --secondary: oklch(0.97 0 0);
- --secondary-foreground: oklch(0.205 0 0);
- --muted: oklch(0.97 0 0);
- --muted-foreground: oklch(0.556 0 0);
- --accent: oklch(0.97 0 0);
- --accent-foreground: oklch(0.205 0 0);
- --destructive: oklch(0.577 0.245 27.325);
- --border: oklch(0.922 0 0);
- --input: oklch(0.922 0 0);
- --ring: oklch(0.708 0 0);
- --chart-1: oklch(0.646 0.222 41.116);
- --chart-2: oklch(0.6 0.118 184.704);
- --chart-3: oklch(0.398 0.07 227.392);
- --chart-4: oklch(0.828 0.189 84.429);
- --chart-5: oklch(0.769 0.188 70.08);
- --sidebar: oklch(0.985 0 0);
- --sidebar-foreground: oklch(0.145 0 0);
- --sidebar-primary: oklch(0.205 0 0);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.97 0 0);
- --sidebar-accent-foreground: oklch(0.205 0 0);
- --sidebar-border: oklch(0.922 0 0);
- --sidebar-ring: oklch(0.708 0 0);
- --background: oklch(1 0 0);
- --foreground: oklch(0.145 0 0);
+ --radius: 0.625rem;
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
}
.dark {
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
- --destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
}
@layer base {
- * {
- @apply border-border outline-ring/50;
- }
- body {
- @apply bg-background text-foreground;
- }
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
}
diff --git a/example/src/app/layout.tsx b/example/src/app/layout.tsx
index 809968ebf..8f8d94933 100644
--- a/example/src/app/layout.tsx
+++ b/example/src/app/layout.tsx
@@ -5,41 +5,41 @@ import { Toaster } from "@/components/ui/sonner";
import { AutumnProvider } from "autumn-js/next";
const geistSans = Geist({
- variable: "--font-geist-sans",
- subsets: ["latin"],
+ variable: "--font-geist-sans",
+ subsets: ["latin"],
});
const geistMono = Geist_Mono({
- variable: "--font-geist-mono",
- subsets: ["latin"],
+ variable: "--font-geist-mono",
+ subsets: ["latin"],
});
export const metadata: Metadata = {
- title: "Next.js Autumn App",
- description: "Starter Next.js app with Autumn",
+ title: "Next.js Autumn App",
+ description: "Starter Next.js app with Autumn",
};
export default function RootLayout({
- children,
+ children,
}: Readonly<{
- children: React.ReactNode;
+ children: React.ReactNode;
}>) {
- return (
-
-
-
-
- {children}
-
-
-
- );
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
}
diff --git a/example/src/app/page.tsx b/example/src/app/page.tsx
index 7220729d3..d203027a6 100644
--- a/example/src/app/page.tsx
+++ b/example/src/app/page.tsx
@@ -3,19 +3,19 @@
import { useAutumn } from "autumn-js/next";
export default function Home() {
- const { customer, attach } = useAutumn();
- return (
-
-
{customer?.name}
-
-
- );
+ const { customer, attach } = useAutumn();
+ return (
+
+
{customer?.name}
+
+
+ );
}
diff --git a/example/src/app/referrals/functions.tsx b/example/src/app/referrals/functions.tsx
index 4312ee4b1..8431102f8 100644
--- a/example/src/app/referrals/functions.tsx
+++ b/example/src/app/referrals/functions.tsx
@@ -3,26 +3,26 @@
import { Autumn } from "@/sdk/autumn";
export const getReferralCode = async (customerId: string) => {
- const autumn = new Autumn();
- const referralCode = await autumn.referrals.createCode({
- customerId,
- referralId: "referral",
- });
- return referralCode;
+ const autumn = new Autumn();
+ const referralCode = await autumn.referrals.createCode({
+ customerId,
+ referralId: "referral",
+ });
+ return referralCode;
};
export const redeemReferralCode = async ({
- customerId,
- referralCode,
+ customerId,
+ referralCode,
}: {
- customerId: string;
- referralCode: string;
+ customerId: string;
+ referralCode: string;
}) => {
- const autumn = new Autumn();
- const redemption = await autumn.referrals.redeem({
- customerId,
- code: referralCode,
- });
+ const autumn = new Autumn();
+ const redemption = await autumn.referrals.redeem({
+ customerId,
+ code: referralCode,
+ });
- return redemption;
+ return redemption;
};
diff --git a/example/src/app/referrals/page.tsx b/example/src/app/referrals/page.tsx
index 108289c19..23d6a9485 100644
--- a/example/src/app/referrals/page.tsx
+++ b/example/src/app/referrals/page.tsx
@@ -7,163 +7,163 @@ import { attachProduct, getCustomer } from "../autumn-functions";
import { Input } from "@/components/ui/input";
const useReferralCode = (referrerId: string) => {
- const [referralCode, setReferralCode] = useState(null);
- const [loading, setLoading] = useState(false);
+ const [referralCode, setReferralCode] = useState(null);
+ const [loading, setLoading] = useState(false);
- useEffect(() => {
- const fetchReferralCode = async () => {
- setLoading(true);
- try {
- const { code } = await getReferralCode(referrerId);
- setReferralCode(code);
- } catch (error) {
- toast.error(`Error fetching referral code: ${error}`);
- }
- setLoading(false);
- };
- fetchReferralCode();
- }, []);
+ useEffect(() => {
+ const fetchReferralCode = async () => {
+ setLoading(true);
+ try {
+ const { code } = await getReferralCode(referrerId);
+ setReferralCode(code);
+ } catch (error) {
+ toast.error(`Error fetching referral code: ${error}`);
+ }
+ setLoading(false);
+ };
+ fetchReferralCode();
+ }, []);
- return { referralCode, isLoading: loading };
+ return { referralCode, isLoading: loading };
};
const useCustomer = (customerId: string) => {
- const [customer, setCustomer] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
+ const [customer, setCustomer] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
- const fetchCustomer = async () => {
- setIsLoading(true);
- const customerData = await getCustomer(customerId);
- setCustomer(customerData);
- setIsLoading(false);
- };
+ const fetchCustomer = async () => {
+ setIsLoading(true);
+ const customerData = await getCustomer(customerId);
+ setCustomer(customerData);
+ setIsLoading(false);
+ };
- const refresh = async () => {
- const customerData = await getCustomer(customerId);
- setCustomer(customerData);
- };
+ const refresh = async () => {
+ const customerData = await getCustomer(customerId);
+ setCustomer(customerData);
+ };
- useEffect(() => {
- fetchCustomer();
- }, []);
+ useEffect(() => {
+ fetchCustomer();
+ }, []);
- return { ...customer, isLoading, refresh };
+ return { ...customer, isLoading, refresh };
};
export default function ReferralsPage() {
- const { referralCode, isLoading } = useReferralCode("ayush");
- const { entitlements } = useCustomer("ayush");
+ const { referralCode, isLoading } = useReferralCode("ayush");
+ const { entitlements } = useCustomer("ayush");
- const referrerId = "ayush";
- const referee1Id = "john";
- const [referral1Code, setReferral1Code] = useState("");
+ const referrerId = "ayush";
+ const referee1Id = "john";
+ const [referral1Code, setReferral1Code] = useState("");
- return (
-
-
-
- Referral Program
-
+ return (
+
+
+
+ Referral Program
+
-
- {/* Referrer Section */}
-
-
-
-
- USER ID:
-
-
- {referrerId}
-
-
-
- Your Referral Code
-
-
-
- {isLoading
- ? "Loading..."
- : referralCode || "No code available"}
-
-
- {entitlements && (
-
-
- Your Features
-
- {entitlements.map((entitlement: any, index: number) => (
-
-
- {entitlement.feature_id}
-
-
- {entitlement.balance}
-
-
- ))}
-
- )}
-
-
+
+ {/* Referrer Section */}
+
+
+
+
+ USER ID:
+
+
+ {referrerId}
+
+
+
+ Your Referral Code
+
+
+
+ {isLoading
+ ? "Loading..."
+ : referralCode || "No code available"}
+
+
+ {entitlements && (
+
+
+ Your Features
+
+ {entitlements.map((entitlement: any, index: number) => (
+
+
+ {entitlement.feature_id}
+
+
+ {entitlement.balance}
+
+
+ ))}
+
+ )}
+
+
- {/* Referee Section */}
-
-
-
-
- USER ID:
-
-
- {referee1Id}
-
-
-
Redeem Code
-
-
- Enter referral code to get started
-
-
setReferral1Code(e.target.value)}
- placeholder="Enter code"
- className="font-mono text-base"
- />
-
-
-
- );
+ if (checkout_url) {
+ window.open(checkout_url, "_blank");
+ } else {
+ toast.error("Something went wrong");
+ }
+ } catch (error) {
+ console.log("Failed to redeem code", error);
+ toast.error("Failed to redeem code");
+ }
+ }}
+ className="w-full mt-4 bg-zinc-800 hover:bg-zinc-700 text-white font-mono py-2 px-4 rounded-md transition-colors"
+ >
+ Redeem & Purchase Pro
+
+
+
+
+
+
+
+ );
}
diff --git a/example/src/components/application.tsx b/example/src/components/application.tsx
index 551c45494..3ee5b4edf 100644
--- a/example/src/components/application.tsx
+++ b/example/src/components/application.tsx
@@ -3,64 +3,64 @@ import { MessageSquare } from "lucide-react";
import { toast } from "sonner";
export default function Application() {
- const { entitled, sendEvent, refetch } = useAutumn();
+ const { entitled, sendEvent, refetch } = useAutumn();
- const sendMessageClicked = async (featureId: string) => {
- const { allowed } = await entitled({
- featureId,
- });
+ const sendMessageClicked = async (featureId: string) => {
+ const { allowed } = await entitled({
+ featureId,
+ });
- if (!allowed) {
- toast.error(`You're out of ${featureId}!`);
- return;
- }
+ if (!allowed) {
+ toast.error(`You're out of ${featureId}!`);
+ return;
+ }
- await sendEvent({
- featureId,
- });
+ await sendEvent({
+ featureId,
+ });
- await refetch();
+ await refetch();
- toast.success(`${featureId} used!`);
- };
+ toast.success(`${featureId} used!`);
+ };
- return (
-
-
-
-
-
Feature Access Example
-
- Test how our feature access and event sending works
-
-
-
-
-
-
-
+ return (
+
+
+
+
+
Feature Access Example
+
+ Test how our feature access and event sending works
+
+
+
+
+
+
+
-
-
-
How it works:
-
- - First calls /entitled to check message allowance
- - If allowed, calls /events to record the message
- - Updates remaining message count
-
-
-
+
+
+
How it works:
+
+ - First calls /entitled to check message allowance
+ - If allowed, calls /events to record the message
+ - Updates remaining message count
+
+
+
-
- {
- await sendMessageClicked("chat-messages");
- }}
- >
- Use Chat Message
-
-
-
- );
+
+ {
+ await sendMessageClicked("chat-messages");
+ }}
+ >
+ Use Chat Message
+
+
+
+ );
}
diff --git a/example/src/components/billing.tsx b/example/src/components/billing.tsx
index c9f6d3c88..e016d4a4f 100644
--- a/example/src/components/billing.tsx
+++ b/example/src/components/billing.tsx
@@ -4,100 +4,100 @@ import { CreditCard } from "lucide-react";
import { toast } from "sonner";
import { useAutumn } from "autumn-js/next";
export default function CustomerDetailsExample() {
- const { customer, attach, openBillingPortal } = useAutumn();
- const productId = "pro-example";
+ const { customer, attach, openBillingPortal } = useAutumn();
+ const productId = "pro-example";
- const getEntitlement = (featureId: string) => {
- return customer?.features.find(
- (entitlement: any) => entitlement.feature_id === featureId
- );
- };
+ const getEntitlement = (featureId: string) => {
+ return customer?.features.find(
+ (entitlement: any) => entitlement.feature_id === featureId,
+ );
+ };
- const upgradeClicked = async () => {
- try {
- await attach({
- productId,
- });
- } catch (error: any) {
- toast.error(`${error.message}`);
- }
- };
+ const upgradeClicked = async () => {
+ try {
+ await attach({
+ productId,
+ });
+ } catch (error: any) {
+ toast.error(`${error.message}`);
+ }
+ };
- const manageBillingClicked = async () => {
- try {
- await openBillingPortal();
- } catch (error: any) {
- toast.error(`${error.message}`);
- }
- };
+ const manageBillingClicked = async () => {
+ try {
+ await openBillingPortal();
+ } catch (error: any) {
+ toast.error(`${error.message}`);
+ }
+ };
- const messageCredits = getEntitlement("chat-messages");
+ const messageCredits = getEntitlement("chat-messages");
- const hasPro =
- customer?.products?.length && customer?.products[0].id === productId;
+ const hasPro =
+ customer?.products?.length && customer?.products[0].id === productId;
- return (
-
-
-
-
-
Customer Details
-
- Current subscription and feature access
-
-
-
-
-
-
-
+ return (
+
+
+
+
+
Customer Details
+
+ Current subscription and feature access
+
+
+
+
+
+
+
-
-
-
- Customer ID
-
- {customer?.id}
-
-
+
+
+
+ Customer ID
+
+ {customer?.id}
+
+
-
- Chat Messages Remaining
-
- {messageCredits?.unlimited
- ? "Unlimited"
- : messageCredits?.balance || 0}
-
-
+
+ Chat Messages Remaining
+
+ {messageCredits?.unlimited
+ ? "Unlimited"
+ : messageCredits?.balance || 0}
+
+
-
- Current Plan
-
- {hasPro ? (
-
- Pro
-
- ) : (
-
- Free
-
- )}
-
-
-
-
-
- {!hasPro && (
-
- Upgrade to Pro
-
- )}
-
-
- Manage Billing
-
-
-
-
- );
+
+ Current Plan
+
+ {hasPro ? (
+
+ Pro
+
+ ) : (
+
+ Free
+
+ )}
+
+
+
+
+
+ {!hasPro && (
+
+ Upgrade to Pro
+
+ )}
+
+
+ Manage Billing
+
+
+
+
+ );
}
diff --git a/example/src/components/introduction.tsx b/example/src/components/introduction.tsx
index 9f5e363f1..9b5f5e801 100644
--- a/example/src/components/introduction.tsx
+++ b/example/src/components/introduction.tsx
@@ -1,52 +1,52 @@
import Link from "next/link";
export default function Intro() {
- return (
-
- {/* Header Section */}
-
-
- Welcome to the Next.js Autumn template
-
-
- Get started with Autumn by setting up your account and exploring the
- core features.
-
-
+ return (
+
+ {/* Header Section */}
+
+
+ Welcome to the Next.js Autumn template
+
+
+ Get started with Autumn by setting up your account and exploring the
+ core features.
+
+
- {/* Setup Requirements */}
-
-
Before you get started
-
- -
-
-
- Create your Autumn secret key{" "}
-
- here
- {" "}
- and add it to the .env.local file
-
-
- -
-
-
- Connect your Stripe account{" "}
-
- here
-
-
-
-
-
-
- );
+ {/* Setup Requirements */}
+
+
Before you get started
+
+ -
+
+
+ Create your Autumn secret key{" "}
+
+ here
+ {" "}
+ and add it to the .env.local file
+
+
+ -
+
+
+ Connect your Stripe account{" "}
+
+ here
+
+
+
+
+
+
+ );
}
diff --git a/example/src/components/ui/input.tsx b/example/src/components/ui/input.tsx
index 03295ca6a..433a51c55 100644
--- a/example/src/components/ui/input.tsx
+++ b/example/src/components/ui/input.tsx
@@ -1,21 +1,21 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
- return (
-
- )
+ return (
+
+ );
}
-export { Input }
+export { Input };
diff --git a/example/src/components/ui/sonner.tsx b/example/src/components/ui/sonner.tsx
index 957524edb..e79c99b58 100644
--- a/example/src/components/ui/sonner.tsx
+++ b/example/src/components/ui/sonner.tsx
@@ -1,25 +1,25 @@
-"use client"
+"use client";
-import { useTheme } from "next-themes"
-import { Toaster as Sonner, ToasterProps } from "sonner"
+import { useTheme } from "next-themes";
+import { Toaster as Sonner, ToasterProps } from "sonner";
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme = "system" } = useTheme()
+ const { theme = "system" } = useTheme();
- return (
-
- )
-}
+ return (
+
+ );
+};
-export { Toaster }
+export { Toaster };
diff --git a/example/src/hooks/use-mobile.ts b/example/src/hooks/use-mobile.ts
index 2b0fe1dfe..0a892310f 100644
--- a/example/src/hooks/use-mobile.ts
+++ b/example/src/hooks/use-mobile.ts
@@ -1,19 +1,21 @@
-import * as React from "react"
+import * as React from "react";
-const MOBILE_BREAKPOINT = 768
+const MOBILE_BREAKPOINT = 768;
export function useIsMobile() {
- const [isMobile, setIsMobile] = React.useState(undefined)
+ const [isMobile, setIsMobile] = React.useState(
+ undefined,
+ );
- React.useEffect(() => {
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
- const onChange = () => {
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- }
- mql.addEventListener("change", onChange)
- setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
- return () => mql.removeEventListener("change", onChange)
- }, [])
+ React.useEffect(() => {
+ const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
+ const onChange = () => {
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ };
+ mql.addEventListener("change", onChange);
+ setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
+ return () => mql.removeEventListener("change", onChange);
+ }, []);
- return !!isMobile
+ return !!isMobile;
}
diff --git a/example/src/lib/utils.ts b/example/src/lib/utils.ts
index bd0c391dd..3200be284 100644
--- a/example/src/lib/utils.ts
+++ b/example/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}
diff --git a/example/tsconfig.json b/example/tsconfig.json
index e5d8b8f20..f6aed0338 100644
--- a/example/tsconfig.json
+++ b/example/tsconfig.json
@@ -1,28 +1,28 @@
{
- "compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "noImplicitAny": false,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "preserve",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules"]
+ "compilerOptions": {
+ "target": "ES2017",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "noImplicitAny": false,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
}
diff --git a/package.json b/package.json
index 7be32b2d5..b3a5c3104 100644
--- a/package.json
+++ b/package.json
@@ -1,51 +1,51 @@
{
- "name": "autumn",
- "private": true,
- "workspaces": [
- "server",
- "shared",
- "vite"
- ],
- "type": "module",
- "scripts": {
- "vite:build": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun",
- "vite:start": "bun -F @autumn/vite start:bun",
- "shared": "bun -F @autumn/shared build",
- "server": "bun -F @autumn/shared build && bun -F @autumn/server start",
- "workers": "bun -F @autumn/shared build && bun -F @autumn/server workers",
- "cron": "bun -F @autumn/shared build && bun -F @autumn/server cron",
- "check": "bun -F @autumn/shared build && bun -F @autumn/server check",
- "server:cron": "pnpm -F server cron:start",
- "server:check": "NODE_ENV=production pnpm -F server check",
- "dev": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"redis-server\"",
- "dev:wsl": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"cd shared && npm run dev\"",
- "setup": "node setup.js",
- "setupci": "node setupci",
+ "name": "autumn",
+ "private": true,
+ "workspaces": [
+ "server",
+ "shared",
+ "vite"
+ ],
+ "type": "module",
+ "scripts": {
+ "vite:build": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun",
+ "vite:start": "bun -F @autumn/vite start:bun",
+ "shared": "bun -F @autumn/shared build",
+ "server": "bun -F @autumn/shared build && bun -F @autumn/server start",
+ "workers": "bun -F @autumn/shared build && bun -F @autumn/server workers",
+ "cron": "bun -F @autumn/shared build && bun -F @autumn/server cron",
+ "check": "bun -F @autumn/shared build && bun -F @autumn/server check",
+ "server:cron": "pnpm -F server cron:start",
+ "server:check": "NODE_ENV=production pnpm -F server check",
+ "dev": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"redis-server\"",
+ "dev:wsl": "concurrently \"cd server && npm run dev\" \"cd vite && npm run dev\" \"cd shared && npm run dev\"",
+ "setup": "node setup.js",
+ "setupci": "node setupci",
- "db:push": " bun -F @autumn/shared db:push",
- "db:generate": "bun -F @autumn/shared db:generate",
- "db:migrate": " bun -F @autumn/shared db:migrate",
+ "db:push": " bun -F @autumn/shared db:push",
+ "db:generate": "bun -F @autumn/shared db:generate",
+ "db:migrate": " bun -F @autumn/shared db:migrate",
- "docker:up": "docker compose -f docker-compose.dev.yml up --build",
- "docker:up:unix": "docker compose -f docker-compose.unix.yml up --build",
- "docker:up:ci": "docker compose -f docker-compose.ci.yml up --build",
- "build:all": "pnpm -F shared build && pnpm -F server prod:build && pnpm -F vite build",
- "vite:build:bun": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun",
- "vite:start:bun": "bun -F @autumn/shared build && bun -F @autumn/vite start:bun",
- "dev:bun": "concurrently \"cd server && bun run dev\" \"cd vite && bun run dev\" \"bun -F @autumn/shared dev:bun\"",
- "build:all:bun": "bun run -F @autumn/shared build:bun && bun run -F @autumn/server prod:build:bun && bun run -F @autumn/vite build:bun"
- },
- "dependencies": {
- "@wooorm/starry-night": "^3.8.0",
- "ag-charts-react": "^12.0.1",
- "chalk": "^5.3.0",
- "drizzle-kit": "^0.31.1",
- "tailwind-scrollbar-hide": "^4.0.0"
- },
- "devDependencies": {
- "@types/node": "^24.0.3",
- "concurrently": "^9.1.2",
- "dotenv": "^16.5.0",
- "inquirer": "^12.6.3"
- }
+ "docker:up": "docker compose -f docker-compose.dev.yml up --build",
+ "docker:up:unix": "docker compose -f docker-compose.unix.yml up --build",
+ "docker:up:ci": "docker compose -f docker-compose.ci.yml up --build",
+ "build:all": "pnpm -F shared build && pnpm -F server prod:build && pnpm -F vite build",
+ "vite:build:bun": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun",
+ "vite:start:bun": "bun -F @autumn/shared build && bun -F @autumn/vite start:bun",
+ "dev:bun": "concurrently \"cd server && bun run dev\" \"cd vite && bun run dev\" \"bun -F @autumn/shared dev:bun\"",
+ "build:all:bun": "bun run -F @autumn/shared build:bun && bun run -F @autumn/server prod:build:bun && bun run -F @autumn/vite build:bun"
+ },
+ "dependencies": {
+ "@wooorm/starry-night": "^3.8.0",
+ "ag-charts-react": "^12.0.1",
+ "chalk": "^5.3.0",
+ "drizzle-kit": "^0.31.1",
+ "tailwind-scrollbar-hide": "^4.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^24.0.3",
+ "concurrently": "^9.1.2",
+ "dotenv": "^16.5.0",
+ "inquirer": "^12.6.3"
+ }
}
diff --git a/server/src/check.ts b/server/src/check.ts
index a1d74644d..02125163b 100644
--- a/server/src/check.ts
+++ b/server/src/check.ts
@@ -2,25 +2,25 @@ import { config } from "dotenv";
config();
import {
- getAllEntities,
- getAllFullCustomers,
+ getAllEntities,
+ getAllFullCustomers,
} from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js";
import { initDrizzle } from "@/db/initDrizzle.js";
import {
- AppEnv,
- CusProductStatus,
- FullCusProduct,
- FullCustomer,
- Organization,
- Entity,
+ AppEnv,
+ CusProductStatus,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
+ Entity,
} from "@autumn/shared";
import Stripe from "stripe";
import assert from "assert";
import { cusProductToPrices } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import {
- getAllStripeSchedules,
- getAllStripeSubscriptions,
+ getAllStripeSchedules,
+ getAllStripeSubscriptions,
} from "@/utils/scriptUtils/getAll/getAllStripeSubs.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
@@ -37,8 +37,8 @@ const { db } = initDrizzle({ maxConnections: 5 });
let orgSlugs = process.env.ORG_SLUGS!.split(",");
const skipEmails = process.env.SKIP_EMAILS!.split(",");
const skipIds = [
- "cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx",
- "DxG668K7uDd0Vahk54YWjvCGVgf2",
+ "cus_2tXCCwC6iyiftgA6ndSo1Ubb2dx",
+ "DxG668K7uDd0Vahk54YWjvCGVgf2",
];
orgSlugs = ["supermemory"];
@@ -46,364 +46,364 @@ let customerId = null;
// customerId = "co1VPgUU59q43d5P2rFt4c";
const getSingleCustomer = async ({
- stripeCli,
- customerId,
- orgId,
- env,
+ stripeCli,
+ customerId,
+ orgId,
+ env,
}: {
- stripeCli: Stripe;
- customerId: string;
- orgId: string;
- env: AppEnv;
+ stripeCli: Stripe;
+ customerId: string;
+ orgId: string;
+ env: AppEnv;
}) => {
- const customers = [
- await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId,
- env,
- }),
- ];
+ const customers = [
+ await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ }),
+ ];
- const stripeCusId = customers[0].processor?.id;
- const stripeSubs = stripeCusId
- ? (
- await stripeCli.subscriptions.list({
- customer: stripeCusId,
- expand: ["data.discounts.coupon"],
- })
- ).data
- : [];
+ const stripeCusId = customers[0].processor?.id;
+ const stripeSubs = stripeCusId
+ ? (
+ await stripeCli.subscriptions.list({
+ customer: stripeCusId,
+ expand: ["data.discounts.coupon"],
+ })
+ ).data
+ : [];
- // const stripeSubs = await getStripeSubs({
- // stripeCli,
- // subIds: customers[0].customer_products.flatMap(
- // (cp) => cp.subscription_ids || []
- // ),
- // });
+ // const stripeSubs = await getStripeSubs({
+ // stripeCli,
+ // subIds: customers[0].customer_products.flatMap(
+ // (cp) => cp.subscription_ids || []
+ // ),
+ // });
- let scheduleIds = customers[0].customer_products.flatMap(
- (cp) => cp.scheduled_ids || []
- );
+ let scheduleIds = customers[0].customer_products.flatMap(
+ (cp) => cp.scheduled_ids || [],
+ );
- scheduleIds = Array.from(new Set(scheduleIds));
+ scheduleIds = Array.from(new Set(scheduleIds));
- const stripeSchedules = await getStripeSchedules({
- stripeCli,
- scheduleIds,
- });
+ const stripeSchedules = await getStripeSchedules({
+ stripeCli,
+ scheduleIds,
+ });
- const entities = await EntityService.list({
- db,
- internalCustomerId: customers[0].internal_id,
- });
+ const entities = await EntityService.list({
+ db,
+ internalCustomerId: customers[0].internal_id,
+ });
- return { customers, stripeSubs, stripeSchedules, entities };
+ return { customers, stripeSubs, stripeSchedules, entities };
};
const checkCustomerCorrect = async ({
- fullCus,
- subs,
- schedules,
- org,
- entities,
+ fullCus,
+ subs,
+ schedules,
+ org,
+ entities,
}: {
- fullCus: FullCustomer;
- subs: Stripe.Subscription[];
- schedules: Stripe.SubscriptionSchedule[];
- org: Organization;
- entities: Entity[];
+ fullCus: FullCustomer;
+ subs: Stripe.Subscription[];
+ schedules: Stripe.SubscriptionSchedule[];
+ org: Organization;
+ entities: Entity[];
}) => {
- if (skipIds.includes(fullCus.internal_id!)) return;
+ if (skipIds.includes(fullCus.internal_id!)) return;
- if (skipEmails.some((skipEmail) => skipEmail === fullCus.email)) {
- return;
- }
+ if (skipEmails.some((skipEmail) => skipEmail === fullCus.email)) {
+ return;
+ }
- fullCus.entities = entities.filter(
- (entity) => entity.internal_customer_id === fullCus.internal_id
- );
+ fullCus.entities = entities.filter(
+ (entity) => entity.internal_customer_id === fullCus.internal_id,
+ );
- // console.log(`Checking ${fullCus.email} (${fullCus.id})`);
- const cusProducts = fullCus.customer_products;
+ // console.log(`Checking ${fullCus.email} (${fullCus.id})`);
+ const cusProducts = fullCus.customer_products;
- await checkCusSubCorrect({
- db,
- fullCus,
- subs,
- schedules,
- org,
- env: AppEnv.Live,
- });
+ await checkCusSubCorrect({
+ db,
+ fullCus,
+ subs,
+ schedules,
+ org,
+ env: AppEnv.Live,
+ });
- for (const cusProduct of cusProducts) {
- if (!cusProduct.subscription_ids) continue;
+ for (const cusProduct of cusProducts) {
+ if (!cusProduct.subscription_ids) continue;
- if (cusProduct.status == CusProductStatus.Scheduled) {
- // Check if there's a main product elsewhere
- let mainCusProd = cusProducts.find(
- (cp: FullCusProduct) =>
- cp.product.group === cusProduct.product.group &&
- cp.id !== cusProduct.id &&
- cp.status !== CusProductStatus.Scheduled &&
- (cusProduct.internal_entity_id
- ? cusProduct.internal_entity_id == cp.internal_entity_id
- : true)
- );
+ if (cusProduct.status == CusProductStatus.Scheduled) {
+ // Check if there's a main product elsewhere
+ let mainCusProd = cusProducts.find(
+ (cp: FullCusProduct) =>
+ cp.product.group === cusProduct.product.group &&
+ cp.id !== cusProduct.id &&
+ cp.status !== CusProductStatus.Scheduled &&
+ (cusProduct.internal_entity_id
+ ? cusProduct.internal_entity_id == cp.internal_entity_id
+ : true),
+ );
- assert(
- mainCusProd,
- `Found scheduled cus product with no main product (${cusProduct.product.name})`
- );
- }
+ assert(
+ mainCusProd,
+ `Found scheduled cus product with no main product (${cusProduct.product.name})`,
+ );
+ }
- if (
- !cusProduct.product.is_add_on &&
- cusProduct.status !== CusProductStatus.Scheduled
- ) {
- let group = cusProduct.product.group;
- let otherCusProd = cusProducts.find(
- (cp: FullCusProduct) =>
- cp.product.group === group &&
- cp.id !== cusProduct.id &&
- !cp.product.is_add_on &&
- cp.status !== CusProductStatus.Scheduled &&
- cp.internal_entity_id == cusProduct.internal_entity_id
- );
+ if (
+ !cusProduct.product.is_add_on &&
+ cusProduct.status !== CusProductStatus.Scheduled
+ ) {
+ let group = cusProduct.product.group;
+ let otherCusProd = cusProducts.find(
+ (cp: FullCusProduct) =>
+ cp.product.group === group &&
+ cp.id !== cusProduct.id &&
+ !cp.product.is_add_on &&
+ cp.status !== CusProductStatus.Scheduled &&
+ cp.internal_entity_id == cusProduct.internal_entity_id,
+ );
- assert(
- !otherCusProd,
- `found two cus products from the same group: ${otherCusProd?.product.name} and ${cusProduct.product.name}`
- );
- }
+ assert(
+ !otherCusProd,
+ `found two cus products from the same group: ${otherCusProd?.product.name} and ${cusProduct.product.name}`,
+ );
+ }
- let stripeSubs = subs.filter((sub: any) =>
- cusProduct.subscription_ids!.some((id: string) => id === sub.id)
- );
+ let stripeSubs = subs.filter((sub: any) =>
+ cusProduct.subscription_ids!.some((id: string) => id === sub.id),
+ );
- assert(
- stripeSubs.length === cusProduct.subscription_ids!.length,
- "number of stripe subs should be the same as number of subscription ids"
- );
+ assert(
+ stripeSubs.length === cusProduct.subscription_ids!.length,
+ "number of stripe subs should be the same as number of subscription ids",
+ );
- // let subItems = stripeSubs.flatMap((sub: any) => sub.items.data);
+ // let subItems = stripeSubs.flatMap((sub: any) => sub.items.data);
- const prices = cusProductToPrices({ cusProduct });
+ const prices = cusProductToPrices({ cusProduct });
- if (
- isOneOff(prices) ||
- isFreeProduct(prices) ||
- cusProduct.status == CusProductStatus.Scheduled
- ) {
- continue;
- }
+ if (
+ isOneOff(prices) ||
+ isFreeProduct(prices) ||
+ cusProduct.status == CusProductStatus.Scheduled
+ ) {
+ continue;
+ }
- for (const cusEnt of cusProduct.customer_entitlements) {
- let cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
+ for (const cusEnt of cusProduct.customer_entitlements) {
+ let cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
- if (cusEnt.usage_allowed && !cusPrice) {
- assert.fail(
- `Feature ${cusEnt.feature_id} has usage allowed but no related cus price`
- );
- }
- }
- }
+ if (cusEnt.usage_allowed && !cusPrice) {
+ assert.fail(
+ `Feature ${cusEnt.feature_id} has usage allowed but no related cus price`,
+ );
+ }
+ }
+ }
- // Other checks to perform
+ // Other checks to perform
};
const checkCustomerHandleError = async ({
- fullCus,
- subs,
- org,
- schedules,
- entities,
+ fullCus,
+ subs,
+ org,
+ schedules,
+ entities,
}: {
- fullCus: FullCustomer;
- subs: Stripe.Subscription[];
- org: Organization;
- schedules: Stripe.SubscriptionSchedule[];
- entities: Entity[];
+ fullCus: FullCustomer;
+ subs: Stripe.Subscription[];
+ org: Organization;
+ schedules: Stripe.SubscriptionSchedule[];
+ entities: Entity[];
}) => {
- try {
- await checkCustomerCorrect({
- fullCus,
- subs,
- org,
- schedules,
- entities,
- });
+ try {
+ await checkCustomerCorrect({
+ fullCus,
+ subs,
+ org,
+ schedules,
+ entities,
+ });
- return undefined;
- } catch (error: any) {
- return {
- id: fullCus.id,
- name: fullCus.name,
- email: fullCus.email,
- error: error.message,
- };
- }
+ return undefined;
+ } catch (error: any) {
+ return {
+ id: fullCus.id,
+ name: fullCus.name,
+ email: fullCus.email,
+ error: error.message,
+ };
+ }
};
export const check = async () => {
- const env = AppEnv.Live;
- const sb = createSupabaseClient();
+ const env = AppEnv.Live;
+ const sb = createSupabaseClient();
- const today = new Date().toISOString().slice(0, 16);
+ const today = new Date().toISOString().slice(0, 16);
- for (const slug of orgSlugs) {
- const org = await OrgService.getBySlug({
- db,
- slug,
- });
+ for (const slug of orgSlugs) {
+ const org = await OrgService.getBySlug({
+ db,
+ slug,
+ });
- if (!org) {
- console.log(`Org ${slug} not found`);
- continue;
- }
+ if (!org) {
+ console.log(`Org ${slug} not found`);
+ continue;
+ }
- const fileName = `errors/${today}-${org.slug}.json`;
+ const fileName = `errors/${today}-${org.slug}.json`;
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- console.log("--------------------------------");
- console.log(`Running error check for ${org.name}`);
+ console.log("--------------------------------");
+ console.log(`Running error check for ${org.name}`);
- let customers: FullCustomer[] = [];
- let stripeSubs: Stripe.Subscription[] = [];
- let stripeSchedules: Stripe.SubscriptionSchedule[] = [];
- let entities: Entity[] = [];
+ let customers: FullCustomer[] = [];
+ let stripeSubs: Stripe.Subscription[] = [];
+ let stripeSchedules: Stripe.SubscriptionSchedule[] = [];
+ let entities: Entity[] = [];
- if (customerId) {
- const res = await getSingleCustomer({
- stripeCli,
- customerId,
- orgId: org.id,
- env,
- });
+ if (customerId) {
+ const res = await getSingleCustomer({
+ stripeCli,
+ customerId,
+ orgId: org.id,
+ env,
+ });
- customers = res.customers;
- stripeSubs = res.stripeSubs;
- entities = res.entities;
- } else {
- const [customersRes, stripeSubsRes, stripeSchedulesRes, entitiesRes] =
- await Promise.all([
- getAllFullCustomers({
- db,
- orgId: org.id,
- env,
- }),
- getAllStripeSubscriptions({
- stripeCli,
- waitForSeconds: 1,
- }),
- getAllStripeSchedules({
- stripeCli,
- waitForSeconds: 1,
- }),
- getAllEntities({
- db,
- orgId: org.id,
- env,
- }),
- ]);
+ customers = res.customers;
+ stripeSubs = res.stripeSubs;
+ entities = res.entities;
+ } else {
+ const [customersRes, stripeSubsRes, stripeSchedulesRes, entitiesRes] =
+ await Promise.all([
+ getAllFullCustomers({
+ db,
+ orgId: org.id,
+ env,
+ }),
+ getAllStripeSubscriptions({
+ stripeCli,
+ waitForSeconds: 1,
+ }),
+ getAllStripeSchedules({
+ stripeCli,
+ waitForSeconds: 1,
+ }),
+ getAllEntities({
+ db,
+ orgId: org.id,
+ env,
+ }),
+ ]);
- customers = customersRes;
- stripeSubs = stripeSubsRes.subscriptions;
- stripeSchedules = stripeSchedulesRes.schedules;
- entities = entitiesRes;
- }
+ customers = customersRes;
+ stripeSubs = stripeSubsRes.subscriptions;
+ stripeSchedules = stripeSchedulesRes.schedules;
+ entities = entitiesRes;
+ }
- const batchSize = 1;
- const allErrors = [];
- for (let i = 0; i < customers.length; i += batchSize) {
- const batch = customers.slice(i, i + batchSize);
+ const batchSize = 1;
+ const allErrors = [];
+ for (let i = 0; i < customers.length; i += batchSize) {
+ const batch = customers.slice(i, i + batchSize);
- const batchCheck: any = [];
- for (const customer of batch) {
- batchCheck.push(
- checkCustomerHandleError({
- fullCus: customer,
- subs: stripeSubs,
- schedules: stripeSchedules,
- org,
- entities,
- })
- );
- }
+ const batchCheck: any = [];
+ for (const customer of batch) {
+ batchCheck.push(
+ checkCustomerHandleError({
+ fullCus: customer,
+ subs: stripeSubs,
+ schedules: stripeSchedules,
+ org,
+ entities,
+ }),
+ );
+ }
- let results = await Promise.all(batchCheck);
- results = results.filter(notNullish);
- allErrors.push(...results);
- }
+ let results = await Promise.all(batchCheck);
+ results = results.filter(notNullish);
+ allErrors.push(...results);
+ }
- console.log(`Found ${allErrors.length} errors`);
+ console.log(`Found ${allErrors.length} errors`);
- if (allErrors.length > 0 && customers.length > 1) {
- await sb.storage
- .from("autumn")
- .upload(fileName, JSON.stringify(allErrors, null, 2));
+ if (allErrors.length > 0 && customers.length > 1) {
+ await sb.storage
+ .from("autumn")
+ .upload(fileName, JSON.stringify(allErrors, null, 2));
- if (allErrors.length > 0) {
- const slackBody = {
- text: `Error check for ${org.name}`,
- blocks: [
- {
- type: "section",
- text: {
- type: "mrkdwn",
- text: `*Error check for ${org.name}*: found ${allErrors.length} errors\nSee results at ${process.env.SUPABASE_URL}/storage/v1/object/public/autumn/${fileName}`,
- },
- },
- ],
- };
+ if (allErrors.length > 0) {
+ const slackBody = {
+ text: `Error check for ${org.name}`,
+ blocks: [
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `*Error check for ${org.name}*: found ${allErrors.length} errors\nSee results at ${process.env.SUPABASE_URL}/storage/v1/object/public/autumn/${fileName}`,
+ },
+ },
+ ],
+ };
- await fetch(process.env.SLACK_WEBHOOK_URL!, {
- method: "POST",
- body: JSON.stringify(slackBody),
- });
- }
- } else {
- console.log(allErrors);
- }
- }
+ await fetch(process.env.SLACK_WEBHOOK_URL!, {
+ method: "POST",
+ body: JSON.stringify(slackBody),
+ });
+ }
+ } else {
+ console.log(allErrors);
+ }
+ }
- console.log(
- `COMPLETED ERROR CHECK FOR ${new Date().toISOString().slice(0, 16)}`
- );
+ console.log(
+ `COMPLETED ERROR CHECK FOR ${new Date().toISOString().slice(0, 16)}`,
+ );
- if (process.env.NODE_ENV == "production") {
- const slackBody = {
- text: `Completed error check for ${new Date().toISOString().slice(0, 16)}`,
- blocks: [
- {
- type: "section",
- text: {
- type: "mrkdwn",
- text: `Error check completed for ${new Date().toISOString().slice(0, 16)}`,
- },
- },
- ],
- };
+ if (process.env.NODE_ENV == "production") {
+ const slackBody = {
+ text: `Completed error check for ${new Date().toISOString().slice(0, 16)}`,
+ blocks: [
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: `Error check completed for ${new Date().toISOString().slice(0, 16)}`,
+ },
+ },
+ ],
+ };
- await fetch(process.env.SLACK_WEBHOOK_URL!, {
- method: "POST",
- body: JSON.stringify(slackBody),
- });
- }
+ await fetch(process.env.SLACK_WEBHOOK_URL!, {
+ method: "POST",
+ body: JSON.stringify(slackBody),
+ });
+ }
};
check()
- .catch((error) => {
- console.error(error);
- process.exit(1);
- })
- .finally(() => {
- process.exit(0);
- });
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ })
+ .finally(() => {
+ process.exit(0);
+ });
// let missingUsageCount = 0;
diff --git a/server/src/check2.ts b/server/src/check2.ts
index ba9ed94af..79beba1e7 100644
--- a/server/src/check2.ts
+++ b/server/src/check2.ts
@@ -2,25 +2,25 @@ import { config } from "dotenv";
config();
import {
- getAllEntities,
- getAllFullCustomers,
+ getAllEntities,
+ getAllFullCustomers,
} from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js";
import { initDrizzle } from "@/db/initDrizzle.js";
import {
- AppEnv,
- CusProductStatus,
- FullCusProduct,
- FullCustomer,
- Organization,
- Entity,
+ AppEnv,
+ CusProductStatus,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
+ Entity,
} from "@autumn/shared";
import Stripe from "stripe";
import assert from "assert";
import { cusProductToPrices } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import {
- getAllStripeSchedules,
- getAllStripeSubscriptions,
+ getAllStripeSchedules,
+ getAllStripeSubscriptions,
} from "@/utils/scriptUtils/getAll/getAllStripeSubs.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
@@ -35,33 +35,33 @@ import { EntityService } from "./internal/api/entities/EntityService.js";
const { db } = initDrizzle({ maxConnections: 5 });
export const check = async () => {
- const env = AppEnv.Live;
- const orgId = "99XYziU2vChNNpdeEpvse09b8UF6BPME";
+ const env = AppEnv.Live;
+ const orgId = "99XYziU2vChNNpdeEpvse09b8UF6BPME";
- let fullCustomers = await getAllFullCustomers({
- db,
- orgId,
- env,
- });
+ let fullCustomers = await getAllFullCustomers({
+ db,
+ orgId,
+ env,
+ });
- const checkCustomers = ["9bafd636-0c52-46b3-8ecd-1708d6faa373"];
+ const checkCustomers = ["9bafd636-0c52-46b3-8ecd-1708d6faa373"];
- fullCustomers = fullCustomers.filter((customer) =>
- checkCustomers.includes(customer.id || "")
- );
+ fullCustomers = fullCustomers.filter((customer) =>
+ checkCustomers.includes(customer.id || ""),
+ );
- for (const customer of fullCustomers) {
- console.log(`Checking ${customer.name} (${customer.id})`);
- const cusProducts = customer.customer_products;
- const cusEnts = cusProducts.flatMap((cp) => cp.customer_entitlements);
- }
+ for (const customer of fullCustomers) {
+ console.log(`Checking ${customer.name} (${customer.id})`);
+ const cusProducts = customer.customer_products;
+ const cusEnts = cusProducts.flatMap((cp) => cp.customer_entitlements);
+ }
};
check()
- .catch((error) => {
- console.error(error);
- process.exit(1);
- })
- .finally(() => {
- process.exit(0);
- });
+ .catch((error) => {
+ console.error(error);
+ process.exit(1);
+ })
+ .finally(() => {
+ process.exit(0);
+ });
diff --git a/server/src/cron.ts b/server/src/cron.ts
index a7fd855e3..0a955e45f 100644
--- a/server/src/cron.ts
+++ b/server/src/cron.ts
@@ -1,8 +1,8 @@
import dotenv from "dotenv";
import {
- CustomerEntitlement,
- FullCusEntWithProduct,
- ResetCusEnt,
+ CustomerEntitlement,
+ FullCusEntWithProduct,
+ ResetCusEnt,
} from "@autumn/shared";
import { CusEntService } from "./internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { format } from "date-fns";
@@ -18,76 +18,76 @@ dotenv.config();
const { db, client } = initDrizzle();
export const cronTask = async () => {
- console.log(
- "\n----------------------------------\nRUNNING RESET CRON:",
- format(new UTCDate(), "yyyy-MM-dd HH:mm:ss")
- );
+ console.log(
+ "\n----------------------------------\nRUNNING RESET CRON:",
+ format(new UTCDate(), "yyyy-MM-dd HH:mm:ss"),
+ );
- try {
- const cusEnts: ResetCusEnt[] = await CusEntService.getActiveResetPassed({
- db,
- batchSize: 500,
- });
+ try {
+ const cusEnts: ResetCusEnt[] = await CusEntService.getActiveResetPassed({
+ db,
+ batchSize: 500,
+ });
- const cacheEnabledOrgs = await OrgService.getCacheEnabledOrgs({ db });
+ const cacheEnabledOrgs = await OrgService.getCacheEnabledOrgs({ db });
- const batchSize = 100;
- for (let i = 0; i < cusEnts.length; i += batchSize) {
- const batch = cusEnts.slice(i, i + batchSize);
- const batchResets = [];
- for (const cusEnt of batch) {
- batchResets.push(
- resetCustomerEntitlement({
- db,
- cusEnt: cusEnt,
- cacheEnabledOrgs,
- })
- );
- }
+ const batchSize = 100;
+ for (let i = 0; i < cusEnts.length; i += batchSize) {
+ const batch = cusEnts.slice(i, i + batchSize);
+ const batchResets = [];
+ for (const cusEnt of batch) {
+ batchResets.push(
+ resetCustomerEntitlement({
+ db,
+ cusEnt: cusEnt,
+ cacheEnabledOrgs,
+ }),
+ );
+ }
- let results = await Promise.all(batchResets);
+ let results = await Promise.all(batchResets);
- let toUpsert = results.filter(notNullish);
- await CusEntService.upsert({
- db,
- data: toUpsert as CustomerEntitlement[],
- });
- console.log(`Upserted ${toUpsert.length} short entitlements`);
- }
+ let toUpsert = results.filter(notNullish);
+ await CusEntService.upsert({
+ db,
+ data: toUpsert as CustomerEntitlement[],
+ });
+ console.log(`Upserted ${toUpsert.length} short entitlements`);
+ }
- console.log(
- "FINISHED RESET CRON:",
- format(new UTCDate(), "yyyy-MM-dd HH:mm:ss")
- );
- console.log("----------------------------------\n");
- } catch (error) {
- console.error("Error getting entitlements for reset:", error);
- return;
- }
+ console.log(
+ "FINISHED RESET CRON:",
+ format(new UTCDate(), "yyyy-MM-dd HH:mm:ss"),
+ );
+ console.log("----------------------------------\n");
+ } catch (error) {
+ console.error("Error getting entitlements for reset:", error);
+ return;
+ }
- // await client.end();
+ // await client.end();
};
const job = new CronJob(
- "* * * * *", // Run every minute
- function () {
- cronTask();
- },
- null, // onComplete
- true, // start immediately
- "UTC" // timezone (adjust as needed)
+ "* * * * *", // Run every minute
+ function () {
+ cronTask();
+ },
+ null, // onComplete
+ true, // start immediately
+ "UTC", // timezone (adjust as needed)
);
cronTask();
process.on("SIGTERM", async () => {
- console.log("Received SIGTERM signal, closing database connection...");
- await client.end();
- process.exit(0);
+ console.log("Received SIGTERM signal, closing database connection...");
+ await client.end();
+ process.exit(0);
});
process.on("SIGINT", async () => {
- console.log("Received SIGINT signal, closing database connection...");
- await client.end();
- process.exit(0);
+ console.log("Received SIGINT signal, closing database connection...");
+ await client.end();
+ process.exit(0);
});
diff --git a/server/src/cron/cronUtils.ts b/server/src/cron/cronUtils.ts
index a1c2e7b5f..2678259f7 100644
--- a/server/src/cron/cronUtils.ts
+++ b/server/src/cron/cronUtils.ts
@@ -1,11 +1,11 @@
import {
- AllowanceType,
- AppEnv,
- EntInterval,
- FullCusEntWithProduct,
- FullEntitlement,
- Organization,
- ResetCusEnt,
+ AllowanceType,
+ AppEnv,
+ EntInterval,
+ FullCusEntWithProduct,
+ FullEntitlement,
+ Organization,
+ ResetCusEnt,
} from "@autumn/shared";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
@@ -15,8 +15,8 @@ import chalk from "chalk";
import { format, getDate, getMonth, setDate } from "date-fns";
import {
- getRelatedCusPrice,
- getResetBalance,
+ getRelatedCusPrice,
+ getResetBalance,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils.js";
import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js";
@@ -31,300 +31,300 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
import { Decimal } from "decimal.js";
const checkSubAnchor = async ({
- db,
- cusEnt,
- nextResetAt,
+ db,
+ cusEnt,
+ nextResetAt,
}: {
- db: DrizzleCli;
- cusEnt: FullCusEntWithProduct;
- nextResetAt: number;
+ db: DrizzleCli;
+ cusEnt: FullCusEntWithProduct;
+ nextResetAt: number;
}) => {
- let nextResetAtDate = new UTCDate(nextResetAt);
+ let nextResetAtDate = new UTCDate(nextResetAt);
- // If nextResetAt is on the 28th of March, or Day 30, then do this check.
- const nextResetAtDay = getDate(nextResetAtDate);
- const nextResetAtMonth = getMonth(nextResetAtDate);
+ // If nextResetAt is on the 28th of March, or Day 30, then do this check.
+ const nextResetAtDay = getDate(nextResetAtDate);
+ const nextResetAtMonth = getMonth(nextResetAtDate);
- const shouldCheck =
- nextResetAtDay === 30 || (nextResetAtDay === 28 && nextResetAtMonth === 2);
+ const shouldCheck =
+ nextResetAtDay === 30 || (nextResetAtDay === 28 && nextResetAtMonth === 2);
- if (!shouldCheck) {
- return nextResetAt;
- }
+ if (!shouldCheck) {
+ return nextResetAt;
+ }
- // 1. Get the customer product
- const cusProduct = await CusProductService.getByIdForReset({
- db,
- id: cusEnt.customer_product_id,
- });
+ // 1. Get the customer product
+ const cusProduct = await CusProductService.getByIdForReset({
+ db,
+ id: cusEnt.customer_product_id,
+ });
- // Get org and env
- const env = cusProduct.product.env as AppEnv;
- const org = cusProduct.product.org as Organization;
+ // Get org and env
+ const env = cusProduct.product.env as AppEnv;
+ const org = cusProduct.product.org as Organization;
- const stripeCli = createStripeCli({ org, env });
- if (!cusProduct.subscription_ids || cusProduct.subscription_ids.length == 0) {
- return nextResetAt;
- }
+ const stripeCli = createStripeCli({ org, env });
+ if (!cusProduct.subscription_ids || cusProduct.subscription_ids.length == 0) {
+ return nextResetAt;
+ }
- const subId = cusProduct.subscription_ids[0];
- const sub = await stripeCli.subscriptions.retrieve(subId);
+ const subId = cusProduct.subscription_ids[0];
+ const sub = await stripeCli.subscriptions.retrieve(subId);
- const billingCycleAnchor = sub.billing_cycle_anchor * 1000;
- console.log("Checking billing cycle anchor");
- console.log(
- "Next reset at ",
- format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
- );
- console.log(
- "Billing cycle anchor",
- format(new UTCDate(billingCycleAnchor), "dd MMM yyyy HH:mm:ss")
- );
+ const billingCycleAnchor = sub.billing_cycle_anchor * 1000;
+ console.log("Checking billing cycle anchor");
+ console.log(
+ "Next reset at ",
+ format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss"),
+ );
+ console.log(
+ "Billing cycle anchor",
+ format(new UTCDate(billingCycleAnchor), "dd MMM yyyy HH:mm:ss"),
+ );
- const billingCycleDay = getDate(new UTCDate(billingCycleAnchor));
- const nextResetDay = getDate(nextResetAtDate);
+ const billingCycleDay = getDate(new UTCDate(billingCycleAnchor));
+ const nextResetDay = getDate(nextResetAtDate);
- if (billingCycleDay > nextResetDay) {
- nextResetAtDate = setDate(nextResetAtDate, billingCycleDay);
- return nextResetAtDate.getTime();
- } else {
- return nextResetAt;
- }
+ if (billingCycleDay > nextResetDay) {
+ nextResetAtDate = setDate(nextResetAtDate, billingCycleDay);
+ return nextResetAtDate.getTime();
+ } else {
+ return nextResetAt;
+ }
};
const handleShortDurationCusEnt = async ({
- db,
- cusEnt,
- cacheEnabledOrgs,
+ db,
+ cusEnt,
+ cacheEnabledOrgs,
}: {
- db: DrizzleCli;
- cusEnt: ResetCusEnt;
- cacheEnabledOrgs: any[];
+ db: DrizzleCli;
+ cusEnt: ResetCusEnt;
+ cacheEnabledOrgs: any[];
}) => {
- let ent = cusEnt.entitlement as FullEntitlement;
+ let ent = cusEnt.entitlement as FullEntitlement;
- let resetCusEnt = {
- ...cusEnt,
- next_reset_at: getNextResetAt({
- curReset: new UTCDate(cusEnt.next_reset_at!),
- interval: ent.interval as EntInterval,
- intervalCount: ent.interval_count,
- }),
- adjustment: 0,
- ...getResetBalancesUpdate({
- cusEnt,
- allowance: new Decimal(ent.allowance || 0)
- .mul(cusEnt.customer_product.quantity)
- .toNumber(),
- }),
- };
- let newCusEnt = resetCusEnt;
+ let resetCusEnt = {
+ ...cusEnt,
+ next_reset_at: getNextResetAt({
+ curReset: new UTCDate(cusEnt.next_reset_at!),
+ interval: ent.interval as EntInterval,
+ intervalCount: ent.interval_count,
+ }),
+ adjustment: 0,
+ ...getResetBalancesUpdate({
+ cusEnt,
+ allowance: new Decimal(ent.allowance || 0)
+ .mul(cusEnt.customer_product.quantity)
+ .toNumber(),
+ }),
+ };
+ let newCusEnt = resetCusEnt;
- let rolloverUpdate = getRolloverUpdates({
- cusEnt,
- nextResetAt: cusEnt.next_reset_at! as number,
- });
+ let rolloverUpdate = getRolloverUpdates({
+ cusEnt,
+ nextResetAt: cusEnt.next_reset_at! as number,
+ });
- if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
- await RolloverService.insert({
- db,
- rows: rolloverUpdate.toInsert,
- fullCusEnt: cusEnt,
- });
- }
+ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
+ await RolloverService.insert({
+ db,
+ rows: rolloverUpdate.toInsert,
+ fullCusEnt: cusEnt,
+ });
+ }
- console.log(
- `Reseting short cus ent (${cusEnt.feature_id}) [${ent.interval}], customer: ${cusEnt.customer_id}, org: ${cusEnt.customer.org_id}`
- );
+ console.log(
+ `Reseting short cus ent (${cusEnt.feature_id}) [${ent.interval}], customer: ${cusEnt.customer_id}, org: ${cusEnt.customer.org_id}`,
+ );
- let org = await OrgService.get({
- db,
- orgId: cusEnt.customer.org_id,
- });
- await deleteCusCache({
- db,
- customerId: cusEnt.customer.id!,
- org: org,
- env: cusEnt.customer.env,
- });
+ let org = await OrgService.get({
+ db,
+ orgId: cusEnt.customer.org_id,
+ });
+ await deleteCusCache({
+ db,
+ customerId: cusEnt.customer.id!,
+ org: org,
+ env: cusEnt.customer.env,
+ });
- return newCusEnt;
+ return newCusEnt;
};
const shortDurations = [EntInterval.Minute, EntInterval.Hour, EntInterval.Day];
export const resetCustomerEntitlement = async ({
- db,
- cusEnt,
- cacheEnabledOrgs,
+ db,
+ cusEnt,
+ cacheEnabledOrgs,
}: {
- db: DrizzleCli;
- cusEnt: ResetCusEnt;
- cacheEnabledOrgs: any[];
+ db: DrizzleCli;
+ cusEnt: ResetCusEnt;
+ cacheEnabledOrgs: any[];
}) => {
- try {
- let ent = cusEnt.entitlement as FullEntitlement;
+ try {
+ let ent = cusEnt.entitlement as FullEntitlement;
- if (
- ent.allowance_type == AllowanceType.Fixed &&
- shortDurations.includes(ent.interval as EntInterval)
- ) {
- return await handleShortDurationCusEnt({
- db,
- cusEnt,
- cacheEnabledOrgs,
- });
- }
+ if (
+ ent.allowance_type == AllowanceType.Fixed &&
+ shortDurations.includes(ent.interval as EntInterval)
+ ) {
+ return await handleShortDurationCusEnt({
+ db,
+ cusEnt,
+ cacheEnabledOrgs,
+ });
+ }
- // Fetch related price
- const cusPrices = await CusPriceService.getByCustomerProductId({
- db,
- customerProductId: cusEnt.customer_product_id,
- });
+ // Fetch related price
+ const cusPrices = await CusPriceService.getByCustomerProductId({
+ db,
+ customerProductId: cusEnt.customer_product_id,
+ });
- // 2. Quantity is from prices...
- const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
- if (relatedCusPrice) {
- return;
- }
+ // 2. Quantity is from prices...
+ const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
+ if (relatedCusPrice) {
+ return;
+ }
- const entOptions = getEntOptions(
- cusEnt.customer_product.options,
- cusEnt.entitlement
- );
+ const entOptions = getEntOptions(
+ cusEnt.customer_product.options,
+ cusEnt.entitlement,
+ );
- // Handle if entitlement changed to unlimited...
- let entitlement = cusEnt.entitlement;
- if (entitlement.allowance_type === AllowanceType.Unlimited) {
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- unlimited: true,
- next_reset_at: null,
- },
- });
+ // Handle if entitlement changed to unlimited...
+ let entitlement = cusEnt.entitlement;
+ if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ unlimited: true,
+ next_reset_at: null,
+ },
+ });
- console.log(
- `Reset ${cusEnt.id} | customer: ${chalk.yellow(
- cusEnt.customer_id
- )} | feature: ${chalk.yellow(
- cusEnt.feature_id
- )} | new balance: unlimited`
- );
- return;
- }
+ console.log(
+ `Reset ${cusEnt.id} | customer: ${chalk.yellow(
+ cusEnt.customer_id,
+ )} | feature: ${chalk.yellow(
+ cusEnt.feature_id,
+ )} | new balance: unlimited`,
+ );
+ return;
+ }
- if (entitlement.interval === EntInterval.Lifetime) {
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- next_reset_at: null,
- },
- });
+ if (entitlement.interval === EntInterval.Lifetime) {
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ next_reset_at: null,
+ },
+ });
- console.log(
- `Reset ${cusEnt.id} | customer: ${chalk.yellow(
- cusEnt.customer_id
- )} | feature: ${chalk.yellow(
- cusEnt.feature_id
- )} | reset to lifetime (next_reset_at: null)`
- );
- return;
- }
+ console.log(
+ `Reset ${cusEnt.id} | customer: ${chalk.yellow(
+ cusEnt.customer_id,
+ )} | feature: ${chalk.yellow(
+ cusEnt.feature_id,
+ )} | reset to lifetime (next_reset_at: null)`,
+ );
+ return;
+ }
- const resetBalance = getResetBalance({
- entitlement: cusEnt.entitlement,
- options: entOptions,
- relatedPrice: undefined,
- productQuantity: cusEnt.customer_product.quantity,
- });
+ const resetBalance = getResetBalance({
+ entitlement: cusEnt.entitlement,
+ options: entOptions,
+ relatedPrice: undefined,
+ productQuantity: cusEnt.customer_product.quantity,
+ });
- // 1. Check if should reset
+ // 1. Check if should reset
- let nextResetAt = getNextResetAt({
- curReset: new UTCDate(cusEnt.next_reset_at!),
- interval: cusEnt.entitlement.interval as EntInterval,
- intervalCount: cusEnt.entitlement.interval_count,
- });
+ let nextResetAt = getNextResetAt({
+ curReset: new UTCDate(cusEnt.next_reset_at!),
+ interval: cusEnt.entitlement.interval as EntInterval,
+ intervalCount: cusEnt.entitlement.interval_count,
+ });
- let rolloverUpdate = getRolloverUpdates({
- cusEnt,
- nextResetAt: cusEnt.next_reset_at! as number,
- });
+ let rolloverUpdate = getRolloverUpdates({
+ cusEnt,
+ nextResetAt: cusEnt.next_reset_at! as number,
+ });
- let resetBalanceUpdate = getResetBalancesUpdate({
- cusEnt,
- allowance: resetBalance || undefined,
- });
+ let resetBalanceUpdate = getResetBalancesUpdate({
+ cusEnt,
+ allowance: resetBalance || undefined,
+ });
- try {
- nextResetAt = await checkSubAnchor({
- db,
- cusEnt,
- nextResetAt,
- });
- } catch (error) {
- console.log("WARNING: Failed to check sub anchor");
- console.log(error);
- }
+ try {
+ nextResetAt = await checkSubAnchor({
+ db,
+ cusEnt,
+ nextResetAt,
+ });
+ } catch (error) {
+ console.log("WARNING: Failed to check sub anchor");
+ console.log(error);
+ }
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- ...resetBalanceUpdate,
- next_reset_at: nextResetAt,
- adjustment: 0,
- },
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ ...resetBalanceUpdate,
+ next_reset_at: nextResetAt,
+ adjustment: 0,
+ },
+ });
- if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
- await RolloverService.insert({
- db,
- rows: rolloverUpdate.toInsert,
- fullCusEnt: cusEnt,
- });
- }
+ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
+ await RolloverService.insert({
+ db,
+ rows: rolloverUpdate.toInsert,
+ fullCusEnt: cusEnt,
+ });
+ }
- console.log(
- `Reset ${cusEnt.id} | customer: ${chalk.yellow(
- cusEnt.customer_id
- )} | feature: ${chalk.yellow(
- cusEnt.feature_id
- )} | new balance: ${chalk.green(
- resetBalance
- )} | new next_reset_at: ${chalk.green(
- format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss")
- )}`
- );
+ console.log(
+ `Reset ${cusEnt.id} | customer: ${chalk.yellow(
+ cusEnt.customer_id,
+ )} | feature: ${chalk.yellow(
+ cusEnt.feature_id,
+ )} | new balance: ${chalk.green(
+ resetBalance,
+ )} | new next_reset_at: ${chalk.green(
+ format(new UTCDate(nextResetAt), "dd MMM yyyy HH:mm:ss"),
+ )}`,
+ );
- // let cacheOrg = cacheEnabledOrgs.find(
- // (org) => org.id === cusEnt.customer.org_id
- // );
+ // let cacheOrg = cacheEnabledOrgs.find(
+ // (org) => org.id === cusEnt.customer.org_id
+ // );
- let org = await OrgService.get({
- db,
- orgId: cusEnt.customer.org_id,
- });
- await deleteCusCache({
- db,
- customerId: cusEnt.customer.id!,
- org: org,
- env: cusEnt.customer.env,
- });
- // if (cacheOrg) {
- // await deleteCusCache({
- // db,
- // customerId: cusEnt.customer.id!,
- // org: cacheOrg,
- // env: cusEnt.customer.env,
- // });
- // }
- } catch (error: any) {
- console.log(
- `Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`
- );
- }
+ let org = await OrgService.get({
+ db,
+ orgId: cusEnt.customer.org_id,
+ });
+ await deleteCusCache({
+ db,
+ customerId: cusEnt.customer.id!,
+ org: org,
+ env: cusEnt.customer.env,
+ });
+ // if (cacheOrg) {
+ // await deleteCusCache({
+ // db,
+ // customerId: cusEnt.customer.id!,
+ // org: cacheOrg,
+ // env: cusEnt.customer.env,
+ // });
+ // }
+ } catch (error: any) {
+ console.log(
+ `Failed to reset ${cusEnt.id} | ${cusEnt.customer_id} | ${cusEnt.feature_id}, error: ${error}`,
+ );
+ }
};
diff --git a/server/src/db/dbUtils.ts b/server/src/db/dbUtils.ts
index 1de432242..c14d5fec3 100644
--- a/server/src/db/dbUtils.ts
+++ b/server/src/db/dbUtils.ts
@@ -2,17 +2,17 @@ import { getTableColumns, sql, SQL } from "drizzle-orm";
import { PgTable } from "drizzle-orm/pg-core";
export const buildConflictUpdateColumns = (
- table: T,
- excludeColumns: (keyof T["_"]["columns"])[] = [],
+ table: T,
+ excludeColumns: (keyof T["_"]["columns"])[] = [],
) => {
- const cls = getTableColumns(table);
- const updateSet: Record = {};
+ const cls = getTableColumns(table);
+ const updateSet: Record = {};
- for (const [columnKey, columnConfig] of Object.entries(cls)) {
- if (!excludeColumns.includes(columnKey as keyof T["_"]["columns"])) {
- updateSet[columnKey] = sql.raw(`excluded.${columnConfig.name}`);
- }
- }
+ for (const [columnKey, columnConfig] of Object.entries(cls)) {
+ if (!excludeColumns.includes(columnKey as keyof T["_"]["columns"])) {
+ updateSet[columnKey] = sql.raw(`excluded.${columnConfig.name}`);
+ }
+ }
- return updateSet;
+ return updateSet;
};
diff --git a/server/src/db/initClickHouse.ts b/server/src/db/initClickHouse.ts
index f2fee1f10..23fb3bcd3 100644
--- a/server/src/db/initClickHouse.ts
+++ b/server/src/db/initClickHouse.ts
@@ -1,8 +1,8 @@
import { ClickHouseClient, createClient } from "@clickhouse/client";
export const clickhouseClient: ClickHouseClient = createClient({
- url: process.env.CLICKHOUSE_URL!,
- username: process.env.CLICKHOUSE_USERNAME!,
- password: process.env.CLICKHOUSE_PASSWORD!,
- max_open_connections: 10,
-});
\ No newline at end of file
+ url: process.env.CLICKHOUSE_URL!,
+ username: process.env.CLICKHOUSE_USERNAME!,
+ password: process.env.CLICKHOUSE_PASSWORD!,
+ max_open_connections: 10,
+});
diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts
index 3a8b4dea8..784e8beb2 100644
--- a/server/src/db/initDrizzle.ts
+++ b/server/src/db/initDrizzle.ts
@@ -9,16 +9,16 @@ export let client = postgres(process.env.DATABASE_URL!);
export let db = drizzle(client, { schema });
export const initDrizzle = (params?: { maxConnections?: number }) => {
- let maxConnections = params?.maxConnections || 10;
- const client = postgres(process.env.DATABASE_URL!, {
- max: maxConnections,
- });
+ let maxConnections = params?.maxConnections || 10;
+ const client = postgres(process.env.DATABASE_URL!, {
+ max: maxConnections,
+ });
- const db = drizzle(client, {
- schema,
- });
+ const db = drizzle(client, {
+ schema,
+ });
- return { db, client };
+ return { db, client };
};
export type DrizzleCli = ReturnType["db"];
diff --git a/server/src/errors/errCodes.ts b/server/src/errors/errCodes.ts
index 5271f9ffc..e04b72354 100644
--- a/server/src/errors/errCodes.ts
+++ b/server/src/errors/errCodes.ts
@@ -1,81 +1,81 @@
export const ErrCode = {
- // General
- InvalidRequest: "invalid_request",
- InvalidId: "invalid_id",
+ // General
+ InvalidRequest: "invalid_request",
+ InvalidId: "invalid_id",
- // Org
- CreateClerkOrgFailed: "create_clerk_org_failed",
- AssignUserToOrgFailed: "assign_user_to_org_failed",
+ // Org
+ CreateClerkOrgFailed: "create_clerk_org_failed",
+ AssignUserToOrgFailed: "assign_user_to_org_failed",
- // Feature
- FeatureNotFound: "feature_not_found",
- InvalidFeature: "invalid_feature",
- DuplicateFeatureId: "duplicate_feature_id",
- UpdateFeatureFailed: "update_feature_failed",
+ // Feature
+ FeatureNotFound: "feature_not_found",
+ InvalidFeature: "invalid_feature",
+ DuplicateFeatureId: "duplicate_feature_id",
+ UpdateFeatureFailed: "update_feature_failed",
- // Internal
- InternalError: "internal_error",
- DuplicateCustomerId: "duplicate_customer_id",
- StripeKeyNotFound: "stripe_key_not_found",
+ // Internal
+ InternalError: "internal_error",
+ DuplicateCustomerId: "duplicate_customer_id",
+ StripeKeyNotFound: "stripe_key_not_found",
- // Stripe
- StripeKeyInvalid: "stripe_key_invalid",
- StripeConfigNotFound: "stripe_config_not_found",
- StripeDeleteCustomerFailed: "stripe_delete_customer_failed",
- StripeCreateCustomerFailed: "stripe_create_customer_failed",
- StripeCreateProductFailed: "stripe_create_product_failed",
- StripeCancelSubscriptionFailed: "stripe_cancel_subscription_failed",
+ // Stripe
+ StripeKeyInvalid: "stripe_key_invalid",
+ StripeConfigNotFound: "stripe_config_not_found",
+ StripeDeleteCustomerFailed: "stripe_delete_customer_failed",
+ StripeCreateCustomerFailed: "stripe_create_customer_failed",
+ StripeCreateProductFailed: "stripe_create_product_failed",
+ StripeCancelSubscriptionFailed: "stripe_cancel_subscription_failed",
- // Price
- PriceNotFound: "price_not_found",
- CreatePriceFailed: "create_price_failed",
- InvalidPrice: "invalid_price",
- InvalidPriceId: "invalid_price_id",
- InvalidPriceOptions: "invalid_price_options",
- InvalidPriceConfig: "invalid_price_config",
+ // Price
+ PriceNotFound: "price_not_found",
+ CreatePriceFailed: "create_price_failed",
+ InvalidPrice: "invalid_price",
+ InvalidPriceId: "invalid_price_id",
+ InvalidPriceOptions: "invalid_price_options",
+ InvalidPriceConfig: "invalid_price_config",
- // Customer
- InvalidCustomer: "invalid_customer",
- CreateCustomerFailed: "create_customer_failed",
- CustomerNotFound: "customer_not_found",
- CustomerAlreadyHasProduct: "customer_already_has_product",
- CustomerHasNoPaymentMethod: "customer_has_no_payment_method",
- CustomerHasNoBaseProduct: "customer_has_no_base_product",
- AttachProductToCustomerFailed: "attach_product_to_customer_failed",
- MultipleProductsFound: "multiple_products_found",
- MultipleCustomersFound: "multiple_customers_found",
- GetCusWithProductsFailed: "get_cus_with_products_failed",
+ // Customer
+ InvalidCustomer: "invalid_customer",
+ CreateCustomerFailed: "create_customer_failed",
+ CustomerNotFound: "customer_not_found",
+ CustomerAlreadyHasProduct: "customer_already_has_product",
+ CustomerHasNoPaymentMethod: "customer_has_no_payment_method",
+ CustomerHasNoBaseProduct: "customer_has_no_base_product",
+ AttachProductToCustomerFailed: "attach_product_to_customer_failed",
+ MultipleProductsFound: "multiple_products_found",
+ MultipleCustomersFound: "multiple_customers_found",
+ GetCusWithProductsFailed: "get_cus_with_products_failed",
- // Product
- InvalidProduct: "invalid_product",
- ProductAlreadyExists: "product_already_exists",
- ProductNotFound: "product_not_found",
- ProductHasCustomers: "product_has_customers",
- ProductHasNoPrices: "product_has_no_prices",
- ProductHasDifferentRecurringIntervals:
- "product_has_different_recurring_intervals",
- CreateStripeProductFailed: "create_stripe_product_failed",
- DeleteStripeProductFailed: "delete_stripe_product_failed",
- CreateStripeSubscriptionFailed: "create_stripe_subscription_failed",
- UpdateCusProductFailed: "update_customer_product_failed",
- DefaultProductNotAllowedPrice: "default_product_not_allowed_price",
- InvalidOptions: "invalid_options",
+ // Product
+ InvalidProduct: "invalid_product",
+ ProductAlreadyExists: "product_already_exists",
+ ProductNotFound: "product_not_found",
+ ProductHasCustomers: "product_has_customers",
+ ProductHasNoPrices: "product_has_no_prices",
+ ProductHasDifferentRecurringIntervals:
+ "product_has_different_recurring_intervals",
+ CreateStripeProductFailed: "create_stripe_product_failed",
+ DeleteStripeProductFailed: "delete_stripe_product_failed",
+ CreateStripeSubscriptionFailed: "create_stripe_subscription_failed",
+ UpdateCusProductFailed: "update_customer_product_failed",
+ DefaultProductNotAllowedPrice: "default_product_not_allowed_price",
+ InvalidOptions: "invalid_options",
- // Entitlements
- InvalidEntitlement: "invalid_entitlement",
- CreateEntitlementFailed: "create_entitlement_failed",
+ // Entitlements
+ InvalidEntitlement: "invalid_entitlement",
+ CreateEntitlementFailed: "create_entitlement_failed",
- // Invoice
- CreateInvoiceFailed: "create_invoice_failed",
- PayInvoiceFailed: "pay_invoice_failed",
+ // Invoice
+ CreateInvoiceFailed: "create_invoice_failed",
+ PayInvoiceFailed: "pay_invoice_failed",
- // Payment errors
- CardDeclinedError: "card_declined_error",
+ // Payment errors
+ CardDeclinedError: "card_declined_error",
- // Entity
- EntityNotFound: "entity_not_found",
+ // Entity
+ EntityNotFound: "entity_not_found",
- // Analytics
- NoEventsFound: "no_events_found",
- ClickHouseNotEnabled: "clickhouse_not_enabled",
+ // Analytics
+ NoEventsFound: "no_events_found",
+ ClickHouseNotEnabled: "clickhouse_not_enabled",
};
diff --git a/server/src/errors/errMessages.ts b/server/src/errors/errMessages.ts
index de9af1a81..bcc6c2ce1 100644
--- a/server/src/errors/errMessages.ts
+++ b/server/src/errors/errMessages.ts
@@ -1,3 +1,3 @@
export enum ErrorMessages {
- InternalError = "Internal error...please try again or contact us at +44 7498317257!",
+ InternalError = "Internal error...please try again or contact us at +44 7498317257!",
}
diff --git a/server/src/errors/logger.ts b/server/src/errors/logger.ts
index 0b300317b..371f973d6 100644
--- a/server/src/errors/logger.ts
+++ b/server/src/errors/logger.ts
@@ -3,148 +3,148 @@ import { Writable } from "stream";
// Custom log formatter for Bun compatibility
const createDevLogStream = () => {
- const colors = {
- reset: "\x1b[0m",
- bright: "\x1b[1m",
- dim: "\x1b[2m",
- red: "\x1b[31m",
- green: "\x1b[32m",
- yellow: "\x1b[33m",
- blue: "\x1b[34m",
- magenta: "\x1b[35m",
- cyan: "\x1b[36m",
- white: "\x1b[37m",
- gray: "\x1b[90m",
- bgRed: "\x1b[41m",
- };
+ const colors = {
+ reset: "\x1b[0m",
+ bright: "\x1b[1m",
+ dim: "\x1b[2m",
+ red: "\x1b[31m",
+ green: "\x1b[32m",
+ yellow: "\x1b[33m",
+ blue: "\x1b[34m",
+ magenta: "\x1b[35m",
+ cyan: "\x1b[36m",
+ white: "\x1b[37m",
+ gray: "\x1b[90m",
+ bgRed: "\x1b[41m",
+ };
- const levelColors: Record = {
- // Numeric levels
- 10: colors.gray, // trace
- 20: colors.blue, // debug
- 30: colors.green, // info
- 40: colors.yellow, // warn
- 50: colors.red, // error
- 60: colors.bgRed, // fatal
- // String levels
- TRACE: colors.gray,
- DEBUG: colors.blue,
- INFO: colors.green,
- WARN: colors.yellow,
- ERROR: colors.red,
- FATAL: colors.bgRed,
- };
+ const levelColors: Record = {
+ // Numeric levels
+ 10: colors.gray, // trace
+ 20: colors.blue, // debug
+ 30: colors.green, // info
+ 40: colors.yellow, // warn
+ 50: colors.red, // error
+ 60: colors.bgRed, // fatal
+ // String levels
+ TRACE: colors.gray,
+ DEBUG: colors.blue,
+ INFO: colors.green,
+ WARN: colors.yellow,
+ ERROR: colors.red,
+ FATAL: colors.bgRed,
+ };
- const levelNames: Record = {
- // Numeric levels
- 10: "TRACE",
- 20: "DEBUG",
- 30: "INFO",
- 40: "WARN",
- 50: "ERROR",
- 60: "FATAL",
- // String levels (pass through)
- TRACE: "TRACE",
- DEBUG: "DEBUG",
- INFO: "INFO",
- WARN: "WARN",
- ERROR: "ERROR",
- FATAL: "FATAL",
- };
+ const levelNames: Record = {
+ // Numeric levels
+ 10: "TRACE",
+ 20: "DEBUG",
+ 30: "INFO",
+ 40: "WARN",
+ 50: "ERROR",
+ 60: "FATAL",
+ // String levels (pass through)
+ TRACE: "TRACE",
+ DEBUG: "DEBUG",
+ INFO: "INFO",
+ WARN: "WARN",
+ ERROR: "ERROR",
+ FATAL: "FATAL",
+ };
- return new Writable({
- write(chunk, encoding, callback) {
- try {
- const log = JSON.parse(chunk.toString());
- const timestamp = new Date(log.time)
- .toISOString()
- .replace("T", " ")
- .replace("Z", "");
- const level = log.level;
- const levelColor = levelColors[level] || colors.white;
- const levelName =
- levelNames[level] || (typeof level === "string" ? level : "UNKNOWN");
+ return new Writable({
+ write(chunk, encoding, callback) {
+ try {
+ const log = JSON.parse(chunk.toString());
+ const timestamp = new Date(log.time)
+ .toISOString()
+ .replace("T", " ")
+ .replace("Z", "");
+ const level = log.level;
+ const levelColor = levelColors[level] || colors.white;
+ const levelName =
+ levelNames[level] || (typeof level === "string" ? level : "UNKNOWN");
- // Format the message
- let message = log.msg || "";
+ // Format the message
+ let message = log.msg || "";
- // Add any additional fields (excluding standard pino fields)
- const excludeFields = [
- "time",
- "level",
- "msg",
- "pid",
- "hostname",
- "res",
- "statusCode",
- "worker",
- "context",
- "req",
- "data",
- ];
- const additionalFields = Object.keys(log)
- .filter((key) => !excludeFields.includes(key))
- .reduce((acc, key) => {
- acc[key] = log[key];
- return acc;
- }, {} as any);
+ // Add any additional fields (excluding standard pino fields)
+ const excludeFields = [
+ "time",
+ "level",
+ "msg",
+ "pid",
+ "hostname",
+ "res",
+ "statusCode",
+ "worker",
+ "context",
+ "req",
+ "data",
+ ];
+ const additionalFields = Object.keys(log)
+ .filter((key) => !excludeFields.includes(key))
+ .reduce((acc, key) => {
+ acc[key] = log[key];
+ return acc;
+ }, {} as any);
- if (Object.keys(additionalFields).length > 0) {
- message += " " + JSON.stringify(additionalFields, null, 2);
- }
+ if (Object.keys(additionalFields).length > 0) {
+ message += " " + JSON.stringify(additionalFields, null, 2);
+ }
- // Format the final log line
- const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}\n`;
+ // Format the final log line
+ const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}\n`;
- process.stdout.write(formattedLog);
- callback();
- } catch (error) {
- // Fallback for malformed JSON
- process.stdout.write(chunk);
- callback();
- }
- },
- });
+ process.stdout.write(formattedLog);
+ callback();
+ } catch (error) {
+ // Fallback for malformed JSON
+ process.stdout.write(chunk);
+ callback();
+ }
+ },
+ });
};
export const initLogger = () => {
- // Create separate streams for console and HyperDX
- const streams: pino.StreamEntry[] = [];
+ // Create separate streams for console and HyperDX
+ const streams: pino.StreamEntry[] = [];
- if (process.env.NODE_ENV === "development") {
- streams.push({
- level: process.env.NODE_ENV === "development" ? "debug" : "info",
- stream: createDevLogStream(),
- });
- }
+ if (process.env.NODE_ENV === "development") {
+ streams.push({
+ level: process.env.NODE_ENV === "development" ? "debug" : "info",
+ stream: createDevLogStream(),
+ });
+ }
- if (process.env.AXIOM_TOKEN) {
- streams.push({
- level: "info",
- stream: pino.transport({
- target: "@axiomhq/pino",
- options: {
- dataset: "express",
- token: process.env.AXIOM_TOKEN,
- },
- }),
- });
- }
+ if (process.env.AXIOM_TOKEN) {
+ streams.push({
+ level: "info",
+ stream: pino.transport({
+ target: "@axiomhq/pino",
+ options: {
+ dataset: "express",
+ token: process.env.AXIOM_TOKEN,
+ },
+ }),
+ });
+ }
- const logger = pino(
- {
- level: process.env.NODE_ENV === "development" ? "debug" : "info",
- formatters: {
- level: (label: any) => {
- return {
- level: label.toUpperCase(),
- };
- },
- },
- },
- // Use multistream to send logs to multiple destinations
- pino.multistream(streams)
- );
+ const logger = pino(
+ {
+ level: process.env.NODE_ENV === "development" ? "debug" : "info",
+ formatters: {
+ level: (label: any) => {
+ return {
+ level: label.toUpperCase(),
+ };
+ },
+ },
+ },
+ // Use multistream to send logs to multiple destinations
+ pino.multistream(streams),
+ );
- return logger;
+ return logger;
};
diff --git a/server/src/external/autumn/autumnUtils.ts b/server/src/external/autumn/autumnUtils.ts
index 1b5fe1ae1..0e769a4db 100644
--- a/server/src/external/autumn/autumnUtils.ts
+++ b/server/src/external/autumn/autumnUtils.ts
@@ -4,85 +4,85 @@ import RecaseError from "@/utils/errorUtils.js";
import { Autumn } from "autumn-js";
export enum FeatureId {
- Products = "products",
- Features = "features",
- Revenue = "revenue",
+ Products = "products",
+ Features = "features",
+ Revenue = "revenue",
}
export const sendProductEvent = async ({
- org,
- env,
- incrementBy,
+ org,
+ env,
+ incrementBy,
}: {
- org: Organization;
- env: AppEnv;
- incrementBy: number;
+ org: Organization;
+ env: AppEnv;
+ incrementBy: number;
}) => {
- if (env !== AppEnv.Live) {
- return;
- }
+ if (env !== AppEnv.Live) {
+ return;
+ }
- try {
- const autumn = new Autumn();
+ try {
+ const autumn = new Autumn();
- await autumn.track({
- customer_id: org.id,
- event_name: "product",
- value: incrementBy,
- customer_data: {
- name: org.slug,
- },
- });
- console.log("sent product event", incrementBy);
- } catch (error: any) {
- console.log("Failed to send product event", error?.message || error);
- }
+ await autumn.track({
+ customer_id: org.id,
+ event_name: "product",
+ value: incrementBy,
+ customer_data: {
+ name: org.slug,
+ },
+ });
+ console.log("sent product event", incrementBy);
+ } catch (error: any) {
+ console.log("Failed to send product event", error?.message || error);
+ }
};
export const isEntitled = async ({
- org,
- env,
- featureId,
+ org,
+ env,
+ featureId,
}: {
- org: Organization;
- env: AppEnv;
- featureId: FeatureId;
+ org: Organization;
+ env: AppEnv;
+ featureId: FeatureId;
}) => {
- if (env !== AppEnv.Live) {
- return true;
- }
+ if (env !== AppEnv.Live) {
+ return true;
+ }
- const autumn = new Autumn();
+ const autumn = new Autumn();
- console.log("Checking entitlement for", org.id, featureId);
+ console.log("Checking entitlement for", org.id, featureId);
- const { data, error } = await autumn.check({
- customer_id: org.id,
- feature_id: featureId,
- customer_data: {
- name: org.slug,
- },
- });
+ const { data, error } = await autumn.check({
+ customer_id: org.id,
+ feature_id: featureId,
+ customer_data: {
+ name: org.slug,
+ },
+ });
- if (error) {
- throw new RecaseError({
- message: "Failed to check entitlement...",
- code: ErrCode.InternalError,
- });
- }
+ if (error) {
+ throw new RecaseError({
+ message: "Failed to check entitlement...",
+ code: ErrCode.InternalError,
+ });
+ }
- if (data?.allowed) {
- return true;
- }
+ if (data?.allowed) {
+ return true;
+ }
- let errText = `You've used up your allowance for ${featureId}.`;
- if (featureId === FeatureId.Revenue) {
- errText = `Looks like you've hit your monthly revenue limit for our plan, congrats 😉.`;
- }
+ let errText = `You've used up your allowance for ${featureId}.`;
+ if (featureId === FeatureId.Revenue) {
+ errText = `Looks like you've hit your monthly revenue limit for our plan, congrats 😉.`;
+ }
- throw new RecaseError({
- message: `${errText} Please upgrade your plan or contact hey@useautumn.com to get more!`,
- code: ErrCode.InternalError,
- data: data,
- });
+ throw new RecaseError({
+ message: `${errText} Please upgrade your plan or contact hey@useautumn.com to get more!`,
+ code: ErrCode.InternalError,
+ data: data,
+ });
};
diff --git a/server/src/external/autumn/autumnWebhookRouter.ts b/server/src/external/autumn/autumnWebhookRouter.ts
index 5bb0cd24c..bda38acfd 100644
--- a/server/src/external/autumn/autumnWebhookRouter.ts
+++ b/server/src/external/autumn/autumnWebhookRouter.ts
@@ -6,81 +6,81 @@ import { Webhook } from "svix";
export const autumnWebhookRouter: Router = express.Router();
const verifyAutumnWebhook = async (req: any, res: any) => {
- const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
+ const wh = new Webhook(process.env.AUTUMN_WEBHOOK_SECRET!);
- const headers = req.headers;
- const payload = req.body;
+ const headers = req.headers;
+ const payload = req.body;
- const svix_id = headers["svix-id"];
- const svix_timestamp = headers["svix-timestamp"];
- const svix_signature = headers["svix-signature"];
+ const svix_id = headers["svix-id"];
+ const svix_timestamp = headers["svix-timestamp"];
+ const svix_signature = headers["svix-signature"];
- if (!svix_id || !svix_timestamp || !svix_signature) {
- throw new RecaseError({
- message: "Error: Missing svix headers",
- code: ErrCode.InvalidInputs,
- });
- // res.status(400).json({
- // success: false,
- // message: "Error: Missing svix headers",
- // });
- // return;
- }
+ if (!svix_id || !svix_timestamp || !svix_signature) {
+ throw new RecaseError({
+ message: "Error: Missing svix headers",
+ code: ErrCode.InvalidInputs,
+ });
+ // res.status(400).json({
+ // success: false,
+ // message: "Error: Missing svix headers",
+ // });
+ // return;
+ }
- let evt: any;
- try {
- evt = wh.verify(payload, {
- "svix-id": svix_id as string,
- "svix-timestamp": svix_timestamp as string,
- "svix-signature": svix_signature as string,
- });
- } catch (err) {
- throw new RecaseError({
- message: "Error: Could not verify webhook",
- code: ErrCode.InvalidInputs,
- });
- // console.log("Error: Could not verify webhook");
- // res.status(400).json({
- // success: false,
- // message: "Error: Could not verify webhook",
- // });
- // return;
- }
+ let evt: any;
+ try {
+ evt = wh.verify(payload, {
+ "svix-id": svix_id as string,
+ "svix-timestamp": svix_timestamp as string,
+ "svix-signature": svix_signature as string,
+ });
+ } catch (err) {
+ throw new RecaseError({
+ message: "Error: Could not verify webhook",
+ code: ErrCode.InvalidInputs,
+ });
+ // console.log("Error: Could not verify webhook");
+ // res.status(400).json({
+ // success: false,
+ // message: "Error: Could not verify webhook",
+ // });
+ // return;
+ }
- return evt;
+ return evt;
};
autumnWebhookRouter.post(
- "",
- express.raw({ type: "application/json" }),
- async (req, res) => {
- try {
- const evt = await verifyAutumnWebhook(req, res);
- console.log("Received webhook from autumn");
- const { type, data } = evt;
+ "",
+ express.raw({ type: "application/json" }),
+ async (req, res) => {
+ try {
+ const evt = await verifyAutumnWebhook(req, res);
+ console.log("Received webhook from autumn");
+ const { type, data } = evt;
- switch (type) {
- case WebhookEventType.CustomerProductsUpdated:
- console.log(
- `Type: ${type}, Scenario: ${data?.scenario}, Product: ${data?.updated_product?.id}`
- );
- break;
- case WebhookEventType.CustomerThresholdReached:
- console.log(`Type: ${type}`);
- console.log(`Feature: `, data?.feature);
- break;
- }
+ switch (type) {
+ case WebhookEventType.CustomerProductsUpdated:
+ console.log(
+ `Type: ${type}, Scenario: ${data?.scenario}, Product: ${data?.updated_product?.id}`,
+ );
+ break;
+ case WebhookEventType.CustomerThresholdReached:
+ console.log(`Type: ${type}`);
+ console.log(`Feature: `, data?.feature);
+ break;
+ }
- res.status(200).json({
- success: true,
- message: "Webhook received",
- });
- } catch (error) {
- res.status(200).json({
- success: false,
- message: "Error: Could not verify webhook",
- });
- return;
- }
- }
+ res.status(200).json({
+ success: true,
+ message: "Webhook received",
+ });
+ } catch (error) {
+ res.status(200).json({
+ success: false,
+ message: "Error: Could not verify webhook",
+ });
+ return;
+ }
+ },
);
diff --git a/server/src/external/caching/CacheManager.ts b/server/src/external/caching/CacheManager.ts
index 9cd704fc3..bcdb5b8a9 100644
--- a/server/src/external/caching/CacheManager.ts
+++ b/server/src/external/caching/CacheManager.ts
@@ -1,133 +1,137 @@
import { Redis } from "ioredis";
export class CacheManager {
- private static instance: CacheManager | null = null;
- private client: Redis | null = null;
- private initialized = false;
- private initPromise: Promise | null = null;
+ private static instance: CacheManager | null = null;
+ private client: Redis | null = null;
+ private initialized = false;
+ private initPromise: Promise | null = null;
- private constructor() {
- // Empty private constructor
- }
+ private constructor() {
+ // Empty private constructor
+ }
- // Create redis connection
- private async initializeRedis(): Promise {
- console.log("Initializing Cache Manager...");
- if (this.initialized) return;
+ // Create redis connection
+ private async initializeRedis(): Promise {
+ console.log("Initializing Cache Manager...");
+ if (this.initialized) return;
- const redisUrl = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
+ const redisUrl = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
- if (!redisUrl) {
- throw new Error("Cache error: no redis connection string set in env");
- }
+ if (!redisUrl) {
+ throw new Error("Cache error: no redis connection string set in env");
+ }
- this.client = new Redis(redisUrl, {
- retryStrategy: (times) => {
- return 5000;
- },
- });
+ this.client = new Redis(redisUrl, {
+ retryStrategy: () => {
+ return 5000;
+ },
+ });
- this.client.on("error", (error) => {
- console.log(`Cache manager connection error: ${error.message}`);
- });
+ this.client.on("error", (error) => {
+ console.log(`Cache manager connection error: ${error.message}`);
+ });
- // Check if connection is live
- console.log(" 1. Pinging redis...");
- await this.client.ping();
+ // Check if connection is live
+ console.log(" 1. Pinging redis...");
+ await this.client.ping();
- this.initialized = true;
- }
+ this.initialized = true;
+ }
- public static async getInstance(): Promise {
- if (!CacheManager.instance) {
- CacheManager.instance = new CacheManager();
- CacheManager.instance.initPromise =
- CacheManager.instance.initializeRedis();
- }
+ public static async getInstance(): Promise {
+ if (!CacheManager.instance) {
+ CacheManager.instance = new CacheManager();
+ CacheManager.instance.initPromise =
+ CacheManager.instance.initializeRedis();
+ }
- // Wait for initialization to complete
- if (CacheManager.instance.initPromise) {
- await CacheManager.instance.initPromise;
- }
+ // Wait for initialization to complete
+ if (CacheManager.instance.initPromise) {
+ await CacheManager.instance.initPromise;
+ }
- return CacheManager.instance;
- }
+ return CacheManager.instance;
+ }
- public static async getClient() {
- const cache = await CacheManager.getInstance();
- return cache.client;
- }
+ public static async getClient() {
+ const cache = await CacheManager.getInstance();
+ return cache.client;
+ }
- public static async getJson(key: string) {
- let client = await CacheManager.getClient();
+ public static async getJson(key: string) {
+ const client = await CacheManager.getClient();
- if (!client) {
- throw new Error("Cache client not initialized");
- }
+ if (!client) {
+ throw new Error("Cache client not initialized");
+ }
- if (client.status !== "ready") {
- console.warn("Cache client is not in ready state");
- return null;
- }
+ if (client.status !== "ready") {
+ console.warn("Cache client is not in ready state");
+ return null;
+ }
- let res = await client.get(key);
+ const res = await client.get(key);
- if (!res) {
- return null;
- }
+ if (!res) {
+ return null;
+ }
- return JSON.parse(res);
- }
+ return JSON.parse(res);
+ }
- public static async setJson(key: string, value: any, ttl: number | string = 3600) {
- let client = await CacheManager.getClient();
- if (!client) {
- throw new Error("Cache client not initialized");
- }
+ public static async setJson(
+ key: string,
+ value: any,
+ ttl: number | string = 3600,
+ ) {
+ const client = await CacheManager.getClient();
+ if (!client) {
+ throw new Error("Cache client not initialized");
+ }
- if (client.status !== "ready") {
- console.warn("Cache client is not in ready state");
- return;
- }
+ if (client.status !== "ready") {
+ console.warn("Cache client is not in ready state");
+ return;
+ }
- if(typeof ttl === 'number') {
- await client.set(key, JSON.stringify(value), "EX", ttl);
- } else if(typeof ttl === 'string' && ttl.toLowerCase() === 'forever') {
- await client.set(key, JSON.stringify(value));
- }
- }
+ if (typeof ttl === "number") {
+ await client.set(key, JSON.stringify(value), "EX", ttl);
+ } else if (typeof ttl === "string" && ttl.toLowerCase() === "forever") {
+ await client.set(key, JSON.stringify(value));
+ }
+ }
- public static async invalidate({
- action,
- value,
- }: {
- action: string;
- value: string;
- }) {
- let client = await CacheManager.getClient();
- if (!client) {
- throw new Error("Cache client not initialized");
- }
+ public static async invalidate({
+ action,
+ value,
+ }: {
+ action: string;
+ value: string;
+ }) {
+ const client = await CacheManager.getClient();
+ if (!client) {
+ throw new Error("Cache client not initialized");
+ }
- if (client.status !== "ready") {
- console.warn("Cache client is not in ready state");
- return;
- }
+ if (client.status !== "ready") {
+ console.warn("Cache client is not in ready state");
+ return;
+ }
- await client.del(`${action}:${value}`);
- }
+ await client.del(`${action}:${value}`);
+ }
- static async disconnect() {
- let client = await CacheManager.getClient();
- if (!client) {
- throw new Error("Cache client not initialized");
- }
+ static async disconnect() {
+ const client = await CacheManager.getClient();
+ if (!client) {
+ throw new Error("Cache client not initialized");
+ }
- if (client.status !== "ready") {
- console.warn("Cache client is not in ready state");
- return;
- }
+ if (client.status !== "ready") {
+ console.warn("Cache client is not in ready state");
+ return;
+ }
- await client.quit();
- }
+ await client.quit();
+ }
}
diff --git a/server/src/external/caching/cacheActions.ts b/server/src/external/caching/cacheActions.ts
index c9c04128b..c0fbe6841 100644
--- a/server/src/external/caching/cacheActions.ts
+++ b/server/src/external/caching/cacheActions.ts
@@ -1,5 +1,5 @@
export enum CacheType {
- SecretKey = "secret_key",
- PublicKey = "public_key",
- TopEvents = "top_events",
+ SecretKey = "secret_key",
+ PublicKey = "public_key",
+ TopEvents = "top_events",
}
diff --git a/server/src/external/caching/cacheUtils.ts b/server/src/external/caching/cacheUtils.ts
index 5239f72c6..d9ee1ba49 100644
--- a/server/src/external/caching/cacheUtils.ts
+++ b/server/src/external/caching/cacheUtils.ts
@@ -2,36 +2,36 @@ import { notNullish } from "@/utils/genUtils.js";
import { CacheManager } from "./CacheManager.js";
export async function queryWithCache({
- action,
- key,
- fn,
+ action,
+ key,
+ fn,
}: {
- action: string;
- key: string;
- fn: () => Promise;
+ action: string;
+ key: string;
+ fn: () => Promise;
}) {
- let cacheKey = `${action}:${key}`;
- // Try to get from cache
- try {
- const cachedResult = await CacheManager.getJson(cacheKey);
- // console.log(`Cache key: ${cacheKey}`);
- // console.log(`Cached result: ${cachedResult}`);
- if (cachedResult) {
- return cachedResult;
- }
- } catch (error) {}
+ let cacheKey = `${action}:${key}`;
+ // Try to get from cache
+ try {
+ const cachedResult = await CacheManager.getJson(cacheKey);
+ // console.log(`Cache key: ${cacheKey}`);
+ // console.log(`Cached result: ${cachedResult}`);
+ if (cachedResult) {
+ return cachedResult;
+ }
+ } catch (error) {}
- // Cache miss, call original function
+ // Cache miss, call original function
- const data = await fn();
+ const data = await fn();
- try {
- if (notNullish(data)) {
- await CacheManager.setJson(cacheKey, data, 3600);
- }
- } catch (error) {
- console.error("Failed to set cache:", error);
- }
+ try {
+ if (notNullish(data)) {
+ await CacheManager.setJson(cacheKey, data, 3600);
+ }
+ } catch (error) {
+ console.error("Failed to set cache:", error);
+ }
- return data;
+ return data;
}
diff --git a/server/src/external/clerkUtils.ts b/server/src/external/clerkUtils.ts
index db2e5f858..09f603cb6 100644
--- a/server/src/external/clerkUtils.ts
+++ b/server/src/external/clerkUtils.ts
@@ -5,108 +5,108 @@ import { AppEnv } from "@autumn/shared";
import { clerkClient, createClerkClient } from "@clerk/express";
export const createClerkCli = () => {
- return createClerkClient({
- secretKey: process.env.CLERK_SECRET_KEY,
- });
+ return createClerkClient({
+ secretKey: process.env.CLERK_SECRET_KEY,
+ });
};
export const createClerkOrg = async (name: string, slug: string) => {
- const clerkCli = createClerkCli();
+ const clerkCli = createClerkCli();
- try {
- let org = await clerkCli.organizations.createOrganization({
- name,
- slug,
- });
- return org;
- } catch (error: any) {
- if (error.errors && error.errors.length > 0) {
- const errMessage = error.errors[0].message;
- throw new RecaseError({
- code: ErrCode.CreateClerkOrgFailed,
- message: errMessage,
- });
- } else {
- throw new RecaseError({
- code: ErrCode.InternalError,
- message: "Error creating organization",
- });
- }
- }
+ try {
+ let org = await clerkCli.organizations.createOrganization({
+ name,
+ slug,
+ });
+ return org;
+ } catch (error: any) {
+ if (error.errors && error.errors.length > 0) {
+ const errMessage = error.errors[0].message;
+ throw new RecaseError({
+ code: ErrCode.CreateClerkOrgFailed,
+ message: errMessage,
+ });
+ } else {
+ throw new RecaseError({
+ code: ErrCode.InternalError,
+ message: "Error creating organization",
+ });
+ }
+ }
};
export const assignUserToOrg = async (userId: string, orgId: string) => {
- const clerkCli = createClerkCli();
- try {
- await clerkCli.organizations.createOrganizationMembership({
- userId,
- role: "org:admin",
- organizationId: orgId,
- });
- } catch (error: any) {
- if (error.errors && error.errors.length > 0) {
- const errMessage = error.errors[0].message;
- throw new RecaseError({
- code: ErrCode.AssignUserToOrgFailed,
- message: errMessage,
- });
- } else {
- throw new RecaseError({
- code: ErrCode.InternalError,
- message: "Error assigning user to organization",
- });
- }
- }
+ const clerkCli = createClerkCli();
+ try {
+ await clerkCli.organizations.createOrganizationMembership({
+ userId,
+ role: "org:admin",
+ organizationId: orgId,
+ });
+ } catch (error: any) {
+ if (error.errors && error.errors.length > 0) {
+ const errMessage = error.errors[0].message;
+ throw new RecaseError({
+ code: ErrCode.AssignUserToOrgFailed,
+ message: errMessage,
+ });
+ } else {
+ throw new RecaseError({
+ code: ErrCode.InternalError,
+ message: "Error assigning user to organization",
+ });
+ }
+ }
};
export const getOrgById = async (orgId: string) => {
- const orgRes = await clerkClient.organizations.getOrganization({
- organizationId: orgId,
- });
- return orgRes;
+ const orgRes = await clerkClient.organizations.getOrganization({
+ organizationId: orgId,
+ });
+ return orgRes;
};
export const getStripeKey = async (orgId: string, env: AppEnv) => {
- const orgRes = await getOrgById(orgId);
- let meta: any = orgRes.privateMetadata;
+ const orgRes = await getOrgById(orgId);
+ let meta: any = orgRes.privateMetadata;
- // let key =
- // env == AppEnv.Sandbox
- // ? meta.stripe?.test_api_key
- // : meta.stripe?.live_api_key;
+ // let key =
+ // env == AppEnv.Sandbox
+ // ? meta.stripe?.test_api_key
+ // : meta.stripe?.live_api_key;
- // TODO: Change this to conditional
- let key = meta.stripe?.test_api_key;
+ // TODO: Change this to conditional
+ let key = meta.stripe?.test_api_key;
- if (!key) {
- throw new RecaseError({
- code: ErrCode.StripeKeyNotFound,
- message: "Stripe key not found",
- });
- }
+ if (!key) {
+ throw new RecaseError({
+ code: ErrCode.StripeKeyNotFound,
+ message: "Stripe key not found",
+ });
+ }
- return decryptData(key);
+ return decryptData(key);
};
export const createOrgAndAssignUser = async (
- name: string,
- slug: string,
- userId: string
+ name: string,
+ slug: string,
+ userId: string,
) => {
- try {
- const orgRes = await clerkClient.organizations.createOrganization({
- name,
- slug,
- });
+ try {
+ const orgRes = await clerkClient.organizations.createOrganization({
+ name,
+ slug,
+ });
- await clerkClient.organizations.createOrganizationMembership({
- userId,
- role: "org:admin",
- organizationId: orgRes.id,
- });
+ await clerkClient.organizations.createOrganizationMembership({
+ userId,
+ role: "org:admin",
+ organizationId: orgRes.id,
+ });
- return orgRes;
- } catch (error) {
- console.log("Clerk error:", error);
- }
+ return orgRes;
+ } catch (error) {
+ console.log("Clerk error:", error);
+ }
};
diff --git a/server/src/external/clickhouse/ClickHouseManager.ts b/server/src/external/clickhouse/ClickHouseManager.ts
index ece9f839a..1d39a551f 100644
--- a/server/src/external/clickhouse/ClickHouseManager.ts
+++ b/server/src/external/clickhouse/ClickHouseManager.ts
@@ -4,216 +4,216 @@ import { ClickHouseClient, QueryParams } from "@clickhouse/client";
import { clickhouseClient } from "../../db/initClickHouse.js";
export enum ClickHouseQuery {
- CREATE_DATE_RANGE_VIEW = "CREATE_DATE_RANGE_VIEW",
- CREATE_DATE_RANGE_BC_VIEW = "CREATE_DATE_RANGE_BC_VIEW",
- CREATE_ORG_EVENTS_VIEW = "CREATE_ORG_EVENTS_VIEW",
- // CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS",
- // CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS_NO_COUNT",
- GENERATE_EVENT_COUNT_EXPRESSIONS = "GENERATE_EVENT_COUNT_EXPRESSIONS",
- ENSURE_VIEWS_EXIST = "ENSURE_VIEWS_EXIST",
- ENSURE_FUNCTIONS_EXIST = "ENSURE_FUNCTIONS_EXIST",
+ CREATE_DATE_RANGE_VIEW = "CREATE_DATE_RANGE_VIEW",
+ CREATE_DATE_RANGE_BC_VIEW = "CREATE_DATE_RANGE_BC_VIEW",
+ CREATE_ORG_EVENTS_VIEW = "CREATE_ORG_EVENTS_VIEW",
+ // CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS",
+ // CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION = "CREATE_GENERATE_EVENT_COUNTS_EXPRESSIONS_NO_COUNT",
+ GENERATE_EVENT_COUNT_EXPRESSIONS = "GENERATE_EVENT_COUNT_EXPRESSIONS",
+ ENSURE_VIEWS_EXIST = "ENSURE_VIEWS_EXIST",
+ ENSURE_FUNCTIONS_EXIST = "ENSURE_FUNCTIONS_EXIST",
}
export class ClickHouseManager {
- private static instance: ClickHouseManager | null = null;
- private client: ClickHouseClient | null = clickhouseClient;
- private initialized = false;
- private initPromise: Promise | null = null;
- static clickhouseAvailable =
- process.env.CLICKHOUSE_URL &&
- process.env.CLICKHOUSE_USERNAME &&
- process.env.CLICKHOUSE_PASSWORD;
+ private static instance: ClickHouseManager | null = null;
+ private client: ClickHouseClient | null = clickhouseClient;
+ private initialized = false;
+ private initPromise: Promise | null = null;
+ static clickhouseAvailable =
+ process.env.CLICKHOUSE_URL &&
+ process.env.CLICKHOUSE_USERNAME &&
+ process.env.CLICKHOUSE_PASSWORD;
- private constructor() {
- // Empty private constructor
- }
+ private constructor() {
+ // Empty private constructor
+ }
- private async initializeClickHouse(): Promise {
- console.log("Initializing ClickHouse Manager...");
- console.group();
- if (this.initialized) {
- console.log("0. ClickHouse Manager already initialized.");
- console.groupEnd();
- return;
- }
+ private async initializeClickHouse(): Promise {
+ console.log("Initializing ClickHouse Manager...");
+ console.group();
+ if (this.initialized) {
+ console.log("0. ClickHouse Manager already initialized.");
+ console.groupEnd();
+ return;
+ }
- console.log("1. Creating ClickHouse client...");
- this.client = clickhouseClient;
+ console.log("1. Creating ClickHouse client...");
+ this.client = clickhouseClient;
- // console.log("2. Checking SQL files exist...");
- // await ClickHouseManager.ensureSQLFilesExist();
+ // console.log("2. Checking SQL files exist...");
+ // await ClickHouseManager.ensureSQLFilesExist();
- // console.log("3. Ensuring queries exist...");
- // await this.ensureQueriesExist();
+ // console.log("3. Ensuring queries exist...");
+ // await this.ensureQueriesExist();
- console.log("4. ClickHouse Manager initialized.");
- console.groupEnd();
+ console.log("4. ClickHouse Manager initialized.");
+ console.groupEnd();
- this.initialized = true;
- }
+ this.initialized = true;
+ }
- public static async getInstance(): Promise {
- if (!ClickHouseManager.instance) {
- ClickHouseManager.instance = new ClickHouseManager();
- ClickHouseManager.instance.initPromise =
- ClickHouseManager.instance.initializeClickHouse();
- }
+ public static async getInstance(): Promise {
+ if (!ClickHouseManager.instance) {
+ ClickHouseManager.instance = new ClickHouseManager();
+ ClickHouseManager.instance.initPromise =
+ ClickHouseManager.instance.initializeClickHouse();
+ }
- // Wait for initialization to complete
- if (ClickHouseManager.instance.initPromise) {
- await ClickHouseManager.instance.initPromise;
- }
+ // Wait for initialization to complete
+ if (ClickHouseManager.instance.initPromise) {
+ await ClickHouseManager.instance.initPromise;
+ }
- return ClickHouseManager.instance;
- }
+ return ClickHouseManager.instance;
+ }
- public static async getClient(): Promise {
- const manager = await ClickHouseManager.getInstance();
- if (!manager.client) {
- throw new Error("ClickHouse client not initialized");
- }
- return manager.client;
- }
+ public static async getClient(): Promise {
+ const manager = await ClickHouseManager.getInstance();
+ if (!manager.client) {
+ throw new Error("ClickHouse client not initialized");
+ }
+ return manager.client;
+ }
- static async createDateRangeView() {}
- static async createDateRangeBcView() {}
- static async createOrgEventsView() {}
+ static async createDateRangeView() {}
+ static async createDateRangeBcView() {}
+ static async createOrgEventsView() {}
- static async ensureSQLFilesExist() {
- const requiredQueries = [
- ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
- ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
- ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
- // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
- // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION,
- ClickHouseQuery.GENERATE_EVENT_COUNT_EXPRESSIONS,
- ClickHouseQuery.ENSURE_VIEWS_EXIST,
- ClickHouseQuery.ENSURE_FUNCTIONS_EXIST,
- ];
+ static async ensureSQLFilesExist() {
+ const requiredQueries = [
+ ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
+ ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
+ ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
+ // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
+ // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_NO_COUNT_FUNCTION,
+ ClickHouseQuery.GENERATE_EVENT_COUNT_EXPRESSIONS,
+ ClickHouseQuery.ENSURE_VIEWS_EXIST,
+ ClickHouseQuery.ENSURE_FUNCTIONS_EXIST,
+ ];
- const queryResults = await Promise.allSettled(
- requiredQueries.map((query) => ClickHouseManager.readSQLFile(query))
- );
+ const queryResults = await Promise.allSettled(
+ requiredQueries.map((query) => ClickHouseManager.readSQLFile(query)),
+ );
- const failedQueries = queryResults.filter(
- (result) => result.status === "rejected"
- );
+ const failedQueries = queryResults.filter(
+ (result) => result.status === "rejected",
+ );
- if (failedQueries.length > 0) {
- console.error(
- `Failed to read ${failedQueries.length} ClickHouse queries. Please re-pull the latest version of Autumn. `
- );
- failedQueries.forEach((result, index) => {
- if (result.status === "rejected") {
- console.error(
- `Query ${requiredQueries[index]} failed:`,
- result.reason
- );
- }
- });
- process.exit(1);
- }
- }
+ if (failedQueries.length > 0) {
+ console.error(
+ `Failed to read ${failedQueries.length} ClickHouse queries. Please re-pull the latest version of Autumn. `,
+ );
+ failedQueries.forEach((result, index) => {
+ if (result.status === "rejected") {
+ console.error(
+ `Query ${requiredQueries[index]} failed:`,
+ result.reason,
+ );
+ }
+ });
+ process.exit(1);
+ }
+ }
- private async ensureQueriesExist() {
- if (!this.client) {
- throw new Error("ClickHouse client not initialized");
- }
+ private async ensureQueriesExist() {
+ if (!this.client) {
+ throw new Error("ClickHouse client not initialized");
+ }
- if (!ClickHouseManager.clickhouseAvailable) {
- console.log(
- "0. ClickHouse is not available, please set the CLICKHOUSE_URL, CLICKHOUSE_USERNAME, and CLICKHOUSE_PASSWORD environment variables."
- );
- return;
- }
+ if (!ClickHouseManager.clickhouseAvailable) {
+ console.log(
+ "0. ClickHouse is not available, please set the CLICKHOUSE_URL, CLICKHOUSE_USERNAME, and CLICKHOUSE_PASSWORD environment variables.",
+ );
+ return;
+ }
- console.log("1. Creating ClickHouse client...");
- this.client = clickhouseClient;
+ console.log("1. Creating ClickHouse client...");
+ this.client = clickhouseClient;
- // Check if we should skip ensuring queries exist
- if (process.env.CLICKHOUSE_SKIP_ENSURES?.toLowerCase() === "true") {
- console.group();
- console.log(
- "✓ Skipping query ensures - queries assumed to exist already"
- );
- console.groupEnd();
- return;
- }
+ // Check if we should skip ensuring queries exist
+ if (process.env.CLICKHOUSE_SKIP_ENSURES?.toLowerCase() === "true") {
+ console.group();
+ console.log(
+ "✓ Skipping query ensures - queries assumed to exist already",
+ );
+ console.groupEnd();
+ return;
+ }
- const queries = [
- ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
- ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
- ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
+ const queries = [
+ ClickHouseQuery.CREATE_DATE_RANGE_BC_VIEW,
+ ClickHouseQuery.CREATE_DATE_RANGE_VIEW,
+ ClickHouseQuery.CREATE_ORG_EVENTS_VIEW,
- // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
- ];
+ // ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION,
+ ];
- console.group();
+ console.group();
- await Promise.all(
- queries.map(async (query) => {
- try {
- await this.executeQuery(query, this.client!);
- console.log(`✓ Successfully ensured query ${query} exists.`);
- } catch (error) {
- console.error(`✗ Failed to execute query ${query}:`, error);
- process.exit(1);
- }
- })
- );
+ await Promise.all(
+ queries.map(async (query) => {
+ try {
+ await this.executeQuery(query, this.client!);
+ console.log(`✓ Successfully ensured query ${query} exists.`);
+ } catch (error) {
+ console.error(`✗ Failed to execute query ${query}:`, error);
+ process.exit(1);
+ }
+ }),
+ );
- console.groupEnd();
- }
+ console.groupEnd();
+ }
- private async readSQLFile(query: ClickHouseQuery) {
- const queriesDir = path.join(import.meta.dirname, "queries");
- const queryPath = path.join(queriesDir, `${query}.sql`);
- const queryContent = fs.readFileSync(queryPath, "utf8");
- return queryContent;
- }
+ private async readSQLFile(query: ClickHouseQuery) {
+ const queriesDir = path.join(import.meta.dirname, "queries");
+ const queryPath = path.join(queriesDir, `${query}.sql`);
+ const queryContent = fs.readFileSync(queryPath, "utf8");
+ return queryContent;
+ }
- static async readSQLFile(query: ClickHouseQuery) {
- const manager = await ClickHouseManager.getInstance();
- return manager.readSQLFile(query);
- }
+ static async readSQLFile(query: ClickHouseQuery) {
+ const manager = await ClickHouseManager.getInstance();
+ return manager.readSQLFile(query);
+ }
- private async executeQuery(
- query: ClickHouseQuery,
- client: ClickHouseClient,
- options: any = {}
- ) {
- const queryContent = await this.readSQLFile(query);
- if (!queryContent) {
- throw new Error(`Query ${query} not found`);
- }
+ private async executeQuery(
+ query: ClickHouseQuery,
+ client: ClickHouseClient,
+ options: any = {},
+ ) {
+ const queryContent = await this.readSQLFile(query);
+ if (!queryContent) {
+ throw new Error(`Query ${query} not found`);
+ }
- // For CREATE FUNCTION queries, use command() instead of query() to avoid FORMAT clause
- // if (
- // query === ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION
- // ) {
- // const result = await client.command({
- // query: queryContent,
- // ...options,
- // });
- // return result;
- // }
+ // For CREATE FUNCTION queries, use command() instead of query() to avoid FORMAT clause
+ // if (
+ // query === ClickHouseQuery.CREATE_GENERATE_EVENT_COUNT_EXPRESSIONS_FUNCTION
+ // ) {
+ // const result = await client.command({
+ // query: queryContent,
+ // ...options,
+ // });
+ // return result;
+ // }
- const result = await client.query({
- query: queryContent,
- ...options,
- });
- return result;
- }
+ const result = await client.query({
+ query: queryContent,
+ ...options,
+ });
+ return result;
+ }
- static async executeQuery(
- query: ClickHouseQuery,
- client?: ClickHouseClient,
- options: QueryParams = {
- format: "TabSeparatedRaw",
- } as QueryParams
- ) {
- const manager = await ClickHouseManager.getInstance();
- const clickhouseClient = client || (await ClickHouseManager.getClient());
- return manager.executeQuery(query, clickhouseClient, options);
- }
+ static async executeQuery(
+ query: ClickHouseQuery,
+ client?: ClickHouseClient,
+ options: QueryParams = {
+ format: "TabSeparatedRaw",
+ } as QueryParams,
+ ) {
+ const manager = await ClickHouseManager.getInstance();
+ const clickhouseClient = client || (await ClickHouseManager.getClient());
+ return manager.executeQuery(query, clickhouseClient, options);
+ }
}
diff --git a/server/src/external/llm/llmUtils.ts b/server/src/external/llm/llmUtils.ts
index 472bdd4ae..450a51ec3 100644
--- a/server/src/external/llm/llmUtils.ts
+++ b/server/src/external/llm/llmUtils.ts
@@ -4,11 +4,11 @@ import { generateObject } from "ai";
import { z } from "zod";
const anthropic = createAnthropic({
- apiKey: process.env.ANTHROPIC_API_KEY,
+ apiKey: process.env.ANTHROPIC_API_KEY,
});
export const generateFeatureDisplay = async (feature: Feature) => {
- const prompt = `I'm building an entitlement system and my users can create features on my platform. I also help with displaying components (like pricing table) so I need to get the feature name in singular and plural form.
+ const prompt = `I'm building an entitlement system and my users can create features on my platform. I also help with displaying components (like pricing table) so I need to get the feature name in singular and plural form.
Based on the feature name passed in, please generate a singular and plural form, in lowercase.
@@ -20,14 +20,14 @@ export const generateFeatureDisplay = async (feature: Feature) => {
`;
- let { object } = await generateObject({
- model: anthropic("claude-3-5-sonnet-latest"),
- schema: z.object({
- singular: z.string(),
- plural: z.string(),
- }),
- prompt,
- });
+ let { object } = await generateObject({
+ model: anthropic("claude-3-5-sonnet-latest"),
+ schema: z.object({
+ singular: z.string(),
+ plural: z.string(),
+ }),
+ prompt,
+ });
- return object;
+ return object;
};
diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts
index a51df8e75..abb7333e7 100644
--- a/server/src/external/logtail/logtailUtils.ts
+++ b/server/src/external/logtail/logtailUtils.ts
@@ -6,100 +6,100 @@ import { initLogger } from "@/errors/logger.js";
const pinoLogger = initLogger();
const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
- function rewriteAppPath(str: string) {
- if (typeof str !== "string") return str;
- // Replace file:///app/ with ./
- str = str.replace("file:///app/", "./");
- return str.replace(/\/app\//g, "./");
- }
+ function rewriteAppPath(str: string) {
+ if (typeof str !== "string") return str;
+ // Replace file:///app/ with ./
+ str = str.replace("file:///app/", "./");
+ return str.replace(/\/app\//g, "./");
+ }
- function rewriteErrorStack(error: Error) {
- if (error instanceof Error && typeof error.stack === "string") {
- const newError = new Error(error.message);
- newError.stack = rewriteAppPath(error.stack);
- return newError;
- }
+ function rewriteErrorStack(error: Error) {
+ if (error instanceof Error && typeof error.stack === "string") {
+ const newError = new Error(error.message);
+ newError.stack = rewriteAppPath(error.stack);
+ return newError;
+ }
- return error;
- }
+ return error;
+ }
- return (...args: any[]) => {
- let message = "";
- let mergedObj = {};
+ return (...args: any[]) => {
+ let message = "";
+ let mergedObj = {};
- // Helper function to convert Error objects to plain objects
+ // Helper function to convert Error objects to plain objects
- const strings = args
- .filter((arg) => typeof arg === "string")
- .map(rewriteAppPath);
+ const strings = args
+ .filter((arg) => typeof arg === "string")
+ .map(rewriteAppPath);
- const objects = args
- .filter((arg) => typeof arg !== "string" && arg !== null)
- .map((obj) => (obj instanceof Error ? rewriteErrorStack(obj) : obj));
+ const objects = args
+ .filter((arg) => typeof arg !== "string" && arg !== null)
+ .map((obj) => (obj instanceof Error ? rewriteErrorStack(obj) : obj));
- // Use last string as message, or use Error message if no strings provided
- if (strings.length > 0) {
- message = strings[strings.length - 1];
- } else {
- // If no string message but we have an Error object, use its stack trace
- const errorObject = args.find((arg) => arg instanceof Error);
- if (errorObject) {
- message = rewriteAppPath(
- errorObject.stack || errorObject.message || "Error occurred"
- );
- }
- }
+ // Use last string as message, or use Error message if no strings provided
+ if (strings.length > 0) {
+ message = strings[strings.length - 1];
+ } else {
+ // If no string message but we have an Error object, use its stack trace
+ const errorObject = args.find((arg) => arg instanceof Error);
+ if (errorObject) {
+ message = rewriteAppPath(
+ errorObject.stack || errorObject.message || "Error occurred",
+ );
+ }
+ }
- // Merge all objects
- mergedObj = Object.assign({}, ...objects);
+ // Merge all objects
+ mergedObj = Object.assign({}, ...objects);
- // Pino format: object first, message second (if object exists)
- if (Object.keys(mergedObj).length > 0) {
- pinoMethod(mergedObj, message);
- } else {
- pinoMethod(message);
- }
+ // Pino format: object first, message second (if object exists)
+ if (Object.keys(mergedObj).length > 0) {
+ pinoMethod(mergedObj, message);
+ } else {
+ pinoMethod(message);
+ }
- if (!logtailMethod) {
- return;
- }
+ if (!logtailMethod) {
+ return;
+ }
- // Logtail format: message first, then object (if exists)
- if (Object.keys(mergedObj).length > 0) {
- logtailMethod(message, mergedObj);
- } else {
- logtailMethod(message);
- }
- };
+ // Logtail format: message first, then object (if exists)
+ if (Object.keys(mergedObj).length > 0) {
+ logtailMethod(message, mergedObj);
+ } else {
+ logtailMethod(message);
+ }
+ };
};
export const createLogger = () => {
- // Helper function to create logger structure recursively
- const createLoggerStructure = (basePinoLogger: any) => {
- return {
- debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)),
- info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)),
- warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)),
- error: createLogMethod(basePinoLogger.error.bind(basePinoLogger)),
- child: ({
- context,
- onlyProd = false,
- }: {
- context: any;
- onlyProd?: boolean;
- }) => {
- if (onlyProd && process.env.NODE_ENV !== "production") {
- return createLoggerStructure(basePinoLogger);
- }
+ // Helper function to create logger structure recursively
+ const createLoggerStructure = (basePinoLogger: any) => {
+ return {
+ debug: createLogMethod(basePinoLogger.debug.bind(basePinoLogger)),
+ info: createLogMethod(basePinoLogger.info.bind(basePinoLogger)),
+ warn: createLogMethod(basePinoLogger.warn.bind(basePinoLogger)),
+ error: createLogMethod(basePinoLogger.error.bind(basePinoLogger)),
+ child: ({
+ context,
+ onlyProd = false,
+ }: {
+ context: any;
+ onlyProd?: boolean;
+ }) => {
+ if (onlyProd && process.env.NODE_ENV !== "production") {
+ return createLoggerStructure(basePinoLogger);
+ }
- const childPinoLogger = basePinoLogger.child(context);
- return createLoggerStructure(childPinoLogger);
- },
- };
- };
+ const childPinoLogger = basePinoLogger.child(context);
+ return createLoggerStructure(childPinoLogger);
+ },
+ };
+ };
- // Create the root logger using the helper function
- return createLoggerStructure(pinoLogger);
+ // Create the root logger using the helper function
+ return createLoggerStructure(pinoLogger);
};
// export const createLogtailAll = () => {
diff --git a/server/src/external/posthog/createPosthogCli.ts b/server/src/external/posthog/createPosthogCli.ts
index d90a599d0..c74a02cb7 100644
--- a/server/src/external/posthog/createPosthogCli.ts
+++ b/server/src/external/posthog/createPosthogCli.ts
@@ -5,12 +5,12 @@ import { PostHog } from "posthog-node";
import { logger } from "../logtail/logtailUtils.js";
export const createPosthogCli = () => {
- if (!process.env.POSTHOG_API_KEY) {
- logger.warn("POSTHOG_API_KEY not set, skipping posthog");
- return null;
- }
+ if (!process.env.POSTHOG_API_KEY) {
+ logger.warn("POSTHOG_API_KEY not set, skipping posthog");
+ return null;
+ }
- return new PostHog(process.env.POSTHOG_API_KEY, {
- host: process.env.POSTHOG_HOST_URL ?? "https://us.i.posthog.com",
- });
+ return new PostHog(process.env.POSTHOG_API_KEY, {
+ host: process.env.POSTHOG_HOST_URL ?? "https://us.i.posthog.com",
+ });
};
diff --git a/server/src/external/posthog/posthogCapture.ts b/server/src/external/posthog/posthogCapture.ts
index 2ab1231c2..4a75378e5 100644
--- a/server/src/external/posthog/posthogCapture.ts
+++ b/server/src/external/posthog/posthogCapture.ts
@@ -1,20 +1,20 @@
import { EventMessage, PostHog } from "posthog-node";
export const posthogCapture = ({
- posthog,
- params,
+ posthog,
+ params,
}: {
- posthog?: PostHog;
- params: EventMessage;
+ posthog?: PostHog;
+ params: EventMessage;
}) => {
- try {
- if (process.env.NODE_ENV === "development" || !posthog) {
- return;
- }
+ try {
+ if (process.env.NODE_ENV === "development" || !posthog) {
+ return;
+ }
- posthog.capture(params);
- } catch (error) {
- console.error("Failed to capture posthog event", params);
- console.error(error);
- }
+ posthog.capture(params);
+ } catch (error) {
+ console.error("Failed to capture posthog event", params);
+ console.error(error);
+ }
};
diff --git a/server/src/external/redis/redisUtils.ts b/server/src/external/redis/redisUtils.ts
index fd79f3071..577b42ba1 100644
--- a/server/src/external/redis/redisUtils.ts
+++ b/server/src/external/redis/redisUtils.ts
@@ -3,118 +3,118 @@ import { QueueManager } from "@/queue/QueueManager.js";
import RecaseError from "@/utils/errorUtils.js";
export const handleAttachRaceCondition = async ({
- req,
- res,
+ req,
+ res,
}: {
- req: any;
- res: any;
+ req: any;
+ res: any;
}) => {
- const redisConn = await QueueManager.getConnection({ useBackup: false });
- const customerId = req.body.customer_id;
- const orgId = req.orgId;
- const env = req.env;
- try {
- const lockKey = `attach_${customerId}_${orgId}_${env}`;
- const existingLock = await redisConn.get(lockKey);
- if (existingLock) {
- throw new RecaseError({
- message: `Attach already runnning for customer ${customerId}, try again in a few seconds`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- // Create lock with 5 second timeout
- await redisConn.set(lockKey, "1", "PX", 5000, "NX");
+ const redisConn = await QueueManager.getConnection({ useBackup: false });
+ const customerId = req.body.customer_id;
+ const orgId = req.orgId;
+ const env = req.env;
+ try {
+ const lockKey = `attach_${customerId}_${orgId}_${env}`;
+ const existingLock = await redisConn.get(lockKey);
+ if (existingLock) {
+ throw new RecaseError({
+ message: `Attach already runnning for customer ${customerId}, try again in a few seconds`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ // Create lock with 5 second timeout
+ await redisConn.set(lockKey, "1", "PX", 5000, "NX");
- let originalJson = res.json;
- res.json = async function (body: any) {
- try {
- await clearLock({ lockKey, logger: req.logtail });
- } catch (error) {
- req.logtail.warn("❗️❗️ Error clearing lock", {
- error,
- });
- }
- originalJson.call(this, body);
- };
+ let originalJson = res.json;
+ res.json = async function (body: any) {
+ try {
+ await clearLock({ lockKey, logger: req.logtail });
+ } catch (error) {
+ req.logtail.warn("❗️❗️ Error clearing lock", {
+ error,
+ });
+ }
+ originalJson.call(this, body);
+ };
- return lockKey;
- } catch (error) {
- if (error instanceof RecaseError) {
- throw error;
- }
+ return lockKey;
+ } catch (error) {
+ if (error instanceof RecaseError) {
+ throw error;
+ }
- req.logtail.warn("❗️❗️ Error acquiring lock", {
- error,
- });
- return null;
- }
+ req.logtail.warn("❗️❗️ Error acquiring lock", {
+ error,
+ });
+ return null;
+ }
};
export const handleCustomerRaceCondition = async ({
- action,
- customerId,
- orgId,
- env,
- res,
- logger,
+ action,
+ customerId,
+ orgId,
+ env,
+ res,
+ logger,
}: {
- action: any;
- customerId: string;
- orgId: string;
- env: string;
- res: any;
- logger: any;
+ action: any;
+ customerId: string;
+ orgId: string;
+ env: string;
+ res: any;
+ logger: any;
}) => {
- const redisConn = await QueueManager.getConnection({ useBackup: false });
- try {
- const lockKey = `${action}_${customerId}_${orgId}_${env}`;
- const existingLock = await redisConn.get(lockKey);
- if (existingLock) {
- throw new RecaseError({
- message: `Action ${action} already running for customer ${customerId}, try again in a few seconds`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- // Create lock with 5 second timeout
- await redisConn.set(lockKey, "1", "PX", 5000, "NX");
+ const redisConn = await QueueManager.getConnection({ useBackup: false });
+ try {
+ const lockKey = `${action}_${customerId}_${orgId}_${env}`;
+ const existingLock = await redisConn.get(lockKey);
+ if (existingLock) {
+ throw new RecaseError({
+ message: `Action ${action} already running for customer ${customerId}, try again in a few seconds`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ // Create lock with 5 second timeout
+ await redisConn.set(lockKey, "1", "PX", 5000, "NX");
- let originalJson = res.json;
- res.json = async function (body: any) {
- try {
- await clearLock({ lockKey, logger });
- } catch (error) {
- logger.warn("❗️❗️ Error clearing lock");
- logger.warn(error);
- }
- originalJson.call(this, body);
- };
+ let originalJson = res.json;
+ res.json = async function (body: any) {
+ try {
+ await clearLock({ lockKey, logger });
+ } catch (error) {
+ logger.warn("❗️❗️ Error clearing lock");
+ logger.warn(error);
+ }
+ originalJson.call(this, body);
+ };
- return lockKey;
- } catch (error) {
- if (error instanceof RecaseError) {
- throw error;
- }
+ return lockKey;
+ } catch (error) {
+ if (error instanceof RecaseError) {
+ throw error;
+ }
- logger.warn("❗️❗️ Error acquiring lock");
- logger.warn(error);
- return null;
- }
+ logger.warn("❗️❗️ Error acquiring lock");
+ logger.warn(error);
+ return null;
+ }
};
export const clearLock = async ({
- lockKey,
- logger,
+ lockKey,
+ logger,
}: {
- lockKey: string;
- logger: any;
+ lockKey: string;
+ logger: any;
}) => {
- try {
- const redisConn = await QueueManager.getConnection({ useBackup: false });
- await redisConn.del(lockKey);
- } catch (error) {
- logger.warn("❗️❗️ Error clearing lock");
- logger.warn(error);
- }
+ try {
+ const redisConn = await QueueManager.getConnection({ useBackup: false });
+ await redisConn.del(lockKey);
+ } catch (error) {
+ logger.warn("❗️❗️ Error clearing lock");
+ logger.warn(error);
+ }
};
diff --git a/server/src/external/redis/stripeWebhookLocks.ts b/server/src/external/redis/stripeWebhookLocks.ts
index 7f9c57baf..4c5ac0792 100644
--- a/server/src/external/redis/stripeWebhookLocks.ts
+++ b/server/src/external/redis/stripeWebhookLocks.ts
@@ -1,40 +1,40 @@
import { QueueManager } from "@/queue/QueueManager.js";
export const getWebhookLock = async ({
- lockKey,
- logger,
+ lockKey,
+ logger,
}: {
- lockKey: string;
- logger: any;
+ lockKey: string;
+ logger: any;
}) => {
- const redisConn = await QueueManager.getConnection({ useBackup: false });
- try {
- const existingLock = await redisConn.get(lockKey);
- if (existingLock) {
- return false;
- }
- // Create lock with 5 second timeout
- await redisConn.set(lockKey, "1", "PX", 5000, "NX");
- return true;
- } catch (error) {
- logger.error("❗️❗️ Error acquiring lock");
- logger.error(error);
- return false;
- }
+ const redisConn = await QueueManager.getConnection({ useBackup: false });
+ try {
+ const existingLock = await redisConn.get(lockKey);
+ if (existingLock) {
+ return false;
+ }
+ // Create lock with 5 second timeout
+ await redisConn.set(lockKey, "1", "PX", 5000, "NX");
+ return true;
+ } catch (error) {
+ logger.error("❗️❗️ Error acquiring lock");
+ logger.error(error);
+ return false;
+ }
};
export const releaseWebhookLock = async ({
- lockKey,
- logger,
+ lockKey,
+ logger,
}: {
- lockKey: string;
- logger: any;
+ lockKey: string;
+ logger: any;
}) => {
- try {
- const redisConn = await QueueManager.getConnection({ useBackup: false });
- await redisConn.del(lockKey);
- } catch (error) {
- logger.error("❗️❗️ Error releasing lock");
- logger.error(error);
- }
+ try {
+ const redisConn = await QueueManager.getConnection({ useBackup: false });
+ await redisConn.del(lockKey);
+ } catch (error) {
+ logger.error("❗️❗️ Error releasing lock");
+ logger.error(error);
+ }
};
diff --git a/server/src/external/resend/loopsUtils.ts b/server/src/external/resend/loopsUtils.ts
index 52d0abf55..33bdba40b 100644
--- a/server/src/external/resend/loopsUtils.ts
+++ b/server/src/external/resend/loopsUtils.ts
@@ -3,27 +3,27 @@ import { logger } from "../logtail/logtailUtils.js";
import { User } from "better-auth";
const createLoopsCli = () => {
- return new LoopsClient(process.env.LOOPS_API_KEY || "");
+ return new LoopsClient(process.env.LOOPS_API_KEY || "");
};
export const createLoopsContact = async (user: User) => {
- if (!process.env.LOOPS_API_KEY) return;
+ if (!process.env.LOOPS_API_KEY) return;
- try {
- let email = user.email;
- let firstName = user.name?.split(" ")[0] || "";
- let lastName = user.name?.split(" ")[1] || "";
- const loops = createLoopsCli();
+ try {
+ let email = user.email;
+ let firstName = user.name?.split(" ")[0] || "";
+ let lastName = user.name?.split(" ")[1] || "";
+ const loops = createLoopsCli();
- const resp = await loops.createContact(email, {
- firstName,
- lastName,
- });
+ const resp = await loops.createContact(email, {
+ firstName,
+ lastName,
+ });
- return resp;
- } catch (error) {
- logger.error("Error creating loops contact", { error });
- }
+ return resp;
+ } catch (error) {
+ logger.error("Error creating loops contact", { error });
+ }
};
export { createLoopsCli };
diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts
index 28d74ccab..371808e85 100644
--- a/server/src/external/resend/resendUtils.ts
+++ b/server/src/external/resend/resendUtils.ts
@@ -2,74 +2,74 @@ import { Resend } from "resend";
import { logger } from "../logtail/logtailUtils.js";
export interface ResendEmailProps {
- to: string;
- subject: string;
- body: string;
- from: string;
- fromEmail?: string;
- replyTo?: string;
+ to: string;
+ subject: string;
+ body: string;
+ from: string;
+ fromEmail?: string;
+ replyTo?: string;
}
export const createResendCli = () => {
- return new Resend(process.env.RESEND_API_KEY);
+ return new Resend(process.env.RESEND_API_KEY);
};
export const sendTextEmail = async ({
- from,
- to,
- subject,
- body,
+ from,
+ to,
+ subject,
+ body,
}: ResendEmailProps) => {
- const resend = createResendCli();
+ const resend = createResendCli();
- try {
- logger.info(`Sending email to ${to} with subject ${subject}`);
- const { data, error } = await resend.emails.send({
- from: from,
- to: to,
- subject: subject,
- text: body,
- });
+ try {
+ logger.info(`Sending email to ${to} with subject ${subject}`);
+ const { data, error } = await resend.emails.send({
+ from: from,
+ to: to,
+ subject: subject,
+ text: body,
+ });
- if (error) {
- logger.error(`Error sending email`, {
- error,
- data: {
- from,
- to,
- subject,
- body,
- },
- });
- }
- } catch (error) {
- logger.error(`Error sending email`, {
- error,
- data: {
- from,
- to,
- subject,
- body,
- },
- });
- throw error;
- }
+ if (error) {
+ logger.error(`Error sending email`, {
+ error,
+ data: {
+ from,
+ to,
+ subject,
+ body,
+ },
+ });
+ }
+ } catch (error) {
+ logger.error(`Error sending email`, {
+ error,
+ data: {
+ from,
+ to,
+ subject,
+ body,
+ },
+ });
+ throw error;
+ }
};
export const sendHtmlEmail = async ({
- from,
- to,
- subject,
- body,
- replyTo,
+ from,
+ to,
+ subject,
+ body,
+ replyTo,
}: ResendEmailProps) => {
- const resend = createResendCli();
+ const resend = createResendCli();
- await resend.emails.send({
- from: from,
- to: to,
- subject: subject,
- html: body,
- replyTo,
- });
+ await resend.emails.send({
+ from: from,
+ to: to,
+ subject: subject,
+ html: body,
+ replyTo,
+ });
};
diff --git a/server/src/external/resend/safeResend.ts b/server/src/external/resend/safeResend.ts
index d48e626d3..f6725569f 100644
--- a/server/src/external/resend/safeResend.ts
+++ b/server/src/external/resend/safeResend.ts
@@ -1,23 +1,23 @@
import { logger } from "../logtail/logtailUtils.js";
export function safeResend any>({
- fn,
- action,
+ fn,
+ action,
}: {
- fn: T;
- action: string;
+ fn: T;
+ action: string;
}): (...args: Parameters) => Promise | undefined> {
- return async (...args: Parameters) => {
- if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
- logger.warn(
- `RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
- );
- return;
- }
- try {
- return await fn(...args);
- } catch (error) {
- logger.error(`Error ${action}: ${error}`);
- }
- };
+ return async (...args: Parameters) => {
+ if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
+ logger.warn(
+ `RESEND_API_KEY or RESEND_DOMAIN is not set, skipping ${action}`,
+ );
+ return;
+ }
+ try {
+ return await fn(...args);
+ } catch (error) {
+ logger.error(`Error ${action}: ${error}`);
+ }
+ };
}
diff --git a/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts b/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts
index 05562fb93..f51e4bca6 100644
--- a/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts
+++ b/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts
@@ -1,16 +1,16 @@
import {
- getBillingType,
- getPriceEntitlement,
+ getBillingType,
+ getPriceEntitlement,
} from "@/internal/products/prices/priceUtils.js";
import {
- Price,
- UsagePriceConfig,
- TierInfinite,
- EntitlementWithFeature,
- Organization,
- Product,
- BillingInterval,
- BillingType,
+ Price,
+ UsagePriceConfig,
+ TierInfinite,
+ EntitlementWithFeature,
+ Organization,
+ Product,
+ BillingInterval,
+ BillingType,
} from "@autumn/shared";
import Stripe from "stripe";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
@@ -20,218 +20,218 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { Decimal } from "decimal.js";
export interface StripeMeteredPriceParams {
- db: DrizzleCli;
- stripeCli: Stripe;
- price: Price;
- entitlements: EntitlementWithFeature[];
- product: Product;
- org: Organization;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ price: Price;
+ entitlements: EntitlementWithFeature[];
+ product: Product;
+ org: Organization;
}
export const createStripeMeteredPrice = async ({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
}: StripeMeteredPriceParams) => {
- const config = price.config as UsagePriceConfig;
- const ent = getPriceEntitlement(price, entitlements);
- const feature = ent.feature;
+ const config = price.config as UsagePriceConfig;
+ const ent = getPriceEntitlement(price, entitlements);
+ const feature = ent.feature;
- let meter;
- try {
- meter = await stripeCli.billing.meters.create({
- display_name: `${product.name} - ${feature!.name}`,
- event_name: price.id!,
- default_aggregation: {
- formula: "sum",
- },
- });
- } catch (error: any) {
- const meters = await stripeCli.billing.meters.list({
- limit: 100,
- status: "active",
- });
- meter = meters.data.find((m) => m.event_name == price.id!);
- if (!meter) {
- throw error;
- }
- }
+ let meter;
+ try {
+ meter = await stripeCli.billing.meters.create({
+ display_name: `${product.name} - ${feature!.name}`,
+ event_name: price.id!,
+ default_aggregation: {
+ formula: "sum",
+ },
+ });
+ } catch (error: any) {
+ const meters = await stripeCli.billing.meters.list({
+ limit: 100,
+ status: "active",
+ });
+ meter = meters.data.find((m) => m.event_name == price.id!);
+ if (!meter) {
+ throw error;
+ }
+ }
- const tiers = priceToInArrearTiers(price, ent);
+ const tiers = priceToInArrearTiers(price, ent);
- let priceAmountData = {};
- if (ent.allowance == 0 && tiers.length == 1) {
- priceAmountData = {
- unit_amount_decimal: tiers[0].unit_amount_decimal,
- };
- } else {
- priceAmountData = {
- billing_scheme: "tiered",
- tiers_mode: "graduated",
- tiers: tiers,
- };
- }
+ let priceAmountData = {};
+ if (ent.allowance == 0 && tiers.length == 1) {
+ priceAmountData = {
+ unit_amount_decimal: tiers[0].unit_amount_decimal,
+ };
+ } else {
+ priceAmountData = {
+ billing_scheme: "tiered",
+ tiers_mode: "graduated",
+ tiers: tiers,
+ };
+ }
- let productData = {};
- if (config.stripe_product_id) {
- productData = {
- product: config.stripe_product_id,
- };
- } else {
- productData = {
- product_data: {
- name: `${product.name} - ${feature!.name}`,
- },
- };
- }
+ let productData = {};
+ if (config.stripe_product_id) {
+ productData = {
+ product: config.stripe_product_id,
+ };
+ } else {
+ productData = {
+ product_data: {
+ name: `${product.name} - ${feature!.name}`,
+ },
+ };
+ }
- const stripePrice = await stripeCli.prices.create({
- ...productData,
- ...priceAmountData,
- currency: org.default_currency || "usd",
- nickname: `Autumn Price (${feature!.name}) [Placeholder]`,
- recurring: {
- ...(billingIntervalToStripe({
- interval: price.config!.interval,
- intervalCount: price.config!.interval_count,
- }) as any),
- meter: meter!.id,
- usage_type: "metered",
- },
- });
+ const stripePrice = await stripeCli.prices.create({
+ ...productData,
+ ...priceAmountData,
+ currency: org.default_currency || "usd",
+ nickname: `Autumn Price (${feature!.name}) [Placeholder]`,
+ recurring: {
+ ...(billingIntervalToStripe({
+ interval: price.config!.interval,
+ intervalCount: price.config!.interval_count,
+ }) as any),
+ meter: meter!.id,
+ usage_type: "metered",
+ },
+ });
- return stripePrice;
+ return stripePrice;
};
export const arrearProratedToStripeTiers = (
- price: Price,
- entitlement: EntitlementWithFeature
+ price: Price,
+ entitlement: EntitlementWithFeature,
) => {
- let usageConfig = structuredClone(price.config) as UsagePriceConfig;
+ let usageConfig = structuredClone(price.config) as UsagePriceConfig;
- const billingUnits = usageConfig.billing_units;
- const numFree = entitlement.allowance
- ? Math.round(entitlement.allowance! / billingUnits!)
- : 0;
+ const billingUnits = usageConfig.billing_units;
+ const numFree = entitlement.allowance
+ ? Math.round(entitlement.allowance! / billingUnits!)
+ : 0;
- const tiers: any[] = [];
+ const tiers: any[] = [];
- if (numFree > 0) {
- tiers.push({
- unit_amount_decimal: 0,
- up_to: numFree,
- });
- }
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- const tier = usageConfig.usage_tiers[i];
- // const amount = tier.amount * 100;
- const amount = new Decimal(tier.amount).mul(100).toNumber();
- const upTo =
- tier.to == -1 || tier.to == TierInfinite
- ? "inf"
- : Math.round((tier.to - numFree) / billingUnits!) + numFree;
+ if (numFree > 0) {
+ tiers.push({
+ unit_amount_decimal: 0,
+ up_to: numFree,
+ });
+ }
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ const tier = usageConfig.usage_tiers[i];
+ // const amount = tier.amount * 100;
+ const amount = new Decimal(tier.amount).mul(100).toNumber();
+ const upTo =
+ tier.to == -1 || tier.to == TierInfinite
+ ? "inf"
+ : Math.round((tier.to - numFree) / billingUnits!) + numFree;
- tiers.push({
- unit_amount_decimal: amount,
- up_to: upTo,
- });
- }
+ tiers.push({
+ unit_amount_decimal: amount,
+ up_to: upTo,
+ });
+ }
- return tiers;
+ return tiers;
};
export const createStripeArrearProrated = async ({
- db,
- price,
- product,
- org,
- entitlements,
- curStripeProd,
- stripeCli,
+ db,
+ price,
+ product,
+ org,
+ entitlements,
+ curStripeProd,
+ stripeCli,
}: {
- db: DrizzleCli;
- price: Price;
- product: Product;
- org: Organization;
- entitlements: EntitlementWithFeature[];
- curStripeProd: Stripe.Product | null;
- stripeCli: Stripe;
+ db: DrizzleCli;
+ price: Price;
+ product: Product;
+ org: Organization;
+ entitlements: EntitlementWithFeature[];
+ curStripeProd: Stripe.Product | null;
+ stripeCli: Stripe;
}) => {
- const relatedEnt = getPriceEntitlement(price, entitlements);
+ const relatedEnt = getPriceEntitlement(price, entitlements);
- let recurringData = undefined;
- if (price.config!.interval != BillingInterval.OneOff) {
- recurringData = billingIntervalToStripe({
- interval: price.config!.interval,
- intervalCount: price.config!.interval_count,
- });
- }
+ let recurringData = undefined;
+ if (price.config!.interval != BillingInterval.OneOff) {
+ recurringData = billingIntervalToStripe({
+ interval: price.config!.interval,
+ intervalCount: price.config!.interval_count,
+ });
+ }
- const config = price.config as UsagePriceConfig;
+ const config = price.config as UsagePriceConfig;
- // 1. Product name
- let productName = `${product.name} - ${
- config.billing_units == 1 ? "" : `${config.billing_units} `
- }${relatedEnt.feature.name}`;
+ // 1. Product name
+ let productName = `${product.name} - ${
+ config.billing_units == 1 ? "" : `${config.billing_units} `
+ }${relatedEnt.feature.name}`;
- let productData = curStripeProd
- ? { product: curStripeProd.id }
- : {
- product_data: {
- name: productName,
- },
- };
+ let productData = curStripeProd
+ ? { product: curStripeProd.id }
+ : {
+ product_data: {
+ name: productName,
+ },
+ };
- // let tiers = arrearProratedToStripeTiers(price, relatedEnt);
- let tiers = priceToInArrearTiers(price, relatedEnt);
+ // let tiers = arrearProratedToStripeTiers(price, relatedEnt);
+ let tiers = priceToInArrearTiers(price, relatedEnt);
- let priceAmountData = {};
- if (tiers.length == 1) {
- priceAmountData = {
- unit_amount_decimal: tiers[0].unit_amount_decimal,
- };
- } else {
- priceAmountData = {
- billing_scheme: "tiered",
- tiers_mode: "graduated",
- tiers: tiers,
- };
- }
+ let priceAmountData = {};
+ if (tiers.length == 1) {
+ priceAmountData = {
+ unit_amount_decimal: tiers[0].unit_amount_decimal,
+ };
+ } else {
+ priceAmountData = {
+ billing_scheme: "tiered",
+ tiers_mode: "graduated",
+ tiers: tiers,
+ };
+ }
- let stripePrice = await stripeCli.prices.create({
- ...productData,
- currency: org.default_currency || "usd",
- ...priceAmountData,
- recurring: {
- ...(recurringData as any),
- },
- nickname: `Autumn Price (${relatedEnt.feature.name})`,
- });
+ let stripePrice = await stripeCli.prices.create({
+ ...productData,
+ currency: org.default_currency || "usd",
+ ...priceAmountData,
+ recurring: {
+ ...(recurringData as any),
+ },
+ nickname: `Autumn Price (${relatedEnt.feature.name})`,
+ });
- config.stripe_price_id = stripePrice.id;
- config.stripe_product_id = stripePrice.product as string;
- let billingType = getBillingType(price.config!);
+ config.stripe_price_id = stripePrice.id;
+ config.stripe_product_id = stripePrice.product as string;
+ let billingType = getBillingType(price.config!);
- // CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING
- if (billingType == BillingType.InArrearProrated) {
- let placeholderPrice = await createStripeMeteredPrice({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- });
- config.stripe_placeholder_price_id = placeholderPrice.id;
- }
+ // CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING
+ if (billingType == BillingType.InArrearProrated) {
+ let placeholderPrice = await createStripeMeteredPrice({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ });
+ config.stripe_placeholder_price_id = placeholderPrice.id;
+ }
- price.config = config;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
+ price.config = config;
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
};
diff --git a/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts b/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts
index a37118b80..cfcdb84e6 100644
--- a/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts
+++ b/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts
@@ -7,41 +7,41 @@ import { Decimal } from "decimal.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const createStripeFixedPrice = async ({
- db,
- stripeCli,
- price,
- product,
- org,
+ db,
+ stripeCli,
+ price,
+ product,
+ org,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- price: Price;
- product: Product;
- org: Organization;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ price: Price;
+ product: Product;
+ org: Organization;
}) => {
- const config = price.config as FixedPriceConfig;
+ const config = price.config as FixedPriceConfig;
- let amount = new Decimal(config.amount).mul(100).toNumber();
+ let amount = new Decimal(config.amount).mul(100).toNumber();
- const stripePrice = await stripeCli.prices.create({
- product: product.processor!.id,
- unit_amount: amount,
- currency: org.default_currency!,
- recurring: {
- ...(billingIntervalToStripe({
- interval: config.interval,
- intervalCount: config.interval_count,
- }) as any),
- },
+ const stripePrice = await stripeCli.prices.create({
+ product: product.processor!.id,
+ unit_amount: amount,
+ currency: org.default_currency!,
+ recurring: {
+ ...(billingIntervalToStripe({
+ interval: config.interval,
+ intervalCount: config.interval_count,
+ }) as any),
+ },
- nickname: `Autumn Price (Fixed)`,
- });
+ nickname: `Autumn Price (Fixed)`,
+ });
- config.stripe_price_id = stripePrice.id;
+ config.stripe_price_id = stripePrice.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
};
diff --git a/server/src/external/stripe/createStripePrice/createStripeInArrear.ts b/server/src/external/stripe/createStripePrice/createStripeInArrear.ts
index 64d898f47..0f47d0ba1 100644
--- a/server/src/external/stripe/createStripePrice/createStripeInArrear.ts
+++ b/server/src/external/stripe/createStripePrice/createStripeInArrear.ts
@@ -1,15 +1,15 @@
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import {
- Product,
- Price,
- Organization,
- EntitlementWithFeature,
- UsagePriceConfig,
- Feature,
- TierInfinite,
- Entitlement,
- ErrCode,
+ Product,
+ Price,
+ Organization,
+ EntitlementWithFeature,
+ UsagePriceConfig,
+ Feature,
+ TierInfinite,
+ Entitlement,
+ ErrCode,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
@@ -20,259 +20,259 @@ import { StatusCodes } from "http-status-codes";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const searchStripeMeter = async ({
- stripeCli,
- eventName,
- meterId,
- logger,
+ stripeCli,
+ eventName,
+ meterId,
+ logger,
}: {
- stripeCli: Stripe;
- eventName: string;
- meterId?: string;
- logger: any;
+ stripeCli: Stripe;
+ eventName: string;
+ meterId?: string;
+ logger: any;
}) => {
- let allStripeMeters = [];
- let hasMore = true;
- let startingAfter;
+ let allStripeMeters = [];
+ let hasMore = true;
+ let startingAfter;
- const start = performance.now();
- while (hasMore) {
- const response: any = await stripeCli.billing.meters.list({
- limit: 100,
- status: "active",
- starting_after: startingAfter,
- });
+ const start = performance.now();
+ while (hasMore) {
+ const response: any = await stripeCli.billing.meters.list({
+ limit: 100,
+ status: "active",
+ starting_after: startingAfter,
+ });
- allStripeMeters.push(...response.data);
- hasMore = response.has_more;
+ allStripeMeters.push(...response.data);
+ hasMore = response.has_more;
- if (hasMore && response.data.length > 0) {
- startingAfter = response.data[response.data.length - 1].id;
- }
- }
- const end = performance.now();
- logger.info(`Stripe meter list took ${end - start}ms`);
+ if (hasMore && response.data.length > 0) {
+ startingAfter = response.data[response.data.length - 1].id;
+ }
+ }
+ const end = performance.now();
+ logger.info(`Stripe meter list took ${end - start}ms`);
- let stripeMeter = allStripeMeters.find(
- (m) => m.event_name == eventName || m.id == meterId
- );
+ let stripeMeter = allStripeMeters.find(
+ (m) => m.event_name == eventName || m.id == meterId,
+ );
- return stripeMeter;
+ return stripeMeter;
};
export const getStripeMeter = async ({
- product,
- feature,
- stripeCli,
- price,
- logger,
+ product,
+ feature,
+ stripeCli,
+ price,
+ logger,
}: {
- product: Product;
- feature: Feature;
- stripeCli: Stripe;
- price: Price;
- logger: any;
+ product: Product;
+ feature: Feature;
+ stripeCli: Stripe;
+ price: Price;
+ logger: any;
}) => {
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- let createNew = false;
- try {
- let stripeMeter = await searchStripeMeter({
- stripeCli,
- eventName: price.id!,
- meterId: config.stripe_meter_id!,
- logger,
- });
+ let createNew = false;
+ try {
+ let stripeMeter = await searchStripeMeter({
+ stripeCli,
+ eventName: price.id!,
+ meterId: config.stripe_meter_id!,
+ logger,
+ });
- if (!stripeMeter) {
- createNew = true;
- } else {
- logger.info(
- `✅ Found existing meter for ${product.name} - ${feature!.name}`
- );
- return stripeMeter;
- }
- } catch (error) {
- createNew = true;
- }
- let meter = await stripeCli.billing.meters.create({
- display_name: `${product.name} - ${feature!.name}`,
- event_name: price.id!,
- default_aggregation: {
- formula: "sum",
- },
- });
- return meter;
+ if (!stripeMeter) {
+ createNew = true;
+ } else {
+ logger.info(
+ `✅ Found existing meter for ${product.name} - ${feature!.name}`,
+ );
+ return stripeMeter;
+ }
+ } catch (error) {
+ createNew = true;
+ }
+ let meter = await stripeCli.billing.meters.create({
+ display_name: `${product.name} - ${feature!.name}`,
+ event_name: price.id!,
+ default_aggregation: {
+ formula: "sum",
+ },
+ });
+ return meter;
};
// IN ARREAR
export const priceToInArrearTiers = (
- price: Price,
- entitlement: Entitlement
+ price: Price,
+ entitlement: Entitlement,
) => {
- let usageConfig = structuredClone(price.config) as UsagePriceConfig;
- const tiers: any[] = [];
- if (entitlement.allowance) {
- tiers.push({
- unit_amount: 0,
- up_to: entitlement.allowance,
- });
+ let usageConfig = structuredClone(price.config) as UsagePriceConfig;
+ const tiers: any[] = [];
+ if (entitlement.allowance) {
+ tiers.push({
+ unit_amount: 0,
+ up_to: entitlement.allowance,
+ });
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- let tier = usageConfig.usage_tiers[i];
- if (tier.to != -1 && tier.to != TierInfinite) {
- usageConfig.usage_tiers[i].to = (tier.to || 0) + entitlement.allowance;
- }
- }
- }
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ let tier = usageConfig.usage_tiers[i];
+ if (tier.to != -1 && tier.to != TierInfinite) {
+ usageConfig.usage_tiers[i].to = (tier.to || 0) + entitlement.allowance;
+ }
+ }
+ }
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- const tier = usageConfig.usage_tiers[i];
- let amount = new Decimal(tier.amount)
- .div(usageConfig.billing_units ?? 1)
- .mul(100)
- .toDecimalPlaces(10)
- .toString();
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ const tier = usageConfig.usage_tiers[i];
+ let amount = new Decimal(tier.amount)
+ .div(usageConfig.billing_units ?? 1)
+ .mul(100)
+ .toDecimalPlaces(10)
+ .toString();
- tiers.push({
- unit_amount_decimal: amount,
- up_to: tier.to == -1 ? "inf" : tier.to,
- });
- }
+ tiers.push({
+ unit_amount_decimal: amount,
+ up_to: tier.to == -1 ? "inf" : tier.to,
+ });
+ }
- return tiers;
+ return tiers;
};
export const createStripeInArrearPrice = async ({
- db,
- stripeCli,
- product,
- price,
- entitlements,
- org,
- logger,
- curStripePrice,
- curStripeProduct,
- internalEntityId,
- useCheckout = false,
+ db,
+ stripeCli,
+ product,
+ price,
+ entitlements,
+ org,
+ logger,
+ curStripePrice,
+ curStripeProduct,
+ internalEntityId,
+ useCheckout = false,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- product: Product;
- price: Price;
- org: Organization;
- entitlements: EntitlementWithFeature[];
- logger: any;
- curStripePrice?: Stripe.Price | null;
- curStripeProduct?: Stripe.Product | null;
- internalEntityId?: string;
- useCheckout?: boolean;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ product: Product;
+ price: Price;
+ org: Organization;
+ entitlements: EntitlementWithFeature[];
+ logger: any;
+ curStripePrice?: Stripe.Price | null;
+ curStripeProduct?: Stripe.Product | null;
+ internalEntityId?: string;
+ useCheckout?: boolean;
}) => {
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- // 1. Create meter
- let relatedEnt = getPriceEntitlement(price, entitlements);
- let feature = relatedEnt?.feature;
+ // 1. Create meter
+ let relatedEnt = getPriceEntitlement(price, entitlements);
+ let feature = relatedEnt?.feature;
- // 1. If internal entity ID and not curStripe product, create product
- if (internalEntityId && !useCheckout) {
- if (!curStripeProduct) {
- logger.info(
- `Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`
- );
- let stripeProduct = await stripeCli.products.create({
- name: `${product.name} - ${feature!.name}`,
- });
- config.stripe_product_id = stripeProduct.id;
+ // 1. If internal entity ID and not curStripe product, create product
+ if (internalEntityId && !useCheckout) {
+ if (!curStripeProduct) {
+ logger.info(
+ `Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`,
+ );
+ let stripeProduct = await stripeCli.products.create({
+ name: `${product.name} - ${feature!.name}`,
+ });
+ config.stripe_product_id = stripeProduct.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
- }
- return;
- }
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
+ }
+ return;
+ }
- // 2. If no internal entity ID, create Stripe price if not exists...
- if (curStripePrice) {
- return;
- }
+ // 2. If no internal entity ID, create Stripe price if not exists...
+ if (curStripePrice) {
+ return;
+ }
- logger.info(
- `Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`
- );
+ logger.info(
+ `Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`,
+ );
- if (!feature) {
- throw new RecaseError({
- message: `createStripeInArrearPrice: feature not found for price ${price.id}`,
- code: ErrCode.FeatureNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `createStripeInArrearPrice: feature not found for price ${price.id}`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- // 1. Get meter by event_name
- let meter = await getStripeMeter({
- product,
- feature,
- stripeCli,
- price,
- logger,
- });
+ // 1. Get meter by event_name
+ let meter = await getStripeMeter({
+ product,
+ feature,
+ stripeCli,
+ price,
+ logger,
+ });
- config.stripe_meter_id = meter.id;
+ config.stripe_meter_id = meter.id;
- const tiers = priceToInArrearTiers(
- price,
- getPriceEntitlement(price, entitlements)
- );
+ const tiers = priceToInArrearTiers(
+ price,
+ getPriceEntitlement(price, entitlements),
+ );
- let priceAmountData = {};
- if (tiers.length == 1) {
- priceAmountData = {
- unit_amount_decimal: tiers[0].unit_amount_decimal,
- };
- } else {
- priceAmountData = {
- billing_scheme: "tiered",
- tiers_mode: "graduated",
- tiers: tiers,
- };
- }
+ let priceAmountData = {};
+ if (tiers.length == 1) {
+ priceAmountData = {
+ unit_amount_decimal: tiers[0].unit_amount_decimal,
+ };
+ } else {
+ priceAmountData = {
+ billing_scheme: "tiered",
+ tiers_mode: "graduated",
+ tiers: tiers,
+ };
+ }
- let productData = {};
- if (config.stripe_product_id) {
- productData = {
- product: config.stripe_product_id,
- };
- } else {
- productData = {
- product_data: {
- name: `${product.name} - ${feature!.name}`,
- },
- };
- }
+ let productData = {};
+ if (config.stripe_product_id) {
+ productData = {
+ product: config.stripe_product_id,
+ };
+ } else {
+ productData = {
+ product_data: {
+ name: `${product.name} - ${feature!.name}`,
+ },
+ };
+ }
- const stripePrice = await stripeCli.prices.create({
- ...productData,
- ...priceAmountData,
- currency: org.default_currency!,
- recurring: {
- ...(billingIntervalToStripe({
- interval: price.config!.interval,
- intervalCount: price.config!.interval_count,
- }) as any),
- meter: meter!.id,
- usage_type: "metered",
- },
- nickname: `Autumn Price (${relatedEnt.feature.name})`,
- });
+ const stripePrice = await stripeCli.prices.create({
+ ...productData,
+ ...priceAmountData,
+ currency: org.default_currency!,
+ recurring: {
+ ...(billingIntervalToStripe({
+ interval: price.config!.interval,
+ intervalCount: price.config!.interval_count,
+ }) as any),
+ meter: meter!.id,
+ usage_type: "metered",
+ },
+ nickname: `Autumn Price (${relatedEnt.feature.name})`,
+ });
- config.stripe_price_id = stripePrice.id;
- config.stripe_product_id = stripePrice.product as string;
- config.stripe_meter_id = meter!.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
+ config.stripe_price_id = stripePrice.id;
+ config.stripe_product_id = stripePrice.product as string;
+ config.stripe_meter_id = meter!.id;
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
};
diff --git a/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts b/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts
index 3c1eedcc9..c859f5314 100644
--- a/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts
+++ b/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts
@@ -2,42 +2,42 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import {
- EntitlementWithFeature,
- Price,
- Product,
- UsagePriceConfig,
+ EntitlementWithFeature,
+ Price,
+ Product,
+ UsagePriceConfig,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
export const createStripeOneOffTieredProduct = async ({
- db,
- stripeCli,
- price,
- entitlements,
- product,
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- price: Price;
- entitlements: EntitlementWithFeature[];
- product: Product;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ price: Price;
+ entitlements: EntitlementWithFeature[];
+ product: Product;
}) => {
- let config = price.config as UsagePriceConfig;
- let relatedEnt = getPriceEntitlement(price, entitlements);
- let productName = `${product.name} - ${
- config.billing_units == 1 ? "" : `${config.billing_units} `
- }${relatedEnt.feature.name}`;
+ let config = price.config as UsagePriceConfig;
+ let relatedEnt = getPriceEntitlement(price, entitlements);
+ let productName = `${product.name} - ${
+ config.billing_units == 1 ? "" : `${config.billing_units} `
+ }${relatedEnt.feature.name}`;
- let stripeProduct = await stripeCli.products.create({
- name: productName,
- });
+ let stripeProduct = await stripeCli.products.create({
+ name: productName,
+ });
- config.stripe_product_id = stripeProduct.id;
+ config.stripe_product_id = stripeProduct.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
};
diff --git a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts
index 4736ca35b..1610d92e1 100644
--- a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts
+++ b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts
@@ -1,152 +1,152 @@
import {
- BillingInterval,
- BillingType,
- Entitlement,
- EntitlementWithFeature,
- Organization,
- Price,
- Product,
- TierInfinite,
- UsagePriceConfig,
+ BillingInterval,
+ BillingType,
+ Entitlement,
+ EntitlementWithFeature,
+ Organization,
+ Price,
+ Product,
+ TierInfinite,
+ UsagePriceConfig,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import {
- formatPrice,
- getBillingType,
- getPriceEntitlement,
+ formatPrice,
+ getBillingType,
+ getPriceEntitlement,
} from "@/internal/products/prices/priceUtils.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { Decimal } from "decimal.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const prepaidToStripeTiers = (
- price: Price,
- entitlement: EntitlementWithFeature
+ price: Price,
+ entitlement: EntitlementWithFeature,
) => {
- let usageConfig = structuredClone(price.config) as UsagePriceConfig;
+ let usageConfig = structuredClone(price.config) as UsagePriceConfig;
- const billingUnits = usageConfig.billing_units;
- // const numFree = entitlement.allowance
- // ? Math.round(entitlement.allowance! / billingUnits!)
- // : 0;
+ const billingUnits = usageConfig.billing_units;
+ // const numFree = entitlement.allowance
+ // ? Math.round(entitlement.allowance! / billingUnits!)
+ // : 0;
- const tiers: any[] = [];
+ const tiers: any[] = [];
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- const tier = usageConfig.usage_tiers[i];
- const amount = new Decimal(tier.amount).mul(100).toNumber();
- const upTo =
- tier.to == -1 || tier.to == TierInfinite
- ? "inf"
- : Math.round(tier.to / billingUnits!);
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ const tier = usageConfig.usage_tiers[i];
+ const amount = new Decimal(tier.amount).mul(100).toNumber();
+ const upTo =
+ tier.to == -1 || tier.to == TierInfinite
+ ? "inf"
+ : Math.round(tier.to / billingUnits!);
- tiers.push({
- unit_amount_decimal: amount,
- up_to: upTo,
- });
- }
+ tiers.push({
+ unit_amount_decimal: amount,
+ up_to: upTo,
+ });
+ }
- return tiers;
+ return tiers;
};
export const createStripePrepaid = async ({
- db,
- price,
- product,
- org,
- entitlements,
- curStripeProd,
- stripeCli,
+ db,
+ price,
+ product,
+ org,
+ entitlements,
+ curStripeProd,
+ stripeCli,
}: {
- db: DrizzleCli;
- price: Price;
- product: Product;
- org: Organization;
- entitlements: EntitlementWithFeature[];
- curStripeProd: Stripe.Product | null;
- stripeCli: Stripe;
+ db: DrizzleCli;
+ price: Price;
+ product: Product;
+ org: Organization;
+ entitlements: EntitlementWithFeature[];
+ curStripeProd: Stripe.Product | null;
+ stripeCli: Stripe;
}) => {
- const relatedEnt = getPriceEntitlement(price, entitlements);
+ const relatedEnt = getPriceEntitlement(price, entitlements);
- let recurringData = undefined;
- if (price.config!.interval != BillingInterval.OneOff) {
- recurringData = billingIntervalToStripe({
- interval: price.config!.interval,
- intervalCount: price.config!.interval_count,
- });
- }
+ let recurringData = undefined;
+ if (price.config!.interval != BillingInterval.OneOff) {
+ recurringData = billingIntervalToStripe({
+ interval: price.config!.interval,
+ intervalCount: price.config!.interval_count,
+ });
+ }
- const config = price.config as UsagePriceConfig;
+ const config = price.config as UsagePriceConfig;
- // 1. Product name
- let productName = `${product.name} - ${
- config.billing_units == 1 ? "" : `${config.billing_units} `
- }${relatedEnt.feature.name}`;
+ // 1. Product name
+ let productName = `${product.name} - ${
+ config.billing_units == 1 ? "" : `${config.billing_units} `
+ }${relatedEnt.feature.name}`;
- let productData = curStripeProd
- ? { product: curStripeProd.id }
- : {
- product_data: {
- name: productName,
- },
- };
+ let productData = curStripeProd
+ ? { product: curStripeProd.id }
+ : {
+ product_data: {
+ name: productName,
+ },
+ };
- // 2. If billing interval is one off
- let stripePrice = null;
- if (price.config!.interval == BillingInterval.OneOff) {
- const amount = config.usage_tiers[0].amount;
+ // 2. If billing interval is one off
+ let stripePrice = null;
+ if (price.config!.interval == BillingInterval.OneOff) {
+ const amount = config.usage_tiers[0].amount;
- let unitAmountDecimalStr = new Decimal(amount)
- .mul(100)
- .toDecimalPlaces(10)
- .toString();
+ let unitAmountDecimalStr = new Decimal(amount)
+ .mul(100)
+ .toDecimalPlaces(10)
+ .toString();
- stripePrice = await stripeCli.prices.create({
- ...productData,
- unit_amount_decimal: unitAmountDecimalStr,
- currency: org.default_currency!,
- });
+ stripePrice = await stripeCli.prices.create({
+ ...productData,
+ unit_amount_decimal: unitAmountDecimalStr,
+ currency: org.default_currency!,
+ });
- config.stripe_product_id = stripePrice.product as string;
- config.stripe_price_id = stripePrice.id;
- } else {
- let tiers = prepaidToStripeTiers(price, relatedEnt);
+ config.stripe_product_id = stripePrice.product as string;
+ config.stripe_price_id = stripePrice.id;
+ } else {
+ let tiers = prepaidToStripeTiers(price, relatedEnt);
- let priceAmountData = {};
- if (tiers.length == 1) {
- priceAmountData = {
- unit_amount_decimal: tiers[0].unit_amount_decimal,
- };
- } else {
- priceAmountData = {
- billing_scheme: "tiered",
- tiers_mode: "graduated",
- tiers: tiers,
- };
- }
+ let priceAmountData = {};
+ if (tiers.length == 1) {
+ priceAmountData = {
+ unit_amount_decimal: tiers[0].unit_amount_decimal,
+ };
+ } else {
+ priceAmountData = {
+ billing_scheme: "tiered",
+ tiers_mode: "graduated",
+ tiers: tiers,
+ };
+ }
- stripePrice = await stripeCli.prices.create({
- ...productData,
- currency: org.default_currency!,
- ...priceAmountData,
- recurring: {
- ...(recurringData as any),
- },
- nickname: `Autumn Price (${relatedEnt.feature.name})`,
- });
+ stripePrice = await stripeCli.prices.create({
+ ...productData,
+ currency: org.default_currency!,
+ ...priceAmountData,
+ recurring: {
+ ...(recurringData as any),
+ },
+ nickname: `Autumn Price (${relatedEnt.feature.name})`,
+ });
- config.stripe_price_id = stripePrice.id;
- config.stripe_product_id = stripePrice.product as string;
- let billingType = getBillingType(price.config!);
- }
+ config.stripe_price_id = stripePrice.id;
+ config.stripe_product_id = stripePrice.product as string;
+ let billingType = getBillingType(price.config!);
+ }
- // New config
- price.config = config;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
+ // New config
+ price.config = config;
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
};
diff --git a/server/src/external/stripe/createStripePrice/createStripePrice.ts b/server/src/external/stripe/createStripePrice/createStripePrice.ts
index d8a2762f1..d3bb06b28 100644
--- a/server/src/external/stripe/createStripePrice/createStripePrice.ts
+++ b/server/src/external/stripe/createStripePrice/createStripePrice.ts
@@ -1,15 +1,15 @@
import {
- getBillingType,
- getPriceEntitlement,
- priceIsOneOffAndTiered,
+ getBillingType,
+ getPriceEntitlement,
+ priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import {
- Price,
- EntitlementWithFeature,
- Product,
- Organization,
- UsagePriceConfig,
- BillingType,
+ Price,
+ EntitlementWithFeature,
+ Product,
+ Organization,
+ UsagePriceConfig,
+ BillingType,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -18,226 +18,226 @@ import { createStripePrepaid } from "./createStripePrepaid.js";
import { createStripeOneOffTieredProduct } from "./createStripeOneOffTiered.js";
import { createStripeInArrearPrice } from "./createStripeInArrear.js";
import {
- createStripeArrearProrated,
- createStripeMeteredPrice,
+ createStripeArrearProrated,
+ createStripeMeteredPrice,
} from "./createStripeArrearProrated.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
export const checkCurStripePrice = async ({
- price,
- stripeCli,
- currency,
+ price,
+ stripeCli,
+ currency,
}: {
- price: Price;
- stripeCli: Stripe;
- currency: string;
+ price: Price;
+ stripeCli: Stripe;
+ currency: string;
}) => {
- let config = price.config! as UsagePriceConfig;
+ let config = price.config! as UsagePriceConfig;
- let stripePrice: Stripe.Price | null = null;
- if (!config.stripe_price_id) {
- stripePrice = null;
- } else {
- try {
- stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!, {
- expand: ["product"],
- });
+ let stripePrice: Stripe.Price | null = null;
+ if (!config.stripe_price_id) {
+ stripePrice = null;
+ } else {
+ try {
+ stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!, {
+ expand: ["product"],
+ });
- if (!stripePrice.active) {
- stripePrice = await stripeCli.prices.update(config.stripe_price_id!, {
- active: true,
- });
- }
+ if (!stripePrice.active) {
+ stripePrice = await stripeCli.prices.update(config.stripe_price_id!, {
+ active: true,
+ });
+ }
- if (
- stripePrice &&
- stripePrice.currency.toLowerCase() !== currency.toLowerCase()
- ) {
- stripePrice = null;
- }
- } catch (error) {
- stripePrice = null;
- }
- }
+ if (
+ stripePrice &&
+ stripePrice.currency.toLowerCase() !== currency.toLowerCase()
+ ) {
+ stripePrice = null;
+ }
+ } catch (error) {
+ stripePrice = null;
+ }
+ }
- // Get stripe product
- let stripeProd: Stripe.Product | null = null;
- if (!config.stripe_product_id) {
- stripeProd = null;
- } else {
- try {
- stripeProd = await stripeCli.products.retrieve(config.stripe_product_id!);
- if (!stripeProd.active) {
- stripeProd = null;
- }
- } catch (error) {
- stripeProd = null;
- }
- }
+ // Get stripe product
+ let stripeProd: Stripe.Product | null = null;
+ if (!config.stripe_product_id) {
+ stripeProd = null;
+ } else {
+ try {
+ stripeProd = await stripeCli.products.retrieve(config.stripe_product_id!);
+ if (!stripeProd.active) {
+ stripeProd = null;
+ }
+ } catch (error) {
+ stripeProd = null;
+ }
+ }
- return {
- stripePrice,
- stripeProd,
- };
+ return {
+ stripePrice,
+ stripeProd,
+ };
};
export const createStripePriceIFNotExist = async ({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- logger,
- internalEntityId,
- useCheckout = false,
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ logger,
+ internalEntityId,
+ useCheckout = false,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- price: Price;
- entitlements: EntitlementWithFeature[];
- product: Product;
- org: Organization;
- logger: any;
- internalEntityId?: string;
- useCheckout?: boolean;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ price: Price;
+ entitlements: EntitlementWithFeature[];
+ product: Product;
+ org: Organization;
+ logger: any;
+ internalEntityId?: string;
+ useCheckout?: boolean;
}) => {
- // Fetch latest price data...
+ // Fetch latest price data...
- const billingType = getBillingType(price.config!);
+ const billingType = getBillingType(price.config!);
- let { stripePrice, stripeProd } = await checkCurStripePrice({
- price,
- stripeCli,
- currency: org.default_currency || "usd",
- });
+ let { stripePrice, stripeProd } = await checkCurStripePrice({
+ price,
+ stripeCli,
+ currency: org.default_currency || "usd",
+ });
- let config = price.config! as UsagePriceConfig;
- config.stripe_price_id = stripePrice?.id;
- config.stripe_product_id = stripeProd?.id;
+ let config = price.config! as UsagePriceConfig;
+ config.stripe_price_id = stripePrice?.id;
+ config.stripe_product_id = stripeProd?.id;
- let relatedEnt = getPriceEntitlement(price, entitlements);
- let isOneOffAndTiered = priceIsOneOffAndTiered(price, relatedEnt);
+ let relatedEnt = getPriceEntitlement(price, entitlements);
+ let isOneOffAndTiered = priceIsOneOffAndTiered(price, relatedEnt);
- // 1. If fixed price, just create price
- if (
- billingType == BillingType.FixedCycle ||
- billingType == BillingType.OneOff
- ) {
- if (!stripePrice) {
- await createStripeFixedPrice({
- db,
- stripeCli,
- price,
- product,
- org,
- });
- }
- }
+ // 1. If fixed price, just create price
+ if (
+ billingType == BillingType.FixedCycle ||
+ billingType == BillingType.OneOff
+ ) {
+ if (!stripePrice) {
+ await createStripeFixedPrice({
+ db,
+ stripeCli,
+ price,
+ product,
+ org,
+ });
+ }
+ }
- // 2. If prepaid
- if (billingType == BillingType.UsageInAdvance) {
- if (isOneOffAndTiered && !stripeProd) {
- logger.info(`Creating stripe one off tiered product`);
- await createStripeOneOffTieredProduct({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- });
- }
+ // 2. If prepaid
+ if (billingType == BillingType.UsageInAdvance) {
+ if (isOneOffAndTiered && !stripeProd) {
+ logger.info(`Creating stripe one off tiered product`);
+ await createStripeOneOffTieredProduct({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ });
+ }
- if (!isOneOffAndTiered && !stripePrice) {
- logger.info(`Creating stripe prepaid price`);
- await createStripePrepaid({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- curStripeProd: stripeProd,
- });
- }
- }
+ if (!isOneOffAndTiered && !stripePrice) {
+ logger.info(`Creating stripe prepaid price`);
+ await createStripePrepaid({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ curStripeProd: stripeProd,
+ });
+ }
+ }
- if (billingType == BillingType.InArrearProrated) {
- if (!stripePrice) {
- logger.info(`Creating stripe in arrear prorated product`);
- await createStripeArrearProrated({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- curStripeProd: stripeProd,
- });
- } else if (!config.stripe_placeholder_price_id) {
- logger.info(`Creating stripe placeholder price`);
- let placeholderPrice = await createStripeMeteredPrice({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- });
- config.stripe_placeholder_price_id = placeholderPrice.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
- }
- }
+ if (billingType == BillingType.InArrearProrated) {
+ if (!stripePrice) {
+ logger.info(`Creating stripe in arrear prorated product`);
+ await createStripeArrearProrated({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ curStripeProd: stripeProd,
+ });
+ } else if (!config.stripe_placeholder_price_id) {
+ logger.info(`Creating stripe placeholder price`);
+ let placeholderPrice = await createStripeMeteredPrice({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ });
+ config.stripe_placeholder_price_id = placeholderPrice.id;
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
+ }
+ }
- if (billingType == BillingType.UsageInArrear) {
- await createStripeInArrearPrice({
- db,
- stripeCli,
- price,
- entitlements,
- product,
- org,
- logger,
- curStripePrice: stripePrice,
- curStripeProduct: stripeProd,
- internalEntityId,
- useCheckout,
- });
+ if (billingType == BillingType.UsageInArrear) {
+ await createStripeInArrearPrice({
+ db,
+ stripeCli,
+ price,
+ entitlements,
+ product,
+ org,
+ logger,
+ curStripePrice: stripePrice,
+ curStripeProduct: stripeProd,
+ internalEntityId,
+ useCheckout,
+ });
- if (!config.stripe_empty_price_id) {
- try {
- logger.info(`Creating stripe empty price`);
- // console.log(`Product: ${config.stripe_product_id || stripeProd?.id}`);
- const emptyPrice = await stripeCli.prices.create({
- // product: stripeProd!.id,
- product: config.stripe_product_id || product.processor?.id,
- unit_amount: 0,
- currency: org.default_currency || "usd",
- recurring: {
- ...(billingIntervalToStripe({
- interval: price.config!.interval!,
- intervalCount: price.config!.interval_count!,
- }) as any),
- },
- });
+ if (!config.stripe_empty_price_id) {
+ try {
+ logger.info(`Creating stripe empty price`);
+ // console.log(`Product: ${config.stripe_product_id || stripeProd?.id}`);
+ const emptyPrice = await stripeCli.prices.create({
+ // product: stripeProd!.id,
+ product: config.stripe_product_id || product.processor?.id,
+ unit_amount: 0,
+ currency: org.default_currency || "usd",
+ recurring: {
+ ...(billingIntervalToStripe({
+ interval: price.config!.interval!,
+ intervalCount: price.config!.interval_count!,
+ }) as any),
+ },
+ });
- config.stripe_empty_price_id = emptyPrice.id;
- await PriceService.update({
- db,
- id: price.id!,
- update: { config },
- });
- } catch (error) {
- logger.error(`Error creating stripe empty price!`, {
- error,
- });
- }
- }
- }
+ config.stripe_empty_price_id = emptyPrice.id;
+ await PriceService.update({
+ db,
+ id: price.id!,
+ update: { config },
+ });
+ } catch (error) {
+ logger.error(`Error creating stripe empty price!`, {
+ error,
+ });
+ }
+ }
+ }
};
diff --git a/server/src/external/stripe/paymentMethodUtils.ts b/server/src/external/stripe/paymentMethodUtils.ts
index 60c82187b..d1174b4a6 100644
--- a/server/src/external/stripe/paymentMethodUtils.ts
+++ b/server/src/external/stripe/paymentMethodUtils.ts
@@ -1,7 +1,7 @@
import Stripe from "stripe";
const classifyStripePaymentMethod = (paymentMethod: Stripe.PaymentMethod) => {
- let cardPaymentMethods = [];
+ let cardPaymentMethods = [];
};
// Note: us_bank_account -> ACH
diff --git a/server/src/external/stripe/priceToStripeItem/priceToArrearProrated.ts b/server/src/external/stripe/priceToStripeItem/priceToArrearProrated.ts
index 7caa6d5f7..3fef8fd69 100644
--- a/server/src/external/stripe/priceToStripeItem/priceToArrearProrated.ts
+++ b/server/src/external/stripe/priceToStripeItem/priceToArrearProrated.ts
@@ -2,25 +2,25 @@ import { UsagePriceConfig } from "@autumn/shared";
import { Price } from "@autumn/shared";
export const priceToInArrearProrated = ({
- price,
- isCheckout,
- existingUsage,
+ price,
+ isCheckout,
+ existingUsage,
}: {
- price: Price;
- isCheckout: boolean;
- existingUsage: number;
+ price: Price;
+ isCheckout: boolean;
+ existingUsage: number;
}) => {
- const config = price.config as UsagePriceConfig;
- let quantity = existingUsage || 0;
+ const config = price.config as UsagePriceConfig;
+ let quantity = existingUsage || 0;
- if (quantity == 0 && isCheckout) {
- return {
- price: config.stripe_placeholder_price_id,
- };
- } else {
- return {
- price: config.stripe_price_id,
- quantity,
- };
- }
+ if (quantity == 0 && isCheckout) {
+ return {
+ price: config.stripe_placeholder_price_id,
+ };
+ } else {
+ return {
+ price: config.stripe_price_id,
+ quantity,
+ };
+ }
};
diff --git a/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts b/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts
index c6cd67d89..425807bb4 100644
--- a/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts
+++ b/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts
@@ -1,184 +1,184 @@
import {
- getBillingType,
- priceIsOneOffAndTiered,
+ getBillingType,
+ priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- BillingType,
- ErrCode,
- FeatureOptions,
- FixedPriceConfig,
- FullProduct,
- Organization,
- ProductOptions,
- UsagePriceConfig,
+ BillingType,
+ ErrCode,
+ FeatureOptions,
+ FixedPriceConfig,
+ FullProduct,
+ Organization,
+ ProductOptions,
+ UsagePriceConfig,
} from "@autumn/shared";
import { EntitlementWithFeature, Price, APIVersion } from "@autumn/shared";
import {
- priceToOneOffAndTiered,
- priceToUsageInAdvance,
+ priceToOneOffAndTiered,
+ priceToUsageInAdvance,
} from "./priceToUsageInAdvance.js";
import { priceToInArrearProrated } from "./priceToArrearProrated.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const getEmptyPriceItem = ({
- price,
- org,
+ price,
+ org,
}: {
- price: Price;
- org: Organization;
+ price: Price;
+ org: Organization;
}) => {
- return {
- price_data: {
- product: price.config!.stripe_product_id!,
- unit_amount: 0,
- currency: org.default_currency || "usd",
- recurring: {
- ...billingIntervalToStripe({
- interval: price.config!.interval!,
- intervalCount: price.config!.interval_count!,
- }),
- },
- },
- quantity: 1,
- };
+ return {
+ price_data: {
+ product: price.config!.stripe_product_id!,
+ unit_amount: 0,
+ currency: org.default_currency || "usd",
+ recurring: {
+ ...billingIntervalToStripe({
+ interval: price.config!.interval!,
+ intervalCount: price.config!.interval_count!,
+ }),
+ },
+ },
+ quantity: 1,
+ };
};
// GET STRIPE LINE / SUB ITEM
export const priceToStripeItem = ({
- price,
- relatedEnt,
- product,
- org,
- options,
- existingUsage,
- withEntity = false,
- isCheckout = false,
- apiVersion,
- productOptions,
+ price,
+ relatedEnt,
+ product,
+ org,
+ options,
+ existingUsage,
+ withEntity = false,
+ isCheckout = false,
+ apiVersion,
+ productOptions,
}: {
- price: Price;
- relatedEnt: EntitlementWithFeature;
- product: FullProduct;
- org: Organization;
- options: FeatureOptions | undefined | null;
- existingUsage: number;
- withEntity: boolean;
- isCheckout: boolean;
- apiVersion?: APIVersion;
- productOptions?: ProductOptions | undefined;
+ price: Price;
+ relatedEnt: EntitlementWithFeature;
+ product: FullProduct;
+ org: Organization;
+ options: FeatureOptions | undefined | null;
+ existingUsage: number;
+ withEntity: boolean;
+ isCheckout: boolean;
+ apiVersion?: APIVersion;
+ productOptions?: ProductOptions | undefined;
}) => {
- // TODO: Implement this
- const billingType = getBillingType(price.config!);
- const stripeProductId = product.processor?.id;
+ // TODO: Implement this
+ const billingType = getBillingType(price.config!);
+ const stripeProductId = product.processor?.id;
- const quantityMultiplier = notNullish(productOptions?.quantity)
- ? productOptions?.quantity!
- : 1;
+ const quantityMultiplier = notNullish(productOptions?.quantity)
+ ? productOptions?.quantity!
+ : 1;
- if (!stripeProductId) {
- throw new RecaseError({
- code: ErrCode.ProductNotFound,
- message: "Product not created in Stripe",
- statusCode: 400,
- });
- }
+ if (!stripeProductId) {
+ throw new RecaseError({
+ code: ErrCode.ProductNotFound,
+ message: "Product not created in Stripe",
+ statusCode: 400,
+ });
+ }
- let lineItemMeta = null;
- let lineItem = null;
+ let lineItemMeta = null;
+ let lineItem = null;
- // 1. FIXED PRICE
- if (
- billingType == BillingType.FixedCycle ||
- billingType == BillingType.OneOff
- ) {
- const config = price.config as FixedPriceConfig;
+ // 1. FIXED PRICE
+ if (
+ billingType == BillingType.FixedCycle ||
+ billingType == BillingType.OneOff
+ ) {
+ const config = price.config as FixedPriceConfig;
- lineItem = {
- price: config.stripe_price_id,
- quantity: quantityMultiplier,
- };
- }
+ lineItem = {
+ price: config.stripe_price_id,
+ quantity: quantityMultiplier,
+ };
+ }
- // 2. PREPAID, TIERED, ONE OFF
- else if (
- billingType == BillingType.UsageInAdvance &&
- priceIsOneOffAndTiered(price, relatedEnt)
- ) {
- lineItem = priceToOneOffAndTiered({
- price,
- options,
- relatedEnt,
- org,
- stripeProductId,
- });
- }
+ // 2. PREPAID, TIERED, ONE OFF
+ else if (
+ billingType == BillingType.UsageInAdvance &&
+ priceIsOneOffAndTiered(price, relatedEnt)
+ ) {
+ lineItem = priceToOneOffAndTiered({
+ price,
+ options,
+ relatedEnt,
+ org,
+ stripeProductId,
+ });
+ }
- // 3. PREPAID
- else if (billingType == BillingType.UsageInAdvance) {
- lineItem = priceToUsageInAdvance({
- price,
- options,
- isCheckout,
- relatedEnt,
- });
- }
+ // 3. PREPAID
+ else if (billingType == BillingType.UsageInAdvance) {
+ lineItem = priceToUsageInAdvance({
+ price,
+ options,
+ isCheckout,
+ relatedEnt,
+ });
+ }
- // 4. USAGE IN ARREAR
- else if (billingType == BillingType.UsageInArrear) {
- const config = price.config as UsagePriceConfig;
- const priceId = config.stripe_price_id;
+ // 4. USAGE IN ARREAR
+ else if (billingType == BillingType.UsageInArrear) {
+ const config = price.config as UsagePriceConfig;
+ const priceId = config.stripe_price_id;
- if (withEntity && !isCheckout) {
- return {
- lineItem: {
- price: config.stripe_empty_price_id,
- quantity: 0,
- },
- };
- }
+ if (withEntity && !isCheckout) {
+ return {
+ lineItem: {
+ price: config.stripe_empty_price_id,
+ quantity: 0,
+ },
+ };
+ }
- if (apiVersion === APIVersion.v1_4 && !isCheckout) {
- return {
- lineItem: {
- // lineItem: getEmptyPriceItem({ price, org }),
- price: config.stripe_empty_price_id,
- quantity: 0,
- },
- };
- }
+ if (apiVersion === APIVersion.v1_4 && !isCheckout) {
+ return {
+ lineItem: {
+ // lineItem: getEmptyPriceItem({ price, org }),
+ price: config.stripe_empty_price_id,
+ quantity: 0,
+ },
+ };
+ }
- if (!priceId) {
- throw new RecaseError({
- code: ErrCode.PriceNotFound,
- message: `Couldn't find Autumn price: ${price.id} in Stripe`,
- statusCode: 400,
- });
- }
+ if (!priceId) {
+ throw new RecaseError({
+ code: ErrCode.PriceNotFound,
+ message: `Couldn't find Autumn price: ${price.id} in Stripe`,
+ statusCode: 400,
+ });
+ }
- lineItem = {
- price: priceId,
- };
- }
+ lineItem = {
+ price: priceId,
+ };
+ }
- // 5. USAGE ARREAR PRORATED
- else if (billingType == BillingType.InArrearProrated) {
- lineItem = priceToInArrearProrated({
- price,
- isCheckout,
- existingUsage,
- });
- }
+ // 5. USAGE ARREAR PRORATED
+ else if (billingType == BillingType.InArrearProrated) {
+ lineItem = priceToInArrearProrated({
+ price,
+ isCheckout,
+ existingUsage,
+ });
+ }
- if (!lineItem) {
- return null;
- }
+ if (!lineItem) {
+ return null;
+ }
- return {
- lineItem,
- lineItemMeta,
- };
+ return {
+ lineItem,
+ lineItemMeta,
+ };
};
diff --git a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts
index b50dfe932..f2907fc2a 100644
--- a/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts
+++ b/server/src/external/stripe/priceToStripeItem/priceToUsageInAdvance.ts
@@ -6,93 +6,93 @@ import { Price } from "@autumn/shared";
import { Decimal } from "decimal.js";
export const priceToOneOffAndTiered = ({
- price,
- relatedEnt,
- options,
- stripeProductId,
- org,
+ price,
+ relatedEnt,
+ options,
+ stripeProductId,
+ org,
}: {
- price: Price;
- relatedEnt: EntitlementWithFeature;
- options: FeatureOptions | undefined | null;
- org: Organization;
- stripeProductId: string;
+ price: Price;
+ relatedEnt: EntitlementWithFeature;
+ options: FeatureOptions | undefined | null;
+ org: Organization;
+ stripeProductId: string;
}) => {
- const config = price.config as UsagePriceConfig;
- let quantity = options?.quantity!;
- let overage = new Decimal(quantity).mul(config.billing_units!).toNumber();
- // let overage = quantity * config.billing_units! - relatedEnt.allowance!;
+ const config = price.config as UsagePriceConfig;
+ let quantity = options?.quantity!;
+ let overage = new Decimal(quantity).mul(config.billing_units!).toNumber();
+ // let overage = quantity * config.billing_units! - relatedEnt.allowance!;
- // if (overage <= 0) {
- // return null;
- // }
+ // if (overage <= 0) {
+ // return null;
+ // }
- const amount = getPriceForOverage(price, overage);
- if (!config.stripe_product_id) {
- console.log(
- `WARNING: One off & tiered in advance price has no stripe product id: ${price.id}, ${relatedEnt.feature.name}`
- );
- }
- return {
- price_data: {
- product: config.stripe_product_id
- ? config.stripe_product_id
- : stripeProductId,
- unit_amount: Number(amount.toFixed(2)) * 100,
- currency: org.default_currency,
- },
+ const amount = getPriceForOverage(price, overage);
+ if (!config.stripe_product_id) {
+ console.log(
+ `WARNING: One off & tiered in advance price has no stripe product id: ${price.id}, ${relatedEnt.feature.name}`,
+ );
+ }
+ return {
+ price_data: {
+ product: config.stripe_product_id
+ ? config.stripe_product_id
+ : stripeProductId,
+ unit_amount: Number(amount.toFixed(2)) * 100,
+ currency: org.default_currency,
+ },
- quantity: 1,
- };
+ quantity: 1,
+ };
};
export const priceToUsageInAdvance = ({
- price,
- relatedEnt,
- options,
- isCheckout,
+ price,
+ relatedEnt,
+ options,
+ isCheckout,
}: {
- price: Price;
- relatedEnt: EntitlementWithFeature;
- options: FeatureOptions | undefined | null;
- isCheckout: boolean;
+ price: Price;
+ relatedEnt: EntitlementWithFeature;
+ options: FeatureOptions | undefined | null;
+ isCheckout: boolean;
}) => {
- const config = price.config as UsagePriceConfig;
- let optionsQuantity = options?.quantity;
- let finalQuantity = optionsQuantity;
+ const config = price.config as UsagePriceConfig;
+ let optionsQuantity = options?.quantity;
+ let finalQuantity = optionsQuantity;
- // 1. If adjustable quantity is set, use that, else if quantity is undefined, adjustable is true, else false
- let adjustable = notNullish(options?.adjustable_quantity)
- ? options!.adjustable_quantity
- : nullish(optionsQuantity)
- ? true
- : false;
+ // 1. If adjustable quantity is set, use that, else if quantity is undefined, adjustable is true, else false
+ let adjustable = notNullish(options?.adjustable_quantity)
+ ? options!.adjustable_quantity
+ : nullish(optionsQuantity)
+ ? true
+ : false;
- if (optionsQuantity === 0 && isCheckout) {
- // 1. If quantity is 0 and is checkout, skip over line item
- return null;
- } else if (nullish(optionsQuantity) && isCheckout) {
- // 2. If quantity is nullish and is checkout, default to 1
- finalQuantity = 1;
- }
+ if (optionsQuantity === 0 && isCheckout) {
+ // 1. If quantity is 0 and is checkout, skip over line item
+ return null;
+ } else if (nullish(optionsQuantity) && isCheckout) {
+ // 2. If quantity is nullish and is checkout, default to 1
+ finalQuantity = 1;
+ }
- // Divide final quantity by billing units...?
+ // Divide final quantity by billing units...?
- // let minimum = new Decimal(relatedEnt.allowance!)
- // .div(config.billing_units || 1)
- // .toNumber();
+ // let minimum = new Decimal(relatedEnt.allowance!)
+ // .div(config.billing_units || 1)
+ // .toNumber();
- const adjustableQuantity =
- isCheckout && adjustable
- ? {
- enabled: true,
- maximum: 999999,
- }
- : undefined;
+ const adjustableQuantity =
+ isCheckout && adjustable
+ ? {
+ enabled: true,
+ maximum: 999999,
+ }
+ : undefined;
- return {
- price: config.stripe_price_id,
- quantity: finalQuantity,
- adjustable_quantity: adjustableQuantity,
- };
+ return {
+ price: config.stripe_price_id,
+ quantity: finalQuantity,
+ adjustable_quantity: adjustableQuantity,
+ };
};
diff --git a/server/src/external/stripe/stripeCardUtils.ts b/server/src/external/stripe/stripeCardUtils.ts
index acb8e35e4..17cad6c4f 100644
--- a/server/src/external/stripe/stripeCardUtils.ts
+++ b/server/src/external/stripe/stripeCardUtils.ts
@@ -1,11 +1,11 @@
export const isStripeCardDeclined = (error: any) => {
- return (
- error.code === "card_declined" ||
- error.code === "expired_card" ||
- error.code === "incorrect_cvc" ||
- error.code === "processing_error" ||
- error.code === "incorrect_number" ||
- error.code == "subscription_payment_intent_requires_action" ||
- error.code == "payment_intent_payment_attempt_failed" // Stripe link
- );
+ return (
+ error.code === "card_declined" ||
+ error.code === "expired_card" ||
+ error.code === "incorrect_cvc" ||
+ error.code === "processing_error" ||
+ error.code === "incorrect_number" ||
+ error.code == "subscription_payment_intent_requires_action" ||
+ error.code == "payment_intent_payment_attempt_failed" // Stripe link
+ );
};
diff --git a/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts b/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts
index df80a9157..f98fcee84 100644
--- a/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts
+++ b/server/src/external/stripe/stripeCouponUtils/deleteCouponFromCus.ts
@@ -1,69 +1,69 @@
import { Stripe } from "stripe";
export const deleteCouponFromSub = async ({
- stripeCli,
- stripeSubId,
- discountId,
- logger,
+ stripeCli,
+ stripeSubId,
+ discountId,
+ logger,
}: {
- stripeCli: Stripe;
- stripeSubId: string;
- discountId: string;
- logger: any;
+ stripeCli: Stripe;
+ stripeSubId: string;
+ discountId: string;
+ logger: any;
}) => {
- try {
- let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
+ try {
+ let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
- let newDiscounts = stripeSub.discounts
- ?.filter((d: any) => d !== discountId)
- .map((d: any) => ({
- discount: d,
- }));
+ let newDiscounts = stripeSub.discounts
+ ?.filter((d: any) => d !== discountId)
+ .map((d: any) => ({
+ discount: d,
+ }));
- if (stripeSub.discounts.some((d: any) => d === discountId)) {
- await stripeCli.subscriptions.deleteDiscount(stripeSubId);
- // console.log("DELETED DISCOUNT FROM SUB", stripeSubId);
- }
- } catch (error: any) {
- // if (!error.message.includes("no active discount for subscription")) {
- logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
- logger.error(error.message);
- // }
- }
+ if (stripeSub.discounts.some((d: any) => d === discountId)) {
+ await stripeCli.subscriptions.deleteDiscount(stripeSubId);
+ // console.log("DELETED DISCOUNT FROM SUB", stripeSubId);
+ }
+ } catch (error: any) {
+ // if (!error.message.includes("no active discount for subscription")) {
+ logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
+ logger.error(error.message);
+ // }
+ }
};
export const deleteCouponFromCus = async ({
- stripeCli,
- stripeSubId,
- stripeCusId,
- discountId,
- logger,
+ stripeCli,
+ stripeSubId,
+ stripeCusId,
+ discountId,
+ logger,
}: {
- stripeCli: Stripe;
- stripeSubId: string;
- stripeCusId: string;
- discountId: string;
- logger: any;
+ stripeCli: Stripe;
+ stripeSubId: string;
+ stripeCusId: string;
+ discountId: string;
+ logger: any;
}) => {
- try {
- let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
- if (stripeSub.discounts.some((d: any) => d === discountId)) {
- await stripeCli.subscriptions.deleteDiscount(stripeSubId);
- }
- } catch (error) {
- logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
- logger.error(error);
- }
+ try {
+ let stripeSub = await stripeCli.subscriptions.retrieve(stripeSubId);
+ if (stripeSub.discounts.some((d: any) => d === discountId)) {
+ await stripeCli.subscriptions.deleteDiscount(stripeSubId);
+ }
+ } catch (error) {
+ logger.error(`Failed to delete discount from subscription ${stripeSubId}`);
+ logger.error(error);
+ }
- try {
- let stripeCus = (await stripeCli.customers.retrieve(
- stripeCusId
- )) as Stripe.Customer;
- if (stripeCus.discount?.id === discountId) {
- await stripeCli.customers.deleteDiscount(stripeCusId, discountId);
- }
- } catch (error) {
- logger.error(`Failed to delete discount from customer ${stripeCusId}`);
- logger.error(error);
- }
+ try {
+ let stripeCus = (await stripeCli.customers.retrieve(
+ stripeCusId,
+ )) as Stripe.Customer;
+ if (stripeCus.discount?.id === discountId) {
+ await stripeCli.customers.deleteDiscount(stripeCusId, discountId);
+ }
+ } catch (error) {
+ logger.error(`Failed to delete discount from customer ${stripeCusId}`);
+ logger.error(error);
+ }
};
diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts
index 17a815763..9e7c9de50 100644
--- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts
+++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts
@@ -1,203 +1,203 @@
import {
- type AppEnv,
- CouponDurationType,
- ErrCode,
- type FixedPriceConfig,
- type Organization,
- type Price,
- PriceType,
- type Product,
- type Reward,
- RewardType,
- type UsagePriceConfig,
+ type AppEnv,
+ CouponDurationType,
+ ErrCode,
+ type FixedPriceConfig,
+ type Organization,
+ type Price,
+ PriceType,
+ type Product,
+ type Reward,
+ RewardType,
+ type UsagePriceConfig,
} from "@autumn/shared";
import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { createStripeCli } from "../utils.js";
const couponToStripeDuration = ({
- coupon,
- isOneOffProduct = false,
+ coupon,
+ isOneOffProduct = false,
}: {
- coupon: Reward;
- isOneOffProduct: boolean;
+ coupon: Reward;
+ isOneOffProduct: boolean;
}) => {
- if (coupon.type === RewardType.FreeProduct) {
- // For one-off products, the coupon should apply once, not repeat
- if (isOneOffProduct) {
- return {
- duration: "once",
- };
- }
- console.log("--------------------------------");
- console.log("rewardName", coupon.name);
- console.log("coupon.free_product_config", coupon.free_product_config);
- console.log("isOneOffProduct", isOneOffProduct);
- console.log("--------------------------------");
- return {
- duration: "repeating",
- duration_in_months: coupon.free_product_config?.duration_value,
- };
- }
+ if (coupon.type === RewardType.FreeProduct) {
+ // For one-off products, the coupon should apply once, not repeat
+ if (isOneOffProduct) {
+ return {
+ duration: "once",
+ };
+ }
+ console.log("--------------------------------");
+ console.log("rewardName", coupon.name);
+ console.log("coupon.free_product_config", coupon.free_product_config);
+ console.log("isOneOffProduct", isOneOffProduct);
+ console.log("--------------------------------");
+ return {
+ duration: "repeating",
+ duration_in_months: coupon.free_product_config?.duration_value,
+ };
+ }
- const discountConfig = coupon.discount_config;
- if (
- coupon.type === RewardType.InvoiceCredits &&
- coupon.discount_config?.duration_type === CouponDurationType.Forever
- ) {
- return {
- duration: "once",
- };
- }
+ const discountConfig = coupon.discount_config;
+ if (
+ coupon.type === RewardType.InvoiceCredits &&
+ coupon.discount_config?.duration_type === CouponDurationType.Forever
+ ) {
+ return {
+ duration: "once",
+ };
+ }
- // For one-off products, always use "once" duration regardless of config
- if (isOneOffProduct) {
- return {
- duration: "once",
- };
- }
+ // For one-off products, always use "once" duration regardless of config
+ if (isOneOffProduct) {
+ return {
+ duration: "once",
+ };
+ }
- switch (discountConfig!.duration_type) {
- case CouponDurationType.Forever:
- return {
- duration: "forever",
- };
- case CouponDurationType.OneOff:
- return {
- duration: "once",
- };
- case CouponDurationType.Months:
- return {
- duration: "repeating",
- duration_in_months: discountConfig!.duration_value,
- };
- }
+ switch (discountConfig!.duration_type) {
+ case CouponDurationType.Forever:
+ return {
+ duration: "forever",
+ };
+ case CouponDurationType.OneOff:
+ return {
+ duration: "once",
+ };
+ case CouponDurationType.Months:
+ return {
+ duration: "repeating",
+ duration_in_months: discountConfig!.duration_value,
+ };
+ }
};
const couponToStripeValue = ({
- reward,
- org,
- prices,
+ reward,
+ org,
+ prices,
}: {
- reward: Reward;
- org: Organization;
- prices?: (Price & { product: Product })[];
+ reward: Reward;
+ org: Organization;
+ prices?: (Price & { product: Product })[];
}) => {
- if (reward.type === RewardType.FreeProduct) {
- const amountOff = Math.round(
- prices?.reduce(
- (acc, price) => acc + (price.config as FixedPriceConfig).amount,
- 0
- ) || 0
- );
+ if (reward.type === RewardType.FreeProduct) {
+ const amountOff = Math.round(
+ prices?.reduce(
+ (acc, price) => acc + (price.config as FixedPriceConfig).amount,
+ 0,
+ ) || 0,
+ );
- console.log("amountOff in couponToStripeValue", amountOff);
- return {
- amount_off: Math.round(amountOff * 100),
- currency: org.default_currency || "usd",
- };
- }
+ console.log("amountOff in couponToStripeValue", amountOff);
+ return {
+ amount_off: Math.round(amountOff * 100),
+ currency: org.default_currency || "usd",
+ };
+ }
- const discountConfig = reward.discount_config;
- if (reward.type === RewardType.PercentageDiscount) {
- return {
- percent_off: discountConfig!.discount_value,
- };
- } else if (
- reward.type === RewardType.FixedDiscount ||
- reward.type === RewardType.InvoiceCredits
- ) {
- return {
- amount_off: Math.round(discountConfig!.discount_value * 100),
- currency: org.default_currency,
- };
- }
+ const discountConfig = reward.discount_config;
+ if (reward.type === RewardType.PercentageDiscount) {
+ return {
+ percent_off: discountConfig!.discount_value,
+ };
+ } else if (
+ reward.type === RewardType.FixedDiscount ||
+ reward.type === RewardType.InvoiceCredits
+ ) {
+ return {
+ amount_off: Math.round(discountConfig!.discount_value * 100),
+ currency: org.default_currency,
+ };
+ }
};
export const createStripeCoupon = async ({
- reward,
- org,
- env,
- prices,
- logger,
- legacyVersion,
+ reward,
+ org,
+ env,
+ prices,
+ logger,
+ legacyVersion,
}: {
- reward: Reward;
- org: Organization;
- env: AppEnv;
- prices: (Price & { product: Product })[];
- logger: any;
- legacyVersion?: boolean;
+ reward: Reward;
+ org: Organization;
+ env: AppEnv;
+ prices: (Price & { product: Product })[];
+ logger: any;
+ legacyVersion?: boolean;
}) => {
- const discountConfig = reward.discount_config;
+ const discountConfig = reward.discount_config;
- const stripeCli = createStripeCli({
- org,
- env,
- legacyVersion,
- });
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ legacyVersion,
+ });
- try {
- await stripeCli.coupons.del(reward.id);
- } catch (_) {}
+ try {
+ await stripeCli.coupons.del(reward.id);
+ } catch (_) {}
- const stripeProdIds = prices.map((price) => {
- if (price.config!.type === PriceType.Fixed) {
- return price.product.processor?.id;
- } else {
- const config = price.config as UsagePriceConfig;
- if (!config.stripe_product_id) {
- logger.warn("No stripe product id for price", { price });
- logger.warn("Config", { config });
- throw new RecaseError({
- message: `No stripe product id for price ${price.id}`,
- code: ErrCode.InternalError,
- });
- }
+ const stripeProdIds = prices.map((price) => {
+ if (price.config!.type === PriceType.Fixed) {
+ return price.product.processor?.id;
+ } else {
+ const config = price.config as UsagePriceConfig;
+ if (!config.stripe_product_id) {
+ logger.warn("No stripe product id for price", { price });
+ logger.warn("Config", { config });
+ throw new RecaseError({
+ message: `No stripe product id for price ${price.id}`,
+ code: ErrCode.InternalError,
+ });
+ }
- return config.stripe_product_id;
- }
- });
+ return config.stripe_product_id;
+ }
+ });
- for (const promoCode of reward.promo_codes) {
- try {
- const stripePromoCode = await stripeCli.promotionCodes.retrieve(
- promoCode.code
- );
- throw new RecaseError({
- message: `Promo code ${promoCode.code} (${stripePromoCode.id}) already exists in Stripe`,
- code: ErrCode.PromoCodeAlreadyExistsInStripe,
- });
- } catch (_) {}
- }
+ for (const promoCode of reward.promo_codes) {
+ try {
+ const stripePromoCode = await stripeCli.promotionCodes.retrieve(
+ promoCode.code,
+ );
+ throw new RecaseError({
+ message: `Promo code ${promoCode.code} (${stripePromoCode.id}) already exists in Stripe`,
+ code: ErrCode.PromoCodeAlreadyExistsInStripe,
+ });
+ } catch (_) {}
+ }
- const stripeCoupon = await stripeCli.coupons.create({
- // id: reward.internal_id,
- id: reward.id,
- ...(couponToStripeDuration({
- coupon: reward,
- isOneOffProduct: pricesOnlyOneOff(prices),
- }) as any),
- ...(couponToStripeValue({ reward, org, prices }) as any),
- name: reward.name,
- metadata: {
- autumn_internal_id: reward.internal_id,
- },
- applies_to:
- reward.type === RewardType.FreeProduct
- ? undefined
- : !discountConfig!.apply_to_all
- ? {
- products: stripeProdIds,
- }
- : undefined,
- });
+ const stripeCoupon = await stripeCli.coupons.create({
+ // id: reward.internal_id,
+ id: reward.id,
+ ...(couponToStripeDuration({
+ coupon: reward,
+ isOneOffProduct: pricesOnlyOneOff(prices),
+ }) as any),
+ ...(couponToStripeValue({ reward, org, prices }) as any),
+ name: reward.name,
+ metadata: {
+ autumn_internal_id: reward.internal_id,
+ },
+ applies_to:
+ reward.type === RewardType.FreeProduct
+ ? undefined
+ : !discountConfig!.apply_to_all
+ ? {
+ products: stripeProdIds,
+ }
+ : undefined,
+ });
- // Create promo codes
- for (const promoCode of reward.promo_codes) {
- await stripeCli.promotionCodes.create({
- coupon: stripeCoupon.id,
- code: promoCode.code,
- });
- }
+ // Create promo codes
+ for (const promoCode of reward.promo_codes) {
+ await stripeCli.promotionCodes.create({
+ coupon: stripeCoupon.id,
+ code: promoCode.code,
+ });
+ }
};
diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts
index 36996b4b7..fe1888a4d 100644
--- a/server/src/external/stripe/stripeCusUtils.ts
+++ b/server/src/external/stripe/stripeCusUtils.ts
@@ -1,9 +1,9 @@
import {
- type AppEnv,
- type Customer,
- ErrCode,
- type Organization,
- ProcessorType,
+ type AppEnv,
+ type Customer,
+ ErrCode,
+ type Organization,
+ ProcessorType,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type { Stripe } from "stripe";
diff --git a/server/src/external/stripe/stripeEnsureUtils.ts b/server/src/external/stripe/stripeEnsureUtils.ts
index 74a029ca8..58f1268c9 100644
--- a/server/src/external/stripe/stripeEnsureUtils.ts
+++ b/server/src/external/stripe/stripeEnsureUtils.ts
@@ -8,81 +8,81 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
import { createStripeCli } from "./utils.js";
export async function ensureStripeProducts({
- db,
- logger,
- req,
- org,
- env,
+ db,
+ logger,
+ req,
+ org,
+ env,
}: {
- db: DrizzleCli;
- logger: any;
- req: ExtendedRequest;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ logger: any;
+ req: ExtendedRequest;
+ org: Organization;
+ env: AppEnv;
}) {
- await ensureStripeProductsWithEnv({
- db,
- logger,
- req,
- env,
- org,
- });
+ await ensureStripeProductsWithEnv({
+ db,
+ logger,
+ req,
+ env,
+ org,
+ });
}
export async function ensureStripeProductsWithEnv({
- db,
- logger,
- req,
- env,
- org,
+ db,
+ logger,
+ req,
+ env,
+ org,
}: {
- db: DrizzleCli;
- logger: any;
- req: ExtendedRequest;
- env: AppEnv;
- org: Organization;
+ db: DrizzleCli;
+ logger: any;
+ req: ExtendedRequest;
+ env: AppEnv;
+ org: Organization;
}) {
- // let existingStripeProducts = await stripe.products.list();
- const fullProducts = await ProductService.listFull({
- db,
- orgId: req.org.id,
- env,
- });
+ // let existingStripeProducts = await stripe.products.list();
+ const fullProducts = await ProductService.listFull({
+ db,
+ orgId: req.org.id,
+ env,
+ });
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- // Fetch updated org data to ensure we have the latest Stripe configuration
- const products = await stripeCli.products.list({ limit: 100 });
- const updatedOrg = await OrgService.get({ db, orgId: req.org.id });
+ // Fetch updated org data to ensure we have the latest Stripe configuration
+ const products = await stripeCli.products.list({ limit: 100 });
+ const updatedOrg = await OrgService.get({ db, orgId: req.org.id });
- const batchInit: Promise[] = [];
- for (let fullProduct of fullProducts) {
- const initProduct = async () => {
- let existsInStripe = products.data.find(
- (p) => p.id === fullProduct.processor?.id
- );
+ const batchInit: Promise[] = [];
+ for (let fullProduct of fullProducts) {
+ const initProduct = async () => {
+ let existsInStripe = products.data.find(
+ (p) => p.id === fullProduct.processor?.id,
+ );
- if (existsInStripe) {
- return;
- }
+ if (existsInStripe) {
+ return;
+ }
- try {
- await initProductInStripe({
- db,
- org: updatedOrg,
- env,
- logger,
- product: fullProduct,
- });
+ try {
+ await initProductInStripe({
+ db,
+ org: updatedOrg,
+ env,
+ logger,
+ product: fullProduct,
+ });
- logger.info(
- `initialized product ${fullProduct.id} in Stripe during Stripe connection, env: ${env}`
- );
- } catch (error) {
- logger.error(`Failed to init product in stripe: ${error}`);
- }
- };
+ logger.info(
+ `initialized product ${fullProduct.id} in Stripe during Stripe connection, env: ${env}`,
+ );
+ } catch (error) {
+ logger.error(`Failed to init product in stripe: ${error}`);
+ }
+ };
- batchInit.push(initProduct());
- }
- await Promise.all(batchInit);
+ batchInit.push(initProduct());
+ }
+ await Promise.all(batchInit);
}
diff --git a/server/src/external/stripe/stripeErrorUtils.ts b/server/src/external/stripe/stripeErrorUtils.ts
index d6a655779..6d4a98abb 100644
--- a/server/src/external/stripe/stripeErrorUtils.ts
+++ b/server/src/external/stripe/stripeErrorUtils.ts
@@ -1,4 +1,4 @@
export const stripeErrToCusMsg = (error: any) => {
- let code = error.code;
- let msg = error.message;
+ let code = error.code;
+ let msg = error.message;
};
diff --git a/server/src/external/stripe/stripeInvoiceSubUtils.ts b/server/src/external/stripe/stripeInvoiceSubUtils.ts
index 24abc26a8..d62de10d0 100644
--- a/server/src/external/stripe/stripeInvoiceSubUtils.ts
+++ b/server/src/external/stripe/stripeInvoiceSubUtils.ts
@@ -1,96 +1,96 @@
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- Customer,
- FreeTrial,
- Organization,
- Price,
- ErrCode,
- BillingInterval,
+ Customer,
+ FreeTrial,
+ Organization,
+ Price,
+ ErrCode,
+ BillingInterval,
} from "@autumn/shared";
import Stripe from "stripe";
import { getCusPaymentMethod } from "./stripeCusUtils.js";
export const createStripeSubThroughInvoice = async ({
- stripeCli,
- customer,
- org,
- items,
- freeTrial,
- metadata = {},
- prices,
+ stripeCli,
+ customer,
+ org,
+ items,
+ freeTrial,
+ metadata = {},
+ prices,
}: {
- stripeCli: Stripe;
- customer: Customer;
- items: any;
- freeTrial: FreeTrial | null;
- org: Organization;
- metadata?: any;
- prices: Price[];
+ stripeCli: Stripe;
+ customer: Customer;
+ items: any;
+ freeTrial: FreeTrial | null;
+ org: Organization;
+ metadata?: any;
+ prices: Price[];
}) => {
- // 1. Get payment method
- let paymentMethod;
- try {
- paymentMethod = await getCusPaymentMethod({
- stripeCli,
- stripeId: customer.processor.id,
- });
- } catch (error) {}
+ // 1. Get payment method
+ let paymentMethod;
+ try {
+ paymentMethod = await getCusPaymentMethod({
+ stripeCli,
+ stripeId: customer.processor.id,
+ });
+ } catch (error) {}
- let paymentMethodData = {};
- if (paymentMethod) {
- paymentMethodData = {
- default_payment_method: paymentMethod.id,
- };
- }
+ let paymentMethodData = {};
+ if (paymentMethod) {
+ paymentMethodData = {
+ default_payment_method: paymentMethod.id,
+ };
+ }
- let subItems = items.filter(
- (i: any, index: number) =>
- prices[index].config!.interval !== BillingInterval.OneOff
- );
- let invoiceItems = items.filter(
- (i: any, index: number) =>
- prices[index].config!.interval === BillingInterval.OneOff
- );
+ let subItems = items.filter(
+ (i: any, index: number) =>
+ prices[index].config!.interval !== BillingInterval.OneOff,
+ );
+ let invoiceItems = items.filter(
+ (i: any, index: number) =>
+ prices[index].config!.interval === BillingInterval.OneOff,
+ );
- try {
- const subscription = await stripeCli.subscriptions.create({
- // ...paymentMethodData,
- customer: customer.processor.id,
- items: subItems as any,
- trial_end: freeTrialToStripeTimestamp({ freeTrial }),
- metadata,
- add_invoice_items: invoiceItems,
- collection_method: "send_invoice",
- days_until_due: 30,
- });
+ try {
+ const subscription = await stripeCli.subscriptions.create({
+ // ...paymentMethodData,
+ customer: customer.processor.id,
+ items: subItems as any,
+ trial_end: freeTrialToStripeTimestamp({ freeTrial }),
+ metadata,
+ add_invoice_items: invoiceItems,
+ collection_method: "send_invoice",
+ days_until_due: 30,
+ });
- return subscription;
- } catch (error: any) {
- // console.log("Error creating stripe subscription", error?.message || error);
- console.log("Warning: Failed to create stripe subscription");
- console.log("Error code:", error.code);
- console.log("Message:", error.message);
- console.log("Decline code:", error.decline_code);
+ return subscription;
+ } catch (error: any) {
+ // console.log("Error creating stripe subscription", error?.message || error);
+ console.log("Warning: Failed to create stripe subscription");
+ console.log("Error code:", error.code);
+ console.log("Message:", error.message);
+ console.log("Decline code:", error.decline_code);
- throw new RecaseError({
- // code: ErrCode.StripeCardDeclined,
- code: ErrCode.CreateStripeSubscriptionFailed,
- message: `Stripe subscription failed (${error.code}): ${error.message}`,
- statusCode: 500,
- });
+ throw new RecaseError({
+ // code: ErrCode.StripeCardDeclined,
+ code: ErrCode.CreateStripeSubscriptionFailed,
+ message: `Stripe subscription failed (${error.code}): ${error.message}`,
+ statusCode: 500,
+ });
- // if (isStripeCardDeclined(error)) {
+ // if (isStripeCardDeclined(error)) {
- // }
+ // }
- // console.log("Error creating stripe subscription", error?.message || error);
- // console.log("Error code:", error.code);
+ // console.log("Error creating stripe subscription", error?.message || error);
+ // console.log("Error code:", error.code);
- // throw new RecaseError({
- // code: ErrCode.CreateStripeSubscriptionFailed,
- // message: "Failed to create stripe subscription",
- // statusCode: 500,
- // });
- }
+ // throw new RecaseError({
+ // code: ErrCode.CreateStripeSubscriptionFailed,
+ // message: "Failed to create stripe subscription",
+ // statusCode: 500,
+ // });
+ }
};
diff --git a/server/src/external/stripe/stripeInvoiceUtils.ts b/server/src/external/stripe/stripeInvoiceUtils.ts
index 2e861b767..f54576571 100644
--- a/server/src/external/stripe/stripeInvoiceUtils.ts
+++ b/server/src/external/stripe/stripeInvoiceUtils.ts
@@ -6,210 +6,210 @@ import { ErrCode, InvoiceDiscount, InvoiceStatus } from "@autumn/shared";
// For API calls
export const getStripeExpandedInvoice = async ({
- stripeCli,
- stripeInvoiceId,
+ stripeCli,
+ stripeInvoiceId,
}: {
- stripeCli: Stripe;
- stripeInvoiceId: string;
+ stripeCli: Stripe;
+ stripeInvoiceId: string;
}) => {
- const invoice = await stripeCli.invoices.retrieve(stripeInvoiceId, {
- expand: ["discounts", "discounts.coupon"],
- });
- return invoice;
+ const invoice = await stripeCli.invoices.retrieve(stripeInvoiceId, {
+ expand: ["discounts", "discounts.coupon"],
+ });
+ return invoice;
};
// For webhooks
export const getFullStripeInvoice = async ({
- stripeCli,
- stripeId,
- expand = [],
+ stripeCli,
+ stripeId,
+ expand = [],
}: {
- stripeCli: Stripe;
- stripeId: string;
- expand?: string[];
+ stripeCli: Stripe;
+ stripeId: string;
+ expand?: string[];
}) => {
- const invoice = await stripeCli.invoices.retrieve(stripeId, {
- expand: [...expand, "discounts", "discounts.coupon"],
- });
+ const invoice = await stripeCli.invoices.retrieve(stripeId, {
+ expand: [...expand, "discounts", "discounts.coupon"],
+ });
- return invoice;
+ return invoice;
};
export const invoiceToSubId = ({ invoice }: { invoice: Stripe.Invoice }) => {
- const subId = invoice.parent?.subscription_details?.subscription;
- return subId as string | undefined;
+ const subId = invoice.parent?.subscription_details?.subscription;
+ return subId as string | undefined;
};
export const payForInvoice = async ({
- stripeCli,
- paymentMethod,
- invoiceId,
- logger,
- errorOnFail = true,
- voidIfFailed = false,
+ stripeCli,
+ paymentMethod,
+ invoiceId,
+ logger,
+ errorOnFail = true,
+ voidIfFailed = false,
}: {
- stripeCli: Stripe;
- paymentMethod?: Stripe.PaymentMethod | null;
- invoiceId: string;
- logger: any;
- errorOnFail?: boolean;
- voidIfFailed?: boolean;
+ stripeCli: Stripe;
+ paymentMethod?: Stripe.PaymentMethod | null;
+ invoiceId: string;
+ logger: any;
+ errorOnFail?: boolean;
+ voidIfFailed?: boolean;
}) => {
- if (!paymentMethod) {
- if (errorOnFail) {
- throw new RecaseError({
- message: "No payment method found",
- code: ErrCode.CustomerHasNoPaymentMethod,
- statusCode: 400,
- });
- } else {
- return {
- paid: false,
- error: new RecaseError({
- message: "No payment method found",
- code: ErrCode.CustomerHasNoPaymentMethod,
- statusCode: 400,
- }),
- invoice: null,
- };
- }
- }
+ if (!paymentMethod) {
+ if (errorOnFail) {
+ throw new RecaseError({
+ message: "No payment method found",
+ code: ErrCode.CustomerHasNoPaymentMethod,
+ statusCode: 400,
+ });
+ } else {
+ return {
+ paid: false,
+ error: new RecaseError({
+ message: "No payment method found",
+ code: ErrCode.CustomerHasNoPaymentMethod,
+ statusCode: 400,
+ }),
+ invoice: null,
+ };
+ }
+ }
- let invoice = await stripeCli.invoices.retrieve(invoiceId);
- if (invoice.status == "paid") {
- logger.info(`Invoice ${invoiceId} is already paid`);
- return {
- paid: true,
- error: null,
- invoice,
- };
- }
+ let invoice = await stripeCli.invoices.retrieve(invoiceId);
+ if (invoice.status == "paid") {
+ logger.info(`Invoice ${invoiceId} is already paid`);
+ return {
+ paid: true,
+ error: null,
+ invoice,
+ };
+ }
- try {
- const invoice = await stripeCli.invoices.pay(invoiceId, {
- payment_method: paymentMethod?.id,
- });
- return {
- paid: true,
- error: null,
- invoice,
- };
- } catch (error: any) {
- logger.error(
- `❌ Stripe error: Failed to pay invoice: ${error?.message || error}`
- );
+ try {
+ const invoice = await stripeCli.invoices.pay(invoiceId, {
+ payment_method: paymentMethod?.id,
+ });
+ return {
+ paid: true,
+ error: null,
+ invoice,
+ };
+ } catch (error: any) {
+ logger.error(
+ `❌ Stripe error: Failed to pay invoice: ${error?.message || error}`,
+ );
- if (voidIfFailed) {
- try {
- await stripeCli.invoices.voidInvoice(invoiceId);
- } catch (error) {
- logger.error(`Failed to void failed invoice: ${invoiceId}`);
- }
- }
+ if (voidIfFailed) {
+ try {
+ await stripeCli.invoices.voidInvoice(invoiceId);
+ } catch (error) {
+ logger.error(`Failed to void failed invoice: ${invoiceId}`);
+ }
+ }
- if (errorOnFail) {
- throw error;
- } else {
- return {
- paid: false,
- error: new RecaseError({
- message: `Failed to pay invoice: ${error?.message || error}`,
- code: ErrCode.PayInvoiceFailed,
- }),
- invoice: null,
- };
- }
+ if (errorOnFail) {
+ throw error;
+ } else {
+ return {
+ paid: false,
+ error: new RecaseError({
+ message: `Failed to pay invoice: ${error?.message || error}`,
+ code: ErrCode.PayInvoiceFailed,
+ }),
+ invoice: null,
+ };
+ }
- // if (isStripeCardDeclined(error)) {
- // return {
- // paid: false,
- // error: new RecaseError({
- // message: `Payment declined: ${error.message}`,
- // code: ErrCode.StripeCardDeclined,
- // statusCode: 400,
- // }),
- // };
- // }
+ // if (isStripeCardDeclined(error)) {
+ // return {
+ // paid: false,
+ // error: new RecaseError({
+ // message: `Payment declined: ${error.message}`,
+ // code: ErrCode.StripeCardDeclined,
+ // statusCode: 400,
+ // }),
+ // };
+ // }
- // return {
- // paid: false,
- // error: new RecaseError({
- // message: "Failed to pay invoice",
- // code: ErrCode.PayInvoiceFailed,
- // }),
- // };
- }
+ // return {
+ // paid: false,
+ // error: new RecaseError({
+ // message: "Failed to pay invoice",
+ // code: ErrCode.PayInvoiceFailed,
+ // }),
+ // };
+ }
};
export const updateInvoiceIfExists = async ({
- db,
- invoice,
+ db,
+ invoice,
}: {
- db: DrizzleCli;
- invoice: Stripe.Invoice;
+ db: DrizzleCli;
+ invoice: Stripe.Invoice;
}) => {
- // TODO: Can optimize this function...
- const existingInvoice = await InvoiceService.getByStripeId({
- db,
- stripeId: invoice.id!,
- });
+ // TODO: Can optimize this function...
+ const existingInvoice = await InvoiceService.getByStripeId({
+ db,
+ stripeId: invoice.id!,
+ });
- if (existingInvoice) {
- await InvoiceService.updateByStripeId({
- db,
- stripeId: invoice.id!,
- updates: {
- status: invoice.status as InvoiceStatus,
- hosted_invoice_url: invoice.hosted_invoice_url,
- },
- });
+ if (existingInvoice) {
+ await InvoiceService.updateByStripeId({
+ db,
+ stripeId: invoice.id!,
+ updates: {
+ status: invoice.status as InvoiceStatus,
+ hosted_invoice_url: invoice.hosted_invoice_url,
+ },
+ });
- return true;
- }
+ return true;
+ }
- return false;
+ return false;
};
export const getInvoiceDiscounts = ({
- expandedInvoice,
+ expandedInvoice,
}: {
- expandedInvoice: Stripe.Invoice;
+ expandedInvoice: Stripe.Invoice;
}) => {
- try {
- if (!expandedInvoice.discounts || expandedInvoice.discounts.length === 0) {
- return [];
- }
+ try {
+ if (!expandedInvoice.discounts || expandedInvoice.discounts.length === 0) {
+ return [];
+ }
- if (typeof expandedInvoice.discounts[0] == "string") {
- return [];
- }
+ if (typeof expandedInvoice.discounts[0] == "string") {
+ return [];
+ }
- let totalDiscountAmounts = expandedInvoice.total_discount_amounts;
+ let totalDiscountAmounts = expandedInvoice.total_discount_amounts;
- let autumnDiscounts = expandedInvoice.discounts.map((discount: any) => {
- const amountOff = discount.coupon.amount_off;
- const amountUsed = totalDiscountAmounts?.find(
- (item) => item.discount === discount.id
- )?.amount;
+ let autumnDiscounts = expandedInvoice.discounts.map((discount: any) => {
+ const amountOff = discount.coupon.amount_off;
+ const amountUsed = totalDiscountAmounts?.find(
+ (item) => item.discount === discount.id,
+ )?.amount;
- let autumnDiscount: InvoiceDiscount = {
- stripe_coupon_id: discount.coupon?.id,
- coupon_name: discount.coupon.name,
- amount_off: amountOff / 100,
- amount_used: (amountUsed || 0) / 100,
- };
+ let autumnDiscount: InvoiceDiscount = {
+ stripe_coupon_id: discount.coupon?.id,
+ coupon_name: discount.coupon.name,
+ amount_off: amountOff / 100,
+ amount_used: (amountUsed || 0) / 100,
+ };
- return autumnDiscount;
- });
+ return autumnDiscount;
+ });
- return autumnDiscounts;
- } catch (error) {
- throw error;
- }
+ return autumnDiscounts;
+ } catch (error) {
+ throw error;
+ }
};
export const getInvoiceExpansion = () => {
- return {
- expand: ["discounts", "discounts.coupon"],
- };
+ return {
+ expand: ["discounts", "discounts.coupon"],
+ };
};
diff --git a/server/src/external/stripe/stripeMeterUtils.ts b/server/src/external/stripe/stripeMeterUtils.ts
index 49a45bfde..6d859d1ca 100644
--- a/server/src/external/stripe/stripeMeterUtils.ts
+++ b/server/src/external/stripe/stripeMeterUtils.ts
@@ -5,44 +5,44 @@ import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
export const submitUsageToStripe = async ({
- price,
- stripeCli,
- usage,
- customer,
- usageTimestamp,
- feature,
- logger,
+ price,
+ stripeCli,
+ usage,
+ customer,
+ usageTimestamp,
+ feature,
+ logger,
}: {
- stripeCli: Stripe;
- price: Price;
- usage: number;
- customer: Customer;
- usageTimestamp?: number;
- feature: Feature;
- logger: any;
+ stripeCli: Stripe;
+ price: Price;
+ usage: number;
+ customer: Customer;
+ usageTimestamp?: number;
+ feature: Feature;
+ logger: any;
}) => {
- let config = price.config as UsagePriceConfig;
- let billingType = getBillingType(config);
+ let config = price.config as UsagePriceConfig;
+ let billingType = getBillingType(config);
- if (billingType != BillingType.UsageInArrear) {
- logger.warn(
- `Price ${price.id} is not usage in arrear type, can't send usage`,
- );
- }
- const stripeMeter = await stripeCli.billing.meters.retrieve(
- config.stripe_meter_id!,
- );
+ if (billingType != BillingType.UsageInArrear) {
+ logger.warn(
+ `Price ${price.id} is not usage in arrear type, can't send usage`,
+ );
+ }
+ const stripeMeter = await stripeCli.billing.meters.retrieve(
+ config.stripe_meter_id!,
+ );
- await stripeCli.billing.meterEvents.create({
- event_name: stripeMeter.event_name,
- payload: {
- stripe_customer_id: customer.processor.id,
- value: usage.toString(),
- },
- timestamp: usageTimestamp || Math.floor(Date.now() / 1000),
- });
+ await stripeCli.billing.meterEvents.create({
+ event_name: stripeMeter.event_name,
+ payload: {
+ stripe_customer_id: customer.processor.id,
+ value: usage.toString(),
+ },
+ timestamp: usageTimestamp || Math.floor(Date.now() / 1000),
+ });
- logger.info(
- `🌟🌟🌟 Submitted meter event for customer ${customer.id}, feature: ${feature.name}, rounded usage: ${usage}`,
- );
+ logger.info(
+ `🌟🌟🌟 Submitted meter event for customer ${customer.id}, feature: ${feature.name}, rounded usage: ${usage}`,
+ );
};
diff --git a/server/src/external/stripe/stripeOnboardingUtils.ts b/server/src/external/stripe/stripeOnboardingUtils.ts
index 9f6c8c1dc..59a628f4f 100644
--- a/server/src/external/stripe/stripeOnboardingUtils.ts
+++ b/server/src/external/stripe/stripeOnboardingUtils.ts
@@ -3,51 +3,51 @@ import { AppEnv, ErrCode } from "@autumn/shared";
import Stripe from "stripe";
export const checkKeyValid = async (apiKey: string) => {
- const stripe = new Stripe(apiKey);
+ const stripe = new Stripe(apiKey);
- // Call customers.list
- const customers = await stripe.customers.list();
+ // Call customers.list
+ const customers = await stripe.customers.list();
- // const account = await stripe.accounts.retrieve();
- // console.log("Account", account);
- // return account;
+ // const account = await stripe.accounts.retrieve();
+ // console.log("Account", account);
+ // return account;
};
export const createWebhookEndpoint = async (
- apiKey: string,
- env: AppEnv,
- orgId: string
+ apiKey: string,
+ env: AppEnv,
+ orgId: string,
) => {
- const stripe = new Stripe(apiKey);
+ const stripe = new Stripe(apiKey);
- const webhookBaseUrl =
- process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL;
+ const webhookBaseUrl =
+ process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL;
- if (!webhookBaseUrl) {
- throw new RecaseError({
- message: "Stripe webhook baseURL not found",
- code: ErrCode.StripeKeyInvalid,
- statusCode: 500,
- });
- }
+ if (!webhookBaseUrl) {
+ throw new RecaseError({
+ message: "Stripe webhook baseURL not found",
+ code: ErrCode.StripeKeyInvalid,
+ statusCode: 500,
+ });
+ }
- const endpoint = await stripe.webhookEndpoints.create({
- url: `${webhookBaseUrl}/webhooks/stripe/${orgId}/${env}`,
- enabled_events: [
- "customer.subscription.created",
- "customer.subscription.updated",
- "customer.subscription.deleted",
- "checkout.session.completed",
- "invoice.paid",
- "invoice.upcoming",
- "invoice.created",
- "invoice.finalized",
- "invoice.updated",
- "subscription_schedule.canceled",
- "subscription_schedule.updated",
- "customer.discount.deleted",
- ],
- });
+ const endpoint = await stripe.webhookEndpoints.create({
+ url: `${webhookBaseUrl}/webhooks/stripe/${orgId}/${env}`,
+ enabled_events: [
+ "customer.subscription.created",
+ "customer.subscription.updated",
+ "customer.subscription.deleted",
+ "checkout.session.completed",
+ "invoice.paid",
+ "invoice.upcoming",
+ "invoice.created",
+ "invoice.finalized",
+ "invoice.updated",
+ "subscription_schedule.canceled",
+ "subscription_schedule.updated",
+ "customer.discount.deleted",
+ ],
+ });
- return endpoint;
+ return endpoint;
};
diff --git a/server/src/external/stripe/stripePriceUtils.ts b/server/src/external/stripe/stripePriceUtils.ts
index ccbc811bf..4cde57493 100644
--- a/server/src/external/stripe/stripePriceUtils.ts
+++ b/server/src/external/stripe/stripePriceUtils.ts
@@ -1,12 +1,12 @@
import {
- BillingInterval,
- Price,
- Feature,
- Customer,
- FullCusProduct,
- UsagePriceConfig,
- FullProduct,
- Organization,
+ BillingInterval,
+ Price,
+ Feature,
+ Customer,
+ FullCusProduct,
+ UsagePriceConfig,
+ FullProduct,
+ Organization,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -16,125 +16,125 @@ import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
import { getCusPriceUsage } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
export const createSubMeta = ({ features }: { features: Feature[] }) => {
- const usageFeatures = features.map((f) => ({
- internal_id: f.internal_id,
- id: f.id,
- }));
- return { usage_features: JSON.stringify(usageFeatures) };
+ const usageFeatures = features.map((f) => ({
+ internal_id: f.internal_id,
+ id: f.id,
+ }));
+ return { usage_features: JSON.stringify(usageFeatures) };
};
export const billingIntervalToStripe = ({
- interval,
- intervalCount,
+ interval,
+ intervalCount,
}: {
- interval: BillingInterval;
- intervalCount?: number | null;
+ interval: BillingInterval;
+ intervalCount?: number | null;
}) => {
- const finalCount = intervalCount ?? 1;
- switch (interval) {
- case BillingInterval.Week:
- return {
- interval: "week",
- interval_count: finalCount,
- };
- case BillingInterval.Month:
- return {
- interval: "month",
- interval_count: finalCount,
- };
- case BillingInterval.Quarter:
- return {
- interval: "month",
- interval_count: finalCount * 3,
- };
- case BillingInterval.SemiAnnual:
- return {
- interval: "month",
- interval_count: finalCount * 6,
- };
- case BillingInterval.Year:
- return {
- interval: "year",
- interval_count: finalCount,
- };
- default:
- break;
- }
+ const finalCount = intervalCount ?? 1;
+ switch (interval) {
+ case BillingInterval.Week:
+ return {
+ interval: "week",
+ interval_count: finalCount,
+ };
+ case BillingInterval.Month:
+ return {
+ interval: "month",
+ interval_count: finalCount,
+ };
+ case BillingInterval.Quarter:
+ return {
+ interval: "month",
+ interval_count: finalCount * 3,
+ };
+ case BillingInterval.SemiAnnual:
+ return {
+ interval: "month",
+ interval_count: finalCount * 6,
+ };
+ case BillingInterval.Year:
+ return {
+ interval: "year",
+ interval_count: finalCount,
+ };
+ default:
+ break;
+ }
};
export const getInvoiceItemForUsage = ({
- stripeInvoiceId,
- price,
- currency,
- customer,
- cusProduct,
- logger,
- periodStart,
- periodEnd,
+ stripeInvoiceId,
+ price,
+ currency,
+ customer,
+ cusProduct,
+ logger,
+ periodStart,
+ periodEnd,
}: {
- stripeInvoiceId: string;
- price: Price;
- currency: string;
- customer: Customer;
- cusProduct: FullCusProduct;
- logger: any;
- periodStart: number;
- periodEnd: number;
+ stripeInvoiceId: string;
+ price: Price;
+ currency: string;
+ customer: Customer;
+ cusProduct: FullCusProduct;
+ logger: any;
+ periodStart: number;
+ periodEnd: number;
}) => {
- const { amount, description } = getCusPriceUsage({
- price,
- cusProduct,
- logger,
- withProdPrefix: true,
- });
+ const { amount, description } = getCusPriceUsage({
+ price,
+ cusProduct,
+ logger,
+ withProdPrefix: true,
+ });
- let config = price.config! as UsagePriceConfig;
+ let config = price.config! as UsagePriceConfig;
- let invoiceItem: Stripe.InvoiceItemCreateParams = {
- invoice: stripeInvoiceId,
- customer: customer.processor.id,
- currency,
- description,
+ let invoiceItem: Stripe.InvoiceItemCreateParams = {
+ invoice: stripeInvoiceId,
+ customer: customer.processor.id,
+ currency,
+ description,
- price_data: {
- product: config.stripe_product_id!,
- unit_amount: Math.max(Math.round(amount * 100), 0),
- currency,
- },
- period: {
- start: periodStart,
- end: periodEnd,
- },
- };
+ price_data: {
+ product: config.stripe_product_id!,
+ unit_amount: Math.max(Math.round(amount * 100), 0),
+ currency,
+ },
+ period: {
+ start: periodStart,
+ end: periodEnd,
+ },
+ };
- logger.info(`🌟🌟 Created invoice item: ${description}`);
+ logger.info(`🌟🌟 Created invoice item: ${description}`);
- return invoiceItem;
+ return invoiceItem;
};
export const getPlaceholderItem = ({
- product,
- org,
- interval,
- intervalCount,
+ product,
+ org,
+ interval,
+ intervalCount,
}: {
- product: FullProduct;
- org: Organization;
- interval: BillingInterval;
- intervalCount: number;
+ product: FullProduct;
+ org: Organization;
+ interval: BillingInterval;
+ intervalCount: number;
}) => {
- return {
- price_data: {
- product: product.processor!.id,
- unit_amount: 0,
- currency: org.default_currency || "usd",
- recurring: {
- ...billingIntervalToStripe({
- interval,
- intervalCount,
- }),
- },
- },
- quantity: 0,
- };
+ return {
+ price_data: {
+ product: product.processor!.id,
+ unit_amount: 0,
+ currency: org.default_currency || "usd",
+ recurring: {
+ ...billingIntervalToStripe({
+ interval,
+ intervalCount,
+ }),
+ },
+ },
+ quantity: 0,
+ };
};
diff --git a/server/src/external/stripe/stripeProductUtils.ts b/server/src/external/stripe/stripeProductUtils.ts
index 1d126365b..3fe4f7f1f 100644
--- a/server/src/external/stripe/stripeProductUtils.ts
+++ b/server/src/external/stripe/stripeProductUtils.ts
@@ -5,143 +5,143 @@ import { ErrCode } from "@/errors/errCodes.js";
import { StatusCodes } from "http-status-codes";
export const createStripeProduct = async (
- org: Organization,
- env: AppEnv,
- product: Product,
+ org: Organization,
+ env: AppEnv,
+ product: Product,
) => {
- try {
- const stripe = createStripeCli({ org, env });
+ try {
+ const stripe = createStripeCli({ org, env });
- const stripeProduct = await stripe.products.create({
- name: product.name,
- metadata: {
- autumn_id: product.id,
- autumn_internal_id: product.internal_id,
- },
- });
+ const stripeProduct = await stripe.products.create({
+ name: product.name,
+ metadata: {
+ autumn_id: product.id,
+ autumn_internal_id: product.internal_id,
+ },
+ });
- return stripeProduct;
- } catch (error: any) {
- throw new RecaseError({
- message: `Error creating product in Stripe. ${error.message}`,
- code: ErrCode.CreateStripeProductFailed,
- statusCode: 500,
- });
- }
+ return stripeProduct;
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Error creating product in Stripe. ${error.message}`,
+ code: ErrCode.CreateStripeProductFailed,
+ statusCode: 500,
+ });
+ }
};
export const deleteStripeProduct = async (
- org: Organization,
- env: AppEnv,
- product: Product,
+ org: Organization,
+ env: AppEnv,
+ product: Product,
) => {
- const stripe = createStripeCli({ org, env });
+ const stripe = createStripeCli({ org, env });
- if (
- !product.processor ||
- !product.processor.id ||
- product.env === AppEnv.Live
- ) {
- // Don't delete live products
- return;
- }
+ if (
+ !product.processor ||
+ !product.processor.id ||
+ product.env === AppEnv.Live
+ ) {
+ // Don't delete live products
+ return;
+ }
- try {
- await stripe.products.del(product.processor.id);
- } catch (error) {
- throw new RecaseError({
- message: "Failed to delete stripe product",
- code: ErrCode.DeleteStripeProductFailed,
- statusCode: 500,
- });
- }
+ try {
+ await stripe.products.del(product.processor.id);
+ } catch (error) {
+ throw new RecaseError({
+ message: "Failed to delete stripe product",
+ code: ErrCode.DeleteStripeProductFailed,
+ statusCode: 500,
+ });
+ }
};
export const deactivateStripeMeters = async ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- let allStripeMeters = [];
- let hasMore = true;
- let startingAfter;
+ let allStripeMeters = [];
+ let hasMore = true;
+ let startingAfter;
- while (hasMore) {
- const response: any = await stripeCli.billing.meters.list({
- limit: 100,
- status: "active",
- starting_after: startingAfter,
- });
+ while (hasMore) {
+ const response: any = await stripeCli.billing.meters.list({
+ limit: 100,
+ status: "active",
+ starting_after: startingAfter,
+ });
- allStripeMeters.push(...response.data);
- hasMore = response.has_more;
+ allStripeMeters.push(...response.data);
+ hasMore = response.has_more;
- if (hasMore && response.data.length > 0) {
- startingAfter = response.data[response.data.length - 1].id;
- }
- }
+ if (hasMore && response.data.length > 0) {
+ startingAfter = response.data[response.data.length - 1].id;
+ }
+ }
- const batchSize = 20;
- for (let i = 0; i < allStripeMeters.length; i += batchSize) {
- const batch = allStripeMeters.slice(i, i + batchSize);
- await Promise.all(
- batch.map((meter) => stripeCli.billing.meters.deactivate(meter.id)),
- );
- console.log(
- `Deactivated ${i + batch.length}/${allStripeMeters.length} meters`,
- );
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
+ const batchSize = 20;
+ for (let i = 0; i < allStripeMeters.length; i += batchSize) {
+ const batch = allStripeMeters.slice(i, i + batchSize);
+ await Promise.all(
+ batch.map((meter) => stripeCli.billing.meters.deactivate(meter.id)),
+ );
+ console.log(
+ `Deactivated ${i + batch.length}/${allStripeMeters.length} meters`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
};
export const deleteAllStripeProducts = async ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- const stripeProducts = await stripeCli.products.list({
- limit: 100,
- active: true,
- });
+ const stripeProducts = await stripeCli.products.list({
+ limit: 100,
+ active: true,
+ });
- if (stripeProducts.data.length === 0) {
- return;
- }
+ if (stripeProducts.data.length === 0) {
+ return;
+ }
- let firstProduct = stripeProducts.data[0];
- if (firstProduct.livemode) {
- throw new RecaseError({
- message: "Cannot delete livemode products",
- code: ErrCode.DeleteStripeProductFailed,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ let firstProduct = stripeProducts.data[0];
+ if (firstProduct.livemode) {
+ throw new RecaseError({
+ message: "Cannot delete livemode products",
+ code: ErrCode.DeleteStripeProductFailed,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- let batchSize = 50;
- for (let i = 0; i < stripeProducts.data.length; i += batchSize) {
- let batch = stripeProducts.data.slice(i, i + batchSize);
- await Promise.all(
- batch.map(async (p) => {
- try {
- await stripeCli.products.del(p.id);
- } catch (error) {
- await stripeCli.products.update(p.id, {
- active: false,
- });
- }
- }),
- );
- console.log(
- `Deleted ${i + batch.length}/${stripeProducts.data.length} products`,
- );
- await new Promise((resolve) => setTimeout(resolve, 1000));
- }
+ let batchSize = 50;
+ for (let i = 0; i < stripeProducts.data.length; i += batchSize) {
+ let batch = stripeProducts.data.slice(i, i + batchSize);
+ await Promise.all(
+ batch.map(async (p) => {
+ try {
+ await stripeCli.products.del(p.id);
+ } catch (error) {
+ await stripeCli.products.update(p.id, {
+ active: false,
+ });
+ }
+ }),
+ );
+ console.log(
+ `Deleted ${i + batch.length}/${stripeProducts.data.length} products`,
+ );
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ }
};
diff --git a/server/src/external/stripe/stripeSubUtils.ts b/server/src/external/stripe/stripeSubUtils.ts
index 7d494ebc3..81010402f 100644
--- a/server/src/external/stripe/stripeSubUtils.ts
+++ b/server/src/external/stripe/stripeSubUtils.ts
@@ -1,12 +1,12 @@
import Stripe from "stripe";
import {
- BillingInterval,
- CusProductStatus,
- Feature,
- FullCusProduct,
- Organization,
- ProrationBehavior,
- UsagePriceConfig,
+ BillingInterval,
+ CusProductStatus,
+ Feature,
+ FullCusProduct,
+ Organization,
+ ProrationBehavior,
+ UsagePriceConfig,
} from "@autumn/shared";
import { differenceInSeconds } from "date-fns";
// import { ProrationBehavior } from "@/internal/customers/change-product/handleUpgrade.js";
@@ -16,295 +16,296 @@ import { getEarliestPeriodEnd } from "./stripeSubUtils/convertSubUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const getFullStripeSub = async ({
- stripeCli,
- stripeId,
+ stripeCli,
+ stripeId,
}: {
- stripeCli: Stripe;
- stripeId: string;
+ stripeCli: Stripe;
+ stripeId: string;
}) => {
- return await stripeCli.subscriptions.retrieve(stripeId);
+ return await stripeCli.subscriptions.retrieve(stripeId);
};
export const getStripeSubs = async ({
- stripeCli,
- subIds,
- expand,
+ stripeCli,
+ subIds,
+ expand,
}: {
- stripeCli: Stripe;
- subIds?: string[] | null;
- expand?: string[];
+ stripeCli: Stripe;
+ subIds?: string[] | null;
+ expand?: string[];
}) => {
- if (!subIds) {
- return [];
- }
- const batchGet = [];
- const getStripeSub = async (subId: string) => {
- try {
- return await stripeCli.subscriptions.retrieve(subId, {
- expand: expand || undefined,
- });
- } catch (error: any) {
- console.log(
- `(warning) getStripeSubs: Failed to get sub ${subId}`,
- error.message
- );
- return null;
- }
- };
+ if (!subIds) {
+ return [];
+ }
+ const batchGet = [];
+ const getStripeSub = async (subId: string) => {
+ try {
+ return await stripeCli.subscriptions.retrieve(subId, {
+ expand: expand || undefined,
+ });
+ } catch (error: any) {
+ console.log(
+ `(warning) getStripeSubs: Failed to get sub ${subId}`,
+ error.message,
+ );
+ return null;
+ }
+ };
- for (const subId of subIds) {
- batchGet.push(getStripeSub(subId));
- }
- let subs = await Promise.all(batchGet);
- subs = subs.filter((sub) => sub !== null);
+ for (const subId of subIds) {
+ batchGet.push(getStripeSub(subId));
+ }
+ let subs = await Promise.all(batchGet);
+ subs = subs.filter((sub) => sub !== null);
- // Sort by current_period_end (latest first)
- subs.sort((a: any, b: any) => {
- return b.current_period_end - a.current_period_end;
- });
+ // Sort by current_period_end (latest first)
+ subs.sort((a: any, b: any) => {
+ return b.current_period_end - a.current_period_end;
+ });
- return subs as Stripe.Subscription[];
+ return subs as Stripe.Subscription[];
};
export const stripeToAutumnSubStatus = (stripeSubStatus: string) => {
- switch (stripeSubStatus) {
- case "trialing":
- return CusProductStatus.Active;
- case "active":
- return CusProductStatus.Active;
- case "past_due":
- return CusProductStatus.PastDue;
+ switch (stripeSubStatus) {
+ case "trialing":
+ return CusProductStatus.Active;
+ case "active":
+ return CusProductStatus.Active;
+ case "past_due":
+ return CusProductStatus.PastDue;
- default:
- return stripeSubStatus;
- }
+ default:
+ return stripeSubStatus;
+ }
};
export const deleteScheduledIds = async ({
- stripeCli,
- scheduledIds,
+ stripeCli,
+ scheduledIds,
}: {
- stripeCli: Stripe;
- scheduledIds: string[];
+ stripeCli: Stripe;
+ scheduledIds: string[];
}) => {
- for (const scheduledId of scheduledIds) {
- try {
- await stripeCli.subscriptionSchedules.cancel(scheduledId);
- } catch (error: any) {
- console.log("Error deleting scheduled id.", error.message);
- }
- }
+ for (const scheduledId of scheduledIds) {
+ try {
+ await stripeCli.subscriptionSchedules.cancel(scheduledId);
+ } catch (error: any) {
+ console.log("Error deleting scheduled id.", error.message);
+ }
+ }
};
// Get in advance sub
export const getUsageBasedSub = async ({
- db,
- stripeCli,
- subIds,
- feature,
- stripeSubs,
+ db,
+ stripeCli,
+ subIds,
+ feature,
+ stripeSubs,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- subIds: string[];
- feature: Feature;
- stripeSubs?: Stripe.Subscription[];
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ subIds: string[];
+ feature: Feature;
+ stripeSubs?: Stripe.Subscription[];
}) => {
- let subs;
- if (stripeSubs) {
- subs = stripeSubs;
- } else {
- subs = await getStripeSubs({
- stripeCli,
- subIds,
- });
- }
+ let subs;
+ if (stripeSubs) {
+ subs = stripeSubs;
+ } else {
+ subs = await getStripeSubs({
+ stripeCli,
+ subIds,
+ });
+ }
- let finalSubIds = subs.map((sub) => sub.id);
+ let finalSubIds = subs.map((sub) => sub.id);
- let autumnSubs = await SubService.getInStripeIds({
- db,
- ids: finalSubIds,
- });
+ let autumnSubs = await SubService.getInStripeIds({
+ db,
+ ids: finalSubIds,
+ });
- for (const stripeSub of subs) {
- let usageFeatures: string[] | null = null;
+ for (const stripeSub of subs) {
+ let usageFeatures: string[] | null = null;
- // 1. Check if there's autumn sub
- let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id);
- if (autumnSub) {
- let containsFeature = autumnSub.usage_features.includes(
- feature.internal_id!
- );
- if (containsFeature) {
- return stripeSub;
- }
- }
+ // 1. Check if there's autumn sub
+ let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id);
+ if (autumnSub) {
+ let containsFeature = autumnSub.usage_features.includes(
+ feature.internal_id!,
+ );
+ if (containsFeature) {
+ return stripeSub;
+ }
+ }
- try {
- usageFeatures = JSON.parse(stripeSub.metadata.usage_features);
- } catch (error) {
- continue;
- }
+ try {
+ usageFeatures = JSON.parse(stripeSub.metadata.usage_features);
+ } catch (error) {
+ continue;
+ }
- if (
- !usageFeatures ||
- usageFeatures.find(
- (feat: any) => feat.internal_id == feature.internal_id
- ) === undefined
- ) {
- continue;
- }
+ if (
+ !usageFeatures ||
+ usageFeatures.find(
+ (feat: any) => feat.internal_id == feature.internal_id,
+ ) === undefined
+ ) {
+ continue;
+ }
- return stripeSub;
- }
+ return stripeSub;
+ }
- return null;
+ return null;
};
export const getSubItemsForCusProduct = async ({
- stripeSub,
- cusProduct,
+ stripeSub,
+ cusProduct,
}: {
- stripeSub: Stripe.Subscription;
- cusProduct: FullCusProduct;
+ stripeSub: Stripe.Subscription;
+ cusProduct: FullCusProduct;
}) => {
- let prices = cusProduct.customer_prices.map((cp) => cp.price);
- let product = cusProduct.product;
+ let prices = cusProduct.customer_prices.map((cp) => cp.price);
+ let product = cusProduct.product;
- let subItems = [];
- for (const item of stripeSub.items.data) {
- if (item.price.product == product.processor?.id) {
- subItems.push(item);
- } else if (
- prices.some(
- (p) =>
- p.config?.stripe_price_id == item.price.id ||
- (p.config as UsagePriceConfig).stripe_product_id == item.price.product
- )
- ) {
- subItems.push(item);
- }
- }
- let otherSubItems = stripeSub.items.data.filter(
- (item) => !subItems.some((i) => i.id == item.id)
- );
+ let subItems = [];
+ for (const item of stripeSub.items.data) {
+ if (item.price.product == product.processor?.id) {
+ subItems.push(item);
+ } else if (
+ prices.some(
+ (p) =>
+ p.config?.stripe_price_id == item.price.id ||
+ (p.config as UsagePriceConfig).stripe_product_id ==
+ item.price.product,
+ )
+ ) {
+ subItems.push(item);
+ }
+ }
+ let otherSubItems = stripeSub.items.data.filter(
+ (item) => !subItems.some((i) => i.id == item.id),
+ );
- return { subItems, otherSubItems };
+ return { subItems, otherSubItems };
};
export const getStripeSchedules = async ({
- stripeCli,
- scheduleIds,
+ stripeCli,
+ scheduleIds,
}: {
- stripeCli: Stripe;
- scheduleIds: string[];
+ stripeCli: Stripe;
+ scheduleIds: string[];
}) => {
- const batchGet = [];
- const getStripeSchedule = async (scheduleId: string) => {
- try {
- const schedule = await stripeCli.subscriptionSchedules.retrieve(
- scheduleId,
- {
- expand: ["phases.items.price"],
- }
- );
+ const batchGet = [];
+ const getStripeSchedule = async (scheduleId: string) => {
+ try {
+ const schedule = await stripeCli.subscriptionSchedules.retrieve(
+ scheduleId,
+ {
+ expand: ["phases.items.price"],
+ },
+ );
- if (schedule.status == "canceled") {
- return null;
- }
+ if (schedule.status == "canceled") {
+ return null;
+ }
- const batchPricesGet = [];
- for (const item of schedule.phases[0].items) {
- batchPricesGet.push(
- stripeCli.prices.retrieve((item.price as Stripe.Price).id as string)
- );
- }
- const prices = await Promise.all(batchPricesGet);
- const interval = prices[0].recurring?.interval;
- // const billingInterval = stripeToAutumnInterval({
- // interval: prices[0].recurring?.interval as string,
- // intervalCount: prices[0].recurring?.interval_count || 1,
- // });
+ const batchPricesGet = [];
+ for (const item of schedule.phases[0].items) {
+ batchPricesGet.push(
+ stripeCli.prices.retrieve((item.price as Stripe.Price).id as string),
+ );
+ }
+ const prices = await Promise.all(batchPricesGet);
+ const interval = prices[0].recurring?.interval;
+ // const billingInterval = stripeToAutumnInterval({
+ // interval: prices[0].recurring?.interval as string,
+ // intervalCount: prices[0].recurring?.interval_count || 1,
+ // });
- return {
- schedule,
- interval: interval as BillingInterval,
- intervalCount: prices[0].recurring?.interval_count || 1,
- prices,
- };
- } catch (error: any) {
- console.log("Error getting stripe schedule.", error.message);
- return null;
- }
- };
+ return {
+ schedule,
+ interval: interval as BillingInterval,
+ intervalCount: prices[0].recurring?.interval_count || 1,
+ prices,
+ };
+ } catch (error: any) {
+ console.log("Error getting stripe schedule.", error.message);
+ return null;
+ }
+ };
- for (const scheduleId of scheduleIds) {
- batchGet.push(getStripeSchedule(scheduleId));
- }
+ for (const scheduleId of scheduleIds) {
+ batchGet.push(getStripeSchedule(scheduleId));
+ }
- let schedulesAndSubs = await Promise.all(batchGet);
+ let schedulesAndSubs = await Promise.all(batchGet);
- return schedulesAndSubs.filter((schedule) => schedule !== null) as {
- schedule: Stripe.SubscriptionSchedule;
- interval: BillingInterval;
- intervalCount: number;
- prices: Stripe.Price[];
- }[];
+ return schedulesAndSubs.filter((schedule) => schedule !== null) as {
+ schedule: Stripe.SubscriptionSchedule;
+ interval: BillingInterval;
+ intervalCount: number;
+ prices: Stripe.Price[];
+ }[];
};
// OTHERS
export const subIsPrematurelyCanceled = (sub: Stripe.Subscription) => {
- if (sub.cancel_at_period_end) {
- return false;
- }
+ if (sub.cancel_at_period_end) {
+ return false;
+ }
- const periodEnd = getEarliestPeriodEnd({ sub });
+ const periodEnd = getEarliestPeriodEnd({ sub });
- return differenceInSeconds(periodEnd * 1000, sub.cancel_at! * 1000) > 20;
+ return differenceInSeconds(periodEnd * 1000, sub.cancel_at! * 1000) > 20;
};
export const autumnToStripeProrationBehavior = ({
- prorationBehavior,
+ prorationBehavior,
}: {
- prorationBehavior: ProrationBehavior;
+ prorationBehavior: ProrationBehavior;
}) => {
- switch (prorationBehavior) {
- case ProrationBehavior.Immediately:
- return "always_invoice";
- case ProrationBehavior.NextBilling:
- return "create_prorations";
- case ProrationBehavior.None:
- return "none";
- }
+ switch (prorationBehavior) {
+ case ProrationBehavior.Immediately:
+ return "always_invoice";
+ case ProrationBehavior.NextBilling:
+ return "create_prorations";
+ case ProrationBehavior.None:
+ return "none";
+ }
};
export const getStripeProrationBehavior = ({
- org,
- prorationBehavior,
+ org,
+ prorationBehavior,
}: {
- org: Organization;
- prorationBehavior?: ProrationBehavior;
+ org: Organization;
+ prorationBehavior?: ProrationBehavior;
}) => {
- let behaviourMap = {
- [ProrationBehavior.Immediately]: "always_invoice",
- [ProrationBehavior.NextBilling]: "create_prorations",
- [ProrationBehavior.None]: "none",
- };
+ let behaviourMap = {
+ [ProrationBehavior.Immediately]: "always_invoice",
+ [ProrationBehavior.NextBilling]: "create_prorations",
+ [ProrationBehavior.None]: "none",
+ };
- if (prorationBehavior) {
- return behaviourMap[prorationBehavior];
- }
+ if (prorationBehavior) {
+ return behaviourMap[prorationBehavior];
+ }
- return org.config.bill_upgrade_immediately
- ? behaviourMap[ProrationBehavior.Immediately]
- : behaviourMap[ProrationBehavior.NextBilling];
+ return org.config.bill_upgrade_immediately
+ ? behaviourMap[ProrationBehavior.Immediately]
+ : behaviourMap[ProrationBehavior.NextBilling];
};
export const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
- return (
- notNullish(sub.canceled_at) ||
- notNullish(sub.cancel_at) ||
- sub.cancel_at_period_end
- );
+ return (
+ notNullish(sub.canceled_at) ||
+ notNullish(sub.cancel_at) ||
+ sub.cancel_at_period_end
+ );
};
diff --git a/server/src/external/stripe/stripeSubUtils/convertSubUtils.ts b/server/src/external/stripe/stripeSubUtils/convertSubUtils.ts
index 5f1ae11bd..a8f958542 100644
--- a/server/src/external/stripe/stripeSubUtils/convertSubUtils.ts
+++ b/server/src/external/stripe/stripeSubUtils/convertSubUtils.ts
@@ -1,67 +1,67 @@
import Stripe from "stripe";
export const getLatestPeriodEnd = ({
- sub,
- subItems,
+ sub,
+ subItems,
}: {
- sub?: Stripe.Subscription;
- subItems?: Stripe.SubscriptionItem[];
+ sub?: Stripe.Subscription;
+ subItems?: Stripe.SubscriptionItem[];
}) => {
- if (!subItems) {
- subItems = sub?.items.data || [];
- }
- if (subItems.length == 0) {
- return Date.now();
- }
+ if (!subItems) {
+ subItems = sub?.items.data || [];
+ }
+ if (subItems.length == 0) {
+ return Date.now();
+ }
- return subItems.reduce((acc, item) => {
- return Math.max(acc, item.current_period_end);
- }, subItems[0].current_period_end);
+ return subItems.reduce((acc, item) => {
+ return Math.max(acc, item.current_period_end);
+ }, subItems[0].current_period_end);
};
export const getEarliestPeriodEnd = ({ sub }: { sub: Stripe.Subscription }) => {
- if (sub.items.data.length == 0) {
- return Date.now();
- }
+ if (sub.items.data.length == 0) {
+ return Date.now();
+ }
- return sub.items.data.reduce((acc, item) => {
- return Math.min(acc, item.current_period_end);
- }, sub.items.data[0].current_period_end);
+ return sub.items.data.reduce((acc, item) => {
+ return Math.min(acc, item.current_period_end);
+ }, sub.items.data[0].current_period_end);
};
export const getEarliestPeriodStart = ({
- sub,
+ sub,
}: {
- sub: Stripe.Subscription;
+ sub: Stripe.Subscription;
}) => {
- if (sub.items.data.length == 0) {
- return Date.now();
- }
+ if (sub.items.data.length == 0) {
+ return Date.now();
+ }
- return sub.items.data.reduce((acc, item) => {
- return Math.min(acc, item.current_period_start);
- }, sub.items.data[0].current_period_start);
+ return sub.items.data.reduce((acc, item) => {
+ return Math.min(acc, item.current_period_start);
+ }, sub.items.data[0].current_period_start);
};
export const getLatestPeriodStart = ({ sub }: { sub: Stripe.Subscription }) => {
- if (sub.items.data.length == 0) {
- return Date.now();
- }
+ if (sub.items.data.length == 0) {
+ return Date.now();
+ }
- return sub.items.data.reduce((acc, item) => {
- return Math.max(acc, item.current_period_start);
- }, sub.items.data[0].current_period_start);
+ return sub.items.data.reduce((acc, item) => {
+ return Math.max(acc, item.current_period_start);
+ }, sub.items.data[0].current_period_start);
};
export const subToPeriodStartEnd = ({ sub }: { sub?: Stripe.Subscription }) => {
- if (!sub || sub.items.data.length == 0) {
- return {
- start: Date.now(),
- end: Date.now(),
- };
- }
+ if (!sub || sub.items.data.length == 0) {
+ return {
+ start: Date.now(),
+ end: Date.now(),
+ };
+ }
- return {
- start: getLatestPeriodStart({ sub }),
- end: getEarliestPeriodEnd({ sub }),
- };
+ return {
+ start: getLatestPeriodStart({ sub }),
+ end: getEarliestPeriodEnd({ sub }),
+ };
};
diff --git a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
index 308cedb31..3fc5b7c8c 100644
--- a/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
+++ b/server/src/external/stripe/stripeSubUtils/getStripeSubItems.ts
@@ -1,333 +1,333 @@
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import {
- getPriceEntitlement,
- getEntOptions,
- getProductForPrice,
+ getPriceEntitlement,
+ getEntOptions,
+ getProductForPrice,
} from "@/internal/products/prices/priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- FullProduct,
- Price,
- EntitlementWithFeature,
- FeatureOptions,
- Organization,
- FullCusProduct,
- BillingInterval,
- Entity,
- APIVersion,
- InsertReplaceable,
- AttachReplaceable,
- ErrCode,
- AttachConfig,
- ProductOptions,
+ FullProduct,
+ Price,
+ EntitlementWithFeature,
+ FeatureOptions,
+ Organization,
+ FullCusProduct,
+ BillingInterval,
+ Entity,
+ APIVersion,
+ InsertReplaceable,
+ AttachReplaceable,
+ ErrCode,
+ AttachConfig,
+ ProductOptions,
} from "@autumn/shared";
import { priceToStripeItem } from "../priceToStripeItem/priceToStripeItem.js";
import { getArrearItems } from "./getStripeSubItems/getArrearItems.js";
import {
- compareBillingIntervals,
- sortPricesByInterval,
+ compareBillingIntervals,
+ sortPricesByInterval,
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import RecaseError from "@/utils/errorUtils.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import {
- intervalKeyToPrice,
- priceToIntervalKey,
- priceToProductOptions,
+ intervalKeyToPrice,
+ priceToIntervalKey,
+ priceToProductOptions,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
const getIntervalToPrices = (prices: Price[]) => {
- const intervalToPrices: Record = {};
+ const intervalToPrices: Record = {};
- for (const price of prices) {
- // const interval = price.config.interval;
- const key = priceToIntervalKey(price);
- if (!intervalToPrices[key]) {
- intervalToPrices[key] = [];
- }
- intervalToPrices[key].push(price);
- }
+ for (const price of prices) {
+ // const interval = price.config.interval;
+ const key = priceToIntervalKey(price);
+ if (!intervalToPrices[key]) {
+ intervalToPrices[key] = [];
+ }
+ intervalToPrices[key].push(price);
+ }
- let oneOffPrices =
- intervalToPrices[BillingInterval.OneOff] &&
- intervalToPrices[BillingInterval.OneOff].length > 0;
+ let oneOffPrices =
+ intervalToPrices[BillingInterval.OneOff] &&
+ intervalToPrices[BillingInterval.OneOff].length > 0;
- // If there are multiple intervals, add one off prices to first interval
- if (oneOffPrices && Object.keys(intervalToPrices).length > 1) {
- const nextIntervalKey = Object.keys(intervalToPrices)[0];
- intervalToPrices[nextIntervalKey!].push(
- ...structuredClone(intervalToPrices[BillingInterval.OneOff])
- );
- delete intervalToPrices[BillingInterval.OneOff];
- }
+ // If there are multiple intervals, add one off prices to first interval
+ if (oneOffPrices && Object.keys(intervalToPrices).length > 1) {
+ const nextIntervalKey = Object.keys(intervalToPrices)[0];
+ intervalToPrices[nextIntervalKey!].push(
+ ...structuredClone(intervalToPrices[BillingInterval.OneOff]),
+ );
+ delete intervalToPrices[BillingInterval.OneOff];
+ }
- return intervalToPrices;
+ return intervalToPrices;
};
export const getStripeSubItems = async ({
- attachParams,
- isCheckout = false,
- carryExistingUsages = false,
+ attachParams,
+ isCheckout = false,
+ carryExistingUsages = false,
}: {
- attachParams: {
- products: FullProduct[];
- productsList?: ProductOptions[];
- prices: Price[];
- entitlements: EntitlementWithFeature[];
- optionsList: FeatureOptions[];
- org: Organization;
- internalEntityId?: string;
- cusProducts?: FullCusProduct[];
- entities: Entity[];
- apiVersion?: APIVersion;
- replaceables: AttachReplaceable[];
- };
- isCheckout?: boolean;
- carryExistingUsages?: boolean;
+ attachParams: {
+ products: FullProduct[];
+ productsList?: ProductOptions[];
+ prices: Price[];
+ entitlements: EntitlementWithFeature[];
+ optionsList: FeatureOptions[];
+ org: Organization;
+ internalEntityId?: string;
+ cusProducts?: FullCusProduct[];
+ entities: Entity[];
+ apiVersion?: APIVersion;
+ replaceables: AttachReplaceable[];
+ };
+ isCheckout?: boolean;
+ carryExistingUsages?: boolean;
}) => {
- const {
- products,
- prices,
- entitlements,
- optionsList,
- org,
- internalEntityId,
- cusProducts,
- entities,
- } = attachParams;
+ const {
+ products,
+ prices,
+ entitlements,
+ optionsList,
+ org,
+ internalEntityId,
+ cusProducts,
+ entities,
+ } = attachParams;
- sortPricesByInterval(prices);
+ sortPricesByInterval(prices);
- const itemSets: any[] = [];
+ const itemSets: any[] = [];
- const intervalToPrices = getIntervalToPrices(prices);
+ const intervalToPrices = getIntervalToPrices(prices);
- for (const intervalKey in intervalToPrices) {
- const prices = intervalToPrices[intervalKey];
+ for (const intervalKey in intervalToPrices) {
+ const prices = intervalToPrices[intervalKey];
- let subItems: any[] = [];
- let usage_features: any[] = [];
+ let subItems: any[] = [];
+ let usage_features: any[] = [];
- for (const price of prices) {
- const prodOptions = priceToProductOptions({
- price,
- options: attachParams.productsList,
- products,
- });
+ for (const price of prices) {
+ const prodOptions = priceToProductOptions({
+ price,
+ options: attachParams.productsList,
+ products,
+ });
- const priceEnt = getPriceEntitlement(price, entitlements);
- const options = getEntOptions(optionsList, priceEnt);
+ const priceEnt = getPriceEntitlement(price, entitlements);
+ const options = getEntOptions(optionsList, priceEnt);
- let existingUsage = getExistingUsageFromCusProducts({
- entitlement: priceEnt,
- cusProducts,
- entities,
- carryExistingUsages,
- internalEntityId,
- });
+ let existingUsage = getExistingUsageFromCusProducts({
+ entitlement: priceEnt,
+ cusProducts,
+ entities,
+ carryExistingUsages,
+ internalEntityId,
+ });
- let replaceables = priceEnt
- ? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
- : [];
+ let replaceables = priceEnt
+ ? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
+ : [];
- existingUsage += replaceables.length;
+ existingUsage += replaceables.length;
- if (isUsagePrice({ price })) {
- usage_features.push({
- internal_id: priceEnt.feature.internal_id,
- id: priceEnt.feature.id,
- });
- }
+ if (isUsagePrice({ price })) {
+ usage_features.push({
+ internal_id: priceEnt.feature.internal_id,
+ id: priceEnt.feature.id,
+ });
+ }
- let product = getProductForPrice(price, products)!;
+ let product = getProductForPrice(price, products)!;
- if (!product) {
- logger.error(
- `Couldn't find product for price ${price.internal_product_id}`,
- {
- data: {
- products: attachParams.products,
- price,
- },
- }
- );
- throw new RecaseError({
- code: ErrCode.ProductNotFound,
- message: `Price internal product ID: ${price.internal_product_id} not found in products`,
- statusCode: 400,
- });
- }
+ if (!product) {
+ logger.error(
+ `Couldn't find product for price ${price.internal_product_id}`,
+ {
+ data: {
+ products: attachParams.products,
+ price,
+ },
+ },
+ );
+ throw new RecaseError({
+ code: ErrCode.ProductNotFound,
+ message: `Price internal product ID: ${price.internal_product_id} not found in products`,
+ statusCode: 400,
+ });
+ }
- const stripeItem = priceToStripeItem({
- price,
- product,
- org,
- options,
- isCheckout,
- relatedEnt: priceEnt,
- existingUsage,
- withEntity: notNullish(attachParams.internalEntityId),
- apiVersion: attachParams.apiVersion,
- productOptions: prodOptions,
- });
+ const stripeItem = priceToStripeItem({
+ price,
+ product,
+ org,
+ options,
+ isCheckout,
+ relatedEnt: priceEnt,
+ existingUsage,
+ withEntity: notNullish(attachParams.internalEntityId),
+ apiVersion: attachParams.apiVersion,
+ productOptions: prodOptions,
+ });
- if (!stripeItem) {
- continue;
- }
+ if (!stripeItem) {
+ continue;
+ }
- const { lineItem } = stripeItem;
+ const { lineItem } = stripeItem;
- subItems.push(lineItem);
- }
+ subItems.push(lineItem);
+ }
- const { interval, intervalCount } = intervalKeyToPrice(intervalKey);
- if (subItems.length == 0) {
- subItems.push(
- ...getArrearItems({
- prices,
- org,
- interval: interval as BillingInterval,
- intervalCount,
- })
- );
- }
+ const { interval, intervalCount } = intervalKeyToPrice(intervalKey);
+ if (subItems.length == 0) {
+ subItems.push(
+ ...getArrearItems({
+ prices,
+ org,
+ interval: interval as BillingInterval,
+ intervalCount,
+ }),
+ );
+ }
- itemSets.push({
- items: subItems,
- interval,
- intervalCount,
- subMeta: {
- usage_features: JSON.stringify(usage_features),
- },
- usageFeatures: usage_features.map((f) => f.internal_id) || [],
- prices,
- });
- }
+ itemSets.push({
+ items: subItems,
+ interval,
+ intervalCount,
+ subMeta: {
+ usage_features: JSON.stringify(usage_features),
+ },
+ usageFeatures: usage_features.map((f) => f.internal_id) || [],
+ prices,
+ });
+ }
- itemSets.sort((a, b) =>
- compareBillingIntervals({
- configA: {
- interval: a.interval,
- intervalCount: a.intervalCount,
- },
- configB: {
- interval: b.interval,
- intervalCount: b.intervalCount,
- },
- })
- );
+ itemSets.sort((a, b) =>
+ compareBillingIntervals({
+ configA: {
+ interval: a.interval,
+ intervalCount: a.intervalCount,
+ },
+ configB: {
+ interval: b.interval,
+ intervalCount: b.intervalCount,
+ },
+ }),
+ );
- return itemSets;
+ return itemSets;
};
export const getStripeSubItems2 = async ({
- attachParams,
- config,
+ attachParams,
+ config,
}: {
- attachParams: AttachParams;
- config: AttachConfig;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- const {
- prices,
- entitlements,
- optionsList,
- cusProducts,
- customer,
- internalEntityId,
- apiVersion,
- products,
- } = attachParams;
+ const {
+ prices,
+ entitlements,
+ optionsList,
+ cusProducts,
+ customer,
+ internalEntityId,
+ apiVersion,
+ products,
+ } = attachParams;
- const subItems: any[] = [];
- const invoiceItems: any[] = [];
- const usageFeatures: any[] = [];
- for (const price of prices) {
- const priceEnt = getPriceEntitlement(price, entitlements);
- const options = getEntOptions(optionsList, priceEnt);
- const prodOptions = priceToProductOptions({
- price,
- options: attachParams.productsList,
- products,
- });
+ const subItems: any[] = [];
+ const invoiceItems: any[] = [];
+ const usageFeatures: any[] = [];
+ for (const price of prices) {
+ const priceEnt = getPriceEntitlement(price, entitlements);
+ const options = getEntOptions(optionsList, priceEnt);
+ const prodOptions = priceToProductOptions({
+ price,
+ options: attachParams.productsList,
+ products,
+ });
- let existingUsage = getExistingUsageFromCusProducts({
- entitlement: priceEnt,
- cusProducts,
- entities: customer.entities,
- carryExistingUsages: config.carryUsage,
- internalEntityId,
- });
+ let existingUsage = getExistingUsageFromCusProducts({
+ entitlement: priceEnt,
+ cusProducts,
+ entities: customer.entities,
+ carryExistingUsages: config.carryUsage,
+ internalEntityId,
+ });
- let replaceables = priceEnt
- ? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
- : [];
+ let replaceables = priceEnt
+ ? attachParams.replaceables.filter((r) => r.ent.id === priceEnt.id)
+ : [];
- existingUsage += replaceables.length;
+ existingUsage += replaceables.length;
- let product = getProductForPrice(price, attachParams.products)!;
+ let product = getProductForPrice(price, attachParams.products)!;
- if (!product) {
- logger.error(
- `Couldn't find product for price ${price.internal_product_id}`,
- {
- data: {
- products: attachParams.products,
- price,
- },
- }
- );
- throw new RecaseError({
- code: ErrCode.ProductNotFound,
- message: `Price internal product ID: ${price.internal_product_id} not found in products`,
- statusCode: 400,
- });
- }
+ if (!product) {
+ logger.error(
+ `Couldn't find product for price ${price.internal_product_id}`,
+ {
+ data: {
+ products: attachParams.products,
+ price,
+ },
+ },
+ );
+ throw new RecaseError({
+ code: ErrCode.ProductNotFound,
+ message: `Price internal product ID: ${price.internal_product_id} not found in products`,
+ statusCode: 400,
+ });
+ }
- const stripeItem = priceToStripeItem({
- price,
- product,
- org: attachParams.org,
- options,
- isCheckout: config.onlyCheckout,
- relatedEnt: priceEnt,
- existingUsage,
- withEntity: notNullish(internalEntityId),
- apiVersion: attachParams.apiVersion,
- productOptions: prodOptions,
- });
+ const stripeItem = priceToStripeItem({
+ price,
+ product,
+ org: attachParams.org,
+ options,
+ isCheckout: config.onlyCheckout,
+ relatedEnt: priceEnt,
+ existingUsage,
+ withEntity: notNullish(internalEntityId),
+ apiVersion: attachParams.apiVersion,
+ productOptions: prodOptions,
+ });
- if (isUsagePrice({ price })) {
- usageFeatures.push(priceEnt.feature.internal_id);
- }
+ if (isUsagePrice({ price })) {
+ usageFeatures.push(priceEnt.feature.internal_id);
+ }
- if (!stripeItem) {
- continue;
- }
+ if (!stripeItem) {
+ continue;
+ }
- const { lineItem } = stripeItem;
+ const { lineItem } = stripeItem;
- // subItems.push(lineItem);
+ // subItems.push(lineItem);
- if (price.config.interval === BillingInterval.OneOff) {
- invoiceItems.push(lineItem);
- } else {
- subItems.push({
- ...lineItem,
- autumnPrice: price,
- });
- }
- }
+ if (price.config.interval === BillingInterval.OneOff) {
+ invoiceItems.push(lineItem);
+ } else {
+ subItems.push({
+ ...lineItem,
+ autumnPrice: price,
+ });
+ }
+ }
- return { subItems, invoiceItems, usageFeatures } as ItemSet;
+ return { subItems, invoiceItems, usageFeatures } as ItemSet;
};
export const sanitizeSubItems = (subItems: any[]) => {
- return subItems.map((si) => {
- const { autumnPrice, ...rest } = si;
- return {
- ...rest,
- };
- });
+ return subItems.map((si) => {
+ const { autumnPrice, ...rest } = si;
+ return {
+ ...rest,
+ };
+ });
};
diff --git a/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts b/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts
index 475e84887..d36df4b3a 100644
--- a/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts
+++ b/server/src/external/stripe/stripeSubUtils/getStripeSubItems/getArrearItems.ts
@@ -1,59 +1,59 @@
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import {
- BillingInterval,
- BillingType,
- intervalsDifferent,
- Organization,
- UsagePriceConfig,
+ BillingInterval,
+ BillingType,
+ intervalsDifferent,
+ Organization,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Price } from "@autumn/shared";
import { billingIntervalToStripe } from "../../stripePriceUtils.js";
export const getArrearItems = ({
- prices,
- org,
- interval,
- intervalCount,
+ prices,
+ org,
+ interval,
+ intervalCount,
}: {
- prices: Price[];
- interval: BillingInterval;
- intervalCount: number;
- org: Organization;
+ prices: Price[];
+ interval: BillingInterval;
+ intervalCount: number;
+ org: Organization;
}) => {
- let placeholderItems: any[] = [];
- for (const price of prices) {
- let billingType = getBillingType(price.config!);
- if (
- intervalsDifferent({
- intervalA: {
- interval: price.config!.interval!,
- intervalCount: price.config!.interval_count!,
- },
- intervalB: { interval, intervalCount },
- })
- ) {
- continue;
- }
+ let placeholderItems: any[] = [];
+ for (const price of prices) {
+ let billingType = getBillingType(price.config!);
+ if (
+ intervalsDifferent({
+ intervalA: {
+ interval: price.config!.interval!,
+ intervalCount: price.config!.interval_count!,
+ },
+ intervalB: { interval, intervalCount },
+ })
+ ) {
+ continue;
+ }
- if (billingType == BillingType.UsageInArrear) {
- let config = price.config! as UsagePriceConfig;
- placeholderItems.push({
- price_data: {
- product: config.stripe_product_id!,
- unit_amount: 1,
- currency: org.default_currency || "usd",
- recurring: {
- ...billingIntervalToStripe({
- interval,
- intervalCount,
- }),
- },
- },
- quantity: 0,
- });
- }
- }
+ if (billingType == BillingType.UsageInArrear) {
+ let config = price.config! as UsagePriceConfig;
+ placeholderItems.push({
+ price_data: {
+ product: config.stripe_product_id!,
+ unit_amount: 1,
+ currency: org.default_currency || "usd",
+ recurring: {
+ ...billingIntervalToStripe({
+ interval,
+ intervalCount,
+ }),
+ },
+ },
+ quantity: 0,
+ });
+ }
+ }
- return placeholderItems;
+ return placeholderItems;
};
diff --git a/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts b/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts
index 3b67ca217..9e40ed546 100644
--- a/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts
+++ b/server/src/external/stripe/stripeSubUtils/getSubItemAmount.ts
@@ -4,62 +4,62 @@ import { Decimal } from "decimal.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
const calculateTieredAmount = ({
- tiers,
- quantity,
+ tiers,
+ quantity,
}: {
- tiers: Stripe.Price.Tier[];
- quantity: number;
+ tiers: Stripe.Price.Tier[];
+ quantity: number;
}) => {
- let total = new Decimal(0);
- let quantityCursor = quantity;
- for (const tier of tiers) {
- const unitAmount = new Decimal(
- tier.unit_amount_decimal || tier.unit_amount!,
- );
+ let total = new Decimal(0);
+ let quantityCursor = quantity;
+ for (const tier of tiers) {
+ const unitAmount = new Decimal(
+ tier.unit_amount_decimal || tier.unit_amount!,
+ );
- if (notNullish(tier.up_to)) {
- const bracketQuantity = Math.min(tier.up_to!, quantityCursor);
- total = total.add(unitAmount.mul(bracketQuantity));
+ if (notNullish(tier.up_to)) {
+ const bracketQuantity = Math.min(tier.up_to!, quantityCursor);
+ total = total.add(unitAmount.mul(bracketQuantity));
- quantityCursor = quantityCursor - bracketQuantity;
- } else {
- total = total.add(unitAmount.mul(quantityCursor));
- quantityCursor = 0;
- }
+ quantityCursor = quantityCursor - bracketQuantity;
+ } else {
+ total = total.add(unitAmount.mul(quantityCursor));
+ quantityCursor = 0;
+ }
- if (quantityCursor <= 0) {
- break;
- }
- }
+ if (quantityCursor <= 0) {
+ break;
+ }
+ }
- return total.toNumber();
+ return total.toNumber();
};
export const getSubItemAmount = ({
- subItem,
+ subItem,
}: {
- subItem: Stripe.SubscriptionItem;
+ subItem: Stripe.SubscriptionItem;
}) => {
- let price = subItem.price;
+ let price = subItem.price;
- const quantity = subItem.quantity || 0;
+ const quantity = subItem.quantity || 0;
- if (price.billing_scheme == "tiered") {
- let tieredAmount = calculateTieredAmount({
- tiers: price.tiers!,
- quantity,
- });
+ if (price.billing_scheme == "tiered") {
+ let tieredAmount = calculateTieredAmount({
+ tiers: price.tiers!,
+ quantity,
+ });
- return tieredAmount;
- }
+ return tieredAmount;
+ }
- if (price.billing_scheme == "per_unit") {
- if (price.unit_amount_decimal) {
- return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber();
- } else {
- return new Decimal(price.unit_amount || 0).mul(quantity).toNumber();
- }
- }
+ if (price.billing_scheme == "per_unit") {
+ if (price.unit_amount_decimal) {
+ return new Decimal(price.unit_amount_decimal).mul(quantity).toNumber();
+ } else {
+ return new Decimal(price.unit_amount || 0).mul(quantity).toNumber();
+ }
+ }
- return 0;
+ return 0;
};
diff --git a/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts b/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts
index 714fe393b..0649eb84e 100644
--- a/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts
+++ b/server/src/external/stripe/stripeSubUtils/stripeSubItemUtils.ts
@@ -3,280 +3,280 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- BillingType,
- FullCusProduct,
- Organization,
- Price,
- prices,
- PriceType,
- UsagePriceConfig,
+ BillingType,
+ FullCusProduct,
+ Organization,
+ Price,
+ prices,
+ PriceType,
+ UsagePriceConfig,
} from "@autumn/shared";
import Stripe from "stripe";
const autumnStripePricesMatch = ({
- stripePrice,
- autumnPrice,
- stripeProdId,
+ stripePrice,
+ autumnPrice,
+ stripeProdId,
}: {
- stripePrice: Stripe.Price;
- autumnPrice: Price;
- stripeProdId?: string;
+ stripePrice: Stripe.Price;
+ autumnPrice: Price;
+ stripeProdId?: string;
}) => {
- const config = autumnPrice.config as UsagePriceConfig;
+ const config = autumnPrice.config as UsagePriceConfig;
- if (config.type == PriceType.Fixed) {
- return (
- config.stripe_price_id == stripePrice.id ||
- (stripeProdId && stripePrice.product == stripeProdId)
- );
- } else {
- return (
- config.stripe_price_id == stripePrice.id ||
- config.stripe_product_id == stripePrice.product ||
- config.stripe_empty_price_id == stripePrice.id
- );
- }
+ if (config.type == PriceType.Fixed) {
+ return (
+ config.stripe_price_id == stripePrice.id ||
+ (stripeProdId && stripePrice.product == stripeProdId)
+ );
+ } else {
+ return (
+ config.stripe_price_id == stripePrice.id ||
+ config.stripe_product_id == stripePrice.product ||
+ config.stripe_empty_price_id == stripePrice.id
+ );
+ }
};
export const priceToScheduleItem = ({
- price,
- scheduleItems,
- stripeProdId,
+ price,
+ scheduleItems,
+ stripeProdId,
}: {
- price: Price;
- scheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
- stripeProdId?: string;
+ price: Price;
+ scheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
+ stripeProdId?: string;
}) => {
- for (const scheduleItem of scheduleItems) {
- // 1. If price is fixed
- const schedulePrice = scheduleItem.price as Stripe.Price;
+ for (const scheduleItem of scheduleItems) {
+ // 1. If price is fixed
+ const schedulePrice = scheduleItem.price as Stripe.Price;
- if (
- autumnStripePricesMatch({
- stripePrice: schedulePrice,
- autumnPrice: price,
- stripeProdId,
- })
- ) {
- return scheduleItem;
- }
- }
+ if (
+ autumnStripePricesMatch({
+ stripePrice: schedulePrice,
+ autumnPrice: price,
+ stripeProdId,
+ })
+ ) {
+ return scheduleItem;
+ }
+ }
- return undefined;
+ return undefined;
};
// TO FIX
export const findStripeItemForPrice = ({
- price,
- stripeItems,
- invoiceLineItems,
- stripeProdId,
+ price,
+ stripeItems,
+ invoiceLineItems,
+ stripeProdId,
}: {
- price: Price;
- stripeItems?: Stripe.SubscriptionItem[] | Stripe.LineItem[];
- invoiceLineItems?: Stripe.InvoiceLineItem[];
- stripeProdId?: string;
+ price: Price;
+ stripeItems?: Stripe.SubscriptionItem[] | Stripe.LineItem[];
+ invoiceLineItems?: Stripe.InvoiceLineItem[];
+ stripeProdId?: string;
}) => {
- if (invoiceLineItems) {
- return invoiceLineItems.find((li) => {
- return li.pricing?.price_details?.price == price.config.stripe_price_id;
- });
- }
+ if (invoiceLineItems) {
+ return invoiceLineItems.find((li) => {
+ return li.pricing?.price_details?.price == price.config.stripe_price_id;
+ });
+ }
- if (stripeItems) {
- const stripeItem = stripeItems.find(
- (si: Stripe.SubscriptionItem | Stripe.LineItem) => {
- const config = price.config as UsagePriceConfig;
+ if (stripeItems) {
+ const stripeItem = stripeItems.find(
+ (si: Stripe.SubscriptionItem | Stripe.LineItem) => {
+ const config = price.config as UsagePriceConfig;
- return (
- config.stripe_price_id == si.price?.id ||
- config.stripe_product_id == si.price?.product ||
- config.stripe_empty_price_id == si.price?.id
- );
- }
- );
+ return (
+ config.stripe_price_id == si.price?.id ||
+ config.stripe_product_id == si.price?.product ||
+ config.stripe_empty_price_id == si.price?.id
+ );
+ },
+ );
- if (stripeItem) return stripeItem;
+ if (stripeItem) return stripeItem;
- // Fallback to fixed price
- if (isFixedPrice({ price })) {
- return stripeItems.find(
- (si: Stripe.SubscriptionItem | Stripe.LineItem) => {
- const config = price.config as UsagePriceConfig;
+ // Fallback to fixed price
+ if (isFixedPrice({ price })) {
+ return stripeItems.find(
+ (si: Stripe.SubscriptionItem | Stripe.LineItem) => {
+ const config = price.config as UsagePriceConfig;
- return (
- config.stripe_price_id == si.price?.id ||
- (stripeProdId && si.price?.product == stripeProdId)
- );
- }
- );
- }
+ return (
+ config.stripe_price_id == si.price?.id ||
+ (stripeProdId && si.price?.product == stripeProdId)
+ );
+ },
+ );
+ }
- return undefined;
+ return undefined;
- // return stripeItems.find((si: Stripe.SubscriptionItem | Stripe.LineItem) => {
- // const config = price.config as UsagePriceConfig;
+ // return stripeItems.find((si: Stripe.SubscriptionItem | Stripe.LineItem) => {
+ // const config = price.config as UsagePriceConfig;
- // if (config.type == PriceType.Fixed) {
- // return (
- // config.stripe_price_id == si.price?.id ||
- // (stripeProdId && si.price?.product == stripeProdId)
- // );
- // } else {
- // return (
- // config.stripe_price_id == si.price?.id ||
- // config.stripe_product_id == si.price?.product ||
- // config.stripe_empty_price_id == si.price?.id
- // );
- // }
- // });
- }
+ // if (config.type == PriceType.Fixed) {
+ // return (
+ // config.stripe_price_id == si.price?.id ||
+ // (stripeProdId && si.price?.product == stripeProdId)
+ // );
+ // } else {
+ // return (
+ // config.stripe_price_id == si.price?.id ||
+ // config.stripe_product_id == si.price?.product ||
+ // config.stripe_empty_price_id == si.price?.id
+ // );
+ // }
+ // });
+ }
};
export const findPriceInStripeItems = ({
- prices,
- subItem,
- lineItem,
- billingType,
+ prices,
+ subItem,
+ lineItem,
+ billingType,
}: {
- prices: Price[];
- subItem?: Stripe.SubscriptionItem;
- lineItem?: Stripe.InvoiceItem | Stripe.InvoiceLineItem;
- billingType?: BillingType;
+ prices: Price[];
+ subItem?: Stripe.SubscriptionItem;
+ lineItem?: Stripe.InvoiceItem | Stripe.InvoiceLineItem;
+ billingType?: BillingType;
}) => {
- return prices.find((p: Price) => {
- let config = p.config;
+ return prices.find((p: Price) => {
+ let config = p.config;
- let itemMatch;
- if (subItem) {
- itemMatch =
- config.stripe_price_id == subItem.price?.id ||
- config.stripe_product_id == subItem.price?.product;
- }
+ let itemMatch;
+ if (subItem) {
+ itemMatch =
+ config.stripe_price_id == subItem.price?.id ||
+ config.stripe_product_id == subItem.price?.product;
+ }
- if (lineItem) {
- const priceDetails = lineItem.pricing?.price_details;
- itemMatch =
- config.stripe_price_id == priceDetails?.price ||
- config.stripe_product_id == priceDetails?.product;
- }
+ if (lineItem) {
+ const priceDetails = lineItem.pricing?.price_details;
+ itemMatch =
+ config.stripe_price_id == priceDetails?.price ||
+ config.stripe_product_id == priceDetails?.product;
+ }
- const priceBillingType = getBillingType(config);
- let billingTypeMatch = billingType ? priceBillingType == billingType : true;
+ const priceBillingType = getBillingType(config);
+ let billingTypeMatch = billingType ? priceBillingType == billingType : true;
- return itemMatch && billingTypeMatch;
- });
+ return itemMatch && billingTypeMatch;
+ });
};
export const findStripePriceFromPrices = ({
- stripePrices,
- autumnPrice,
+ stripePrices,
+ autumnPrice,
}: {
- stripePrices: Stripe.Price[];
- autumnPrice: Price;
+ stripePrices: Stripe.Price[];
+ autumnPrice: Price;
}) => {
- return stripePrices.find((p: Stripe.Price) =>
- autumnStripePricesMatch({
- stripePrice: p,
- autumnPrice,
- })
- );
+ return stripePrices.find((p: Stripe.Price) =>
+ autumnStripePricesMatch({
+ stripePrice: p,
+ autumnPrice,
+ }),
+ );
};
export const lineItemInCusProduct = ({
- cusProduct,
- lineItem,
+ cusProduct,
+ lineItem,
}: {
- cusProduct: FullCusProduct;
- lineItem: Stripe.InvoiceLineItem;
+ cusProduct: FullCusProduct;
+ lineItem: Stripe.InvoiceLineItem;
}) => {
- let stripeProdId = cusProduct.product.processor?.id;
+ let stripeProdId = cusProduct.product.processor?.id;
- let prices = cusProductToPrices({ cusProduct });
- let price = findPriceInStripeItems({ prices, lineItem });
+ let prices = cusProductToPrices({ cusProduct });
+ let price = findPriceInStripeItems({ prices, lineItem });
- const priceDetails = lineItem.pricing?.price_details;
+ const priceDetails = lineItem.pricing?.price_details;
- return stripeProdId == priceDetails?.product || notNullish(price);
+ return stripeProdId == priceDetails?.product || notNullish(price);
};
export const subItemInCusProduct = ({
- cusProduct,
- subItem,
+ cusProduct,
+ subItem,
}: {
- cusProduct: FullCusProduct;
- subItem: Stripe.SubscriptionItem;
+ cusProduct: FullCusProduct;
+ subItem: Stripe.SubscriptionItem;
}) => {
- let stripeProdId = cusProduct.product.processor?.id;
+ let stripeProdId = cusProduct.product.processor?.id;
- let prices = cusProductToPrices({ cusProduct });
- let price = findPriceInStripeItems({ prices, subItem });
+ let prices = cusProductToPrices({ cusProduct });
+ let price = findPriceInStripeItems({ prices, subItem });
- return stripeProdId == subItem.price.product || notNullish(price);
+ return stripeProdId == subItem.price.product || notNullish(price);
};
export const scheduleItemToPrice = ({
- scheduleItem,
- cusProducts,
+ scheduleItem,
+ cusProducts,
}: {
- scheduleItem: Stripe.SubscriptionSchedule.Phase.Item;
- cusProducts: FullCusProduct[];
+ scheduleItem: Stripe.SubscriptionSchedule.Phase.Item;
+ cusProducts: FullCusProduct[];
}) => {
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const price = prices.find((p) => {
- const stripePrice = scheduleItem.price as Stripe.Price;
- return autumnStripePricesMatch({
- stripePrice,
- autumnPrice: p,
- });
- });
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const price = prices.find((p) => {
+ const stripePrice = scheduleItem.price as Stripe.Price;
+ return autumnStripePricesMatch({
+ stripePrice,
+ autumnPrice: p,
+ });
+ });
- if (price) {
- return price;
- }
- }
+ if (price) {
+ return price;
+ }
+ }
- return undefined;
+ return undefined;
};
export const scheduleItemInCusProduct = ({
- cusProduct,
- scheduleItem,
+ cusProduct,
+ scheduleItem,
}: {
- cusProduct: FullCusProduct;
- scheduleItem: Stripe.SubscriptionSchedule.Phase.Item;
+ cusProduct: FullCusProduct;
+ scheduleItem: Stripe.SubscriptionSchedule.Phase.Item;
}) => {
- let stripeProdId = cusProduct.product.processor?.id;
+ let stripeProdId = cusProduct.product.processor?.id;
- let autumnPrices = cusProductToPrices({ cusProduct });
- let price = autumnPrices.find((p) => {
- const stripePrice = scheduleItem.price as Stripe.Price;
+ let autumnPrices = cusProductToPrices({ cusProduct });
+ let price = autumnPrices.find((p) => {
+ const stripePrice = scheduleItem.price as Stripe.Price;
- return autumnStripePricesMatch({
- stripePrice,
- autumnPrice: p,
- stripeProdId,
- });
- });
+ return autumnStripePricesMatch({
+ stripePrice,
+ autumnPrice: p,
+ stripeProdId,
+ });
+ });
- return notNullish(price);
+ return notNullish(price);
};
export const isLicenseItem = ({
- stripeItem,
+ stripeItem,
}: {
- stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
+ stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
}) => {
- return stripeItem.price?.recurring?.usage_type == "licensed";
+ return stripeItem.price?.recurring?.usage_type == "licensed";
};
export const isMeteredItem = ({
- stripeItem,
+ stripeItem,
}: {
- stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
+ stripeItem: Stripe.SubscriptionItem | Stripe.LineItem;
}) => {
- return stripeItem.price?.recurring?.usage_type == "metered";
+ return stripeItem.price?.recurring?.usage_type == "metered";
};
// Get sub item from product
diff --git a/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts b/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts
index eb343ab93..e3d5273ec 100644
--- a/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts
+++ b/server/src/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.ts
@@ -6,110 +6,110 @@ import { ErrCode } from "@autumn/shared";
import { buildInvoiceMemoFromEntitlements } from "@/internal/invoices/invoiceMemoUtils.js";
export const undoSubUpdate = async ({
- stripeCli,
- curSub,
- updatedSub,
+ stripeCli,
+ curSub,
+ updatedSub,
}: {
- stripeCli: Stripe;
- curSub: Stripe.Subscription;
- updatedSub: Stripe.Subscription;
+ stripeCli: Stripe;
+ curSub: Stripe.Subscription;
+ updatedSub: Stripe.Subscription;
}) => {
- const prevItems = curSub.items.data.map((item) => {
- return {
- price: item.price.id,
- quantity: item.quantity,
- };
- });
+ const prevItems = curSub.items.data.map((item) => {
+ return {
+ price: item.price.id,
+ quantity: item.quantity,
+ };
+ });
- const deleteNewItems = updatedSub.items.data
- .filter(
- (item) =>
- !prevItems.some((prevItem) =>
- curSub.items.data.some(
- (curItem) => curItem.price.id === item.price.id
- )
- )
- )
- .map((item) => {
- return {
- id: item.id,
- deleted: true,
- };
- });
+ const deleteNewItems = updatedSub.items.data
+ .filter(
+ (item) =>
+ !prevItems.some((prevItem) =>
+ curSub.items.data.some(
+ (curItem) => curItem.price.id === item.price.id,
+ ),
+ ),
+ )
+ .map((item) => {
+ return {
+ id: item.id,
+ deleted: true,
+ };
+ });
- await stripeCli.subscriptions.update(curSub.id, {
- items: [...prevItems, ...deleteNewItems],
- proration_behavior: "none",
- });
+ await stripeCli.subscriptions.update(curSub.id, {
+ items: [...prevItems, ...deleteNewItems],
+ proration_behavior: "none",
+ });
};
export const createProrationInvoice = async ({
- attachParams,
- invoiceOnly,
- curSub,
- updatedSub,
- logger,
+ attachParams,
+ invoiceOnly,
+ curSub,
+ updatedSub,
+ logger,
}: {
- attachParams: AttachParams;
- invoiceOnly: boolean;
- curSub: Stripe.Subscription;
- updatedSub: Stripe.Subscription;
- logger: any;
+ attachParams: AttachParams;
+ invoiceOnly: boolean;
+ curSub: Stripe.Subscription;
+ updatedSub: Stripe.Subscription;
+ logger: any;
}) => {
- const { stripeCli, customer, paymentMethod } = attachParams;
+ const { stripeCli, customer, paymentMethod } = attachParams;
- let proratedItems = [];
- // How to retrieve upcoming invoice items?
- const items = await stripeCli.invoiceItems.list({
- customer: customer.processor.id,
- pending: true,
- });
+ let proratedItems = [];
+ // How to retrieve upcoming invoice items?
+ const items = await stripeCli.invoiceItems.list({
+ customer: customer.processor.id,
+ pending: true,
+ });
- if (items.data.length == 0) {
- logger.info(`No items to prorate, skipping invoice creation`);
- return null;
- }
+ if (items.data.length == 0) {
+ logger.info(`No items to prorate, skipping invoice creation`);
+ return null;
+ }
- // const shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
- // const invoiceMemo = shouldMemo
- // ? await buildInvoiceMemoFromEntitlements({
- // org: attachParams.org,
- // entitlements: attachParams.entitlements,
- // features: attachParams.features,
- // })
- // : undefined;
+ // const shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
+ // const invoiceMemo = shouldMemo
+ // ? await buildInvoiceMemoFromEntitlements({
+ // org: attachParams.org,
+ // entitlements: attachParams.entitlements,
+ // features: attachParams.features,
+ // })
+ // : undefined;
- let invoice = await stripeCli.invoices.create({
- customer: customer.processor.id,
- subscription: curSub.id,
- auto_advance: false,
- // ...(shouldMemo ? { description: invoiceMemo } : {}),
- });
+ let invoice = await stripeCli.invoices.create({
+ customer: customer.processor.id,
+ subscription: curSub.id,
+ auto_advance: false,
+ // ...(shouldMemo ? { description: invoiceMemo } : {}),
+ });
- if (invoiceOnly) return invoice;
+ if (invoiceOnly) return invoice;
- await stripeCli.invoices.finalizeInvoice(invoice.id!, {
- auto_advance: false,
- });
+ await stripeCli.invoices.finalizeInvoice(invoice.id!, {
+ auto_advance: false,
+ });
- try {
- const { invoice: subInvoice } = await payForInvoice({
- stripeCli,
- paymentMethod: paymentMethod || null,
- invoiceId: invoice.id!,
- logger,
- voidIfFailed: true,
- });
+ try {
+ const { invoice: subInvoice } = await payForInvoice({
+ stripeCli,
+ paymentMethod: paymentMethod || null,
+ invoiceId: invoice.id!,
+ logger,
+ voidIfFailed: true,
+ });
- return subInvoice;
- } catch (error: any) {
- await undoSubUpdate({ stripeCli, curSub, updatedSub });
+ return subInvoice;
+ } catch (error: any) {
+ await undoSubUpdate({ stripeCli, curSub, updatedSub });
- throw new RecaseError({
- code: ErrCode.UpdateSubscriptionFailed,
- message: `Failed to update subscription. ${error.message}`,
- statusCode: 500,
- data: `Stripe error: ${error.message}`,
- });
- }
+ throw new RecaseError({
+ code: ErrCode.UpdateSubscriptionFailed,
+ message: `Failed to update subscription. ${error.message}`,
+ statusCode: 500,
+ data: `Stripe error: ${error.message}`,
+ });
+ }
};
diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts
index d05cc56c7..7aa8687fd 100644
--- a/server/src/external/stripe/stripeWebhooks.ts
+++ b/server/src/external/stripe/stripeWebhooks.ts
@@ -10,9 +10,9 @@ import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js
import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js";
import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js";
import {
- getStripeWebhookSecret,
- isStripeConnected,
- unsetOrgStripeKeys,
+ getStripeWebhookSecret,
+ isStripeConnected,
+ unsetOrgStripeKeys,
} from "@/internal/orgs/orgUtils.js";
import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js";
import { handleRequestError } from "@/utils/errorUtils.js";
@@ -31,317 +31,317 @@ import { disconnectStripe } from "@/internal/orgs/handlers/handleDeleteStripe.js
export const stripeWebhookRouter: Router = express.Router();
const logStripeWebhook = ({
- req,
- event,
+ req,
+ event,
}: {
- req: ExtendedRequest;
- event: Stripe.Event;
+ req: ExtendedRequest;
+ event: Stripe.Event;
}) => {
- req.logtail.info(
- `${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${req.org.slug} | ${event.id}`
- );
+ req.logtail.info(
+ `${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${req.org.slug} | ${event.id}`,
+ );
};
stripeWebhookRouter.post(
- "/:orgId/:env",
- express.raw({ type: "application/json" }),
- async (request: any, response: any) => {
- const sig = request.headers["stripe-signature"];
- let event;
+ "/:orgId/:env",
+ express.raw({ type: "application/json" }),
+ async (request: any, response: any) => {
+ const sig = request.headers["stripe-signature"];
+ let event;
- const { orgId, env } = request.params;
- const { db } = request;
+ const { orgId, env } = request.params;
+ const { db } = request;
- let org: Organization;
+ let org: Organization;
- const data = await OrgService.getWithFeatures({
- db: request.db,
- orgId,
- env,
- allowNotFound: true,
- });
+ const data = await OrgService.getWithFeatures({
+ db: request.db,
+ orgId,
+ env,
+ allowNotFound: true,
+ });
- if (!data) {
- response.status(200).send(`Org ${orgId} not found`);
- return;
- }
+ if (!data) {
+ response.status(200).send(`Org ${orgId} not found`);
+ return;
+ }
- request.org = data.org;
- request.features = data.features;
- request.env = env;
- org = data.org;
+ request.org = data.org;
+ request.features = data.features;
+ request.env = env;
+ org = data.org;
- if (!isStripeConnected({ org, env })) {
- console.log(`Org ${orgId} and env ${env} is not connected to stripe`);
- response
- .status(200)
- .send(`Org ${orgId} and env ${env} is not connected to stripe`);
- return;
- }
+ if (!isStripeConnected({ org, env })) {
+ console.log(`Org ${orgId} and env ${env} is not connected to stripe`);
+ response
+ .status(200)
+ .send(`Org ${orgId} and env ${env} is not connected to stripe`);
+ return;
+ }
- try {
- const webhookSecret = getStripeWebhookSecret(org, env);
+ try {
+ const webhookSecret = getStripeWebhookSecret(org, env);
- event = await stripe.webhooks.constructEventAsync(
- request.body,
- sig,
- webhookSecret
- );
- } catch (err: any) {
- response.status(400).send(`Webhook Error: ${err.message}`);
- return;
- }
+ event = await stripe.webhooks.constructEventAsync(
+ request.body,
+ sig,
+ webhookSecret,
+ );
+ } catch (err: any) {
+ response.status(400).send(`Webhook Error: ${err.message}`);
+ return;
+ }
- // event = JSON.parse(request.body);
+ // event = JSON.parse(request.body);
- try {
- request.body = JSON.parse(request.body);
- request.authType = AuthType.Stripe;
- } catch (error) {
- console.log("Error parsing body", error);
- }
+ try {
+ request.body = JSON.parse(request.body);
+ request.authType = AuthType.Stripe;
+ } catch (error) {
+ console.log("Error parsing body", error);
+ }
- // event = request.body;
+ // event = request.body;
- request.logtail = request.logtail.child({
- context: {
- context: {
- // body: request.body,
- event_type: event.type,
- event_id: event.id,
- // @ts-ignore
- object_id: `${event.data?.object?.id}` || "N/A",
- authType: AuthType.Stripe,
- org_id: orgId,
- org_slug: org.slug,
- env,
- },
- },
- });
+ request.logtail = request.logtail.child({
+ context: {
+ context: {
+ // body: request.body,
+ event_type: event.type,
+ event_id: event.id,
+ // @ts-ignore
+ object_id: `${event.data?.object?.id}` || "N/A",
+ authType: AuthType.Stripe,
+ org_id: orgId,
+ org_slug: org.slug,
+ env,
+ },
+ },
+ });
- let logger = request.logtail;
- logStripeWebhook({ req: request, event });
+ let logger = request.logtail;
+ logStripeWebhook({ req: request, event });
- try {
- const stripeCli = createStripeCli({ org, env });
- switch (event.type) {
- case "customer.subscription.created":
- await handleSubCreated({
- db,
- org,
- subData: event.data.object,
- env,
- logger,
- });
- break;
+ try {
+ const stripeCli = createStripeCli({ org, env });
+ switch (event.type) {
+ case "customer.subscription.created":
+ await handleSubCreated({
+ db,
+ org,
+ subData: event.data.object,
+ env,
+ logger,
+ });
+ break;
- case "customer.subscription.updated":
- const subscription = event.data.object;
- await handleSubscriptionUpdated({
- req: request,
- db,
- org,
- subscription,
- previousAttributes: event.data.previous_attributes,
- env,
- logger,
- });
- break;
+ case "customer.subscription.updated":
+ const subscription = event.data.object;
+ await handleSubscriptionUpdated({
+ req: request,
+ db,
+ org,
+ subscription,
+ previousAttributes: event.data.previous_attributes,
+ env,
+ logger,
+ });
+ break;
- case "customer.subscription.deleted":
- await handleSubDeleted({
- req: request,
- stripeCli,
- data: event.data.object,
- logger,
- });
- break;
+ case "customer.subscription.deleted":
+ await handleSubDeleted({
+ req: request,
+ stripeCli,
+ data: event.data.object,
+ logger,
+ });
+ break;
- case "checkout.session.completed":
- const checkoutSession = event.data.object;
- await handleCheckoutSessionCompleted({
- req: request,
- db,
- data: checkoutSession,
- org,
- env,
- logger,
- });
- break;
+ case "checkout.session.completed":
+ const checkoutSession = event.data.object;
+ await handleCheckoutSessionCompleted({
+ req: request,
+ db,
+ data: checkoutSession,
+ org,
+ env,
+ logger,
+ });
+ break;
- // Triggered when payment through Stripe is successful
- case "invoice.paid":
- const invoice = event.data.object;
- await handleInvoicePaid({
- db,
- org,
- invoiceData: invoice,
- env,
- event,
- req: request,
- });
- break;
+ // Triggered when payment through Stripe is successful
+ case "invoice.paid":
+ const invoice = event.data.object;
+ await handleInvoicePaid({
+ db,
+ org,
+ invoiceData: invoice,
+ env,
+ event,
+ req: request,
+ });
+ break;
- case "invoice.updated":
- await handleInvoiceUpdated({
- stripeCli,
- env,
- event,
- req: request,
- });
- break;
+ case "invoice.updated":
+ await handleInvoiceUpdated({
+ stripeCli,
+ env,
+ event,
+ req: request,
+ });
+ break;
- case "invoice.created":
- const createdInvoice = event.data.object;
- await handleInvoiceCreated({
- db,
- org,
- data: createdInvoice,
- env,
- logger,
- });
- break;
+ case "invoice.created":
+ const createdInvoice = event.data.object;
+ await handleInvoiceCreated({
+ db,
+ org,
+ data: createdInvoice,
+ env,
+ logger,
+ });
+ break;
- case "invoice.finalized":
- const finalizedInvoice = event.data.object;
- await handleInvoiceFinalized({
- db,
- org,
- data: finalizedInvoice,
- env,
- logger,
- });
- break;
+ case "invoice.finalized":
+ const finalizedInvoice = event.data.object;
+ await handleInvoiceFinalized({
+ db,
+ org,
+ data: finalizedInvoice,
+ env,
+ logger,
+ });
+ break;
- case "subscription_schedule.canceled":
- const canceledSchedule = event.data.object;
- await handleSubscriptionScheduleCanceled({
- db,
- org,
- env,
- schedule: canceledSchedule,
- logger,
- });
- break;
+ case "subscription_schedule.canceled":
+ const canceledSchedule = event.data.object;
+ await handleSubscriptionScheduleCanceled({
+ db,
+ org,
+ env,
+ schedule: canceledSchedule,
+ logger,
+ });
+ break;
- case "customer.discount.deleted":
- await handleCusDiscountDeleted({
- db,
- org,
- discount: event.data.object,
- env,
- logger,
- res: response,
- });
- break;
- }
- } catch (error) {
- if (error instanceof Stripe.errors.StripeError) {
- if (error.message.includes("No such customer")) {
- logger.warn(`stripe customer missing: ${error.message}`);
- response.status(200).json({ message: "ok" });
- return;
- }
+ case "customer.discount.deleted":
+ await handleCusDiscountDeleted({
+ db,
+ org,
+ discount: event.data.object,
+ env,
+ logger,
+ res: response,
+ });
+ break;
+ }
+ } catch (error) {
+ if (error instanceof Stripe.errors.StripeError) {
+ if (error.message.includes("No such customer")) {
+ logger.warn(`stripe customer missing: ${error.message}`);
+ response.status(200).json({ message: "ok" });
+ return;
+ }
- if (error.message.includes("Expired API Key provided")) {
- // Disconnect Stripe
- await unsetOrgStripeKeys({
- db,
- org,
- env,
- });
+ if (error.message.includes("Expired API Key provided")) {
+ // Disconnect Stripe
+ await unsetOrgStripeKeys({
+ db,
+ org,
+ env,
+ });
- response.status(200).json({ message: "ok" });
- return;
- }
- }
+ response.status(200).json({ message: "ok" });
+ return;
+ }
+ }
- handleRequestError({
- req: request,
- error,
- res: response,
- action: "stripe webhook",
- });
- return;
- }
+ handleRequestError({
+ req: request,
+ error,
+ res: response,
+ action: "stripe webhook",
+ });
+ return;
+ }
- try {
- await handleStripeWebhookRefresh({
- eventType: event.type,
- data: event.data,
- db,
- org,
- env,
- logger,
- });
- } catch (error) {
- logger.error(`Stripe webhook, error refreshing cache!`, { error });
- }
+ try {
+ await handleStripeWebhookRefresh({
+ eventType: event.type,
+ data: event.data,
+ db,
+ org,
+ env,
+ logger,
+ });
+ } catch (error) {
+ logger.error(`Stripe webhook, error refreshing cache!`, { error });
+ }
- // DO NOT DELETE -- RESPONSIBLE FOR SENDING SUCCESSFUL RESPONSE TO STRIPE...
- response.status(200).send();
- }
+ // DO NOT DELETE -- RESPONSIBLE FOR SENDING SUCCESSFUL RESPONSE TO STRIPE...
+ response.status(200).send();
+ },
);
const coreEvents = [
- "customer.subscription.created",
- "customer.subscription.updated",
- "customer.subscription.deleted",
- "invoice.paid",
- "invoice.created",
- "invoice.finalized",
- "subscription_schedule.canceled",
- "checkout.session.completed",
+ "customer.subscription.created",
+ "customer.subscription.updated",
+ "customer.subscription.deleted",
+ "invoice.paid",
+ "invoice.created",
+ "invoice.finalized",
+ "subscription_schedule.canceled",
+ "checkout.session.completed",
];
export const handleStripeWebhookRefresh = async ({
- eventType,
- data,
- db,
- org,
- env,
- logger,
+ eventType,
+ data,
+ db,
+ org,
+ env,
+ logger,
}: {
- eventType: string;
- data: any;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- logger: any;
+ eventType: string;
+ data: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ logger: any;
}) => {
- if (coreEvents.includes(eventType)) {
- let stripeCusId = data.object.customer;
- if (!stripeCusId) {
- logger.warn(
- `stripe webhook cache refresh, object doesn't contain customer id`,
- {
- data: {
- eventType,
- object: data.object,
- },
- }
- );
- return;
- }
+ if (coreEvents.includes(eventType)) {
+ let stripeCusId = data.object.customer;
+ if (!stripeCusId) {
+ logger.warn(
+ `stripe webhook cache refresh, object doesn't contain customer id`,
+ {
+ data: {
+ eventType,
+ object: data.object,
+ },
+ },
+ );
+ return;
+ }
- let cus = await CusService.getByStripeId({
- db,
- stripeId: stripeCusId,
- });
+ let cus = await CusService.getByStripeId({
+ db,
+ stripeId: stripeCusId,
+ });
- if (!cus) {
- logger.warn(
- `Searched for customer by stripe id, but not found: ${stripeCusId}`
- );
- return;
- }
+ if (!cus) {
+ logger.warn(
+ `Searched for customer by stripe id, but not found: ${stripeCusId}`,
+ );
+ return;
+ }
- // logger.info(`Deleting cache for customer ${cus.id}`);
- await deleteCusCache({
- db,
- customerId: cus.id!,
- org,
- env,
- });
- }
+ // logger.info(`Deleting cache for customer ${cus.id}`);
+ await deleteCusCache({
+ db,
+ customerId: cus.id!,
+ org,
+ env,
+ });
+ }
};
diff --git a/server/src/external/stripe/utils.ts b/server/src/external/stripe/utils.ts
index 96f11f375..a1cea0c62 100644
--- a/server/src/external/stripe/utils.ts
+++ b/server/src/external/stripe/utils.ts
@@ -2,103 +2,103 @@ import { ErrCode } from "@/errors/errCodes.js";
import { decryptData } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- BillingInterval,
- Feature,
- FullProduct,
- Infinite,
- Organization,
- UsagePriceConfig,
+ BillingInterval,
+ Feature,
+ FullProduct,
+ Infinite,
+ Organization,
+ UsagePriceConfig,
} from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import Stripe from "stripe";
export const createStripeCli = ({
- org,
- env,
- // apiVersion,
- legacyVersion,
+ org,
+ env,
+ // apiVersion,
+ legacyVersion,
}: {
- org: Organization;
- env: AppEnv;
- // apiVersion?: string;
- legacyVersion?: boolean;
+ org: Organization;
+ env: AppEnv;
+ // apiVersion?: string;
+ legacyVersion?: boolean;
}) => {
- let encrypted =
- env == AppEnv.Sandbox
- ? org.stripe_config?.test_api_key
- : org.stripe_config?.live_api_key;
+ let encrypted =
+ env == AppEnv.Sandbox
+ ? org.stripe_config?.test_api_key
+ : org.stripe_config?.live_api_key;
- if (!encrypted) {
- throw new RecaseError({
- message: `Please connect your Stripe ${env == AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env == AppEnv.Sandbox ? "/test" : ""}/apikeys`,
- code: ErrCode.StripeConfigNotFound,
- statusCode: 400,
- });
- }
+ if (!encrypted) {
+ throw new RecaseError({
+ message: `Please connect your Stripe ${env == AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env == AppEnv.Sandbox ? "/test" : ""}/apikeys`,
+ code: ErrCode.StripeConfigNotFound,
+ statusCode: 400,
+ });
+ }
- let decrypted = decryptData(encrypted);
- return new Stripe(decrypted, {
- apiVersion: legacyVersion
- ? ("2025-02-24.acacia" as any)
- : "2025-07-30.basil",
- });
+ let decrypted = decryptData(encrypted);
+ return new Stripe(decrypted, {
+ apiVersion: legacyVersion
+ ? ("2025-02-24.acacia" as any)
+ : "2025-07-30.basil",
+ });
};
export const calculateMetered1Price = ({
- product,
- numEvents,
- metered1Feature,
+ product,
+ numEvents,
+ metered1Feature,
}: {
- product: any;
- numEvents: number;
- metered1Feature: Feature;
+ product: any;
+ numEvents: number;
+ metered1Feature: Feature;
}) => {
- const allowance = product.entitlements.metered1.allowance;
- const usagePrice = product.prices.find(
- (p: any) => p.config.feature_id === metered1Feature.id
- );
+ const allowance = product.entitlements.metered1.allowance;
+ const usagePrice = product.prices.find(
+ (p: any) => p.config.feature_id === metered1Feature.id,
+ );
- const usageConfig = usagePrice.config as UsagePriceConfig;
- let usage = numEvents - allowance;
+ const usageConfig = usagePrice.config as UsagePriceConfig;
+ let usage = numEvents - allowance;
- let totalPrice = 0;
- // console.log("Usage: ", usage);
+ let totalPrice = 0;
+ // console.log("Usage: ", usage);
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- const tier = usageConfig.usage_tiers[i];
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ const tier = usageConfig.usage_tiers[i];
- let amtUsed;
- if (tier.to == -1 || tier.to == Infinite) {
- amtUsed = usage;
- } else {
- amtUsed = Math.min(usage, tier.to);
- }
- const price = tier.amount * (amtUsed / (usageConfig.billing_units ?? 1));
- totalPrice += price;
- usage -= amtUsed;
- }
+ let amtUsed;
+ if (tier.to == -1 || tier.to == Infinite) {
+ amtUsed = usage;
+ } else {
+ amtUsed = Math.min(usage, tier.to);
+ }
+ const price = tier.amount * (amtUsed / (usageConfig.billing_units ?? 1));
+ totalPrice += price;
+ usage -= amtUsed;
+ }
- return totalPrice;
+ return totalPrice;
};
export const subToAutumnInterval = (sub: Stripe.Subscription) => {
- let recuringItem = sub.items.data.find((i) => i.price.recurring != null);
- if (!recuringItem) {
- return {
- interval: BillingInterval.OneOff,
- intervalCount: 1,
- };
- }
+ let recuringItem = sub.items.data.find((i) => i.price.recurring != null);
+ if (!recuringItem) {
+ return {
+ interval: BillingInterval.OneOff,
+ intervalCount: 1,
+ };
+ }
- return {
- interval: recuringItem.price.recurring!.interval as BillingInterval,
- intervalCount: recuringItem.price.recurring!.interval_count || 1,
- };
- // return stripeToAutumnInterval({
- // interval: recuringItem.price.recurring!.interval,
- // intervalCount: recuringItem.price.recurring!.interval_count,
- // });
+ return {
+ interval: recuringItem.price.recurring!.interval as BillingInterval,
+ intervalCount: recuringItem.price.recurring!.interval_count || 1,
+ };
+ // return stripeToAutumnInterval({
+ // interval: recuringItem.price.recurring!.interval,
+ // intervalCount: recuringItem.price.recurring!.interval_count,
+ // });
};
// export const stripeToAutumnInterval = ({
@@ -128,12 +128,12 @@ export const subToAutumnInterval = (sub: Stripe.Subscription) => {
// }
// };
export const subItemToAutumnInterval = (item: Stripe.SubscriptionItem) => {
- return {
- interval: item.price.recurring?.interval as BillingInterval,
- intervalCount: item.price.recurring?.interval_count || 1,
- };
- // return stripeToAutumnInterval({
- // interval: item.price.recurring?.interval!,
- // intervalCount: item.price.recurring?.interval_count!,
- // });
+ return {
+ interval: item.price.recurring?.interval as BillingInterval,
+ intervalCount: item.price.recurring?.interval_count || 1,
+ };
+ // return stripeToAutumnInterval({
+ // interval: item.price.recurring?.interval!,
+ // intervalCount: item.price.recurring?.interval_count!,
+ // });
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
index 79f760f64..ff9eff5e9 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts
@@ -1,9 +1,9 @@
import {
- type AppEnv,
- AttachScenario,
- CusProductStatus,
- notNullish,
- type Organization,
+ type AppEnv,
+ AttachScenario,
+ CusProductStatus,
+ notNullish,
+ type Organization,
} from "@autumn/shared";
import type { Stripe } from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
@@ -29,205 +29,205 @@ import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOpti
import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js";
export const handleCheckoutSessionCompleted = async ({
- req,
- db,
- org,
- data,
- env,
- logger,
+ req,
+ db,
+ org,
+ data,
+ env,
+ logger,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- org: Organization;
- data: Stripe.Checkout.Session;
- env: AppEnv;
- logger: any;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ org: Organization;
+ data: Stripe.Checkout.Session;
+ env: AppEnv;
+ logger: any;
}) => {
- const metadata = await getMetadataFromCheckoutSession(data, db);
- if (!metadata) {
- console.log("checkout.completed: metadata not found, skipping");
- return;
- }
+ const metadata = await getMetadataFromCheckoutSession(data, db);
+ if (!metadata) {
+ console.log("checkout.completed: metadata not found, skipping");
+ return;
+ }
- // Get options
- const stripeCli = createStripeCli({ org, env });
- const attachParams: AttachParams = metadata.data;
- const checkoutSession = await stripeCli.checkout.sessions.retrieve(data.id, {
- expand: ["line_items", "subscription"],
- });
+ // Get options
+ const stripeCli = createStripeCli({ org, env });
+ const attachParams: AttachParams = metadata.data;
+ const checkoutSession = await stripeCli.checkout.sessions.retrieve(data.id, {
+ expand: ["line_items", "subscription"],
+ });
- attachParams.req = req;
- attachParams.stripeCli = stripeCli;
+ attachParams.req = req;
+ attachParams.stripeCli = stripeCli;
- if (attachParams.org.id != org.id) {
- console.log("checkout.completed: org doesn't match, skipping");
- return;
- }
+ if (attachParams.org.id != org.id) {
+ console.log("checkout.completed: org doesn't match, skipping");
+ return;
+ }
- if (attachParams.customer.env != env) {
- console.log("checkout.completed: environments don't match, skipping");
- return;
- }
+ if (attachParams.customer.env != env) {
+ console.log("checkout.completed: environments don't match, skipping");
+ return;
+ }
- await getOptionsFromCheckoutSession({
- checkoutSession,
- attachParams,
- });
+ await getOptionsFromCheckoutSession({
+ checkoutSession,
+ attachParams,
+ });
- console.log(
- "Handling checkout.completed: autumn metadata:",
- checkoutSession.metadata?.autumn_metadata_id
- );
+ console.log(
+ "Handling checkout.completed: autumn metadata:",
+ checkoutSession.metadata?.autumn_metadata_id,
+ );
- if (attachParams.setupPayment) {
- await handleSetupCheckout({
- req,
- db,
- attachParams,
- });
- return;
- }
+ if (attachParams.setupPayment) {
+ await handleSetupCheckout({
+ req,
+ db,
+ attachParams,
+ });
+ return;
+ }
- const checkoutSub =
- checkoutSession.subscription as Stripe.Subscription | null;
+ const checkoutSub =
+ checkoutSession.subscription as Stripe.Subscription | null;
- if (checkoutSub) {
- const activeCusProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: checkoutSub.id,
- orgId: org.id,
- env,
- inStatuses: [CusProductStatus.Active],
- });
+ if (checkoutSub) {
+ const activeCusProducts = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: checkoutSub.id,
+ orgId: org.id,
+ env,
+ inStatuses: [CusProductStatus.Active],
+ });
- if (activeCusProducts && activeCusProducts.length > 0) {
- console.log("✅ checkout.completed: subscription already exists");
- return true;
- }
- }
+ if (activeCusProducts && activeCusProducts.length > 0) {
+ console.log("✅ checkout.completed: subscription already exists");
+ return true;
+ }
+ }
- await handleCheckoutSub({
- stripeCli,
- db,
- subscription: checkoutSub,
- attachParams,
- logger,
- });
+ await handleCheckoutSub({
+ stripeCli,
+ db,
+ subscription: checkoutSub,
+ attachParams,
+ logger,
+ });
- // Create other subscriptions
- const { invoiceIds } = await handleRemainingSets({
- stripeCli,
- db,
- org,
- checkoutSession,
- attachParams,
- checkoutSub,
- logger,
- });
+ // Create other subscriptions
+ const { invoiceIds } = await handleRemainingSets({
+ stripeCli,
+ db,
+ org,
+ checkoutSession,
+ attachParams,
+ checkoutSub,
+ logger,
+ });
- const anchorToUnix = checkoutSub
- ? getEarliestPeriodEnd({ sub: checkoutSub! }) * 1000
- : undefined;
- if (attachParams.productsList) {
- console.log("Inserting products list");
- for (const productOptions of attachParams.productsList) {
- const product = attachParams.products.find(
- (p) => p.id === productOptions.product_id
- );
+ const anchorToUnix = checkoutSub
+ ? getEarliestPeriodEnd({ sub: checkoutSub! }) * 1000
+ : undefined;
+ if (attachParams.productsList) {
+ console.log("Inserting products list");
+ for (const productOptions of attachParams.productsList) {
+ const product = attachParams.products.find(
+ (p) => p.id === productOptions.product_id,
+ );
- if (!product) {
- logger.error(
- `checkout.completed: product not found for productOptions: ${JSON.stringify(
- productOptions
- )}`
- );
- continue;
- }
+ if (!product) {
+ logger.error(
+ `checkout.completed: product not found for productOptions: ${JSON.stringify(
+ productOptions,
+ )}`,
+ );
+ continue;
+ }
- await createFullCusProduct({
- db,
- attachParams: attachToInsertParams(
- attachParams,
- product,
- productOptions.entity_id || undefined
- ),
- subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
- anchorToUnix,
- scenario: AttachScenario.New,
- logger,
- productOptions,
- });
- }
- } else {
- const products = attachParams.products;
- for (const product of products) {
- await createFullCusProduct({
- db,
- attachParams: attachToInsertParams(attachParams, product),
- subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
- anchorToUnix,
- scenario: AttachScenario.New,
- logger,
- });
- }
- }
+ await createFullCusProduct({
+ db,
+ attachParams: attachToInsertParams(
+ attachParams,
+ product,
+ productOptions.entity_id || undefined,
+ ),
+ subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
+ anchorToUnix,
+ scenario: AttachScenario.New,
+ logger,
+ productOptions,
+ });
+ }
+ } else {
+ const products = attachParams.products;
+ for (const product of products) {
+ await createFullCusProduct({
+ db,
+ attachParams: attachToInsertParams(attachParams, product),
+ subscriptionIds: checkoutSub ? [checkoutSub?.id!] : undefined,
+ anchorToUnix,
+ scenario: AttachScenario.New,
+ logger,
+ });
+ }
+ }
- console.log("✅ checkout.completed: successfully created cus product");
- const batchInsertInvoice: any = [];
+ console.log("✅ checkout.completed: successfully created cus product");
+ const batchInsertInvoice: any = [];
- for (const invoiceId of invoiceIds) {
- batchInsertInvoice.push(
- insertInvoiceFromAttach({
- db,
- attachParams,
- invoiceId,
- logger,
- })
- );
- }
+ for (const invoiceId of invoiceIds) {
+ batchInsertInvoice.push(
+ insertInvoiceFromAttach({
+ db,
+ attachParams,
+ invoiceId,
+ logger,
+ }),
+ );
+ }
- await Promise.all(batchInsertInvoice);
- console.log("✅ checkout.completed: successfully inserted invoices");
+ await Promise.all(batchInsertInvoice);
+ console.log("✅ checkout.completed: successfully inserted invoices");
- for (const product of attachParams.products) {
- console.log("Adding task to queue for trigger checkout reward");
- console.log("Adding task to queue for trigger checkout reward");
- await addTaskToQueue({
- jobName: JobName.TriggerCheckoutReward,
- payload: {
- customer: attachParams.customer,
- product,
- org,
- env: attachParams.customer.env,
- subId: checkoutSub?.id as string,
- },
- });
- }
+ for (const product of attachParams.products) {
+ console.log("Adding task to queue for trigger checkout reward");
+ console.log("Adding task to queue for trigger checkout reward");
+ await addTaskToQueue({
+ jobName: JobName.TriggerCheckoutReward,
+ payload: {
+ customer: attachParams.customer,
+ product,
+ org,
+ env: attachParams.customer.env,
+ subId: checkoutSub?.id as string,
+ },
+ });
+ }
- // If the customer in Autumn is missing metadata, and Stripe has atleast one of the fields, update the customer in Autumn
- // with whatever is present in Stripe.
- // Skip if both are missing in Stripe.
+ // If the customer in Autumn is missing metadata, and Stripe has atleast one of the fields, update the customer in Autumn
+ // with whatever is present in Stripe.
+ // Skip if both are missing in Stripe.
- const updates = {
- name:
- !attachParams.customer.name &&
- notNullish(checkoutSession.customer_details?.name)
- ? checkoutSession.customer_details?.name
- : undefined,
- email:
- !attachParams.customer.email &&
- notNullish(checkoutSession.customer_details?.email)
- ? checkoutSession.customer_details?.email
- : undefined,
- };
+ const updates = {
+ name:
+ !attachParams.customer.name &&
+ notNullish(checkoutSession.customer_details?.name)
+ ? checkoutSession.customer_details?.name
+ : undefined,
+ email:
+ !attachParams.customer.email &&
+ notNullish(checkoutSession.customer_details?.email)
+ ? checkoutSession.customer_details?.email
+ : undefined,
+ };
- if (updates.name || updates.email) {
- await CusService.update({
- db,
- internalCusId: attachParams.customer.internal_id,
- update: updates,
- });
- }
+ if (updates.name || updates.email) {
+ await CusService.update({
+ db,
+ internalCusId: attachParams.customer.internal_id,
+ update: updates,
+ });
+ }
- return;
+ return;
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts
index de863259a..939627aac 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/getOptionsFromCheckout.ts
@@ -1,70 +1,70 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- formatPrice,
- getBillingType,
- getPriceEntitlement,
- priceIsOneOffAndTiered,
+ formatPrice,
+ getBillingType,
+ getPriceEntitlement,
+ priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import { BillingType, UsagePriceConfig } from "@autumn/shared";
import Stripe from "stripe";
import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js";
export const getOptionsFromCheckoutSession = async ({
- checkoutSession,
- attachParams,
+ checkoutSession,
+ attachParams,
}: {
- checkoutSession: Stripe.Checkout.Session;
- attachParams: AttachParams;
+ checkoutSession: Stripe.Checkout.Session;
+ attachParams: AttachParams;
}) => {
- const usageInAdvanceExists = attachParams.prices.some(
- (price) =>
- getBillingType(price.config as UsagePriceConfig) ==
- BillingType.UsageInAdvance
- );
+ const usageInAdvanceExists = attachParams.prices.some(
+ (price) =>
+ getBillingType(price.config as UsagePriceConfig) ==
+ BillingType.UsageInAdvance,
+ );
- if (!usageInAdvanceExists) {
- return;
- }
+ if (!usageInAdvanceExists) {
+ return;
+ }
- const { prices, entitlements: ents, optionsList } = attachParams;
- const lineItems: Stripe.LineItem[] = checkoutSession.line_items?.data || [];
+ const { prices, entitlements: ents, optionsList } = attachParams;
+ const lineItems: Stripe.LineItem[] = checkoutSession.line_items?.data || [];
- // Should still work with old method?
- for (const price of prices) {
- let config = price.config as UsagePriceConfig;
+ // Should still work with old method?
+ for (const price of prices) {
+ let config = price.config as UsagePriceConfig;
- if (getBillingType(config) != BillingType.UsageInAdvance) continue;
+ if (getBillingType(config) != BillingType.UsageInAdvance) continue;
- const lineItem = findStripeItemForPrice({
- price,
- stripeItems: lineItems,
- });
+ const lineItem = findStripeItemForPrice({
+ price,
+ stripeItems: lineItems,
+ });
- let quantity = 0;
+ let quantity = 0;
- if (lineItem) {
- let relatedEnt = getPriceEntitlement(price, ents);
+ if (lineItem) {
+ let relatedEnt = getPriceEntitlement(price, ents);
- if (priceIsOneOffAndTiered(price, relatedEnt)) {
- // quantity = lineItem.quantity || 0;
- continue;
- } else {
- quantity = lineItem.quantity || 0;
- }
- }
+ if (priceIsOneOffAndTiered(price, relatedEnt)) {
+ // quantity = lineItem.quantity || 0;
+ continue;
+ } else {
+ quantity = lineItem.quantity || 0;
+ }
+ }
- const index = optionsList.findIndex(
- (feature) => feature.internal_feature_id == config.internal_feature_id
- );
+ const index = optionsList.findIndex(
+ (feature) => feature.internal_feature_id == config.internal_feature_id,
+ );
- if (index == -1) {
- attachParams.optionsList.push({
- feature_id: config.feature_id,
- internal_feature_id: config.internal_feature_id,
- quantity,
- });
- } else {
- attachParams.optionsList[index].quantity = quantity;
- }
- }
+ if (index == -1) {
+ attachParams.optionsList.push({
+ feature_id: config.feature_id,
+ internal_feature_id: config.internal_feature_id,
+ quantity,
+ });
+ } else {
+ attachParams.optionsList[index].quantity = quantity;
+ }
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts
index 137b58c3d..0c4799c31 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts
@@ -5,121 +5,121 @@ import { SubService } from "@/internal/subscriptions/SubService.js";
import { constructSub } from "@/internal/subscriptions/subUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- findPriceFromPlaceholderId,
- findPriceFromStripeId,
+ findPriceFromPlaceholderId,
+ findPriceFromStripeId,
} from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { getArrearItems } from "../../stripeSubUtils/getStripeSubItems/getArrearItems.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
export const handleCheckoutSub = async ({
- stripeCli,
- db,
- subscription,
- attachParams,
- logger,
+ stripeCli,
+ db,
+ subscription,
+ attachParams,
+ logger,
}: {
- stripeCli: Stripe;
- db: DrizzleCli;
- subscription: Stripe.Subscription | null;
- attachParams: AttachParams;
- logger: any;
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ subscription: Stripe.Subscription | null;
+ attachParams: AttachParams;
+ logger: any;
}) => {
- const { org, customer } = attachParams;
+ const { org, customer } = attachParams;
- if (!subscription) return;
+ if (!subscription) return;
- const { start, end } = subToPeriodStartEnd({ sub: subscription });
+ const { start, end } = subToPeriodStartEnd({ sub: subscription });
- await SubService.createSub({
- db,
- sub: constructSub({
- stripeId: subscription.id,
- usageFeatures: attachParams.itemSets?.[0]?.usageFeatures || [],
- orgId: org.id,
- env: attachParams.customer.env,
- currentPeriodStart: start,
- currentPeriodEnd: end,
- }),
- });
+ await SubService.createSub({
+ db,
+ sub: constructSub({
+ stripeId: subscription.id,
+ usageFeatures: attachParams.itemSets?.[0]?.usageFeatures || [],
+ orgId: org.id,
+ env: attachParams.customer.env,
+ currentPeriodStart: start,
+ currentPeriodEnd: end,
+ }),
+ });
- const curSubItems = subscription.items.data;
- let itemsUpdate = [];
+ const curSubItems = subscription.items.data;
+ let itemsUpdate = [];
- for (const item of curSubItems) {
- let stripePriceId = item.price.id;
+ for (const item of curSubItems) {
+ let stripePriceId = item.price.id;
- let arrearProratedPrice = findPriceFromPlaceholderId({
- prices: attachParams.prices,
- placeholderId: stripePriceId,
- });
+ let arrearProratedPrice = findPriceFromPlaceholderId({
+ prices: attachParams.prices,
+ placeholderId: stripePriceId,
+ });
- if (arrearProratedPrice) {
- itemsUpdate.push({
- price: arrearProratedPrice.config.stripe_price_id!,
- quantity: 0,
- });
+ if (arrearProratedPrice) {
+ itemsUpdate.push({
+ price: arrearProratedPrice.config.stripe_price_id!,
+ quantity: 0,
+ });
- itemsUpdate.push({
- id: item.id,
- deleted: true,
- });
- continue;
- }
+ itemsUpdate.push({
+ id: item.id,
+ deleted: true,
+ });
+ continue;
+ }
- let arrearPrice = findPriceFromStripeId({
- prices: attachParams.prices,
- stripePriceId,
- billingType: BillingType.UsageInArrear,
- });
+ let arrearPrice = findPriceFromStripeId({
+ prices: attachParams.prices,
+ stripePriceId,
+ billingType: BillingType.UsageInArrear,
+ });
- if (
- arrearPrice &&
- (attachParams.internalEntityId ||
- attachParams.apiVersion == APIVersion.v1_4)
- ) {
- itemsUpdate.push({
- id: item.id,
- deleted: true,
- });
+ if (
+ arrearPrice &&
+ (attachParams.internalEntityId ||
+ attachParams.apiVersion == APIVersion.v1_4)
+ ) {
+ itemsUpdate.push({
+ id: item.id,
+ deleted: true,
+ });
- const emptyPrice = (arrearPrice.config as UsagePriceConfig)
- .stripe_empty_price_id;
+ const emptyPrice = (arrearPrice.config as UsagePriceConfig)
+ .stripe_empty_price_id;
- itemsUpdate.push(
- emptyPrice
- ? {
- price: emptyPrice,
- quantity: 0,
- }
- : (getEmptyPriceItem({ price: arrearPrice, org }) as any)
- );
- }
- }
+ itemsUpdate.push(
+ emptyPrice
+ ? {
+ price: emptyPrice,
+ quantity: 0,
+ }
+ : (getEmptyPriceItem({ price: arrearPrice, org }) as any),
+ );
+ }
+ }
- // let deletedCount = itemsUpdate.filter((item) => item.deleted).length;
- // if (deletedCount === curSubItems.length) {
- // itemsUpdate = itemsUpdate.concat(
- // getArrearItems({
- // prices: attachParams.prices,
- // interval: attachParams.itemSets?.[0]?.interval,
- // intervalCount: attachParams.itemSets?.[0]?.intervalCount,
- // org,
- // })
- // );
- // }
+ // let deletedCount = itemsUpdate.filter((item) => item.deleted).length;
+ // if (deletedCount === curSubItems.length) {
+ // itemsUpdate = itemsUpdate.concat(
+ // getArrearItems({
+ // prices: attachParams.prices,
+ // interval: attachParams.itemSets?.[0]?.interval,
+ // intervalCount: attachParams.itemSets?.[0]?.intervalCount,
+ // org,
+ // })
+ // );
+ // }
- if (itemsUpdate.length > 0) {
- await stripeCli.subscriptions.update(subscription.id, {
- items: itemsUpdate,
- });
- }
+ if (itemsUpdate.length > 0) {
+ await stripeCli.subscriptions.update(subscription.id, {
+ items: itemsUpdate,
+ });
+ }
- if (subscription.billing_mode.type !== "flexible") {
- await stripeCli.subscriptions.migrate(subscription.id, {
- billing_mode: { type: "flexible" },
- });
- }
+ if (subscription.billing_mode.type !== "flexible") {
+ await stripeCli.subscriptions.migrate(subscription.id, {
+ billing_mode: { type: "flexible" },
+ });
+ }
- return subscription;
+ return subscription;
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts
index de792b439..ec6bb67f4 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts
@@ -6,61 +6,61 @@ import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUt
import { getEmptyPriceItem } from "../../priceToStripeItem/priceToStripeItem.js";
export const handleRemainingSets = async ({
- stripeCli,
- db,
- org,
- checkoutSession,
- attachParams,
- checkoutSub,
- logger,
+ stripeCli,
+ db,
+ org,
+ checkoutSession,
+ attachParams,
+ checkoutSub,
+ logger,
}: {
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- checkoutSession: Stripe.Checkout.Session;
- attachParams: AttachParams;
- checkoutSub: Stripe.Subscription | null;
- logger: any;
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ checkoutSession: Stripe.Checkout.Session;
+ attachParams: AttachParams;
+ checkoutSub: Stripe.Subscription | null;
+ logger: any;
}) => {
- const itemSets = attachParams.itemSets;
- let remainingSets = itemSets ? itemSets.slice(1) : [];
+ const itemSets = attachParams.itemSets;
+ let remainingSets = itemSets ? itemSets.slice(1) : [];
- const remainingItems = remainingSets.flatMap((set) => set.items);
- let invoiceIds: string[] = checkoutSession.invoice
- ? [checkoutSession.invoice as string]
- : [];
+ const remainingItems = remainingSets.flatMap((set) => set.items);
+ let invoiceIds: string[] = checkoutSession.invoice
+ ? [checkoutSession.invoice as string]
+ : [];
- // Replace items with empty price if needed...
- for (const price of attachParams.prices) {
- if (!isUsagePrice({ price })) continue;
+ // Replace items with empty price if needed...
+ for (const price of attachParams.prices) {
+ if (!isUsagePrice({ price })) continue;
- const config = price.config as UsagePriceConfig;
- const emptyPrice = config.stripe_empty_price_id;
+ const config = price.config as UsagePriceConfig;
+ const emptyPrice = config.stripe_empty_price_id;
- if (
- attachParams.internalEntityId ||
- attachParams.apiVersion == APIVersion.v1_4
- ) {
- const replaceIndex = remainingItems.findIndex(
- (item) => item.price == config.stripe_price_id
- );
+ if (
+ attachParams.internalEntityId ||
+ attachParams.apiVersion == APIVersion.v1_4
+ ) {
+ const replaceIndex = remainingItems.findIndex(
+ (item) => item.price == config.stripe_price_id,
+ );
- if (replaceIndex != -1) {
- remainingItems[replaceIndex] = emptyPrice
- ? {
- price: config.stripe_empty_price_id,
- quantity: 0,
- }
- : (getEmptyPriceItem({ price, org }) as any);
- }
- }
- }
+ if (replaceIndex != -1) {
+ remainingItems[replaceIndex] = emptyPrice
+ ? {
+ price: config.stripe_empty_price_id,
+ quantity: 0,
+ }
+ : (getEmptyPriceItem({ price, org }) as any);
+ }
+ }
+ }
- if (remainingItems.length > 0) {
- await stripeCli.subscriptions.update(checkoutSub!.id, {
- items: remainingItems,
- });
- }
+ if (remainingItems.length > 0) {
+ await stripeCli.subscriptions.update(checkoutSub!.id, {
+ items: remainingItems,
+ });
+ }
- return { invoiceIds };
+ return { invoiceIds };
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts
index e0ec4379f..fad3e8bcb 100644
--- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts
@@ -8,27 +8,27 @@ import Stripe from "stripe";
import { createStripeCli } from "../../utils.js";
export const handleSetupCheckout = async ({
- req,
- db,
- attachParams,
+ req,
+ db,
+ attachParams,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- attachParams: AttachParams;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ attachParams: AttachParams;
}) => {
- const logger = req.logger;
- const { org, customer } = attachParams;
+ const logger = req.logger;
+ const { org, customer } = attachParams;
- logger.info(`HANDLING SETUP CHECKOUT COMPLETED`);
+ logger.info(`HANDLING SETUP CHECKOUT COMPLETED`);
- // 1. Check attach prices...
- await handleAddProduct({
- req,
- attachParams: {
- ...attachParams,
- stripeCli: createStripeCli({ org, env: customer.env }),
- },
- branch: AttachBranch.New,
- config: getDefaultAttachConfig(),
- });
+ // 1. Check attach prices...
+ await handleAddProduct({
+ req,
+ attachParams: {
+ ...attachParams,
+ stripeCli: createStripeCli({ org, env: customer.env }),
+ },
+ branch: AttachBranch.New,
+ config: getDefaultAttachConfig(),
+ });
};
diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
index 192c61d2c..2238f8c2f 100644
--- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts
@@ -7,163 +7,163 @@ import { notNullish } from "@/utils/genUtils.js";
import { createStripeCli } from "../utils.js";
export async function handleCusDiscountDeleted({
- db,
- org,
- discount,
- env,
- logger,
- res,
+ db,
+ org,
+ discount,
+ env,
+ logger,
+ res,
}: {
- db: DrizzleCli;
- org: any;
- discount: any;
- env: any;
- logger: any;
- res: any;
+ db: DrizzleCli;
+ org: any;
+ discount: any;
+ env: any;
+ logger: any;
+ res: any;
}) {
- const customer = await CusService.getByStripeId({
- db,
- stripeId: discount.customer,
- });
+ const customer = await CusService.getByStripeId({
+ db,
+ stripeId: discount.customer,
+ });
- if (!customer) {
- logger.warn(`discount.deleted: customer ${discount.customer} not found`);
- return;
- }
+ if (!customer) {
+ logger.warn(`discount.deleted: customer ${discount.customer} not found`);
+ return;
+ }
- if (customer.env !== env || customer.org_id !== org.id) {
- logger.info(
- `discount.deleted: env or org mismatch, skipping, ${customer.env} !== ${env} || ${customer.org_id} !== ${org.id}`
- );
- return;
- }
+ if (customer.env !== env || customer.org_id !== org.id) {
+ logger.info(
+ `discount.deleted: env or org mismatch, skipping, ${customer.env} !== ${env} || ${customer.org_id} !== ${org.id}`,
+ );
+ return;
+ }
- // Check if any redemptions available, and apply to customer if so
- const redemptions = await RewardRedemptionService.getUnappliedRedemptions({
- db,
- internalCustomerId: customer.internal_id,
- });
+ // Check if any redemptions available, and apply to customer if so
+ const redemptions = await RewardRedemptionService.getUnappliedRedemptions({
+ db,
+ internalCustomerId: customer.internal_id,
+ });
- logger.info(
- `discount.deleted:, discount ID: ${discount.id}, found ${redemptions.length} redemptions`
- );
+ logger.info(
+ `discount.deleted:, discount ID: ${discount.id}, found ${redemptions.length} redemptions`,
+ );
- if (redemptions.length == 0) return;
+ if (redemptions.length == 0) return;
- const paidProductRedemption = redemptions.find(
- (r) =>
- r.reward_program.reward.id ===
- (typeof discount.coupon == "string"
- ? discount.coupon
- : discount.coupon.id)
- );
+ const paidProductRedemption = redemptions.find(
+ (r) =>
+ r.reward_program.reward.id ===
+ (typeof discount.coupon == "string"
+ ? discount.coupon
+ : discount.coupon.id),
+ );
- if (discount.subscription) {
- logger.info(
- `Discount is a subscription, paidProductRedemption: ${paidProductRedemption?.id}`
- );
+ if (discount.subscription) {
+ logger.info(
+ `Discount is a subscription, paidProductRedemption: ${paidProductRedemption?.id}`,
+ );
- if (!paidProductRedemption) return;
+ if (!paidProductRedemption) return;
- // Re-apply coupon and mark applied / redeemer applied to true
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ // Re-apply coupon and mark applied / redeemer applied to true
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- // Mark reward redemption as applied / redeemer applied to true
- const sub = await stripeCli.subscriptions.retrieve(discount.subscription);
+ // Mark reward redemption as applied / redeemer applied to true
+ const sub = await stripeCli.subscriptions.retrieve(discount.subscription);
- // can't really test because it modifies subscription affected by test clock...
- try {
- await stripeCli.subscriptions.update(discount.subscription, {
- discounts: [
- ...(sub.discounts as string[]).map((d: string) => ({
- discount: d,
- })),
- {
- coupon: paidProductRedemption.reward_program.reward.id as string,
- },
- ],
- });
- } catch (error: any) {
- logger.error(
- `Failed to update subscription ${discount.subscription} with paid product coupon, error: ${error.message}`
- );
- throw error;
- }
+ // can't really test because it modifies subscription affected by test clock...
+ try {
+ await stripeCli.subscriptions.update(discount.subscription, {
+ discounts: [
+ ...(sub.discounts as string[]).map((d: string) => ({
+ discount: d,
+ })),
+ {
+ coupon: paidProductRedemption.reward_program.reward.id as string,
+ },
+ ],
+ });
+ } catch (error: any) {
+ logger.error(
+ `Failed to update subscription ${discount.subscription} with paid product coupon, error: ${error.message}`,
+ );
+ throw error;
+ }
- // Mark reward redemption as applied / redeemer applied to true
- const isReferrer =
- paidProductRedemption.referral_code.internal_customer_id ===
- customer.internal_id;
+ // Mark reward redemption as applied / redeemer applied to true
+ const isReferrer =
+ paidProductRedemption.referral_code.internal_customer_id ===
+ customer.internal_id;
- await RewardRedemptionService.update({
- db,
- id: paidProductRedemption.id,
- updates: {
- applied: isReferrer ? true : undefined,
- redeemer_applied: isReferrer ? undefined : true,
- },
- });
+ await RewardRedemptionService.update({
+ db,
+ id: paidProductRedemption.id,
+ updates: {
+ applied: isReferrer ? true : undefined,
+ redeemer_applied: isReferrer ? undefined : true,
+ },
+ });
- return;
- }
+ return;
+ }
- const redemption = redemptions[0];
+ const redemption = redemptions[0];
- // Apply redemption to customer
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ // Apply redemption to customer
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- const stripeCus = (await stripeCli.customers.retrieve(
- discount.customer
- )) as Stripe.Customer;
+ const stripeCus = (await stripeCli.customers.retrieve(
+ discount.customer,
+ )) as Stripe.Customer;
- if (stripeCus && notNullish(stripeCus.discount)) {
- logger.info(
- `discount.deleted: stripe customer ${discount.customer} already has a discount`
- );
- return;
- }
+ if (stripeCus && notNullish(stripeCus.discount)) {
+ logger.info(
+ `discount.deleted: stripe customer ${discount.customer} already has a discount`,
+ );
+ return;
+ }
- const reward = await RewardService.get({
- db,
- orgId: org.id,
- env,
- idOrInternalId: redemption.reward_program.internal_reward_id!,
- });
+ const reward = await RewardService.get({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: redemption.reward_program.internal_reward_id!,
+ });
- if (!reward) {
- logger.warn(
- `discount.deleted: reward ${redemption.reward_program.internal_id} not found`
- );
- return;
- }
+ if (!reward) {
+ logger.warn(
+ `discount.deleted: reward ${redemption.reward_program.internal_id} not found`,
+ );
+ return;
+ }
- const legacyStripe = createStripeCli({
- org,
- env,
- legacyVersion: true,
- });
+ const legacyStripe = createStripeCli({
+ org,
+ env,
+ legacyVersion: true,
+ });
- await legacyStripe.customers.update(discount.customer, {
- // @ts-expect-error
- coupon: reward.id,
- });
+ await legacyStripe.customers.update(discount.customer, {
+ // @ts-expect-error
+ coupon: reward.id,
+ });
- await RewardRedemptionService.update({
- db,
- id: redemption.id,
- updates: {
- applied: true,
- },
- });
+ await RewardRedemptionService.update({
+ db,
+ id: redemption.id,
+ updates: {
+ applied: true,
+ },
+ });
- logger.info(
- `discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`
- );
- logger.info(`Redemption ID: ${redemption.id}`);
+ logger.info(
+ `discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`,
+ );
+ logger.info(`Redemption ID: ${redemption.id}`);
}
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts
index 7f4ce7dee..569a64437 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleContUsePrices.ts
@@ -9,98 +9,98 @@ import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
export const handleContUsePrices = async ({
- db,
- cusEnts,
- cusPrice,
- stripeCli,
- invoice,
- usageSub,
- logger,
+ db,
+ cusEnts,
+ cusPrice,
+ stripeCli,
+ invoice,
+ usageSub,
+ logger,
}: {
- db: DrizzleCli;
- cusEnts: FullCustomerEntitlement[];
- cusPrice: FullCustomerPrice;
- stripeCli: Stripe;
+ db: DrizzleCli;
+ cusEnts: FullCustomerEntitlement[];
+ cusPrice: FullCustomerPrice;
+ stripeCli: Stripe;
- invoice: Stripe.Invoice;
- usageSub: Stripe.Subscription;
- logger: any;
+ invoice: Stripe.Invoice;
+ usageSub: Stripe.Subscription;
+ logger: any;
}) => {
- const cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts,
- });
+ const cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts,
+ });
- if (!cusEnt) {
- console.log("No related cus ent found");
- return;
- }
+ if (!cusEnt) {
+ console.log("No related cus ent found");
+ return;
+ }
- // If invoice is not for new period (eg. upgrades, etc, skip)
- const { start } = subToPeriodStartEnd({
- sub: usageSub,
- });
- const isNewPeriod = invoice.period_start !== start;
- if (!isNewPeriod) {
- return;
- }
+ // If invoice is not for new period (eg. upgrades, etc, skip)
+ const { start } = subToPeriodStartEnd({
+ sub: usageSub,
+ });
+ const isNewPeriod = invoice.period_start !== start;
+ if (!isNewPeriod) {
+ return;
+ }
- let feature = cusEnt.entitlement.feature;
- logger.info(
- `Handling invoice.created for in arrear prorated, feature: ${feature.id}`
- );
+ let feature = cusEnt.entitlement.feature;
+ logger.info(
+ `Handling invoice.created for in arrear prorated, feature: ${feature.id}`,
+ );
- let replaceables = cusEnt.replaceables.filter((r) => r.delete_next_cycle);
+ let replaceables = cusEnt.replaceables.filter((r) => r.delete_next_cycle);
- if (replaceables.length == 0) {
- return;
- }
+ if (replaceables.length == 0) {
+ return;
+ }
- logger.info(`🚀 Deleting replaceables for ${feature.id}`);
+ logger.info(`🚀 Deleting replaceables for ${feature.id}`);
- let linkedCusEnts = findLinkedCusEnts({
- cusEnts,
- feature,
- });
+ let linkedCusEnts = findLinkedCusEnts({
+ cusEnts,
+ feature,
+ });
- for (const linkedCusEnt of linkedCusEnts) {
- let { newEntities } = removeReplaceablesFromCusEnt({
- cusEnt: linkedCusEnt,
- replaceableIds: replaceables.map((r) => r.id),
- });
+ for (const linkedCusEnt of linkedCusEnts) {
+ let { newEntities } = removeReplaceablesFromCusEnt({
+ cusEnt: linkedCusEnt,
+ replaceableIds: replaceables.map((r) => r.id),
+ });
- await CusEntService.update({
- db,
- id: linkedCusEnt.id,
- updates: {
- entities: newEntities,
- },
- });
- }
+ await CusEntService.update({
+ db,
+ id: linkedCusEnt.id,
+ updates: {
+ entities: newEntities,
+ },
+ });
+ }
- // let subItem = findStripeItemForPrice({
- // stripeItems: usageSub.items.data,
- // price: cusPrice.price,
- // });
+ // let subItem = findStripeItemForPrice({
+ // stripeItems: usageSub.items.data,
+ // price: cusPrice.price,
+ // });
- // if (subItem) {
- // let newQuantity = (subItem.quantity || 0) - replaceables.length;
- // newQuantity = Math.max(0, newQuantity);
- // await stripeCli.subscriptionItems.update(subItem.id, {
- // quantity: newQuantity,
- // proration_behavior: "always_invoice",
- // });
- // logger.info(`Update sub item quantity to ${newQuantity}`);
- // }
+ // if (subItem) {
+ // let newQuantity = (subItem.quantity || 0) - replaceables.length;
+ // newQuantity = Math.max(0, newQuantity);
+ // await stripeCli.subscriptionItems.update(subItem.id, {
+ // quantity: newQuantity,
+ // proration_behavior: "always_invoice",
+ // });
+ // logger.info(`Update sub item quantity to ${newQuantity}`);
+ // }
- await CusEntService.increment({
- db,
- id: cusEnt.id,
- amount: replaceables.length,
- });
+ await CusEntService.increment({
+ db,
+ id: cusEnt.id,
+ amount: replaceables.length,
+ });
- await RepService.deleteInIds({
- db,
- ids: replaceables.map((r) => r.id),
- });
+ await RepService.deleteInIds({
+ db,
+ ids: replaceables.map((r) => r.id),
+ });
};
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts
index 480bb584c..60ee39ee7 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts
@@ -1,14 +1,14 @@
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- AppEnv,
- BillingType,
- CusProductStatus,
- Customer,
- FullCusProduct,
- FullCustomerEntitlement,
- FullCustomerPrice,
- Organization,
+ AppEnv,
+ BillingType,
+ CusProductStatus,
+ Customer,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ Organization,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -25,8 +25,8 @@ import { FeatureService } from "@/internal/features/FeatureService.js";
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- getFullStripeInvoice,
- invoiceToSubId,
+ getFullStripeInvoice,
+ invoiceToSubId,
} from "../../stripeInvoiceUtils.js";
import { handleUsagePrices } from "./handleUsagePrices.js";
import { handleContUsePrices } from "./handleContUsePrices.js";
@@ -36,136 +36,136 @@ import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtil
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
const handleInArrearProrated = async ({
- db,
- cusEnts,
- cusPrice,
- customer,
- org,
- env,
- invoice,
- usageSub,
- logger,
+ db,
+ cusEnts,
+ cusPrice,
+ customer,
+ org,
+ env,
+ invoice,
+ usageSub,
+ logger,
}: {
- db: DrizzleCli;
+ db: DrizzleCli;
- cusEnts: FullCustomerEntitlement[];
- cusPrice: FullCustomerPrice;
- customer: Customer;
- org: Organization;
- env: AppEnv;
- invoice: Stripe.Invoice;
- usageSub: Stripe.Subscription;
- logger: any;
+ cusEnts: FullCustomerEntitlement[];
+ cusPrice: FullCustomerPrice;
+ customer: Customer;
+ org: Organization;
+ env: AppEnv;
+ invoice: Stripe.Invoice;
+ usageSub: Stripe.Subscription;
+ logger: any;
}) => {
- const cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts,
- });
+ const cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts,
+ });
- if (!cusEnt) {
- console.log("No related cus ent found");
- return;
- }
+ if (!cusEnt) {
+ console.log("No related cus ent found");
+ return;
+ }
- // console.log("Invoice period start:\t", formatUnixToDateTime(invoice.period_start * 1000));
- // console.log("Invoice period end:\t", formatUnixToDateTime(invoice.period_end * 1000));
- // console.log("Sub period start:\t", formatUnixToDateTime(usageSub.current_period_start * 1000));
- // console.log("Sub period end:\t", formatUnixToDateTime(usageSub.current_period_end * 1000));
+ // console.log("Invoice period start:\t", formatUnixToDateTime(invoice.period_start * 1000));
+ // console.log("Invoice period end:\t", formatUnixToDateTime(invoice.period_end * 1000));
+ // console.log("Sub period start:\t", formatUnixToDateTime(usageSub.current_period_start * 1000));
+ // console.log("Sub period end:\t", formatUnixToDateTime(usageSub.current_period_end * 1000));
- // Check if invoice is for new subscription period by comparing billing period
- const { start: periodStart, end: periodEnd } = subToPeriodStartEnd({
- sub: usageSub,
- });
- const isNewPeriod = invoice.period_start !== periodStart;
- if (!isNewPeriod) {
- logger.info("Invoice is not for new subscription period, skipping...");
- return;
- }
+ // Check if invoice is for new subscription period by comparing billing period
+ const { start: periodStart, end: periodEnd } = subToPeriodStartEnd({
+ sub: usageSub,
+ });
+ const isNewPeriod = invoice.period_start !== periodStart;
+ if (!isNewPeriod) {
+ logger.info("Invoice is not for new subscription period, skipping...");
+ return;
+ }
- let feature = cusEnt.entitlement.feature;
- logger.info(
- `Handling invoice.created for in arrear prorated, feature: ${feature.id}`
- );
+ let feature = cusEnt.entitlement.feature;
+ logger.info(
+ `Handling invoice.created for in arrear prorated, feature: ${feature.id}`,
+ );
- let deletedEntities = await EntityService.list({
- db,
- internalCustomerId: customer.internal_id!,
- inFeatureIds: [feature.internal_id!],
- isDeleted: true,
- });
+ let deletedEntities = await EntityService.list({
+ db,
+ internalCustomerId: customer.internal_id!,
+ inFeatureIds: [feature.internal_id!],
+ isDeleted: true,
+ });
- if (deletedEntities.length == 0) {
- logger.info("No deleted entities found");
- return;
- }
+ if (deletedEntities.length == 0) {
+ logger.info("No deleted entities found");
+ return;
+ }
- logger.info(
- `✨ Handling in arrear prorated, customer ${customer.name}, org: ${org.slug}`
- );
+ logger.info(
+ `✨ Handling in arrear prorated, customer ${customer.name}, org: ${org.slug}`,
+ );
- logger.info(
- `Deleting entities, feature ${feature.id}, customer ${customer.id}, org ${org.slug}`,
- deletedEntities
- );
+ logger.info(
+ `Deleting entities, feature ${feature.id}, customer ${customer.id}, org ${org.slug}`,
+ deletedEntities,
+ );
- // Get linked cus ents
+ // Get linked cus ents
- for (const linkedCusEnt of cusEnts) {
- // isLinked
- let isLinked = linkedCusEnt.entitlement.entity_feature_id == feature.id;
+ for (const linkedCusEnt of cusEnts) {
+ // isLinked
+ let isLinked = linkedCusEnt.entitlement.entity_feature_id == feature.id;
- if (!isLinked) {
- continue;
- }
+ if (!isLinked) {
+ continue;
+ }
- logger.info(
- `Linked cus ent: ${linkedCusEnt.feature_id}, isLinked: ${isLinked}`
- );
+ logger.info(
+ `Linked cus ent: ${linkedCusEnt.feature_id}, isLinked: ${isLinked}`,
+ );
- // Delete cus ent ids
- let newEntities = structuredClone(linkedCusEnt.entities!);
- for (const entityId in newEntities) {
- if (deletedEntities.some((e) => e.id == entityId)) {
- delete newEntities[entityId];
- }
- }
+ // Delete cus ent ids
+ let newEntities = structuredClone(linkedCusEnt.entities!);
+ for (const entityId in newEntities) {
+ if (deletedEntities.some((e) => e.id == entityId)) {
+ delete newEntities[entityId];
+ }
+ }
- let updated = await CusEntService.update({
- db,
- id: linkedCusEnt.id,
- updates: {
- entities: newEntities,
- },
- });
- console.log(`Updated ${updated.length} cus ents`);
+ let updated = await CusEntService.update({
+ db,
+ id: linkedCusEnt.id,
+ updates: {
+ entities: newEntities,
+ },
+ });
+ console.log(`Updated ${updated.length} cus ents`);
- logger.info(
- `Feature: ${feature.id}, customer: ${customer.id}, deleted entities from cus ent`
- );
- linkedCusEnt.entities = newEntities;
- }
+ logger.info(
+ `Feature: ${feature.id}, customer: ${customer.id}, deleted entities from cus ent`,
+ );
+ linkedCusEnt.entities = newEntities;
+ }
- await EntityService.deleteInInternalIds({
- db,
- internalIds: deletedEntities.map((e) => e.internal_id!),
- orgId: org.id,
- env,
- });
- logger.info(
- `Feature: ${feature.id}, Deleted ${
- deletedEntities.length
- }, entities: ${deletedEntities.map((e) => `${e.id}`).join(", ")}`
- );
+ await EntityService.deleteInInternalIds({
+ db,
+ internalIds: deletedEntities.map((e) => e.internal_id!),
+ orgId: org.id,
+ env,
+ });
+ logger.info(
+ `Feature: ${feature.id}, Deleted ${
+ deletedEntities.length
+ }, entities: ${deletedEntities.map((e) => `${e.id}`).join(", ")}`,
+ );
- // Increase balance
- if (notNullish(cusEnt.balance)) {
- logger.info(`Incrementing balance for cus ent: ${cusEnt.id}`);
- await CusEntService.increment({
- db,
- id: cusEnt.id,
- amount: deletedEntities.length,
- });
- }
+ // Increase balance
+ if (notNullish(cusEnt.balance)) {
+ logger.info(`Incrementing balance for cus ent: ${cusEnt.id}`);
+ await CusEntService.increment({
+ db,
+ id: cusEnt.id,
+ amount: deletedEntities.length,
+ });
+ }
};
// For cancel at period end: invoice period start = sub period start (cur cycle), invoice period end = sub period end (a month later...)
@@ -174,217 +174,217 @@ const handleInArrearProrated = async ({
// For upgrade, bill_immediately: invoice period start = sub period start (cur cycle), invoice period end cancel immediately date
export const sendUsageAndReset = async ({
- db,
- activeProduct,
- org,
- env,
- invoice,
- stripeSubs,
- logger,
+ db,
+ activeProduct,
+ org,
+ env,
+ invoice,
+ stripeSubs,
+ logger,
}: {
- db: DrizzleCli;
- activeProduct: FullCusProduct;
- org: Organization;
- env: AppEnv;
- invoice: Stripe.Invoice;
- stripeSubs: Stripe.Subscription[];
- logger: any;
+ db: DrizzleCli;
+ activeProduct: FullCusProduct;
+ org: Organization;
+ env: AppEnv;
+ invoice: Stripe.Invoice;
+ stripeSubs: Stripe.Subscription[];
+ logger: any;
}) => {
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- const cusEnts = activeProduct.customer_entitlements;
- const cusPrices = activeProduct.customer_prices;
- const customer = activeProduct.customer!;
+ const cusEnts = activeProduct.customer_entitlements;
+ const cusPrices = activeProduct.customer_prices;
+ const customer = activeProduct.customer!;
- for (const cusPrice of cusPrices) {
- const price = cusPrice.price;
- let billingType = getBillingType(price.config);
+ for (const cusPrice of cusPrices) {
+ const price = cusPrice.price;
+ let billingType = getBillingType(price.config);
- if (isFixedPrice({ price })) {
- continue;
- }
+ if (isFixedPrice({ price })) {
+ continue;
+ }
- let relatedCusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts,
- });
+ let relatedCusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts,
+ });
- if (!relatedCusEnt) {
- continue;
- }
+ if (!relatedCusEnt) {
+ continue;
+ }
- // let usageBasedSub = await getUsageBasedSub({
- // db,
- // stripeCli,
- // subIds: activeProduct.subscription_ids || [],
- // feature: relatedCusEnt.entitlement.feature,
- // stripeSubs,
- // });
- const usageBasedSub = await cusProductToSub({
- cusProduct: activeProduct,
- stripeCli,
- });
+ // let usageBasedSub = await getUsageBasedSub({
+ // db,
+ // stripeCli,
+ // subIds: activeProduct.subscription_ids || [],
+ // feature: relatedCusEnt.entitlement.feature,
+ // stripeSubs,
+ // });
+ const usageBasedSub = await cusProductToSub({
+ cusProduct: activeProduct,
+ stripeCli,
+ });
- const subId = invoiceToSubId({ invoice });
+ const subId = invoiceToSubId({ invoice });
- if (!usageBasedSub || usageBasedSub.id != subId) {
- continue;
- }
+ if (!usageBasedSub || usageBasedSub.id != subId) {
+ continue;
+ }
- // If trial just ended, skip
- const { start, end } = subToPeriodStartEnd({ sub: usageBasedSub });
+ // If trial just ended, skip
+ const { start, end } = subToPeriodStartEnd({ sub: usageBasedSub });
- if (usageBasedSub.trial_end == start) {
- logger.info(`Trial just ended, skipping usage invoice.created`);
- continue;
- }
+ if (usageBasedSub.trial_end == start) {
+ logger.info(`Trial just ended, skipping usage invoice.created`);
+ continue;
+ }
- if (billingType == BillingType.UsageInArrear) {
- await handleUsagePrices({
- db,
- org,
- invoice,
- customer,
- relatedCusEnt,
- stripeCli,
- price,
- usageSub: usageBasedSub,
- logger,
- activeProduct,
- });
- }
+ if (billingType == BillingType.UsageInArrear) {
+ await handleUsagePrices({
+ db,
+ org,
+ invoice,
+ customer,
+ relatedCusEnt,
+ stripeCli,
+ price,
+ usageSub: usageBasedSub,
+ logger,
+ activeProduct,
+ });
+ }
- if (billingType == BillingType.InArrearProrated) {
- await handleContUsePrices({
- db,
- stripeCli,
- cusEnts,
- cusPrice,
- invoice,
- usageSub: usageBasedSub,
- logger,
- });
- }
+ if (billingType == BillingType.InArrearProrated) {
+ await handleContUsePrices({
+ db,
+ stripeCli,
+ cusEnts,
+ cusPrice,
+ invoice,
+ usageSub: usageBasedSub,
+ logger,
+ });
+ }
- if (billingType == BillingType.UsageInAdvance) {
- await handlePrepaidPrices({
- db,
- stripeCli,
- cusPrice,
- cusProduct: activeProduct,
- usageSub: usageBasedSub,
- customer,
- invoice,
- logger,
- });
- }
- }
+ if (billingType == BillingType.UsageInAdvance) {
+ await handlePrepaidPrices({
+ db,
+ stripeCli,
+ cusPrice,
+ cusProduct: activeProduct,
+ usageSub: usageBasedSub,
+ customer,
+ invoice,
+ logger,
+ });
+ }
+ }
};
export const handleInvoiceCreated = async ({
- db,
- org,
- data,
- env,
- logger,
+ db,
+ org,
+ data,
+ env,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- data: Stripe.Invoice;
- env: AppEnv;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ data: Stripe.Invoice;
+ env: AppEnv;
+ logger: any;
}) => {
- const stripeCli = createStripeCli({ org, env });
- const invoice = await getFullStripeInvoice({
- stripeCli,
- stripeId: data.id!,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const invoice = await getFullStripeInvoice({
+ stripeCli,
+ stripeId: data.id!,
+ });
- const subId = invoiceToSubId({ invoice });
+ const subId = invoiceToSubId({ invoice });
- console.log("HANDLING INVOICE CREATED", invoice.id);
+ console.log("HANDLING INVOICE CREATED", invoice.id);
- if (subId) {
- const activeProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: subId,
- orgId: org.id,
- env,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.Expired,
- CusProductStatus.PastDue,
- ],
- });
+ if (subId) {
+ const activeProducts = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: subId,
+ orgId: org.id,
+ env,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.Expired,
+ CusProductStatus.PastDue,
+ ],
+ });
- if (activeProducts.length == 0) {
- logger.warn(
- `Stripe invoice.created -- no active products found (${org.slug})`
- );
- return;
- }
+ if (activeProducts.length == 0) {
+ logger.warn(
+ `Stripe invoice.created -- no active products found (${org.slug})`,
+ );
+ return;
+ }
- let internalEntityId = activeProducts.find(
- (p) => p.internal_entity_id
- )?.internal_entity_id;
+ let internalEntityId = activeProducts.find(
+ (p) => p.internal_entity_id,
+ )?.internal_entity_id;
- let features = await FeatureService.list({
- db,
- orgId: org.id,
- env,
- });
+ let features = await FeatureService.list({
+ db,
+ orgId: org.id,
+ env,
+ });
- if (internalEntityId) {
- // try {
- // let stripeCli = createStripeCli({ org, env });
- // let entity = await EntityService.getByInternalId({
- // db,
- // internalId: internalEntityId,
- // });
- // let feature = features.find(
- // (f) => f.internal_id == entity?.internal_feature_id
- // );
- // let entDetails = "";
- // if (entity.name) {
- // entDetails = `${entity.name}${
- // entity.id ? ` (ID: ${entity.id})` : ""
- // }`;
- // } else if (entity.id) {
- // entDetails = `${entity.id}`;
- // }
- // if (entDetails && feature) {
- // await stripeCli.invoices.update(invoice.id!, {
- // description: `${getFeatureName({
- // feature,
- // plural: false,
- // capitalize: true,
- // })}: ${entDetails}`,
- // });
- // }
- // } catch (error: any) {
- // if (
- // error.message != "Finalized invoices can't be updated in this way"
- // ) {
- // logger.error(`Failed to add entity ID to invoice description`, error);
- // }
- // }
- }
+ if (internalEntityId) {
+ // try {
+ // let stripeCli = createStripeCli({ org, env });
+ // let entity = await EntityService.getByInternalId({
+ // db,
+ // internalId: internalEntityId,
+ // });
+ // let feature = features.find(
+ // (f) => f.internal_id == entity?.internal_feature_id
+ // );
+ // let entDetails = "";
+ // if (entity.name) {
+ // entDetails = `${entity.name}${
+ // entity.id ? ` (ID: ${entity.id})` : ""
+ // }`;
+ // } else if (entity.id) {
+ // entDetails = `${entity.id}`;
+ // }
+ // if (entDetails && feature) {
+ // await stripeCli.invoices.update(invoice.id!, {
+ // description: `${getFeatureName({
+ // feature,
+ // plural: false,
+ // capitalize: true,
+ // })}: ${entDetails}`,
+ // });
+ // }
+ // } catch (error: any) {
+ // if (
+ // error.message != "Finalized invoices can't be updated in this way"
+ // ) {
+ // logger.error(`Failed to add entity ID to invoice description`, error);
+ // }
+ // }
+ }
- const stripeSubs = await getStripeSubs({
- stripeCli: createStripeCli({ org, env }),
- subIds: activeProducts.map((p) => p.subscription_ids || []).flat(),
- });
+ const stripeSubs = await getStripeSubs({
+ stripeCli: createStripeCli({ org, env }),
+ subIds: activeProducts.map((p) => p.subscription_ids || []).flat(),
+ });
- for (const activeProduct of activeProducts) {
- await sendUsageAndReset({
- db,
- activeProduct,
- org,
- env,
- stripeSubs,
- invoice,
- logger,
- });
- }
- }
+ for (const activeProduct of activeProducts) {
+ await sendUsageAndReset({
+ db,
+ activeProduct,
+ org,
+ env,
+ stripeSubs,
+ invoice,
+ logger,
+ });
+ }
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts
index 54d4fd361..c61960c28 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handlePrepaidPrices.ts
@@ -8,140 +8,140 @@ import { CusProductService } from "@/internal/customers/cusProducts/CusProductSe
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- Customer,
- EntInterval,
- FeatureOptions,
- FullCusProduct,
- FullCustomerPrice,
- UsagePriceConfig,
+ Customer,
+ EntInterval,
+ FeatureOptions,
+ FullCusProduct,
+ FullCustomerPrice,
+ UsagePriceConfig,
} from "@autumn/shared";
import Stripe from "stripe";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
export const handlePrepaidPrices = async ({
- db,
- stripeCli,
- cusProduct,
- cusPrice,
- usageSub,
- customer,
- invoice,
- logger,
+ db,
+ stripeCli,
+ cusProduct,
+ cusPrice,
+ usageSub,
+ customer,
+ invoice,
+ logger,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- cusProduct: FullCusProduct;
- cusPrice: FullCustomerPrice;
- usageSub: Stripe.Subscription;
- customer: Customer;
- invoice: Stripe.Invoice;
- logger: any;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ cusProduct: FullCusProduct;
+ cusPrice: FullCustomerPrice;
+ usageSub: Stripe.Subscription;
+ customer: Customer;
+ invoice: Stripe.Invoice;
+ logger: any;
}) => {
- const { start, end } = subToPeriodStartEnd({ sub: usageSub });
- const isNewPeriod = invoice.period_start !== start;
+ const { start, end } = subToPeriodStartEnd({ sub: usageSub });
+ const isNewPeriod = invoice.period_start !== start;
- if (!isNewPeriod) {
- return;
- }
+ if (!isNewPeriod) {
+ return;
+ }
- const cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts: cusProduct.customer_entitlements,
- });
+ const cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts: cusProduct.customer_entitlements,
+ });
- if (!cusEnt) {
- logger.error(
- `Tried to handle prepaid price for ${cusPrice.id} (${cusPrice.price.id}) but no cus ent found`
- );
- return;
- }
+ if (!cusEnt) {
+ logger.error(
+ `Tried to handle prepaid price for ${cusPrice.id} (${cusPrice.price.id}) but no cus ent found`,
+ );
+ return;
+ }
- const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
+ const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
- // const resetBalance = getResetBalance({
- // entitlement: cusEnt.entitlement,
- // options: notNullish(options?.upcoming_quantity)
- // ? {
- // feature_id: options?.feature_id!,
- // quantity: options?.upcoming_quantity!,
- // }
- // : options,
- // relatedPrice: cusPrice.price,
- // });
- let resetQuantity = options?.upcoming_quantity || options?.quantity!;
- let config = cusPrice.price.config as UsagePriceConfig;
- let billingUnits = config.billing_units || 1;
- let newAllowance =
- resetQuantity * billingUnits + (cusEnt.entitlement.allowance || 0);
+ // const resetBalance = getResetBalance({
+ // entitlement: cusEnt.entitlement,
+ // options: notNullish(options?.upcoming_quantity)
+ // ? {
+ // feature_id: options?.feature_id!,
+ // quantity: options?.upcoming_quantity!,
+ // }
+ // : options,
+ // relatedPrice: cusPrice.price,
+ // });
+ let resetQuantity = options?.upcoming_quantity || options?.quantity!;
+ let config = cusPrice.price.config as UsagePriceConfig;
+ let billingUnits = config.billing_units || 1;
+ let newAllowance =
+ resetQuantity * billingUnits + (cusEnt.entitlement.allowance || 0);
- const resetUpdate = getResetBalancesUpdate({
- cusEnt,
- allowance: newAllowance,
- });
+ const resetUpdate = getResetBalancesUpdate({
+ cusEnt,
+ allowance: newAllowance,
+ });
- // console.log("--------------------------------");
- // console.log(`Entity ID: ${cusProduct.entity_id}`);
- // console.log(`Upcoming quantity: ${options?.upcoming_quantity}`);
- // console.log(`Quantity: ${options?.quantity}`);
- // console.log(`New allowance: ${newAllowance}`);
- // console.log(`RESET UPDATE: ${JSON.stringify(resetUpdate)}`);
+ // console.log("--------------------------------");
+ // console.log(`Entity ID: ${cusProduct.entity_id}`);
+ // console.log(`Upcoming quantity: ${options?.upcoming_quantity}`);
+ // console.log(`Quantity: ${options?.quantity}`);
+ // console.log(`New allowance: ${newAllowance}`);
+ // console.log(`RESET UPDATE: ${JSON.stringify(resetUpdate)}`);
- const ent = cusEnt.entitlement;
+ const ent = cusEnt.entitlement;
- let rolloverUpdate = getRolloverUpdates({
- cusEnt,
- nextResetAt: end * 1000,
- });
+ let rolloverUpdate = getRolloverUpdates({
+ cusEnt,
+ nextResetAt: end * 1000,
+ });
- if (notNullish(options?.upcoming_quantity)) {
- const newOptions = cusProduct.options.map((o) => {
- if (o.feature_id == ent.feature_id) {
- return {
- ...o,
- quantity: o.upcoming_quantity,
- upcoming_quantity: undefined,
- };
- }
- return o;
- });
+ if (notNullish(options?.upcoming_quantity)) {
+ const newOptions = cusProduct.options.map((o) => {
+ if (o.feature_id == ent.feature_id) {
+ return {
+ ...o,
+ quantity: o.upcoming_quantity,
+ upcoming_quantity: undefined,
+ };
+ }
+ return o;
+ });
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- options: newOptions as FeatureOptions[],
- },
- });
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ options: newOptions as FeatureOptions[],
+ },
+ });
- if (ent.interval == EntInterval.Lifetime) {
- let difference = options?.quantity! - options?.upcoming_quantity!;
- await CusEntService.decrement({
- db,
- id: cusEnt.id,
- amount: difference,
- });
- return;
- }
- }
+ if (ent.interval == EntInterval.Lifetime) {
+ let difference = options?.quantity! - options?.upcoming_quantity!;
+ await CusEntService.decrement({
+ db,
+ id: cusEnt.id,
+ amount: difference,
+ });
+ return;
+ }
+ }
- if (ent.interval == EntInterval.Lifetime) {
- return;
- }
+ if (ent.interval == EntInterval.Lifetime) {
+ return;
+ }
- if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
- await RolloverService.insert({
- db,
- rows: rolloverUpdate.toInsert,
- fullCusEnt: cusEnt,
- });
- }
+ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
+ await RolloverService.insert({
+ db,
+ rows: rolloverUpdate.toInsert,
+ fullCusEnt: cusEnt,
+ });
+ }
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- ...resetUpdate,
- next_reset_at: end * 1000,
- },
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ ...resetUpdate,
+ next_reset_at: end * 1000,
+ },
+ });
};
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts
index fc79ce7b8..3e25337f6 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts
@@ -3,15 +3,15 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils.js";
import {
- FullCustomerEntitlement,
- Price,
- FullCusProduct,
- UsagePriceConfig,
- EntInterval,
- Customer,
- APIVersion,
- RolloverConfig,
- Organization,
+ FullCustomerEntitlement,
+ Price,
+ FullCusProduct,
+ UsagePriceConfig,
+ EntInterval,
+ Customer,
+ APIVersion,
+ RolloverConfig,
+ Organization,
} from "@autumn/shared";
import { differenceInMinutes, subDays } from "date-fns";
import { submitUsageToStripe } from "../../stripeMeterUtils.js";
@@ -24,148 +24,148 @@ import { notNullish } from "@/utils/genUtils.js";
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
export const handleUsagePrices = async ({
- db,
- org,
- invoice,
- customer,
- relatedCusEnt,
- stripeCli,
- price,
- usageSub,
- logger,
- activeProduct,
+ db,
+ org,
+ invoice,
+ customer,
+ relatedCusEnt,
+ stripeCli,
+ price,
+ usageSub,
+ logger,
+ activeProduct,
}: {
- db: DrizzleCli;
- org: Organization;
- invoice: Stripe.Invoice;
- customer: Customer;
- relatedCusEnt: FullCustomerEntitlement;
- stripeCli: Stripe;
- price: Price;
- usageSub: Stripe.Subscription;
- logger: any;
- activeProduct: FullCusProduct;
+ db: DrizzleCli;
+ org: Organization;
+ invoice: Stripe.Invoice;
+ customer: Customer;
+ relatedCusEnt: FullCustomerEntitlement;
+ stripeCli: Stripe;
+ price: Price;
+ usageSub: Stripe.Subscription;
+ logger: any;
+ activeProduct: FullCusProduct;
}) => {
- let invoiceCreatedRecently =
- Math.abs(
- differenceInMinutes(
- new Date(activeProduct.created_at),
- new Date(invoice.created * 1000)
- )
- ) < 10;
+ let invoiceCreatedRecently =
+ Math.abs(
+ differenceInMinutes(
+ new Date(activeProduct.created_at),
+ new Date(invoice.created * 1000),
+ ),
+ ) < 10;
- let invoiceFromUpgrade =
- invoice.billing_reason == "subscription_update" ||
- invoice.billing_reason == "manual";
+ let invoiceFromUpgrade =
+ invoice.billing_reason == "subscription_update" ||
+ invoice.billing_reason == "manual";
- if (invoiceCreatedRecently) {
- logger.info("Invoice created recently, skipping");
- return;
- }
+ if (invoiceCreatedRecently) {
+ logger.info("Invoice created recently, skipping");
+ return;
+ }
- if (invoiceFromUpgrade) {
- logger.info("Invoice is from upgrade, skipping");
- return;
- }
+ if (invoiceFromUpgrade) {
+ logger.info("Invoice is from upgrade, skipping");
+ return;
+ }
- logger.info(`✨ Handling usage prices for ${customer.name || customer.id}`);
- logger.info(`✨ org: ${org.slug}, product: ${activeProduct.product.id}`);
+ logger.info(`✨ Handling usage prices for ${customer.name || customer.id}`);
+ logger.info(`✨ org: ${org.slug}, product: ${activeProduct.product.id}`);
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- // If relatedCusEnt's balance > 0 and next_reset_at is null, skip...
- if (relatedCusEnt.balance! > 0 && !relatedCusEnt.next_reset_at) {
- logger.info("Balance > 0 and next_reset_at is null, skipping");
- return;
- }
+ // If relatedCusEnt's balance > 0 and next_reset_at is null, skip...
+ if (relatedCusEnt.balance! > 0 && !relatedCusEnt.next_reset_at) {
+ logger.info("Balance > 0 and next_reset_at is null, skipping");
+ return;
+ }
- const subItem = findStripeItemForPrice({
- price,
- stripeItems: usageSub.items.data,
- });
+ const subItem = findStripeItemForPrice({
+ price,
+ stripeItems: usageSub.items.data,
+ });
- const isNewUsageMethod =
- activeProduct.internal_entity_id ||
- activeProduct.api_version === APIVersion.v1_4;
+ const isNewUsageMethod =
+ activeProduct.internal_entity_id ||
+ activeProduct.api_version === APIVersion.v1_4;
- if (isNewUsageMethod) {
- let invoiceItem = getInvoiceItemForUsage({
- stripeInvoiceId: invoice.id!,
- price,
- customer,
- currency: invoice.currency,
- cusProduct: activeProduct,
- logger,
- periodStart: invoice.period_start,
- periodEnd: invoice.period_end,
- });
+ if (isNewUsageMethod) {
+ let invoiceItem = getInvoiceItemForUsage({
+ stripeInvoiceId: invoice.id!,
+ price,
+ customer,
+ currency: invoice.currency,
+ cusProduct: activeProduct,
+ logger,
+ periodStart: invoice.period_start,
+ periodEnd: invoice.period_end,
+ });
- if (invoiceItem.price_data!.unit_amount! > 0) {
- await stripeCli.invoiceItems.create(invoiceItem);
- }
- } else {
- if (!config.stripe_meter_id) {
- logger.warn(
- `Price ${price.id} has no stripe meter id, skipping invoice.created for usage in arrear`
- );
- return;
- }
+ if (invoiceItem.price_data!.unit_amount! > 0) {
+ await stripeCli.invoiceItems.create(invoiceItem);
+ }
+ } else {
+ if (!config.stripe_meter_id) {
+ logger.warn(
+ `Price ${price.id} has no stripe meter id, skipping invoice.created for usage in arrear`,
+ );
+ return;
+ }
- const { roundedUsage } = getCusPriceUsage({
- price,
- cusProduct: activeProduct,
- logger,
- });
+ const { roundedUsage } = getCusPriceUsage({
+ price,
+ cusProduct: activeProduct,
+ logger,
+ });
- const usageTimestamp = Math.round(
- subDays(new Date(invoice.created * 1000), 1).getTime() / 1000
- );
+ const usageTimestamp = Math.round(
+ subDays(new Date(invoice.created * 1000), 1).getTime() / 1000,
+ );
- await submitUsageToStripe({
- price,
- stripeCli,
- usage: roundedUsage,
- customer,
- usageTimestamp,
- feature: relatedCusEnt.entitlement.feature,
- logger,
- });
- }
+ await submitUsageToStripe({
+ price,
+ stripeCli,
+ usage: roundedUsage,
+ customer,
+ usageTimestamp,
+ feature: relatedCusEnt.entitlement.feature,
+ logger,
+ });
+ }
- if (relatedCusEnt.entitlement.interval == EntInterval.Lifetime) {
- return;
- }
+ if (relatedCusEnt.entitlement.interval == EntInterval.Lifetime) {
+ return;
+ }
- let ent = relatedCusEnt.entitlement;
+ let ent = relatedCusEnt.entitlement;
- let resetBalancesUpdate = getResetBalancesUpdate({
- cusEnt: relatedCusEnt,
- allowance: ent.interval == EntInterval.Lifetime ? 0 : ent.allowance!,
- });
+ let resetBalancesUpdate = getResetBalancesUpdate({
+ cusEnt: relatedCusEnt,
+ allowance: ent.interval == EntInterval.Lifetime ? 0 : ent.allowance!,
+ });
- const { end } = subToPeriodStartEnd({ sub: usageSub });
- await CusEntService.update({
- db,
- id: relatedCusEnt.id,
- updates: {
- ...resetBalancesUpdate,
- adjustment: 0,
- next_reset_at: relatedCusEnt.next_reset_at ? end * 1000 : null,
- },
- });
+ const { end } = subToPeriodStartEnd({ sub: usageSub });
+ await CusEntService.update({
+ db,
+ id: relatedCusEnt.id,
+ updates: {
+ ...resetBalancesUpdate,
+ adjustment: 0,
+ next_reset_at: relatedCusEnt.next_reset_at ? end * 1000 : null,
+ },
+ });
- let rolloverUpdate = getRolloverUpdates({
- cusEnt: relatedCusEnt,
- nextResetAt: end * 1000,
- });
+ let rolloverUpdate = getRolloverUpdates({
+ cusEnt: relatedCusEnt,
+ nextResetAt: end * 1000,
+ });
- if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
- await RolloverService.insert({
- db,
- rows: rolloverUpdate.toInsert,
- fullCusEnt: relatedCusEnt,
- });
- }
+ if (rolloverUpdate?.toInsert && rolloverUpdate.toInsert.length > 0) {
+ await RolloverService.insert({
+ db,
+ rows: rolloverUpdate.toInsert,
+ fullCusEnt: relatedCusEnt,
+ });
+ }
- logger.info("✅ Successfully reset balance");
+ logger.info("✅ Successfully reset balance");
};
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts
index cc38c442b..c79c8d2f5 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts
@@ -1,93 +1,93 @@
import {
- AppEnv,
- CusProductStatus,
- FullCustomerPrice,
- InvoiceStatus,
- Organization,
+ AppEnv,
+ CusProductStatus,
+ FullCustomerPrice,
+ InvoiceStatus,
+ Organization,
} from "@autumn/shared";
import Stripe from "stripe";
import { createStripeCli } from "../utils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- getFullStripeInvoice,
- getStripeExpandedInvoice,
- invoiceToSubId,
- updateInvoiceIfExists,
+ getFullStripeInvoice,
+ getStripeExpandedInvoice,
+ invoiceToSubId,
+ updateInvoiceIfExists,
} from "../stripeInvoiceUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
export const handleInvoiceFinalized = async ({
- db,
- org,
- data,
- env,
- logger,
+ db,
+ org,
+ data,
+ env,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- data: Stripe.Invoice;
- env: AppEnv;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ data: Stripe.Invoice;
+ env: AppEnv;
+ logger: any;
}) => {
- const stripeCli = createStripeCli({ org, env });
- const invoice = await getFullStripeInvoice({
- stripeCli,
- stripeId: data.id!,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const invoice = await getFullStripeInvoice({
+ stripeCli,
+ stripeId: data.id!,
+ });
- const subId = invoiceToSubId({ invoice });
+ const subId = invoiceToSubId({ invoice });
- if (subId) {
- const stripeCli = createStripeCli({ org, env });
- const expandedInvoice = await getStripeExpandedInvoice({
- stripeCli,
- stripeInvoiceId: invoice.id!,
- });
+ if (subId) {
+ const stripeCli = createStripeCli({ org, env });
+ const expandedInvoice = await getStripeExpandedInvoice({
+ stripeCli,
+ stripeInvoiceId: invoice.id!,
+ });
- const activeProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: subId,
- orgId: org.id,
- env,
- inStatuses: [CusProductStatus.Active],
- });
+ const activeProducts = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: subId,
+ orgId: org.id,
+ env,
+ inStatuses: [CusProductStatus.Active],
+ });
- if (activeProducts.length === 0) {
- return;
- }
+ if (activeProducts.length === 0) {
+ return;
+ }
- const updated = await updateInvoiceIfExists({
- db,
- invoice,
- });
+ const updated = await updateInvoiceIfExists({
+ db,
+ invoice,
+ });
- if (updated) {
- return;
- }
+ if (updated) {
+ return;
+ }
- let prices = activeProducts.flatMap((cp) =>
- cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price)
- );
+ let prices = activeProducts.flatMap((cp) =>
+ cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price),
+ );
- let invoiceItems = await getInvoiceItems({
- stripeInvoice: invoice,
- prices: prices,
- logger,
- });
+ let invoiceItems = await getInvoiceItems({
+ stripeInvoice: invoice,
+ prices: prices,
+ logger,
+ });
- await InvoiceService.createInvoiceFromStripe({
- db,
- stripeInvoice: expandedInvoice,
- internalCustomerId: activeProducts[0].internal_customer_id,
- productIds: activeProducts.map((p) => p.product.id),
- internalProductIds: activeProducts.map((p) => p.internal_product_id),
- internalEntityId: activeProducts[0].internal_entity_id,
- status: invoice.status as InvoiceStatus,
- org,
- items: invoiceItems,
- });
- }
+ await InvoiceService.createInvoiceFromStripe({
+ db,
+ stripeInvoice: expandedInvoice,
+ internalCustomerId: activeProducts[0].internal_customer_id,
+ productIds: activeProducts.map((p) => p.product.id),
+ internalProductIds: activeProducts.map((p) => p.internal_product_id),
+ internalEntityId: activeProducts[0].internal_entity_id,
+ status: invoice.status as InvoiceStatus,
+ org,
+ items: invoiceItems,
+ });
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
index 6c727f283..d092e2023 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts
@@ -1,9 +1,9 @@
import type {
- AppEnv,
- FullCusProduct,
- FullCustomerPrice,
- InvoiceStatus,
- Organization,
+ AppEnv,
+ FullCusProduct,
+ FullCustomerPrice,
+ InvoiceStatus,
+ Organization,
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
@@ -15,10 +15,10 @@ import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import { nullish } from "@/utils/genUtils.js";
import {
- getFullStripeInvoice,
- getInvoiceDiscounts,
- invoiceToSubId,
- updateInvoiceIfExists,
+ getFullStripeInvoice,
+ getInvoiceDiscounts,
+ invoiceToSubId,
+ updateInvoiceIfExists,
} from "../stripeInvoiceUtils.js";
import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js";
import { getStripeSubs } from "../stripeSubUtils.js";
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts
index bf781198a..170615382 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts
@@ -1,9 +1,9 @@
import {
- type AppEnv,
- CouponDurationType,
- type Organization,
- type Reward,
- RewardType,
+ type AppEnv,
+ CouponDurationType,
+ type Organization,
+ type Reward,
+ RewardType,
} from "@autumn/shared";
import { addMonths } from "date-fns";
import { Decimal } from "decimal.js";
@@ -14,8 +14,8 @@ import { RewardService } from "@/internal/rewards/RewardService.js";
import { generateId } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import {
- deleteCouponFromCus,
- deleteCouponFromSub,
+ deleteCouponFromCus,
+ deleteCouponFromSub,
} from "../stripeCouponUtils/deleteCouponFromCus.js";
import { invoiceToSubId } from "../stripeInvoiceUtils.js";
import { createStripeCli } from "../utils.js";
diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts
index fbe3765d7..4d10f96fa 100644
--- a/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts
+++ b/server/src/external/stripe/webhookHandlers/handleInvoiceUpdated.ts
@@ -10,120 +10,120 @@ import { CusService } from "@/internal/customers/CusService.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
const handleInvoiceCheckoutVoided = async ({
- db,
- stripeCli,
- invoiceObject,
- logger,
+ db,
+ stripeCli,
+ invoiceObject,
+ logger,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- invoiceObject: Stripe.Invoice;
- logger: any;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ invoiceObject: Stripe.Invoice;
+ logger: any;
}) => {
- const fullInvoice = await getFullStripeInvoice({
- stripeCli,
- stripeId: invoiceObject.id!,
- });
+ const fullInvoice = await getFullStripeInvoice({
+ stripeCli,
+ stripeId: invoiceObject.id!,
+ });
- const metadataId = fullInvoice.metadata?.autumn_metadata_id;
+ const metadataId = fullInvoice.metadata?.autumn_metadata_id;
- if (!metadataId) return;
+ if (!metadataId) return;
- const metadata = await MetadataService.get({
- db,
- id: metadataId,
- });
+ const metadata = await MetadataService.get({
+ db,
+ id: metadataId,
+ });
- const { anchorToUnix, config, ...rest } = metadata?.data;
- const attachParams = rest as AttachParams;
+ const { anchorToUnix, config, ...rest } = metadata?.data;
+ const attachParams = rest as AttachParams;
- if (!attachParams) return;
+ if (!attachParams) return;
- const customer = attachParams.customer;
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customer.id || customer.internal_id,
- orgId: attachParams.org.id,
- env: attachParams.customer.env,
- });
+ const customer = attachParams.customer;
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customer.id || customer.internal_id,
+ orgId: attachParams.org.id,
+ env: attachParams.customer.env,
+ });
- const subId = invoiceToSubId({ invoice: fullInvoice });
+ const subId = invoiceToSubId({ invoice: fullInvoice });
- if (!subId) return;
+ if (!subId) return;
- const cusSubIds = fullCus.customer_products
- .map((cp) => cp.subscription_ids || [])
- .flat();
+ const cusSubIds = fullCus.customer_products
+ .map((cp) => cp.subscription_ids || [])
+ .flat();
- const subIdMatch = cusSubIds.includes(subId);
+ const subIdMatch = cusSubIds.includes(subId);
- if (subIdMatch) return;
+ if (subIdMatch) return;
- try {
- const sub = await stripeCli.subscriptions.retrieve(subId);
+ try {
+ const sub = await stripeCli.subscriptions.retrieve(subId);
- if (sub.status !== "canceled") {
- console.log("Invoice checkout voided, cancelling sub:", subId);
- await stripeCli.subscriptions.cancel(subId);
- }
- } catch (error: any) {
- logger.warn(`Failed to cancel sub ${subId}, error: ${error?.message}`);
- }
+ if (sub.status !== "canceled") {
+ console.log("Invoice checkout voided, cancelling sub:", subId);
+ await stripeCli.subscriptions.cancel(subId);
+ }
+ } catch (error: any) {
+ logger.warn(`Failed to cancel sub ${subId}, error: ${error?.message}`);
+ }
};
export const handleInvoiceUpdated = async ({
- env,
- event,
- stripeCli,
- req,
+ env,
+ event,
+ stripeCli,
+ req,
}: {
- env: AppEnv;
- event: Stripe.Event;
- stripeCli: Stripe;
- req: any;
+ env: AppEnv;
+ event: Stripe.Event;
+ stripeCli: Stripe;
+ req: any;
}) => {
- const invoiceObject = event.data.object as Stripe.Invoice;
- // const invoice = await getFullStripeInvoice({
- // stripeCli,
- // stripeId: invoiceObject.id!,
- // });
+ const invoiceObject = event.data.object as Stripe.Invoice;
+ // const invoice = await getFullStripeInvoice({
+ // stripeCli,
+ // stripeId: invoiceObject.id!,
+ // });
- const prevAttributes = event.data.previous_attributes as any;
- const invoiceVoided =
- prevAttributes?.status !== "void" && invoiceObject.status === "void";
+ const prevAttributes = event.data.previous_attributes as any;
+ const invoiceVoided =
+ prevAttributes?.status !== "void" && invoiceObject.status === "void";
- const { logger } = req;
+ const { logger } = req;
- if (invoiceVoided) {
- logger.info(`Invoice has been voided!`);
+ if (invoiceVoided) {
+ logger.info(`Invoice has been voided!`);
- await handleInvoiceCheckoutVoided({
- db: req.db,
- stripeCli,
- invoiceObject,
- logger,
- });
+ await handleInvoiceCheckoutVoided({
+ db: req.db,
+ stripeCli,
+ invoiceObject,
+ logger,
+ });
- await InvoiceService.updateByStripeId({
- db: req.db,
- stripeId: invoiceObject.id!,
- updates: {
- status: InvoiceStatus.Void,
- },
- });
- }
+ await InvoiceService.updateByStripeId({
+ db: req.db,
+ stripeId: invoiceObject.id!,
+ updates: {
+ status: InvoiceStatus.Void,
+ },
+ });
+ }
- const invoiceOpen =
- prevAttributes?.status !== "open" && invoiceObject.status === "open";
+ const invoiceOpen =
+ prevAttributes?.status !== "open" && invoiceObject.status === "open";
- if (invoiceOpen) {
- // logger.info(`Invoice has been opened!`);
- await InvoiceService.updateByStripeId({
- db: req.db,
- stripeId: invoiceObject.id!,
- updates: {
- status: InvoiceStatus.Open,
- },
- });
- }
+ if (invoiceOpen) {
+ // logger.info(`Invoice has been opened!`);
+ await InvoiceService.updateByStripeId({
+ db: req.db,
+ stripeId: invoiceObject.id!,
+ updates: {
+ status: InvoiceStatus.Open,
+ },
+ });
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts
index bb1120337..f9ab3b0e2 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts
@@ -1,11 +1,11 @@
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- BillingType,
- CusProductStatus,
- FullCusProduct,
- FullCustomerPrice,
- Organization,
- Price,
+ BillingType,
+ CusProductStatus,
+ FullCusProduct,
+ FullCustomerPrice,
+ Organization,
+ Price,
} from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import Stripe from "stripe";
@@ -19,202 +19,202 @@ import { getFullStripeSub } from "../stripeSubUtils.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import {
- getEarliestPeriodEnd,
- getEarliestPeriodStart,
+ getEarliestPeriodEnd,
+ getEarliestPeriodStart,
} from "../stripeSubUtils/convertSubUtils.js";
export const handleSubCreated = async ({
- db,
- subData,
- org,
- env,
- logger,
+ db,
+ subData,
+ org,
+ env,
+ logger,
}: {
- db: DrizzleCli;
- subData: Stripe.Subscription;
- org: Organization;
- env: AppEnv;
- logger: any;
+ db: DrizzleCli;
+ subData: Stripe.Subscription;
+ org: Organization;
+ env: AppEnv;
+ logger: any;
}) => {
- const stripeCli = createStripeCli({ org, env });
- const subscription = await getFullStripeSub({
- stripeCli,
- stripeId: subData.id,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const subscription = await getFullStripeSub({
+ stripeCli,
+ stripeId: subData.id,
+ });
- if (subscription.schedule) {
- const cusProds = await CusProductService.getByStripeScheduledId({
- db,
- stripeScheduledId: subscription.schedule as string,
- orgId: org.id,
- env,
- });
+ if (subscription.schedule) {
+ const cusProds = await CusProductService.getByStripeScheduledId({
+ db,
+ stripeScheduledId: subscription.schedule as string,
+ orgId: org.id,
+ env,
+ });
- if (!cusProds || cusProds.length === 0) {
- console.log("No cus prod found for scheduled id", subscription.schedule);
- return;
- }
+ if (!cusProds || cusProds.length === 0) {
+ console.log("No cus prod found for scheduled id", subscription.schedule);
+ return;
+ }
- // Update autumn sub
- let autumnSub = await SubService.getFromScheduleId({
- db,
- scheduleId: subscription.schedule as string,
- });
+ // Update autumn sub
+ let autumnSub = await SubService.getFromScheduleId({
+ db,
+ scheduleId: subscription.schedule as string,
+ });
- const earliestPeriodStart = getEarliestPeriodStart({ sub: subscription });
- const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
- if (autumnSub) {
- await SubService.updateFromScheduleId({
- db,
- scheduleId: subscription.schedule as string,
- updates: {
- stripe_id: subscription.id,
- current_period_start: earliestPeriodStart,
- current_period_end: earliestPeriodEnd,
- },
- });
- } else {
- let subUsageFeatures = [];
- try {
- subUsageFeatures = JSON.parse(subscription.metadata?.usage_features);
- subUsageFeatures = subUsageFeatures.map(
- (feature: any) => feature.internal_id
- );
- } catch (error) {
- console.log("Error parsing usage features", error);
- }
+ const earliestPeriodStart = getEarliestPeriodStart({ sub: subscription });
+ const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
+ if (autumnSub) {
+ await SubService.updateFromScheduleId({
+ db,
+ scheduleId: subscription.schedule as string,
+ updates: {
+ stripe_id: subscription.id,
+ current_period_start: earliestPeriodStart,
+ current_period_end: earliestPeriodEnd,
+ },
+ });
+ } else {
+ let subUsageFeatures = [];
+ try {
+ subUsageFeatures = JSON.parse(subscription.metadata?.usage_features);
+ subUsageFeatures = subUsageFeatures.map(
+ (feature: any) => feature.internal_id,
+ );
+ } catch (error) {
+ console.log("Error parsing usage features", error);
+ }
- await SubService.createSub({
- db,
- sub: {
- id: generateId("sub"),
- created_at: Date.now(),
- stripe_id: subscription.id,
- stripe_schedule_id: subscription.schedule as string,
- usage_features: subUsageFeatures,
- org_id: org.id,
- env: env,
- current_period_start: earliestPeriodStart,
- current_period_end: earliestPeriodEnd,
- },
- });
- }
+ await SubService.createSub({
+ db,
+ sub: {
+ id: generateId("sub"),
+ created_at: Date.now(),
+ stripe_id: subscription.id,
+ stripe_schedule_id: subscription.schedule as string,
+ usage_features: subUsageFeatures,
+ org_id: org.id,
+ env: env,
+ current_period_start: earliestPeriodStart,
+ current_period_end: earliestPeriodEnd,
+ },
+ });
+ }
- console.log(
- "Handling subscription.created for scheduled cus products:",
- cusProds.length
- );
+ console.log(
+ "Handling subscription.created for scheduled cus products:",
+ cusProds.length,
+ );
- let batchUpdate = [];
- for (const cusProd of cusProds) {
- let subIds = cusProd.subscription_ids
- ? [...cusProd.subscription_ids]
- : [];
- subIds.push(subscription.id);
+ let batchUpdate = [];
+ for (const cusProd of cusProds) {
+ let subIds = cusProd.subscription_ids
+ ? [...cusProd.subscription_ids]
+ : [];
+ subIds.push(subscription.id);
- const updateCusProd = async () => {
- await CusProductService.update({
- db,
- cusProductId: cusProd.id,
- updates: {
- subscription_ids: subIds,
- },
- });
+ const updateCusProd = async () => {
+ await CusProductService.update({
+ db,
+ cusProductId: cusProd.id,
+ updates: {
+ subscription_ids: subIds,
+ },
+ });
- // Fetch latest invoice?
- const stripeCli = createStripeCli({ org, env });
- const invoice = await getStripeExpandedInvoice({
- stripeCli,
- stripeInvoiceId: subscription.latest_invoice as string,
- });
+ // Fetch latest invoice?
+ const stripeCli = createStripeCli({ org, env });
+ const invoice = await getStripeExpandedInvoice({
+ stripeCli,
+ stripeInvoiceId: subscription.latest_invoice as string,
+ });
- let invoiceItems = await getInvoiceItems({
- stripeInvoice: invoice,
- prices: cusProd.customer_prices.map(
- (cpr: FullCustomerPrice) => cpr.price
- ),
- logger,
- });
+ let invoiceItems = await getInvoiceItems({
+ stripeInvoice: invoice,
+ prices: cusProd.customer_prices.map(
+ (cpr: FullCustomerPrice) => cpr.price,
+ ),
+ logger,
+ });
- await InvoiceService.createInvoiceFromStripe({
- db,
- stripeInvoice: invoice,
- internalCustomerId: cusProd.internal_customer_id,
- internalEntityId: cusProd.internal_entity_id,
- productIds: [cusProd.product_id],
- internalProductIds: [cusProd.internal_product_id],
- org,
- items: invoiceItems,
- });
- };
+ await InvoiceService.createInvoiceFromStripe({
+ db,
+ stripeInvoice: invoice,
+ internalCustomerId: cusProd.internal_customer_id,
+ internalEntityId: cusProd.internal_entity_id,
+ productIds: [cusProd.product_id],
+ internalProductIds: [cusProd.internal_product_id],
+ org,
+ items: invoiceItems,
+ });
+ };
- batchUpdate.push(updateCusProd());
- }
+ batchUpdate.push(updateCusProd());
+ }
- await Promise.all(batchUpdate);
- }
+ await Promise.all(batchUpdate);
+ }
- // Get cus prods for sub
- let cusProds = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: subscription.id,
- orgId: org.id,
- env,
- });
+ // Get cus prods for sub
+ let cusProds = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: subscription.id,
+ orgId: org.id,
+ env,
+ });
- let handleInArrearWithEntity = async (cusProd: FullCusProduct) => {
- if (!cusProd.internal_entity_id) {
- return;
- }
+ let handleInArrearWithEntity = async (cusProd: FullCusProduct) => {
+ if (!cusProd.internal_entity_id) {
+ return;
+ }
- let arrearPrices = cusProd.customer_prices
- .map((cp) => cp.price)
- .filter(
- (p: Price) =>
- getBillingType(p.config as any) == BillingType.UsageInArrear
- );
+ let arrearPrices = cusProd.customer_prices
+ .map((cp) => cp.price)
+ .filter(
+ (p: Price) =>
+ getBillingType(p.config as any) == BillingType.UsageInArrear,
+ );
- if (arrearPrices.length == 0) {
- return;
- }
+ if (arrearPrices.length == 0) {
+ return;
+ }
- let itemsToDelete = [];
- for (const arrearPrice of arrearPrices) {
- let subItem = subscription.items.data.find(
- (i) => i.price.id == arrearPrice.config?.stripe_price_id
- );
+ let itemsToDelete = [];
+ for (const arrearPrice of arrearPrices) {
+ let subItem = subscription.items.data.find(
+ (i) => i.price.id == arrearPrice.config?.stripe_price_id,
+ );
- if (!subItem) {
- continue;
- }
+ if (!subItem) {
+ continue;
+ }
- itemsToDelete.push({
- id: subItem.id,
- deleted: true,
- });
- }
+ itemsToDelete.push({
+ id: subItem.id,
+ deleted: true,
+ });
+ }
- if (itemsToDelete.length > 0) {
- try {
- await stripeCli.subscriptions.update(subscription.id, {
- items: itemsToDelete,
- });
- console.log(
- `sub.created, cus product with entity: deleted ${itemsToDelete.length} items`
- );
- } catch (error) {
- logger.error(
- `sub.created, cus product with entity: failed to delete items`,
- error
- );
- }
- }
- };
+ if (itemsToDelete.length > 0) {
+ try {
+ await stripeCli.subscriptions.update(subscription.id, {
+ items: itemsToDelete,
+ });
+ console.log(
+ `sub.created, cus product with entity: deleted ${itemsToDelete.length} items`,
+ );
+ } catch (error) {
+ logger.error(
+ `sub.created, cus product with entity: failed to delete items`,
+ error,
+ );
+ }
+ }
+ };
- let batchUpdate = [];
- for (const cusProd of cusProds) {
- batchUpdate.push(handleInArrearWithEntity(cusProd));
- }
+ let batchUpdate = [];
+ for (const cusProd of cusProds) {
+ batchUpdate.push(handleInArrearWithEntity(cusProd));
+ }
- await Promise.all(batchUpdate);
+ await Promise.all(batchUpdate);
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts
index 4fa6eb606..580c4a1ac 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts
@@ -1,77 +1,77 @@
import Stripe from "stripe";
import {
- getFullStripeSub,
- subIsPrematurelyCanceled,
+ getFullStripeSub,
+ subIsPrematurelyCanceled,
} from "../stripeSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { handleCusProductDeleted } from "./handleSubDeleted/handleCusProductDeleted.js";
export const handleSubDeleted = async ({
- req,
- stripeCli,
- data,
- logger,
+ req,
+ stripeCli,
+ data,
+ logger,
}: {
- req: ExtendedRequest;
- stripeCli: Stripe;
- data: Stripe.Subscription;
- logger: any;
+ req: ExtendedRequest;
+ stripeCli: Stripe;
+ data: Stripe.Subscription;
+ logger: any;
}) => {
- const { db, org, env } = req;
+ const { db, org, env } = req;
- const activeCusProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: data.id,
- orgId: org.id,
- env,
- });
+ const activeCusProducts = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: data.id,
+ orgId: org.id,
+ env,
+ });
- if (activeCusProducts.length === 0) {
- if (data.livemode) {
- logger.warn(
- `subscription.deleted: ${data.id} - no customer products found`
- );
- return;
- }
- }
+ if (activeCusProducts.length === 0) {
+ if (data.livemode) {
+ logger.warn(
+ `subscription.deleted: ${data.id} - no customer products found`,
+ );
+ return;
+ }
+ }
- const subscription = await getFullStripeSub({
- stripeCli,
- stripeId: data.id,
- });
+ const subscription = await getFullStripeSub({
+ stripeCli,
+ stripeId: data.id,
+ });
- const cancellationComment = subscription.cancellation_details?.comment;
- if (
- cancellationComment === "autumn_upgrade" ||
- cancellationComment === "autumn_cancel"
- ) {
- logger.info(
- `sub.deleted: ${subscription.id} from ${cancellationComment}, skipping`
- );
- return;
- }
+ const cancellationComment = subscription.cancellation_details?.comment;
+ if (
+ cancellationComment === "autumn_upgrade" ||
+ cancellationComment === "autumn_cancel"
+ ) {
+ logger.info(
+ `sub.deleted: ${subscription.id} from ${cancellationComment}, skipping`,
+ );
+ return;
+ }
- if (cancellationComment?.includes("trial_canceled")) {
- logger.info(
- `sub.deleted: ${subscription.id} from trial canceled, skipping`
- );
- return;
- }
+ if (cancellationComment?.includes("trial_canceled")) {
+ logger.info(
+ `sub.deleted: ${subscription.id} from trial canceled, skipping`,
+ );
+ return;
+ }
- // Prematurely canceled if cancel_at_period_end is false or cancel_at is more than 20 seconds apart from current_period_end
- let prematurelyCanceled = subIsPrematurelyCanceled(subscription);
+ // Prematurely canceled if cancel_at_period_end is false or cancel_at is more than 20 seconds apart from current_period_end
+ let prematurelyCanceled = subIsPrematurelyCanceled(subscription);
- // const batchUpdate = [];
- for (const cusProduct of activeCusProducts) {
- await handleCusProductDeleted({
- req,
- db,
- stripeCli,
- cusProduct,
- subscription,
- logger,
- prematurelyCanceled,
- });
- }
+ // const batchUpdate = [];
+ for (const cusProduct of activeCusProducts) {
+ await handleCusProductDeleted({
+ req,
+ db,
+ stripeCli,
+ cusProduct,
+ subscription,
+ logger,
+ prematurelyCanceled,
+ });
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts
index 7fc652d80..b75eab067 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts
@@ -2,19 +2,19 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- activateFutureProduct,
- activateDefaultProduct,
- cancelCusProductSubscriptions,
+ activateFutureProduct,
+ activateDefaultProduct,
+ cancelCusProductSubscriptions,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import { cusProductToPrices } from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- FullCusProduct,
- APIVersion,
- BillingType,
- CusProductStatus,
- AttachScenario,
+ FullCusProduct,
+ APIVersion,
+ BillingType,
+ CusProductStatus,
+ AttachScenario,
} from "@autumn/shared";
import Stripe from "stripe";
import { getCusPaymentMethod } from "../../stripeCusUtils.js";
@@ -23,143 +23,143 @@ import { createUsageInvoice } from "@/internal/customers/attach/attachFunctions/
import { CusService } from "@/internal/customers/CusService.js";
export const handleCusProductDeleted = async ({
- req,
- db,
- stripeCli,
- cusProduct,
- subscription,
- logger,
- prematurelyCanceled,
+ req,
+ db,
+ stripeCli,
+ cusProduct,
+ subscription,
+ logger,
+ prematurelyCanceled,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- stripeCli: Stripe;
- cusProduct: FullCusProduct;
- subscription: Stripe.Subscription;
- logger: any;
- prematurelyCanceled: boolean;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ cusProduct: FullCusProduct;
+ subscription: Stripe.Subscription;
+ logger: any;
+ prematurelyCanceled: boolean;
}) => {
- const { org, env } = req;
- const { scheduled_ids } = cusProduct;
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: cusProduct.internal_customer_id,
- orgId: org.id,
- env,
- withEntities: true,
- });
+ const { org, env } = req;
+ const { scheduled_ids } = cusProduct;
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: cusProduct.internal_customer_id,
+ orgId: org.id,
+ env,
+ withEntities: true,
+ });
- const paymentMethod = await getCusPaymentMethod({
- stripeCli,
- stripeId: fullCus.processor?.id,
- });
+ const paymentMethod = await getCusPaymentMethod({
+ stripeCli,
+ stripeId: fullCus.processor?.id,
+ });
- const isV4Usage = cusProduct.api_version === APIVersion.v1_4;
+ const isV4Usage = cusProduct.api_version === APIVersion.v1_4;
- // refer to handleUpgradeFlow.ts, when cancel immediately through API / dashboard, this happens...?
- const isAutumnCancel =
- subscription.cancellation_details?.comment === "autumn_cancel";
+ // refer to handleUpgradeFlow.ts, when cancel immediately through API / dashboard, this happens...?
+ const isAutumnCancel =
+ subscription.cancellation_details?.comment === "autumn_cancel";
- if ((cusProduct.internal_entity_id || isV4Usage) && !isAutumnCancel) {
- const usagePrices = cusProductToPrices({
- cusProduct,
- billingType: BillingType.UsageInArrear,
- });
+ if ((cusProduct.internal_entity_id || isV4Usage) && !isAutumnCancel) {
+ const usagePrices = cusProductToPrices({
+ cusProduct,
+ billingType: BillingType.UsageInArrear,
+ });
- if (usagePrices.length > 0) {
- logger.info(
- `sub.deleted, submitting usage for ${fullCus.id}, ${cusProduct.product.name}`
- );
+ if (usagePrices.length > 0) {
+ logger.info(
+ `sub.deleted, submitting usage for ${fullCus.id}, ${cusProduct.product.name}`,
+ );
- await createUsageInvoice({
- db,
- attachParams: webhookToAttachParams({
- req,
- stripeCli,
- paymentMethod,
- cusProduct,
- fullCus,
- }),
- cusProduct,
- sub: subscription,
- logger,
- });
- }
- }
+ await createUsageInvoice({
+ db,
+ attachParams: webhookToAttachParams({
+ req,
+ stripeCli,
+ paymentMethod,
+ cusProduct,
+ fullCus,
+ }),
+ cusProduct,
+ sub: subscription,
+ logger,
+ });
+ }
+ }
- if (scheduled_ids && scheduled_ids.length > 0 && !prematurelyCanceled) {
- logger.info(
- `sub.deleted: removing sub_id from cus product ${cusProduct.id}`
- );
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- subscription_ids: cusProduct.subscription_ids?.filter(
- (id) => id !== subscription.id
- ),
- },
- });
+ if (scheduled_ids && scheduled_ids.length > 0 && !prematurelyCanceled) {
+ logger.info(
+ `sub.deleted: removing sub_id from cus product ${cusProduct.id}`,
+ );
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ subscription_ids: cusProduct.subscription_ids?.filter(
+ (id) => id !== subscription.id,
+ ),
+ },
+ });
- return;
- }
+ return;
+ }
- logger.info(`sub.deleted: expiring cus product ${cusProduct.id}`);
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- ended_at: subscription.ended_at ? subscription.ended_at * 1000 : null,
- },
- });
+ logger.info(`sub.deleted: expiring cus product ${cusProduct.id}`);
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ ended_at: subscription.ended_at ? subscription.ended_at * 1000 : null,
+ },
+ });
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: cusProduct.internal_customer_id,
- org,
- env,
- customerId: null,
- scenario: AttachScenario.Expired,
- cusProduct,
- logger,
- });
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: cusProduct.internal_customer_id,
+ org,
+ env,
+ customerId: null,
+ scenario: AttachScenario.Expired,
+ cusProduct,
+ logger,
+ });
- if (cusProduct.product.is_add_on) return;
+ if (cusProduct.product.is_add_on) return;
- const activatedFuture = await activateFutureProduct({
- req,
- cusProduct,
- });
+ const activatedFuture = await activateFutureProduct({
+ req,
+ cusProduct,
+ });
- if (activatedFuture) {
- logger.info(`✅ sub.deleted: activated scheduled product`);
- return;
- }
+ if (activatedFuture) {
+ logger.info(`✅ sub.deleted: activated scheduled product`);
+ return;
+ }
- let cusProducts = await CusProductService.list({
- db,
- internalCustomerId: cusProduct.customer!.internal_id,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- });
+ let cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId: cusProduct.customer!.internal_id,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ });
- let { curMainProduct } = getExistingCusProducts({
- product: cusProduct.product,
- cusProducts,
- });
+ let { curMainProduct } = getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts,
+ });
- await activateDefaultProduct({
- req,
- productGroup: cusProduct.product.group,
- fullCus,
- curCusProduct: curMainProduct || undefined,
- });
+ await activateDefaultProduct({
+ req,
+ productGroup: cusProduct.product.group,
+ fullCus,
+ curCusProduct: curMainProduct || undefined,
+ });
- // await cancelCusProductSubscriptions({
- // cusProduct,
- // org,
- // env,
- // excludeIds: [subscription.id],
- // logger,
- // });
+ // await cancelCusProductSubscriptions({
+ // cusProduct,
+ // org,
+ // env,
+ // excludeIds: [subscription.id],
+ // logger,
+ // });
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts
index cfcbafc29..6454c5226 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts
@@ -7,79 +7,79 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
export const handleSubscriptionScheduleCanceled = async ({
- db,
- schedule,
- env,
- org,
- logger,
+ db,
+ schedule,
+ env,
+ org,
+ logger,
}: {
- db: DrizzleCli;
- schedule: Stripe.SubscriptionSchedule;
- org: Organization;
- env: AppEnv;
- logger: any;
+ db: DrizzleCli;
+ schedule: Stripe.SubscriptionSchedule;
+ org: Organization;
+ env: AppEnv;
+ logger: any;
}) => {
- const cusProductsOnSchedule = await CusProductService.getByScheduleId({
- db,
- scheduleId: schedule.id,
- orgId: org.id,
- env,
- });
+ const cusProductsOnSchedule = await CusProductService.getByScheduleId({
+ db,
+ scheduleId: schedule.id,
+ orgId: org.id,
+ env,
+ });
- if (cusProductsOnSchedule.length === 0) return;
+ if (cusProductsOnSchedule.length === 0) return;
- for (const cusProduct of cusProductsOnSchedule) {
- const stripeCli = createStripeCli({ org, env });
+ for (const cusProduct of cusProductsOnSchedule) {
+ const stripeCli = createStripeCli({ org, env });
- // if (cusProduct.status === CusProductStatus.Scheduled) {
- // // let otherScheduledIds = cusProduct.scheduled_ids?.filter(
- // // (id: string) => id !== schedule.id
- // // );
+ // if (cusProduct.status === CusProductStatus.Scheduled) {
+ // // let otherScheduledIds = cusProduct.scheduled_ids?.filter(
+ // // (id: string) => id !== schedule.id
+ // // );
- // // for (const id of otherScheduledIds || []) {
- // // try {
- // // await stripeCli.subscriptionSchedules.cancel(id);
- // // console.log(" - Cancelled scheduled id", id);
- // // } catch (error) {
- // // console.error("Failed to cancel subscription schedule:", id, error);
- // // }
- // // }
+ // // for (const id of otherScheduledIds || []) {
+ // // try {
+ // // await stripeCli.subscriptionSchedules.cancel(id);
+ // // console.log(" - Cancelled scheduled id", id);
+ // // } catch (error) {
+ // // console.error("Failed to cancel subscription schedule:", id, error);
+ // // }
+ // // }
- // await CusProductService.delete({
- // db,
- // cusProductId: cusProduct.id,
- // });
- // } else {
- // // Here -> Should do something different, maybe... reactivate future product?
- // await CusProductService.update({
- // db,
- // cusProductId: cusProduct.id,
- // updates: {
- // scheduled_ids: cusProduct.scheduled_ids?.filter(
- // (id: string) => id !== schedule.id
- // ),
- // },
- // });
- // }
- }
+ // await CusProductService.delete({
+ // db,
+ // cusProductId: cusProduct.id,
+ // });
+ // } else {
+ // // Here -> Should do something different, maybe... reactivate future product?
+ // await CusProductService.update({
+ // db,
+ // cusProductId: cusProduct.id,
+ // updates: {
+ // scheduled_ids: cusProduct.scheduled_ids?.filter(
+ // (id: string) => id !== schedule.id
+ // ),
+ // },
+ // });
+ // }
+ }
- // // Delete from subscriptions
- // try {
- // let autumnSub = await SubService.getFromScheduleId({
- // db,
- // scheduleId: schedule.id,
- // });
+ // // Delete from subscriptions
+ // try {
+ // let autumnSub = await SubService.getFromScheduleId({
+ // db,
+ // scheduleId: schedule.id,
+ // });
- // if (autumnSub && !autumnSub.stripe_id) {
- // await SubService.deleteFromScheduleId({
- // db,
- // scheduleId: schedule.id,
- // });
- // }
- // } catch (error) {
- // logger.error(
- // `handleSubScheduleCanceled: failed to delete from subscriptions table`,
- // error
- // );
- // }
+ // if (autumnSub && !autumnSub.stripe_id) {
+ // await SubService.deleteFromScheduleId({
+ // db,
+ // scheduleId: schedule.id,
+ // });
+ // }
+ // } catch (error) {
+ // logger.error(
+ // `handleSubScheduleCanceled: failed to delete from subscriptions table`,
+ // error
+ // );
+ // }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
index adae748ce..55813a663 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts
@@ -1,8 +1,8 @@
import {
- AppEnv,
- CollectionMethod,
- CusProductStatus,
- Organization,
+ AppEnv,
+ CollectionMethod,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { createStripeCli } from "../utils.js";
@@ -12,185 +12,185 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- handleSubCanceled,
- isSubCanceled,
+ handleSubCanceled,
+ isSubCanceled,
} from "./handleSubUpdated/handleSubCanceled.js";
import { handleSubRenewed } from "./handleSubUpdated/handleSubRenewed.js";
import { handleSchedulePhaseCompleted } from "./handleSubUpdated/handleSchedulePhaseCompleted.js";
export const handleSubscriptionUpdated = async ({
- req,
- db,
- org,
- subscription,
- previousAttributes,
- env,
- logger,
+ req,
+ db,
+ org,
+ subscription,
+ previousAttributes,
+ env,
+ logger,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- subscription: any;
- previousAttributes: any;
- logger: any;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ subscription: any;
+ previousAttributes: any;
+ logger: any;
}) => {
- // handle scheduled updated
- await handleSchedulePhaseCompleted({
- req,
- subObject: subscription,
- prevAttributes: previousAttributes,
- });
+ // handle scheduled updated
+ await handleSchedulePhaseCompleted({
+ req,
+ subObject: subscription,
+ prevAttributes: previousAttributes,
+ });
- // Get cus products by stripe sub id
- const cusProducts = await CusProductService.getByStripeSubId({
- db,
- stripeSubId: subscription.id,
- orgId: org.id,
- env,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- });
+ // Get cus products by stripe sub id
+ const cusProducts = await CusProductService.getByStripeSubId({
+ db,
+ stripeSubId: subscription.id,
+ orgId: org.id,
+ env,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ });
- if (cusProducts.length === 0) return;
+ if (cusProducts.length === 0) return;
- // Handle syncing status
- let stripeCli = createStripeCli({
- org,
- env,
- });
- let fullSub = await stripeCli.subscriptions.retrieve(subscription.id);
+ // Handle syncing status
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ });
+ let fullSub = await stripeCli.subscriptions.retrieve(subscription.id);
- let subStatusMap: {
- [key: string]: CusProductStatus;
- } = {
- trialing: CusProductStatus.Active,
- active: CusProductStatus.Active,
- past_due: CusProductStatus.PastDue,
- };
+ let subStatusMap: {
+ [key: string]: CusProductStatus;
+ } = {
+ trialing: CusProductStatus.Active,
+ active: CusProductStatus.Active,
+ past_due: CusProductStatus.PastDue,
+ };
- // 1. Fetch subscription
- const { canceled, canceledAt } = isSubCanceled({
- previousAttributes,
- sub: fullSub,
- });
+ // 1. Fetch subscription
+ const { canceled, canceledAt } = isSubCanceled({
+ previousAttributes,
+ sub: fullSub,
+ });
- const updatedCusProducts = await CusProductService.updateByStripeSubId({
- db,
- stripeSubId: subscription.id,
- updates: {
- status: subStatusMap[subscription.status] || CusProductStatus.Unknown,
- collection_method: fullSub.collection_method as CollectionMethod,
- // canceled_at: canceled ? canceledAt : null,
- // trial_ends_at:
- // previousAttributes.status === "trialing" &&
- // subscription.status === "active"
- // ? null
- // : undefined,
- },
- });
+ const updatedCusProducts = await CusProductService.updateByStripeSubId({
+ db,
+ stripeSubId: subscription.id,
+ updates: {
+ status: subStatusMap[subscription.status] || CusProductStatus.Unknown,
+ collection_method: fullSub.collection_method as CollectionMethod,
+ // canceled_at: canceled ? canceledAt : null,
+ // trial_ends_at:
+ // previousAttributes.status === "trialing" &&
+ // subscription.status === "active"
+ // ? null
+ // : undefined,
+ },
+ });
- // 2. Update canceled & canceled_at IF sub has no schedule...?
+ // 2. Update canceled & canceled_at IF sub has no schedule...?
- if (updatedCusProducts.length > 0) {
- logger.info(
- `✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? "" : "s"} (${updatedCusProducts.map((cp) => cp.id).join(", ")}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ""}`
- );
- }
+ if (updatedCusProducts.length > 0) {
+ logger.info(
+ `✅ Updated ${updatedCusProducts.length} customer product${updatedCusProducts.length === 1 ? "" : "s"} (${updatedCusProducts.map((cp) => cp.id).join(", ")}) - Status: ${updatedCusProducts[0].status}${updatedCusProducts[0].canceled_at ? `, Canceled: ${new Date(updatedCusProducts[0].canceled_at).toISOString()}` : ""}`,
+ );
+ }
- await handleSubCanceled({
- req,
- previousAttributes,
- sub: fullSub,
- updatedCusProducts,
- stripeCli,
- org,
- });
+ await handleSubCanceled({
+ req,
+ previousAttributes,
+ sub: fullSub,
+ updatedCusProducts,
+ stripeCli,
+ org,
+ });
- await handleSubRenewed({
- req,
- prevAttributes: previousAttributes,
- sub: fullSub,
- updatedCusProducts,
- });
+ await handleSubRenewed({
+ req,
+ prevAttributes: previousAttributes,
+ sub: fullSub,
+ updatedCusProducts,
+ });
- try {
- await SubService.updateFromStripe({
- db,
- stripeSub: fullSub,
- });
- } catch (error) {
- logger.warn(
- `Failed to update sub from stripe. Stripe sub ID: ${subscription.id}, org: ${org.slug}, env: ${env}`,
- error
- );
- }
+ try {
+ await SubService.updateFromStripe({
+ db,
+ stripeSub: fullSub,
+ });
+ } catch (error) {
+ logger.warn(
+ `Failed to update sub from stripe. Stripe sub ID: ${subscription.id}, org: ${org.slug}, env: ${env}`,
+ error,
+ );
+ }
- // Cancel subscription immediately
- if (subscription.status === "past_due" && org.config.cancel_on_past_due) {
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ // Cancel subscription immediately
+ if (subscription.status === "past_due" && org.config.cancel_on_past_due) {
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- const latestInvoice = await stripeCli.invoices.retrieve(
- subscription.latest_invoice
- );
+ const latestInvoice = await stripeCli.invoices.retrieve(
+ subscription.latest_invoice,
+ );
- logger.info(
- `Latest invoice billing reason: ${latestInvoice.billing_reason}`
- );
- logger.info(`Latest invoice status: ${latestInvoice.status}`);
+ logger.info(
+ `Latest invoice billing reason: ${latestInvoice.billing_reason}`,
+ );
+ logger.info(`Latest invoice status: ${latestInvoice.status}`);
- if (
- latestInvoice.status !== "open" ||
- latestInvoice.billing_reason !== "subscription_cycle"
- ) {
- logger.info(
- "sub.updated, latest invoice isn't open or billing reason isn't subscription_update, past_due not forcing cancel",
- {
- data: {
- subscriptionId: subscription.id,
- stripeSubId: subscription.id,
- latestInvoiceId: subscription.latest_invoice,
- latestInvoiceStatus: latestInvoice.status,
- latestInvoiceBillingReason: latestInvoice.billing_reason,
- },
- }
- );
- return;
- }
+ if (
+ latestInvoice.status !== "open" ||
+ latestInvoice.billing_reason !== "subscription_cycle"
+ ) {
+ logger.info(
+ "sub.updated, latest invoice isn't open or billing reason isn't subscription_update, past_due not forcing cancel",
+ {
+ data: {
+ subscriptionId: subscription.id,
+ stripeSubId: subscription.id,
+ latestInvoiceId: subscription.latest_invoice,
+ latestInvoiceStatus: latestInvoice.status,
+ latestInvoiceBillingReason: latestInvoice.billing_reason,
+ },
+ },
+ );
+ return;
+ }
- try {
- logger.info(
- `sub.updated (past_due), cancelling subscription: ${subscription.id}`,
- {
- data: {
- subscriptionId: subscription.id,
- stripeSubId: subscription.id,
- latestInvoiceId: subscription.latest_invoice,
- latestInvoiceStatus: latestInvoice.status,
- latestInvoiceBillingReason: latestInvoice.billing_reason,
- },
- }
- );
- await stripeCli.subscriptions.cancel(subscription.id);
- await stripeCli.invoices.voidInvoice(subscription.latest_invoice);
- } catch (error: any) {
- logger.error(
- `subscription.updated: error cancelling / voiding: ${error.message}`,
- {
- data: {
- subscriptionId: subscription.id,
- stripeSubId: subscription.id,
- error: error.message,
- latestInvoiceId: subscription.latest_invoice,
- latestInvoiceStatus: latestInvoice.status,
- latestInvoiceBillingReason: latestInvoice.billing_reason,
- },
- }
- );
- }
- }
+ try {
+ logger.info(
+ `sub.updated (past_due), cancelling subscription: ${subscription.id}`,
+ {
+ data: {
+ subscriptionId: subscription.id,
+ stripeSubId: subscription.id,
+ latestInvoiceId: subscription.latest_invoice,
+ latestInvoiceStatus: latestInvoice.status,
+ latestInvoiceBillingReason: latestInvoice.billing_reason,
+ },
+ },
+ );
+ await stripeCli.subscriptions.cancel(subscription.id);
+ await stripeCli.invoices.voidInvoice(subscription.latest_invoice);
+ } catch (error: any) {
+ logger.error(
+ `subscription.updated: error cancelling / voiding: ${error.message}`,
+ {
+ data: {
+ subscriptionId: subscription.id,
+ stripeSubId: subscription.id,
+ error: error.message,
+ latestInvoiceId: subscription.latest_invoice,
+ latestInvoiceStatus: latestInvoice.status,
+ latestInvoiceBillingReason: latestInvoice.billing_reason,
+ },
+ },
+ );
+ }
+ }
};
// server-1 | subscription.updated, previous attributes: { status: 'active' }
diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts
index b16e73320..e180ba223 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts
@@ -12,124 +12,124 @@ import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
export const handleSchedulePhaseCompleted = async ({
- req,
- subObject,
- prevAttributes,
+ req,
+ subObject,
+ prevAttributes,
}: {
- req: ExtendedRequest;
- subObject: Stripe.Subscription;
- prevAttributes: any;
+ req: ExtendedRequest;
+ subObject: Stripe.Subscription;
+ prevAttributes: any;
}) => {
- const { db, org, env, logger } = req;
- const phasePossiblyChanged =
- notNullish(prevAttributes?.items) && notNullish(subObject.schedule);
+ const { db, org, env, logger } = req;
+ const phasePossiblyChanged =
+ notNullish(prevAttributes?.items) && notNullish(subObject.schedule);
- if (!phasePossiblyChanged) return;
+ if (!phasePossiblyChanged) return;
- const stripeCli = createStripeCli({ org, env });
- const schedule = await stripeCli.subscriptionSchedules.retrieve(
- subObject.schedule as string,
- {
- expand: ["customer"],
- }
- );
+ const stripeCli = createStripeCli({ org, env });
+ const schedule = await stripeCli.subscriptionSchedules.retrieve(
+ subObject.schedule as string,
+ {
+ expand: ["customer"],
+ },
+ );
- const cusProducts = await CusProductService.getByScheduleId({
- db,
- scheduleId: schedule.id,
- orgId: org.id,
- env,
- });
+ const cusProducts = await CusProductService.getByScheduleId({
+ db,
+ scheduleId: schedule.id,
+ orgId: org.id,
+ env,
+ });
- const now = await getStripeNow({
- stripeCli,
- stripeCus: schedule.customer as Stripe.Customer,
- });
+ const now = await getStripeNow({
+ stripeCli,
+ stripeCus: schedule.customer as Stripe.Customer,
+ });
- for (const cusProduct of cusProducts) {
- const shouldExpire =
- cusProduct.canceled && cusProduct.ended_at && now >= cusProduct.ended_at;
+ for (const cusProduct of cusProducts) {
+ const shouldExpire =
+ cusProduct.canceled && cusProduct.ended_at && now >= cusProduct.ended_at;
- if (shouldExpire) {
- logger.info(
- `Expiring cus product: ${cusProduct.product.name} (entity ID: ${cusProduct.entity_id})`
- );
- await CusProductService.update({
- db: req.db,
- cusProductId: cusProduct.id,
- updates: { status: CusProductStatus.Expired },
- });
+ if (shouldExpire) {
+ logger.info(
+ `Expiring cus product: ${cusProduct.product.name} (entity ID: ${cusProduct.entity_id})`,
+ );
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: cusProduct.id,
+ updates: { status: CusProductStatus.Expired },
+ });
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: cusProduct.internal_customer_id,
- org,
- env,
- customerId: null,
- scenario: AttachScenario.Expired,
- cusProduct: cusProduct,
- logger,
- });
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: cusProduct.internal_customer_id,
+ org,
+ env,
+ customerId: null,
+ scenario: AttachScenario.Expired,
+ cusProduct: cusProduct,
+ logger,
+ });
- // ACTIVATING FUTURE PRODUCT
- const futureCusProduct = await activateFutureProduct({
- req,
- cusProduct,
- });
+ // ACTIVATING FUTURE PRODUCT
+ const futureCusProduct = await activateFutureProduct({
+ req,
+ cusProduct,
+ });
- if (futureCusProduct) {
- const fullFutureProduct = cusProductToProduct({
- cusProduct: futureCusProduct,
- });
+ if (futureCusProduct) {
+ const fullFutureProduct = cusProductToProduct({
+ cusProduct: futureCusProduct,
+ });
- if (
- !isFreeProduct(fullFutureProduct.prices) &&
- !isOneOff(fullFutureProduct.prices)
- ) {
- await CusProductService.update({
- db: req.db,
- cusProductId: futureCusProduct.id,
- updates: {
- subscription_ids: [subObject.id],
- scheduled_ids: [schedule.id],
- },
- });
- }
- }
+ if (
+ !isFreeProduct(fullFutureProduct.prices) &&
+ !isOneOff(fullFutureProduct.prices)
+ ) {
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: futureCusProduct.id,
+ updates: {
+ subscription_ids: [subObject.id],
+ scheduled_ids: [schedule.id],
+ },
+ });
+ }
+ }
- // Maybe activate default product?
- }
- }
+ // Maybe activate default product?
+ }
+ }
- const currentPhase = schedule.phases.findIndex(
- (phase) =>
- phase.start_date <= Math.floor(now / 1000) &&
- (phase.end_date ? phase.end_date > Math.floor(now / 1000) : true)
- );
+ const currentPhase = schedule.phases.findIndex(
+ (phase) =>
+ phase.start_date <= Math.floor(now / 1000) &&
+ (phase.end_date ? phase.end_date > Math.floor(now / 1000) : true),
+ );
- if (
- currentPhase === schedule.phases.length - 1 &&
- schedule.status !== "released"
- ) {
- try {
- // Last phase, cancel schedule
- await stripeCli.subscriptionSchedules.release(schedule.id);
- await CusProductService.updateByStripeScheduledId({
- db: req.db,
- stripeScheduledId: schedule.id,
- updates: {
- scheduled_ids: [],
- },
- });
- } catch (error: any) {
- logger.error(
- `schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`
- );
- }
- }
- // const currentPhase = schedule.phases.find(
- // (phase) =>
- // phase.start_date <= Math.floor(now / 1000) &&
- // phase.end_date > Math.floor(now / 1000)
- // );
+ if (
+ currentPhase === schedule.phases.length - 1 &&
+ schedule.status !== "released"
+ ) {
+ try {
+ // Last phase, cancel schedule
+ await stripeCli.subscriptionSchedules.release(schedule.id);
+ await CusProductService.updateByStripeScheduledId({
+ db: req.db,
+ stripeScheduledId: schedule.id,
+ updates: {
+ scheduled_ids: [],
+ },
+ });
+ } catch (error: any) {
+ logger.error(
+ `schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
+ );
+ }
+ }
+ // const currentPhase = schedule.phases.find(
+ // (phase) =>
+ // phase.start_date <= Math.floor(now / 1000) &&
+ // phase.end_date > Math.floor(now / 1000)
+ // );
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts
index 82c66fe5d..da359c343 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts
@@ -9,205 +9,205 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { productToInsertParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js";
import {
- getLatestPeriodEnd,
- subToPeriodStartEnd,
+ getLatestPeriodEnd,
+ subToPeriodStartEnd,
} from "../../stripeSubUtils/convertSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const isSubCanceled = ({
- previousAttributes,
- sub,
+ previousAttributes,
+ sub,
}: {
- previousAttributes: any;
- sub: Stripe.Subscription;
+ previousAttributes: any;
+ sub: Stripe.Subscription;
}) => {
- // console.log("Previous attributes:", previousAttributes);
- // console.log("Cancel at:", sub.cancel_at);
- // console.log("Cancel at period end:", sub.cancel_at_period_end);
- // console.log("Canceled at:", sub.canceled_at);
+ // console.log("Previous attributes:", previousAttributes);
+ // console.log("Cancel at:", sub.cancel_at);
+ // console.log("Cancel at period end:", sub.cancel_at_period_end);
+ // console.log("Canceled at:", sub.canceled_at);
- if (!sub.cancel_at && !sub.cancel_at_period_end) {
- return {
- canceled: false,
- canceledAt: null,
- };
- }
- const cancelAtPreviousEnd =
- !previousAttributes.cancel_at_period_end && sub.cancel_at_period_end;
+ if (!sub.cancel_at && !sub.cancel_at_period_end) {
+ return {
+ canceled: false,
+ canceledAt: null,
+ };
+ }
+ const cancelAtPreviousEnd =
+ !previousAttributes.cancel_at_period_end && sub.cancel_at_period_end;
- const cancelAt = nullish(previousAttributes.cancel_at) && sub.cancel_at;
- const canceledAt = nullish(previousAttributes.canceled_at) && sub.canceled_at;
+ const cancelAt = nullish(previousAttributes.cancel_at) && sub.cancel_at;
+ const canceledAt = nullish(previousAttributes.canceled_at) && sub.canceled_at;
- return {
- canceled: cancelAtPreviousEnd || cancelAt || canceledAt,
- canceledAt: sub.canceled_at ? sub.canceled_at * 1000 : Date.now(),
- };
+ return {
+ canceled: cancelAtPreviousEnd || cancelAt || canceledAt,
+ canceledAt: sub.canceled_at ? sub.canceled_at * 1000 : Date.now(),
+ };
};
const updateCusProductCanceled = async ({
- db,
- sub,
- canceledAt,
- logger,
+ db,
+ sub,
+ canceledAt,
+ logger,
}: {
- db: DrizzleCli;
- sub: Stripe.Subscription;
- canceledAt?: number | null;
- logger: any;
+ db: DrizzleCli;
+ sub: Stripe.Subscription;
+ canceledAt?: number | null;
+ logger: any;
}) => {
- // 1. Check if sub has schedule
- if (sub.schedule) {
- return;
- }
+ // 1. Check if sub has schedule
+ if (sub.schedule) {
+ return;
+ }
- logger.info(
- `Updating cus products for sub ${sub.id} to canceled | canceled_at: ${canceledAt}`
- );
+ logger.info(
+ `Updating cus products for sub ${sub.id} to canceled | canceled_at: ${canceledAt}`,
+ );
- await CusProductService.updateByStripeSubId({
- db,
- stripeSubId: sub.id,
- updates: { canceled_at: canceledAt || Date.now(), canceled: true },
- });
+ await CusProductService.updateByStripeSubId({
+ db,
+ stripeSubId: sub.id,
+ updates: { canceled_at: canceledAt || Date.now(), canceled: true },
+ });
};
export const handleSubCanceled = async ({
- req,
- previousAttributes,
- org,
- sub,
- updatedCusProducts,
- stripeCli,
+ req,
+ previousAttributes,
+ org,
+ sub,
+ updatedCusProducts,
+ stripeCli,
}: {
- req: ExtendedRequest;
- previousAttributes: any;
- sub: Stripe.Subscription;
- org: Organization;
- updatedCusProducts: FullCusProduct[];
- stripeCli: Stripe;
+ req: ExtendedRequest;
+ previousAttributes: any;
+ sub: Stripe.Subscription;
+ org: Organization;
+ updatedCusProducts: FullCusProduct[];
+ stripeCli: Stripe;
}) => {
- // let isCanceled =
- // nullish(previousAttributes?.canceled_at) && !nullish(sub.canceled_at);
- const { canceled, canceledAt } = isSubCanceled({
- previousAttributes,
- sub,
- });
+ // let isCanceled =
+ // nullish(previousAttributes?.canceled_at) && !nullish(sub.canceled_at);
+ const { canceled, canceledAt } = isSubCanceled({
+ previousAttributes,
+ sub,
+ });
- let isAutumnDowngrade =
- sub.cancellation_details?.comment?.includes("autumn_downgrade") ||
- sub.cancellation_details?.comment?.includes("autumn_cancel");
+ let isAutumnDowngrade =
+ sub.cancellation_details?.comment?.includes("autumn_downgrade") ||
+ sub.cancellation_details?.comment?.includes("autumn_cancel");
- const canceledFromPortal = canceled && !isAutumnDowngrade;
+ const canceledFromPortal = canceled && !isAutumnDowngrade;
- const { db, env, logtail: logger } = req;
+ const { db, env, logtail: logger } = req;
- if (!canceledFromPortal || updatedCusProducts.length == 0) return;
+ if (!canceledFromPortal || updatedCusProducts.length == 0) return;
- await updateCusProductCanceled({
- db,
- sub,
- canceledAt,
- logger,
- });
+ await updateCusProductCanceled({
+ db,
+ sub,
+ canceledAt,
+ logger,
+ });
- if (!org.config.sync_status) return;
+ if (!org.config.sync_status) return;
- // 2. Update canceled & canceled_at IF sub has no schedule...?
+ // 2. Update canceled & canceled_at IF sub has no schedule...?
- // await CusProductService.updateByStripeSubId({
- // db,
- // stripeSubId: sub.id,
- // updates: {
- // canceled_at: canceled ? canceledAt : null,
- // canceled: true,
- // },
- // });
+ // await CusProductService.updateByStripeSubId({
+ // db,
+ // stripeSubId: sub.id,
+ // updates: {
+ // canceled_at: canceled ? canceledAt : null,
+ // canceled: true,
+ // },
+ // });
- let allDefaultProducts = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ let allDefaultProducts = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- let fullCus = await CusService.getFull({
- db,
- idOrInternalId: updatedCusProducts[0].customer!.id!,
- orgId: org.id,
- env,
- withEntities: true,
- inStatuses: [CusProductStatus.Scheduled],
- });
+ let fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: updatedCusProducts[0].customer!.id!,
+ orgId: org.id,
+ env,
+ withEntities: true,
+ inStatuses: [CusProductStatus.Scheduled],
+ });
- let cusProducts = fullCus.customer_products;
- let entities = fullCus.entities;
+ let cusProducts = fullCus.customer_products;
+ let entities = fullCus.entities;
- let defaultProducts = allDefaultProducts.filter((p) =>
- updatedCusProducts.some(
- (cp: FullCusProduct) =>
- cp.product.group == p.group && nullish(cp.internal_entity_id)
- )
- );
+ let defaultProducts = allDefaultProducts.filter((p) =>
+ updatedCusProducts.some(
+ (cp: FullCusProduct) =>
+ cp.product.group == p.group && nullish(cp.internal_entity_id),
+ ),
+ );
- if (defaultProducts.length == 0) return;
+ if (defaultProducts.length == 0) return;
- if (defaultProducts.length > 0) {
- const { end } = subToPeriodStartEnd({ sub });
- const productNames = defaultProducts.map((p) => p.name).join(", ");
- const periodEnd = formatUnixToDateTime(end * 1000);
- logger.info(
- `subscription.updated: canceled -> attempting to schedule default products: ${productNames}, period end: ${periodEnd}`
- );
- }
+ if (defaultProducts.length > 0) {
+ const { end } = subToPeriodStartEnd({ sub });
+ const productNames = defaultProducts.map((p) => p.name).join(", ");
+ const periodEnd = formatUnixToDateTime(end * 1000);
+ logger.info(
+ `subscription.updated: canceled -> attempting to schedule default products: ${productNames}, period end: ${periodEnd}`,
+ );
+ }
- let scheduledCusProducts: FullCusProduct[] = [];
- for (let product of defaultProducts) {
- let alreadyScheduled = cusProducts.some(
- (cp: FullCusProduct) => cp.product.group == product.group
- );
+ let scheduledCusProducts: FullCusProduct[] = [];
+ for (let product of defaultProducts) {
+ let alreadyScheduled = cusProducts.some(
+ (cp: FullCusProduct) => cp.product.group == product.group,
+ );
- if (alreadyScheduled) {
- continue;
- }
+ if (alreadyScheduled) {
+ continue;
+ }
- let insertParams = productToInsertParams({
- req,
- fullCus,
- newProduct: product,
- entities,
- });
+ let insertParams = productToInsertParams({
+ req,
+ fullCus,
+ newProduct: product,
+ entities,
+ });
- const end = getLatestPeriodEnd({ sub });
- let fullCusProduct = await createFullCusProduct({
- db,
- attachParams: insertParams,
- startsAt: end * 1000,
- sendWebhook: false,
- logger,
- });
+ const end = getLatestPeriodEnd({ sub });
+ let fullCusProduct = await createFullCusProduct({
+ db,
+ attachParams: insertParams,
+ startsAt: end * 1000,
+ sendWebhook: false,
+ logger,
+ });
- if (fullCusProduct) {
- scheduledCusProducts.push(fullCusProduct);
- }
- }
+ if (fullCusProduct) {
+ scheduledCusProducts.push(fullCusProduct);
+ }
+ }
- for (let cusProd of updatedCusProducts) {
- console.log("Sending webhook for canceled product: ", cusProd.product.id);
+ for (let cusProd of updatedCusProducts) {
+ console.log("Sending webhook for canceled product: ", cusProd.product.id);
- try {
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: cusProd.internal_customer_id,
- org,
- env,
- customerId: null,
- logger,
- scenario: AttachScenario.Cancel,
- cusProduct: cusProd,
- scheduledCusProduct: scheduledCusProducts.find(
- (cp) => cp.product.group === cusProd.product.group
- ),
- });
- } catch (error) {}
- }
+ try {
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: cusProd.internal_customer_id,
+ org,
+ env,
+ customerId: null,
+ logger,
+ scenario: AttachScenario.Cancel,
+ cusProduct: cusProd,
+ scheduledCusProduct: scheduledCusProducts.find(
+ (cp) => cp.product.group === cusProd.product.group,
+ ),
+ });
+ } catch (error) {}
+ }
};
diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts
index 0def51dbe..d4dcfcd2e 100644
--- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts
+++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts
@@ -9,126 +9,126 @@ import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeU
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js";
const isSubRenewed = ({
- previousAttributes,
- sub,
+ previousAttributes,
+ sub,
}: {
- previousAttributes: any;
- sub: Stripe.Subscription;
+ previousAttributes: any;
+ sub: Stripe.Subscription;
}) => {
- // 1. If previously canceled
- const uncanceledAtPreviousEnd =
- previousAttributes.cancel_at_period_end && !sub.cancel_at_period_end;
+ // 1. If previously canceled
+ const uncanceledAtPreviousEnd =
+ previousAttributes.cancel_at_period_end && !sub.cancel_at_period_end;
- const uncancelAt =
- notNullish(previousAttributes.cancel_at) && nullish(sub.cancel_at);
+ const uncancelAt =
+ notNullish(previousAttributes.cancel_at) && nullish(sub.cancel_at);
- const uncanceledAt =
- notNullish(previousAttributes.canceled_at) && sub.canceled_at;
+ const uncanceledAt =
+ notNullish(previousAttributes.canceled_at) && sub.canceled_at;
- return {
- renewed: uncanceledAtPreviousEnd || uncancelAt || uncanceledAt,
- renewedAt: Date.now(),
- };
+ return {
+ renewed: uncanceledAtPreviousEnd || uncancelAt || uncanceledAt,
+ renewedAt: Date.now(),
+ };
};
const updateCusProductRenewed = async ({
- db,
- sub,
+ db,
+ sub,
}: {
- db: DrizzleCli;
- sub: Stripe.Subscription;
+ db: DrizzleCli;
+ sub: Stripe.Subscription;
}) => {
- if (sub.schedule) {
- return;
- }
+ if (sub.schedule) {
+ return;
+ }
- await CusProductService.updateByStripeSubId({
- db,
- stripeSubId: sub.id,
- updates: { canceled_at: null, canceled: false },
- });
+ await CusProductService.updateByStripeSubId({
+ db,
+ stripeSubId: sub.id,
+ updates: { canceled_at: null, canceled: false },
+ });
- return;
+ return;
};
export const handleSubRenewed = async ({
- req,
- prevAttributes,
- sub,
- updatedCusProducts,
+ req,
+ prevAttributes,
+ sub,
+ updatedCusProducts,
}: {
- req: ExtendedRequest;
- prevAttributes: any;
- sub: Stripe.Subscription;
- updatedCusProducts: FullCusProduct[];
+ req: ExtendedRequest;
+ prevAttributes: any;
+ sub: Stripe.Subscription;
+ updatedCusProducts: FullCusProduct[];
}) => {
- const { db, org, env, logtail: logger } = req;
+ const { db, org, env, logtail: logger } = req;
- const { renewed } = isSubRenewed({
- previousAttributes: prevAttributes,
- sub,
- });
+ const { renewed } = isSubRenewed({
+ previousAttributes: prevAttributes,
+ sub,
+ });
- if (!renewed || updatedCusProducts.length == 0) return;
+ if (!renewed || updatedCusProducts.length == 0) return;
- const subScenario = await getSubScenarioFromCache({ subId: sub.id });
- console.log(`Renewed: ${renewed}, subScenario: ${subScenario}`);
- if (subScenario === AttachScenario.Renew) return;
+ const subScenario = await getSubScenarioFromCache({ subId: sub.id });
+ console.log(`Renewed: ${renewed}, subScenario: ${subScenario}`);
+ if (subScenario === AttachScenario.Renew) return;
- const customer = updatedCusProducts[0].customer;
- let cusProducts = await CusProductService.list({
- db,
- internalCustomerId: customer!.internal_id,
- });
+ const customer = updatedCusProducts[0].customer;
+ let cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId: customer!.internal_id,
+ });
- console.log(`handling sub.renewed!`);
+ console.log(`handling sub.renewed!`);
- if (isMultiProductSub({ sub, cusProducts }) || sub.schedule) return;
+ if (isMultiProductSub({ sub, cusProducts }) || sub.schedule) return;
- await CusProductService.updateByStripeSubId({
- db,
- stripeSubId: sub.id,
- updates: { canceled_at: null, canceled: false },
- });
+ await CusProductService.updateByStripeSubId({
+ db,
+ stripeSubId: sub.id,
+ updates: { canceled_at: null, canceled: false },
+ });
- if (!org.config.sync_status) return;
+ if (!org.config.sync_status) return;
- let { curScheduledProduct } = getExistingCusProducts({
- product: updatedCusProducts[0].product,
- cusProducts,
- internalEntityId: updatedCusProducts[0].internal_entity_id,
- });
+ let { curScheduledProduct } = getExistingCusProducts({
+ product: updatedCusProducts[0].product,
+ cusProducts,
+ internalEntityId: updatedCusProducts[0].internal_entity_id,
+ });
- let deletedCusProducts: FullCusProduct[] = [];
+ let deletedCusProducts: FullCusProduct[] = [];
- if (curScheduledProduct) {
- logger.info(
- `sub.updated: renewed -> removing scheduled: ${curScheduledProduct.product.name}, main product: ${updatedCusProducts[0].product.name}`
- );
+ if (curScheduledProduct) {
+ logger.info(
+ `sub.updated: renewed -> removing scheduled: ${curScheduledProduct.product.name}, main product: ${updatedCusProducts[0].product.name}`,
+ );
- await CusProductService.delete({
- db,
- cusProductId: curScheduledProduct.id,
- });
+ await CusProductService.delete({
+ db,
+ cusProductId: curScheduledProduct.id,
+ });
- deletedCusProducts.push(curScheduledProduct);
- }
+ deletedCusProducts.push(curScheduledProduct);
+ }
- try {
- for (let cusProd of updatedCusProducts) {
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: cusProd.internal_customer_id,
- org,
- env,
- customerId: null,
- logger,
- scenario: AttachScenario.Renew,
- cusProduct: cusProd,
- deletedCusProduct: deletedCusProducts.find(
- (cp) => cp.product.group === cusProd.product.group
- ),
- });
- }
- } catch (error) {}
+ try {
+ for (let cusProd of updatedCusProducts) {
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: cusProd.internal_customer_id,
+ org,
+ env,
+ customerId: null,
+ logger,
+ scenario: AttachScenario.Renew,
+ cusProduct: cusProd,
+ deletedCusProduct: deletedCusProducts.find(
+ (cp) => cp.product.group === cusProd.product.group,
+ ),
+ });
+ }
+ } catch (error) {}
};
diff --git a/server/src/external/stripe/webhookUtils/webhookUtils.ts b/server/src/external/stripe/webhookUtils/webhookUtils.ts
index 3cb424784..91b79954b 100644
--- a/server/src/external/stripe/webhookUtils/webhookUtils.ts
+++ b/server/src/external/stripe/webhookUtils/webhookUtils.ts
@@ -1,50 +1,50 @@
import {
- AttachParams,
- InsertCusProductParams,
+ AttachParams,
+ InsertCusProductParams,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
- cusProductToEnts,
- cusProductToPrices,
- cusProductToProduct,
+ cusProductToEnts,
+ cusProductToPrices,
+ cusProductToProduct,
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { Entity, FullCusProduct, FullCustomer } from "@autumn/shared";
import Stripe from "stripe";
export const webhookToAttachParams = ({
- req,
- stripeCli,
- paymentMethod,
- cusProduct,
- fullCus,
- entities,
+ req,
+ stripeCli,
+ paymentMethod,
+ cusProduct,
+ fullCus,
+ entities,
}: {
- req: ExtendedRequest;
- stripeCli: Stripe;
- paymentMethod?: Stripe.PaymentMethod | null;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
- entities?: Entity[];
+ req: ExtendedRequest;
+ stripeCli: Stripe;
+ paymentMethod?: Stripe.PaymentMethod | null;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
+ entities?: Entity[];
}): AttachParams => {
- const fullProduct = cusProductToProduct({ cusProduct });
+ const fullProduct = cusProductToProduct({ cusProduct });
- const params: AttachParams = {
- stripeCli,
- paymentMethod,
- customer: fullCus,
- org: req.org,
- products: [fullProduct],
- prices: cusProductToPrices({ cusProduct }),
- entitlements: cusProductToEnts({ cusProduct }),
- features: req.features,
- freeTrial: cusProduct.free_trial || null,
- optionsList: cusProduct.options,
- cusProducts: [cusProduct],
+ const params: AttachParams = {
+ stripeCli,
+ paymentMethod,
+ customer: fullCus,
+ org: req.org,
+ products: [fullProduct],
+ prices: cusProductToPrices({ cusProduct }),
+ entitlements: cusProductToEnts({ cusProduct }),
+ features: req.features,
+ freeTrial: cusProduct.free_trial || null,
+ optionsList: cusProduct.options,
+ cusProducts: [cusProduct],
- internalEntityId: cusProduct.internal_entity_id || undefined,
- entities: entities || [],
- replaceables: [],
- };
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ entities: entities || [],
+ replaceables: [],
+ };
- return params;
+ return params;
};
diff --git a/server/src/external/supabase/safeSb.ts b/server/src/external/supabase/safeSb.ts
index 682fadec5..73c54d391 100644
--- a/server/src/external/supabase/safeSb.ts
+++ b/server/src/external/supabase/safeSb.ts
@@ -1,23 +1,23 @@
import { logger } from "../logtail/logtailUtils.js";
export function safeSb any>({
- fn,
- action,
+ fn,
+ action,
}: {
- fn: T;
- action: string;
+ fn: T;
+ action: string;
}): (...args: Parameters) => Promise | undefined> {
- return async (...args: Parameters) => {
- if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
- logger.warn(
- `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, skipping ${action}`,
- );
- return;
- }
- try {
- return await fn(...args);
- } catch (error) {
- logger.error(`Error ${action}: ${error}`);
- }
- };
+ return async (...args: Parameters) => {
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
+ logger.warn(
+ `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, skipping ${action}`,
+ );
+ return;
+ }
+ try {
+ return await fn(...args);
+ } catch (error) {
+ logger.error(`Error ${action}: ${error}`);
+ }
+ };
}
diff --git a/server/src/external/supabase/storageUtils.ts b/server/src/external/supabase/storageUtils.ts
index 7cf2646a8..b881246e7 100644
--- a/server/src/external/supabase/storageUtils.ts
+++ b/server/src/external/supabase/storageUtils.ts
@@ -2,57 +2,57 @@ import { SupabaseClient } from "@supabase/supabase-js";
import { createSupabaseClient } from "../supabaseUtils.js";
export const readFile = async ({
- bucket = "autumn",
- path,
+ bucket = "autumn",
+ path,
}: {
- bucket: string;
- path: string;
+ bucket: string;
+ path: string;
}) => {
- const sb = createSupabaseClient();
- const { data, error } = await sb.storage.from(bucket).download(path);
+ const sb = createSupabaseClient();
+ const { data, error } = await sb.storage.from(bucket).download(path);
- if (error) {
- throw error;
- }
- return data;
+ if (error) {
+ throw error;
+ }
+ return data;
};
export const uploadFile = async ({
- path,
- file,
- contentType,
+ path,
+ file,
+ contentType,
}: {
- path: string;
- file: Buffer;
- contentType?: string;
+ path: string;
+ file: Buffer;
+ contentType?: string;
}) => {
- const sb = createSupabaseClient();
+ const sb = createSupabaseClient();
- const { data, error } = await sb.storage.from("autumn").upload(path, file, {
- upsert: true,
- contentType,
- });
+ const { data, error } = await sb.storage.from("autumn").upload(path, file, {
+ upsert: true,
+ contentType,
+ });
- if (error) {
- throw error;
- }
+ if (error) {
+ throw error;
+ }
- return data;
+ return data;
};
export const getUploadUrl = async ({ path }: { path: string }) => {
- const sb = createSupabaseClient();
- await sb.storage.from("autumn").remove([path]);
+ const sb = createSupabaseClient();
+ await sb.storage.from("autumn").remove([path]);
- const { data, error } = await sb.storage
- .from("autumn")
- .createSignedUploadUrl(path, {
- upsert: true,
- });
+ const { data, error } = await sb.storage
+ .from("autumn")
+ .createSignedUploadUrl(path, {
+ upsert: true,
+ });
- if (error) {
- throw error;
- }
+ if (error) {
+ throw error;
+ }
- return data;
+ return data;
};
diff --git a/server/src/external/supabase/subscribeToOrgUpdates.ts b/server/src/external/supabase/subscribeToOrgUpdates.ts
index f0fa1524d..30b328403 100644
--- a/server/src/external/supabase/subscribeToOrgUpdates.ts
+++ b/server/src/external/supabase/subscribeToOrgUpdates.ts
@@ -2,20 +2,22 @@ import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
import { DrizzleCli, client } from "@/db/initDrizzle.js";
export const subscribeToOrgUpdates = async ({ db }: { db: DrizzleCli }) => {
- try {
- await client.listen("org_updates", async (payload) => {
- try {
- const data = JSON.parse(payload);
- if (data.table === "organizations" && data.operation === "UPDATE") {
- await clearOrgCache({ db, orgId: data.new.id });
- }
- } catch (error) {
- console.warn("Error processing org update notification:", error);
- }
- });
-
- console.log("Successfully subscribed to organization updates via PostgreSQL LISTEN/NOTIFY");
- } catch (error) {
- console.warn("Error subscribing to org updates:", error);
- }
+ try {
+ await client.listen("org_updates", async (payload) => {
+ try {
+ const data = JSON.parse(payload);
+ if (data.table === "organizations" && data.operation === "UPDATE") {
+ await clearOrgCache({ db, orgId: data.new.id });
+ }
+ } catch (error) {
+ console.warn("Error processing org update notification:", error);
+ }
+ });
+
+ console.log(
+ "Successfully subscribed to organization updates via PostgreSQL LISTEN/NOTIFY",
+ );
+ } catch (error) {
+ console.warn("Error subscribing to org updates:", error);
+ }
};
diff --git a/server/src/external/supabaseUtils.ts b/server/src/external/supabaseUtils.ts
index 153dffb66..8e39d6810 100644
--- a/server/src/external/supabaseUtils.ts
+++ b/server/src/external/supabaseUtils.ts
@@ -3,45 +3,45 @@ import fetchRetry from "fetch-retry";
// Wrap the global fetch with fetch-retry
const fetchWithRetry = fetchRetry(fetch, {
- retries: 3,
- retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff starting at 1s, max 30s
- retryOn: (attempt, error, response) => {
- // Retry on gateway errors (502) and Cloudflare errors (520)
+ retries: 3,
+ retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), // Exponential backoff starting at 1s, max 30s
+ retryOn: (attempt, error, response) => {
+ // Retry on gateway errors (502) and Cloudflare errors (520)
- let shouldRetry = false;
- try {
- if (
- error?.message?.includes("cloudflare") ||
- error?.message?.includes("fetch failed")
- ) {
- shouldRetry = true;
- }
- } catch (error) {}
+ let shouldRetry = false;
+ try {
+ if (
+ error?.message?.includes("cloudflare") ||
+ error?.message?.includes("fetch failed")
+ ) {
+ shouldRetry = true;
+ }
+ } catch (error) {}
- if (
- (response && (response.status === 502 || response.status === 520)) ||
- shouldRetry
- ) {
- console.warn(
- `Retrying request... Attempt #${attempt + 1} - Status: ${
- response?.status
- }`
- );
- return true;
- }
+ if (
+ (response && (response.status === 502 || response.status === 520)) ||
+ shouldRetry
+ ) {
+ console.warn(
+ `Retrying request... Attempt #${attempt + 1} - Status: ${
+ response?.status
+ }`,
+ );
+ return true;
+ }
- return false;
- },
+ return false;
+ },
});
export const createSupabaseClient = () => {
- try {
- return createClient(
- process.env.SUPABASE_URL!,
- process.env.SUPABASE_SERVICE_KEY!
- );
- } catch (error) {
- console.error("Error creating Supabase client:", error);
- throw error;
- }
+ try {
+ return createClient(
+ process.env.SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_KEY!,
+ );
+ } catch (error) {
+ console.error("Error creating Supabase client:", error);
+ throw error;
+ }
};
diff --git a/server/src/external/svix/svixHelpers.ts b/server/src/external/svix/svixHelpers.ts
index bdbcc978f..c066d3bcf 100644
--- a/server/src/external/svix/svixHelpers.ts
+++ b/server/src/external/svix/svixHelpers.ts
@@ -2,73 +2,73 @@ import { AppEnv, Organization } from "@autumn/shared";
import { createSvixCli, getSvixAppId, safeSvix } from "./svixUtils.js";
export const createSvixApp = safeSvix({
- fn: async ({
- name,
- orgId,
- env,
- }: {
- name: string;
- orgId: string;
- env: AppEnv;
- }) => {
- const svix = createSvixCli();
- const app = await svix.application.create({
- name,
- metadata: {
- org_id: orgId,
- env,
- },
- });
- return app;
- },
- action: "createSvixApp",
+ fn: async ({
+ name,
+ orgId,
+ env,
+ }: {
+ name: string;
+ orgId: string;
+ env: AppEnv;
+ }) => {
+ const svix = createSvixCli();
+ const app = await svix.application.create({
+ name,
+ metadata: {
+ org_id: orgId,
+ env,
+ },
+ });
+ return app;
+ },
+ action: "createSvixApp",
});
export const deleteSvixApp = safeSvix({
- fn: async ({ appId }: { appId: string }) => {
- const svix = createSvixCli();
- await svix.application.delete(appId);
- },
- action: "deleteSvixApp",
+ fn: async ({ appId }: { appId: string }) => {
+ const svix = createSvixCli();
+ await svix.application.delete(appId);
+ },
+ action: "deleteSvixApp",
});
export const sendSvixEvent = safeSvix({
- fn: async ({
- org,
- env,
- eventType,
- data,
- }: {
- org: Organization;
- env: AppEnv;
- eventType: string;
- data: any;
- }) => {
- const svix = createSvixCli();
- const appId = getSvixAppId({ org, env });
- if (!appId) {
- return null;
- }
- return await svix.message.create(appId, {
- eventType,
- payload: {
- type: eventType,
- data,
- },
- });
- },
- action: "sendSvixEvent",
+ fn: async ({
+ org,
+ env,
+ eventType,
+ data,
+ }: {
+ org: Organization;
+ env: AppEnv;
+ eventType: string;
+ data: any;
+ }) => {
+ const svix = createSvixCli();
+ const appId = getSvixAppId({ org, env });
+ if (!appId) {
+ return null;
+ }
+ return await svix.message.create(appId, {
+ eventType,
+ payload: {
+ type: eventType,
+ data,
+ },
+ });
+ },
+ action: "sendSvixEvent",
});
export const getSvixDashboardUrl = safeSvix({
- fn: async ({ org, env }: { org: Organization; env: AppEnv }) => {
- const appId = getSvixAppId({ org, env });
- if (!appId) {
- return null;
- }
- const svix = createSvixCli();
- const dashboard = await svix.authentication.appPortalAccess(appId, {});
- return dashboard.url;
- },
- action: "getSvixDashboardUrl",
+ fn: async ({ org, env }: { org: Organization; env: AppEnv }) => {
+ const appId = getSvixAppId({ org, env });
+ if (!appId) {
+ return null;
+ }
+ const svix = createSvixCli();
+ const dashboard = await svix.authentication.appPortalAccess(appId, {});
+ return dashboard.url;
+ },
+ action: "getSvixDashboardUrl",
});
diff --git a/server/src/external/svix/svixUtils.ts b/server/src/external/svix/svixUtils.ts
index 1cf76f78b..942697f46 100644
--- a/server/src/external/svix/svixUtils.ts
+++ b/server/src/external/svix/svixUtils.ts
@@ -4,38 +4,38 @@ import { Svix } from "svix";
import { logger } from "../logtail/logtailUtils.js";
export const createSvixCli = () => {
- return new Svix(process.env.SVIX_API_KEY as string);
+ return new Svix(process.env.SVIX_API_KEY as string);
};
export function safeSvix any>({
- fn,
- action,
+ fn,
+ action,
}: {
- fn: T;
- action: string;
+ fn: T;
+ action: string;
}): (...args: Parameters) => Promise | undefined> {
- return async (...args: Parameters) => {
- if (!process.env.SVIX_API_KEY) {
- logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
- return;
- }
- try {
- return await fn(...args);
- } catch (error) {
- logger.error(`Error ${action}: ${error}`);
- }
- };
+ return async (...args: Parameters) => {
+ if (!process.env.SVIX_API_KEY) {
+ logger.warn(`SVIX_API_KEY is not set, skipping ${action}`);
+ return;
+ }
+ try {
+ return await fn(...args);
+ } catch (error) {
+ logger.error(`Error ${action}: ${error}`);
+ }
+ };
}
export const getSvixAppId = ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- const svixConfig = org.svix_config;
- return env == AppEnv.Live
- ? svixConfig?.live_app_id
- : svixConfig?.sandbox_app_id;
+ const svixConfig = org.svix_config;
+ return env == AppEnv.Live
+ ? svixConfig?.live_app_id
+ : svixConfig?.sandbox_app_id;
};
diff --git a/server/src/external/webhooks/webhookUtils.ts b/server/src/external/webhooks/webhookUtils.ts
index 7931550c3..43c504cef 100644
--- a/server/src/external/webhooks/webhookUtils.ts
+++ b/server/src/external/webhooks/webhookUtils.ts
@@ -1,51 +1,51 @@
import crypto from "crypto";
export const verifySvixSignature = async (req: any, res: any) => {
- const SIGNING_SECRET = process.env.CLERK_SIGNING_SECRET;
+ const SIGNING_SECRET = process.env.CLERK_SIGNING_SECRET;
- if (!SIGNING_SECRET) {
- throw new Error(
- "Error: Please add SIGNING_SECRET from Clerk Dashboard to .env"
- );
- }
+ if (!SIGNING_SECRET) {
+ throw new Error(
+ "Error: Please add SIGNING_SECRET from Clerk Dashboard to .env",
+ );
+ }
- const headers = req.headers;
- const svix_id = headers["svix-id"];
- const svix_timestamp = headers["svix-timestamp"];
- const svix_signature = headers["svix-signature"];
+ const headers = req.headers;
+ const svix_id = headers["svix-id"];
+ const svix_timestamp = headers["svix-timestamp"];
+ const svix_signature = headers["svix-signature"];
- // Verify all headers are presen3t
- if (!svix_id || !svix_timestamp || !svix_signature) {
- throw new Error("Error: Missing svix headers");
- }
+ // Verify all headers are presen3t
+ if (!svix_id || !svix_timestamp || !svix_signature) {
+ throw new Error("Error: Missing svix headers");
+ }
- // Verify timestamp is within tolerance (5 minutes)
- const timestamp = parseInt(svix_timestamp);
- const now = Math.floor(Date.now() / 1000);
- if (Math.abs(now - timestamp) > 300) {
- throw new Error("Error: Message timestamp too old");
- }
+ // Verify timestamp is within tolerance (5 minutes)
+ const timestamp = parseInt(svix_timestamp);
+ const now = Math.floor(Date.now() / 1000);
+ if (Math.abs(now - timestamp) > 300) {
+ throw new Error("Error: Message timestamp too old");
+ }
- const body = JSON.stringify(req.body);
- const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
+ const body = JSON.stringify(req.body);
+ const signedContent = `${svix_id}.${svix_timestamp}.${body}`;
- // Need to base64 decode the secret
- const secretBytes = Buffer.from(SIGNING_SECRET.split("_")[1], "base64");
- const signature = crypto
- .createHmac("sha256", secretBytes)
- .update(signedContent)
- .digest("base64");
+ // Need to base64 decode the secret
+ const secretBytes = Buffer.from(SIGNING_SECRET.split("_")[1], "base64");
+ const signature = crypto
+ .createHmac("sha256", secretBytes)
+ .update(signedContent)
+ .digest("base64");
- // Get the actual signature from the header (removing the v1, prefix)
- const svixSignature = svix_signature.split(" ")[0].split(",")[1];
+ // Get the actual signature from the header (removing the v1, prefix)
+ const svixSignature = svix_signature.split(" ")[0].split(",")[1];
- try {
- // Use constant-time comparison to prevent timing attacks
- return crypto.timingSafeEqual(
- Buffer.from(signature),
- Buffer.from(svixSignature)
- );
- } catch (err) {
- return false;
- }
+ try {
+ // Use constant-time comparison to prevent timing attacks
+ return crypto.timingSafeEqual(
+ Buffer.from(signature),
+ Buffer.from(svixSignature),
+ );
+ } catch (err) {
+ return false;
+ }
};
diff --git a/server/src/index.ts b/server/src/index.ts
index 9e6adcfe8..9d941881b 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -33,198 +33,198 @@ checkEnvVars();
// subscribeToOrgUpdates({ db });
const init = async () => {
- const app = express();
+ const app = express();
- // Check if this blocks API calls...
- app.use(
- cors({
- origin: [
- "http://localhost:3000",
- "http://localhost:5173",
- "http://localhost:5174",
- "https://app.useautumn.com",
- "https://staging.useautumn.com",
- "https://*.useautumn.com",
- "https://localhost:8080",
- "https://www.alphalog.ai",
- "https://*.alphalog.ai",
- process.env.CLIENT_URL || "",
- ],
- credentials: true,
- allowedHeaders: [
- "app_env",
- "x-api-version",
- "Authorization",
- "Content-Type",
- "Accept",
- "Origin",
- "X-API-Version",
- "X-Requested-With",
- "Access-Control-Request-Method",
- "Access-Control-Request-Headers",
- "Cache-Control",
- "If-Match",
- "If-None-Match",
- "If-Modified-Since",
- "If-Unmodified-Since",
- ],
- })
- );
+ // Check if this blocks API calls...
+ app.use(
+ cors({
+ origin: [
+ "http://localhost:3000",
+ "http://localhost:5173",
+ "http://localhost:5174",
+ "https://app.useautumn.com",
+ "https://staging.useautumn.com",
+ "https://*.useautumn.com",
+ "https://localhost:8080",
+ "https://www.alphalog.ai",
+ "https://*.alphalog.ai",
+ process.env.CLIENT_URL || "",
+ ],
+ credentials: true,
+ allowedHeaders: [
+ "app_env",
+ "x-api-version",
+ "Authorization",
+ "Content-Type",
+ "Accept",
+ "Origin",
+ "X-API-Version",
+ "X-Requested-With",
+ "Access-Control-Request-Method",
+ "Access-Control-Request-Headers",
+ "Cache-Control",
+ "If-Match",
+ "If-None-Match",
+ "If-Modified-Since",
+ "If-Unmodified-Since",
+ ],
+ }),
+ );
- app.all("/api/auth/*", toNodeHandler(auth));
+ app.all("/api/auth/*", toNodeHandler(auth));
- const server = http.createServer(app);
- const posthog = createPosthogCli();
+ const server = http.createServer(app);
+ const posthog = createPosthogCli();
- server.keepAliveTimeout = 120000; // 120 seconds
- server.headersTimeout = 120000; // 120 seconds should be >= keepAliveTimeout
+ server.keepAliveTimeout = 120000; // 120 seconds
+ server.headersTimeout = 120000; // 120 seconds should be >= keepAliveTimeout
- await QueueManager.getInstance(); // initialize the queue manager
- await CacheManager.getInstance();
- await ClickHouseManager.getInstance();
+ await QueueManager.getInstance(); // initialize the queue manager
+ await CacheManager.getInstance();
+ await ClickHouseManager.getInstance();
- app.use(async (req: any, res: any, next: any) => {
- req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox;
- req.db = db;
- req.clickhouseClient = await ClickHouseManager.getClient();
- req.posthog = posthog;
- req.id = req.headers["rndr-id"] || generateId("local_req");
- req.timestamp = Date.now();
+ app.use(async (req: any, res: any, next: any) => {
+ req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox;
+ req.db = db;
+ req.clickhouseClient = await ClickHouseManager.getClient();
+ req.posthog = posthog;
+ req.id = req.headers["rndr-id"] || generateId("local_req");
+ req.timestamp = Date.now();
- const reqContext = {
- id: req.id,
- env: req.headers["app_env"] || undefined,
- method: req.method,
- url: req.originalUrl,
- timestamp: req.timestamp,
- };
+ const reqContext = {
+ id: req.id,
+ env: req.headers["app_env"] || undefined,
+ method: req.method,
+ url: req.originalUrl,
+ timestamp: req.timestamp,
+ };
- // Create span
- const spanName = `${req.method} ${req.originalUrl} - ${req.id}`;
- const span = tracer.startSpan(spanName);
- span.setAttributes({
- req_id: req.id,
- method: req.method,
- url: req.originalUrl,
- });
+ // Create span
+ const spanName = `${req.method} ${req.originalUrl} - ${req.id}`;
+ const span = tracer.startSpan(spanName);
+ span.setAttributes({
+ req_id: req.id,
+ method: req.method,
+ url: req.originalUrl,
+ });
- // Store span on request for potential use in other middleware/handlers
- req.span = span;
+ // Store span on request for potential use in other middleware/handlers
+ req.span = span;
- req.logtail = logger.child({
- context: {
- req: reqContext,
- },
- });
- req.logger = req.logtail;
+ req.logtail = logger.child({
+ context: {
+ req: reqContext,
+ },
+ });
+ req.logger = req.logtail;
- const endSpan = () => {
- try {
- span.setAttributes({
- "http.response.status_code": res.statusCode,
- "http.response.body.size": res.get("content-length") || 0,
- "http.response.duration": Date.now() - req.timestamp,
- });
- span.end();
+ const endSpan = () => {
+ try {
+ span.setAttributes({
+ "http.response.status_code": res.statusCode,
+ "http.response.body.size": res.get("content-length") || 0,
+ "http.response.duration": Date.now() - req.timestamp,
+ });
+ span.end();
- const closeSpan = tracer.startSpan("response_closed");
- closeSpan.setAttributes({
- req_id: req.id,
- });
- closeSpan.end();
- } catch (error) {
- logger.error("Error ending span", { error });
- }
- };
+ const closeSpan = tracer.startSpan("response_closed");
+ closeSpan.setAttributes({
+ req_id: req.id,
+ });
+ closeSpan.end();
+ } catch (error) {
+ logger.error("Error ending span", { error });
+ }
+ };
- res.on("close", endSpan);
+ res.on("close", endSpan);
- // Run the rest of the request processing within the span's context
- context.with(trace.setSpan(context.active(), span), () => {
- next();
- });
- });
+ // Run the rest of the request processing within the span's context
+ context.with(trace.setSpan(context.active(), span), () => {
+ next();
+ });
+ });
- app.use("/webhooks", webhooksRouter);
+ app.use("/webhooks", webhooksRouter);
- app.use(express.json());
- app.use(async (req: any, res: any, next: any) => {
- req.logtail.info(`${req.method} ${req.originalUrl}`, {
- context: {
- body: req.body,
- },
- });
- next();
- });
+ app.use(express.json());
+ app.use(async (req: any, res: any, next: any) => {
+ req.logtail.info(`${req.method} ${req.originalUrl}`, {
+ context: {
+ body: req.body,
+ },
+ });
+ next();
+ });
- app.use(mainRouter);
- app.use("/v1", apiRouter);
+ app.use(mainRouter);
+ app.use("/v1", apiRouter);
- const PORT = 8080;
+ const PORT = 8080;
- server.listen(PORT, () => {
- console.log(`Server running on port ${PORT}`);
- });
+ server.listen(PORT, () => {
+ console.log(`Server running on port ${PORT}`);
+ });
};
if (process.env.NODE_ENV === "development") {
- init();
- registerShutdownHandlers();
+ init();
+ registerShutdownHandlers();
} else {
- let numCPUs = os.cpus().length;
+ let numCPUs = os.cpus().length;
- if (cluster.isPrimary) {
- console.log(`Master ${process.pid} is running`);
- console.log("Number of CPUs", numCPUs);
+ if (cluster.isPrimary) {
+ console.log(`Master ${process.pid} is running`);
+ console.log("Number of CPUs", numCPUs);
- let numWorkers = 7;
+ let numWorkers = 7;
- for (let i = 0; i < numWorkers; i++) {
- cluster.fork();
- }
+ for (let i = 0; i < numWorkers; i++) {
+ cluster.fork();
+ }
- cluster.on("exit", (worker, code, signal) => {
- logger.error(`WORKER DIED: ${worker.process.pid}`);
- cluster.fork();
- });
- } else {
- init();
- registerShutdownHandlers();
- }
+ cluster.on("exit", (worker, code, signal) => {
+ logger.error(`WORKER DIED: ${worker.process.pid}`);
+ cluster.fork();
+ });
+ } else {
+ init();
+ registerShutdownHandlers();
+ }
}
function registerShutdownHandlers() {
- process.on("SIGTERM", gracefulShutdown);
- process.on("SIGINT", gracefulShutdown);
- // Do NOT use process.on("exit", ...) for async cleanup!
+ process.on("SIGTERM", gracefulShutdown);
+ process.on("SIGINT", gracefulShutdown);
+ // Do NOT use process.on("exit", ...) for async cleanup!
}
async function gracefulShutdown() {
- console.log("Shutting down worker, closing DB connections...");
- try {
- await client.end();
- console.log("DB connection closed. Exiting process.");
- process.exit(0);
- } catch (err) {
- console.error("Error closing DB connection:", err);
- process.exit(1);
- }
+ console.log("Shutting down worker, closing DB connections...");
+ try {
+ await client.end();
+ console.log("DB connection closed. Exiting process.");
+ process.exit(0);
+ } catch (err) {
+ console.error("Error closing DB connection:", err);
+ process.exit(1);
+ }
}
// Close connections gracefully?
const closeConnections = async () => {
- console.log("Closing connections");
- await client.end();
+ console.log("Closing connections");
+ await client.end();
};
process.on("SIGTERM", async () => {
- console.log("SIGTERM received, shutting down gracefully");
- await closeConnections();
- process.exit(0);
+ console.log("SIGTERM received, shutting down gracefully");
+ await closeConnections();
+ process.exit(0);
});
process.on("SIGINT", async () => {
- console.log("SIGINT received, shutting down gracefully");
- await closeConnections();
- process.exit(0);
+ console.log("SIGINT received, shutting down gracefully");
+ await closeConnections();
+ process.exit(0);
});
diff --git a/server/src/instrumentation.ts b/server/src/instrumentation.ts
index 56095c714..45fb19f8a 100644
--- a/server/src/instrumentation.ts
+++ b/server/src/instrumentation.ts
@@ -9,27 +9,27 @@ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
// Initialize OTLP trace exporter with the endpoint URL and headers
if (process.env.AXIOM_TOKEN) {
- const traceExporter = new OTLPTraceExporter({
- url: "https://api.axiom.co/v1/traces",
- headers: {
- Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
- "X-Axiom-Dataset": "express_otel",
- },
- });
+ const traceExporter = new OTLPTraceExporter({
+ url: "https://api.axiom.co/v1/traces",
+ headers: {
+ Authorization: `Bearer ${process.env.AXIOM_TOKEN}`,
+ "X-Axiom-Dataset": "express_otel",
+ },
+ });
- // Creating a resource to identify your service in traces
- const resource = resourceFromAttributes({
- [ATTR_SERVICE_NAME]: "express",
- });
+ // Creating a resource to identify your service in traces
+ const resource = resourceFromAttributes({
+ [ATTR_SERVICE_NAME]: "express",
+ });
- // Configuring the OpenTelemetry Node SDK
- const sdk = new NodeSDK({
- spanProcessor: new BatchSpanProcessor(traceExporter),
- resource: resource,
- instrumentations: [getNodeAutoInstrumentations()],
- });
+ // Configuring the OpenTelemetry Node SDK
+ const sdk = new NodeSDK({
+ spanProcessor: new BatchSpanProcessor(traceExporter),
+ resource: resource,
+ instrumentations: [getNodeAutoInstrumentations()],
+ });
- // Starting the OpenTelemetry SDK to begin collecting telemetry data
- console.log("Starting OpenTelemetry");
- sdk.start();
+ // Starting the OpenTelemetry SDK to begin collecting telemetry data
+ console.log("Starting OpenTelemetry");
+ sdk.start();
}
diff --git a/server/src/internal/admin/adminRouter.ts b/server/src/internal/admin/adminRouter.ts
index ba9cecb51..f82f2cbbe 100644
--- a/server/src/internal/admin/adminRouter.ts
+++ b/server/src/internal/admin/adminRouter.ts
@@ -9,156 +9,156 @@ export const adminRouter: Router = Router();
// adminRouter.post()
adminRouter.get("/users", async (req: any, res: any) => {
- try {
- const { db } = req as ExtendedRequest;
+ try {
+ const { db } = req as ExtendedRequest;
- let { sortKey, search, after, before } = req.query;
+ let { sortKey, search, after, before } = req.query;
- if (after) {
- after = {
- id: after.split(",")[0],
- createdAt: new Date(after.split(",")[1]),
- };
- } else if (before) {
- before = {
- id: before.split(",")[0],
- createdAt: new Date(before.split(",")[1]),
- };
- }
+ if (after) {
+ after = {
+ id: after.split(",")[0],
+ createdAt: new Date(after.split(",")[1]),
+ };
+ } else if (before) {
+ before = {
+ id: before.split(",")[0],
+ createdAt: new Date(before.split(",")[1]),
+ };
+ }
- const users = await db
- .select()
- .from(user)
- .where(
- and(
- search
- ? or(
- ilike(user.email, `%${search as string}%`),
- ilike(user.name, `%${search as string}%`),
- ilike(user.id, `%${search as string}%`),
- )
- : undefined,
- after
- ? or(
- lt(user.createdAt, after.createdAt),
- or(
- and(
- eq(user.createdAt, after.createdAt),
- lt(user.id, after.id),
- ),
- ),
- )
- : undefined,
- before
- ? or(
- gte(user.createdAt, before.createdAt),
- or(
- and(
- eq(user.createdAt, before.createdAt),
- gt(user.id, before.id),
- ),
- ),
- )
- : undefined,
- ),
- )
- .orderBy(desc(user.createdAt), desc(user.id))
- .limit(21);
+ const users = await db
+ .select()
+ .from(user)
+ .where(
+ and(
+ search
+ ? or(
+ ilike(user.email, `%${search as string}%`),
+ ilike(user.name, `%${search as string}%`),
+ ilike(user.id, `%${search as string}%`),
+ )
+ : undefined,
+ after
+ ? or(
+ lt(user.createdAt, after.createdAt),
+ or(
+ and(
+ eq(user.createdAt, after.createdAt),
+ lt(user.id, after.id),
+ ),
+ ),
+ )
+ : undefined,
+ before
+ ? or(
+ gte(user.createdAt, before.createdAt),
+ or(
+ and(
+ eq(user.createdAt, before.createdAt),
+ gt(user.id, before.id),
+ ),
+ ),
+ )
+ : undefined,
+ ),
+ )
+ .orderBy(desc(user.createdAt), desc(user.id))
+ .limit(21);
- res.json({
- rows: users.slice(0, 20),
- hasNextPage: users.length > 20,
- });
- } catch (error) {
- handleFrontendReqError({
- res,
- req,
- error,
- action: "admin: search users",
- });
- }
+ res.json({
+ rows: users.slice(0, 20),
+ hasNextPage: users.length > 20,
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ res,
+ req,
+ error,
+ action: "admin: search users",
+ });
+ }
});
adminRouter.get("/orgs", async (req: any, res: any) => {
- try {
- const { db } = req as ExtendedRequest;
+ try {
+ const { db } = req as ExtendedRequest;
- let { search, after, before } = req.query;
+ let { search, after, before } = req.query;
- if (after) {
- after = {
- id: after.split(",")[0],
- createdAt: new Date(after.split(",")[1]),
- };
- } else if (before) {
- before = {
- id: before.split(",")[0],
- createdAt: new Date(before.split(",")[1]),
- };
- }
+ if (after) {
+ after = {
+ id: after.split(",")[0],
+ createdAt: new Date(after.split(",")[1]),
+ };
+ } else if (before) {
+ before = {
+ id: before.split(",")[0],
+ createdAt: new Date(before.split(",")[1]),
+ };
+ }
- const orgs = await db
- .select()
- .from(organizations)
- .where(
- and(
- search
- ? or(
- ilike(organizations.name, `%${search as string}%`),
- ilike(organizations.id, `%${search as string}%`),
- ilike(organizations.slug, `%${search as string}%`),
- )
- : undefined,
- after
- ? or(
- lt(organizations.createdAt, after.createdAt),
- or(
- and(
- eq(organizations.createdAt, after.createdAt),
- lt(organizations.id, after.id),
- ),
- ),
- )
- : undefined,
- before
- ? or(
- gte(organizations.createdAt, before.createdAt),
- or(
- and(
- eq(organizations.createdAt, before.createdAt),
- gt(organizations.id, before.id),
- ),
- ),
- )
- : undefined,
- ),
- )
- .orderBy(desc(organizations.createdAt), desc(organizations.id))
- .limit(21);
+ const orgs = await db
+ .select()
+ .from(organizations)
+ .where(
+ and(
+ search
+ ? or(
+ ilike(organizations.name, `%${search as string}%`),
+ ilike(organizations.id, `%${search as string}%`),
+ ilike(organizations.slug, `%${search as string}%`),
+ )
+ : undefined,
+ after
+ ? or(
+ lt(organizations.createdAt, after.createdAt),
+ or(
+ and(
+ eq(organizations.createdAt, after.createdAt),
+ lt(organizations.id, after.id),
+ ),
+ ),
+ )
+ : undefined,
+ before
+ ? or(
+ gte(organizations.createdAt, before.createdAt),
+ or(
+ and(
+ eq(organizations.createdAt, before.createdAt),
+ gt(organizations.id, before.id),
+ ),
+ ),
+ )
+ : undefined,
+ ),
+ )
+ .orderBy(desc(organizations.createdAt), desc(organizations.id))
+ .limit(21);
- let orgIds = orgs.map((org) => org.id);
+ let orgIds = orgs.map((org) => org.id);
- let memberships = await db
- .select()
- .from(member)
- .leftJoin(user, eq(member.userId, user.id))
- .where(inArray(member.organizationId, orgIds));
+ let memberships = await db
+ .select()
+ .from(member)
+ .leftJoin(user, eq(member.userId, user.id))
+ .where(inArray(member.organizationId, orgIds));
- res.json({
- rows: orgs.slice(0, 20).map((org) => ({
- ...org,
- users: memberships
- .filter((membership) => membership.member.organizationId === org.id)
- .map((membership) => membership.user),
- })),
- hasNextPage: orgs.length > 20,
- });
- } catch (error) {
- handleFrontendReqError({
- res,
- req,
- error,
- action: "admin: search orgs",
- });
- }
+ res.json({
+ rows: orgs.slice(0, 20).map((org) => ({
+ ...org,
+ users: memberships
+ .filter((membership) => membership.member.organizationId === org.id)
+ .map((membership) => membership.user),
+ })),
+ hasNextPage: orgs.length > 20,
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ res,
+ req,
+ error,
+ action: "admin: search orgs",
+ });
+ }
});
diff --git a/server/src/internal/admin/adminUtils/userAnalytics.ts b/server/src/internal/admin/adminUtils/userAnalytics.ts
index f6a39bff3..6b35c17b6 100644
--- a/server/src/internal/admin/adminUtils/userAnalytics.ts
+++ b/server/src/internal/admin/adminUtils/userAnalytics.ts
@@ -9,6 +9,6 @@ import { count } from "drizzle-orm";
// 3. Churned count
export const getUserCount = async ({ db }: { db: DrizzleCli }) => {
- const userCount = await db.select({ count: count() }).from(user);
- return userCount[0].count;
+ const userCount = await db.select({ count: count() }).from(user);
+ return userCount[0].count;
};
diff --git a/server/src/internal/admin/withAdminAuth.ts b/server/src/internal/admin/withAdminAuth.ts
index b10935137..b117a40f7 100644
--- a/server/src/internal/admin/withAdminAuth.ts
+++ b/server/src/internal/admin/withAdminAuth.ts
@@ -5,28 +5,28 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { NextFunction } from "express";
export const withAdminAuth = async (req: any, res: any, next: NextFunction) => {
- const { logtail: logger, userId } = req as ExtendedRequest;
+ const { logtail: logger, userId } = req as ExtendedRequest;
- try {
- const data = await auth.api.getSession({
- headers: req.headers,
- });
+ try {
+ const data = await auth.api.getSession({
+ headers: req.headers,
+ });
- if (
- !ADMIN_USER_IDs.includes(data?.session?.userId || "") &&
- !ADMIN_USER_IDs.includes(data?.session?.impersonatedBy || "")
- ) {
- return res.status(403).json({
- error: {
- code: ErrCode.InvalidRequest,
- message: "Method not allowed",
- },
- });
- }
+ if (
+ !ADMIN_USER_IDs.includes(data?.session?.userId || "") &&
+ !ADMIN_USER_IDs.includes(data?.session?.impersonatedBy || "")
+ ) {
+ return res.status(403).json({
+ error: {
+ code: ErrCode.InvalidRequest,
+ message: "Method not allowed",
+ },
+ });
+ }
- next();
- } catch (error: any) {
- logger.error(`Admin req failed: ${error.message}`);
- return res.status(400).json();
- }
+ next();
+ } catch (error: any) {
+ logger.error(`Admin req failed: ${error.message}`);
+ return res.status(400).json();
+ }
};
diff --git a/server/src/internal/analytics/ActionService.ts b/server/src/internal/analytics/ActionService.ts
index ca131b101..7d719ef5a 100644
--- a/server/src/internal/analytics/ActionService.ts
+++ b/server/src/internal/analytics/ActionService.ts
@@ -2,9 +2,9 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { type ActionInsert, actions } from "@autumn/shared";
export class ActionService {
- static async insert(db: DrizzleCli, data: ActionInsert | ActionInsert[]) {
- const dataArray = Array.isArray(data) ? data : [data];
+ static async insert(db: DrizzleCli, data: ActionInsert | ActionInsert[]) {
+ const dataArray = Array.isArray(data) ? data : [data];
- await db.insert(actions).values(dataArray);
- }
+ await db.insert(actions).values(dataArray);
+ }
}
diff --git a/server/src/internal/analytics/AnalyticsService.ts b/server/src/internal/analytics/AnalyticsService.ts
index 5e1525cae..fa5afd96b 100644
--- a/server/src/internal/analytics/AnalyticsService.ts
+++ b/server/src/internal/analytics/AnalyticsService.ts
@@ -6,8 +6,8 @@ import { StatusCodes } from "http-status-codes";
import RecaseError from "@/utils/errorUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import {
- generateEventCountExpressions,
- getBillingCycleStartDate,
+ generateEventCountExpressions,
+ getBillingCycleStartDate,
} from "./analyticsUtils.js";
export class AnalyticsService {
diff --git a/server/src/internal/analytics/RevenueService.ts b/server/src/internal/analytics/RevenueService.ts
index 75a3de6d9..7e8b60070 100644
--- a/server/src/internal/analytics/RevenueService.ts
+++ b/server/src/internal/analytics/RevenueService.ts
@@ -23,16 +23,18 @@ WHERE
AND c.org_id = {org_id:String}
AND c.env = {env:String};`;
- const result = await clickhouseClient.query({
- query,
- query_params: {
- org_id: org?.id,
- env: env,
- },
- });
+ const result = await clickhouseClient.query({
+ query,
+ query_params: {
+ org_id: org?.id,
+ env: env,
+ },
+ });
- const resultJson = await result.json();
+ const resultJson = await result.json();
- return (resultJson.data as { total_payment_volume: number, label: string }[])[0];
+ return (
+ resultJson.data as { total_payment_volume: number; label: string }[]
+ )[0];
}
}
diff --git a/server/src/internal/analytics/actionUtils.ts b/server/src/internal/analytics/actionUtils.ts
index 0dabfcdce..eab9258fc 100644
--- a/server/src/internal/analytics/actionUtils.ts
+++ b/server/src/internal/analytics/actionUtils.ts
@@ -1,66 +1,66 @@
import { generateId } from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import {
- Entity,
- Organization,
- AppEnv,
- Customer,
- ActionInsert,
- ActionType,
- AuthType,
+ Entity,
+ Organization,
+ AppEnv,
+ Customer,
+ ActionInsert,
+ ActionType,
+ AuthType,
} from "@autumn/shared";
export const parseReqForAction = (
- req: ExtendedRequest,
+ req: ExtendedRequest,
): Partial => {
- return {
- id: req.id,
- authType: req.authType,
- originalUrl: req.originalUrl,
- method: req.method,
- body: req.body,
- timestamp: Date.now(),
- } as Partial;
+ return {
+ id: req.id,
+ authType: req.authType,
+ originalUrl: req.originalUrl,
+ method: req.method,
+ body: req.body,
+ timestamp: Date.now(),
+ } as Partial;
};
export const constructAction = ({
- org,
- env,
- customer,
- entity,
- type,
- req,
- properties,
+ org,
+ env,
+ customer,
+ entity,
+ type,
+ req,
+ properties,
}: {
- org: Organization;
- env: AppEnv;
- customer: Customer;
- entity?: Entity;
- type: ActionType;
- req: Partial;
- properties?: any;
+ org: Organization;
+ env: AppEnv;
+ customer: Customer;
+ entity?: Entity;
+ type: ActionType;
+ req: Partial;
+ properties?: any;
}): ActionInsert => {
- let timestampVal = req.timestamp ? new Date(req.timestamp) : new Date();
+ let timestampVal = req.timestamp ? new Date(req.timestamp) : new Date();
- return {
- id: generateId("act"),
- org_id: org.id,
- org_slug: org.slug,
- env,
- internal_customer_id: customer.internal_id,
- customer_id: customer.id,
- entity_id: entity?.id,
- internal_entity_id: entity?.internal_id,
- type,
- timestamp: timestampVal,
+ return {
+ id: generateId("act"),
+ org_id: org.id,
+ org_slug: org.slug,
+ env,
+ internal_customer_id: customer.internal_id,
+ customer_id: customer.id,
+ entity_id: entity?.id,
+ internal_entity_id: entity?.internal_id,
+ type,
+ timestamp: timestampVal,
- // Request info
- request_id: req.id || "",
- method: req.method || "",
- path: (req.originalUrl || "").split("?")[0],
- auth_type: req.authType || AuthType.Unknown,
+ // Request info
+ request_id: req.id || "",
+ method: req.method || "",
+ path: (req.originalUrl || "").split("?")[0],
+ auth_type: req.authType || AuthType.Unknown,
- // Properties
- properties: properties || {},
- };
+ // Properties
+ properties: properties || {},
+ };
};
diff --git a/server/src/internal/analytics/analyticsRouter.ts b/server/src/internal/analytics/analyticsRouter.ts
index d105d0bc4..a52a19757 100644
--- a/server/src/internal/analytics/analyticsRouter.ts
+++ b/server/src/internal/analytics/analyticsRouter.ts
@@ -14,84 +14,84 @@ const RangeEnum = z.enum(["24h", "7d", "30d", "90d", "last_cycle"]);
type Range = z.infer;
analyticsRouter.post("", (req, res) =>
- routeHandler({
- req,
- res,
- action: "api query analytics data",
- handler: async (req, res) => {
- const { org, db, env } = req;
- const {
- customer_id,
- feature_id,
- }: { customer_id: string; feature_id: string | string[] } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "api query analytics data",
+ handler: async (req, res) => {
+ const { org, db, env } = req;
+ const {
+ customer_id,
+ feature_id,
+ }: { customer_id: string; feature_id: string | string[] } = req.body;
- if (!customer_id || !feature_id) {
- throw new RecaseError({
- message: "Fields customer_id and feature_id are required",
- code: ErrCode.InvalidInputs,
- statusCode: 400,
- });
- }
+ if (!customer_id || !feature_id) {
+ throw new RecaseError({
+ message: "Fields customer_id and feature_id are required",
+ code: ErrCode.InvalidInputs,
+ statusCode: 400,
+ });
+ }
- let range: any = RangeEnum.nullish().parse(req.body.range);
+ let range: any = RangeEnum.nullish().parse(req.body.range);
- if (range === "last_cycle" || !range) {
- range = "1bc";
- }
+ if (range === "last_cycle" || !range) {
+ range = "1bc";
+ }
- const customer = (await CusService.getFull({
- db,
- orgId: org.id,
- idOrInternalId: customer_id,
- env,
- withSubs: true,
- })) as FullCustomer;
+ const customer = (await CusService.getFull({
+ db,
+ orgId: org.id,
+ idOrInternalId: customer_id,
+ env,
+ withSubs: true,
+ })) as FullCustomer;
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let featureIds: string[] = [];
+ let featureIds: string[] = [];
- if (Array.isArray(feature_id)) featureIds = feature_id;
- else featureIds = [feature_id];
+ if (Array.isArray(feature_id)) featureIds = feature_id;
+ else featureIds = [feature_id];
- let events = await AnalyticsService.getTimeseriesEvents({
- req,
- params: {
- interval: range,
- event_names: featureIds,
- customer_id: customer_id,
- no_count: true,
- },
- customer,
- });
+ let events = await AnalyticsService.getTimeseriesEvents({
+ req,
+ params: {
+ interval: range,
+ event_names: featureIds,
+ customer_id: customer_id,
+ no_count: true,
+ },
+ customer,
+ });
- if (!events) {
- throw new RecaseError({
- message: "No events found",
- code: ErrCode.InternalError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (!events) {
+ throw new RecaseError({
+ message: "No events found",
+ code: ErrCode.InternalError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- events.data.forEach((event: any) => {
- event["period"] = parseInt(format(new Date(event["period"]), "T"));
- });
+ events.data.forEach((event: any) => {
+ event["period"] = parseInt(format(new Date(event["period"]), "T"));
+ });
- let usageList = events.data.filter(
- (event: any) => event.period <= Date.now()
- );
+ let usageList = events.data.filter(
+ (event: any) => event.period <= Date.now(),
+ );
- res.status(200).json({
- list: usageList,
- });
- },
- })
+ res.status(200).json({
+ list: usageList,
+ });
+ },
+ }),
);
export { analyticsRouter };
diff --git a/server/src/internal/analytics/analyticsUtils.ts b/server/src/internal/analytics/analyticsUtils.ts
index 2b72dfe7e..dd8694675 100644
--- a/server/src/internal/analytics/analyticsUtils.ts
+++ b/server/src/internal/analytics/analyticsUtils.ts
@@ -1,235 +1,235 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- ErrCode,
- FullCustomer,
- FullCusProduct,
- CusProductStatus,
- Subscription,
- AppEnv,
- FullProduct,
- CustomerEntitlement,
- FullCustomerEntitlement,
- EntInterval,
+ ErrCode,
+ FullCustomer,
+ FullCusProduct,
+ CusProductStatus,
+ Subscription,
+ AppEnv,
+ FullProduct,
+ CustomerEntitlement,
+ FullCustomerEntitlement,
+ EntInterval,
} from "@autumn/shared";
import { cusProductToProduct } from "@autumn/shared";
import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import { isFreeProduct } from "../products/productUtils.js";
export async function getBillingCycleStartDate(
- env: AppEnv,
- orgId: string,
- customer?: FullCustomer,
- db?: DrizzleCli,
- intervalType?: "1bc" | "3bc"
+ env: AppEnv,
+ orgId: string,
+ customer?: FullCustomer,
+ db?: DrizzleCli,
+ intervalType?: "1bc" | "3bc",
) {
- // If no customer provided, return empty object (for aggregateAll case)
- if (!customer || !db || !intervalType) {
- return {};
- }
+ // If no customer provided, return empty object (for aggregateAll case)
+ if (!customer || !db || !intervalType) {
+ return {};
+ }
- // const customerHasProducts = notNullish(customer.customer_products);
- // // const customerHasSubscriptions = notNullish(customer.subscriptions);
+ // const customerHasProducts = notNullish(customer.customer_products);
+ // // const customerHasSubscriptions = notNullish(customer.subscriptions);
- // // if (!customerHasProducts) {
- // // return {}; // No products, return empty object
- // // }
+ // // if (!customerHasProducts) {
+ // // return {}; // No products, return empty object
+ // // }
- // // const subscriptions = await AnalyticsService.getSubscriptionsIfNeeded(
- // // customer,
- // // customerHasSubscriptions,
- // // db
- // // );
+ // // const subscriptions = await AnalyticsService.getSubscriptionsIfNeeded(
+ // // customer,
+ // // customerHasSubscriptions,
+ // // db
+ // // );
- const subscriptions = customer.subscriptions || [];
- const cusProducts = customer.customer_products.filter(
- (product: FullCusProduct) => ACTIVE_STATUSES.includes(product.status)
- );
+ const subscriptions = customer.subscriptions || [];
+ const cusProducts = customer.customer_products.filter(
+ (product: FullCusProduct) => ACTIVE_STATUSES.includes(product.status),
+ );
- if (cusProducts.length === 0) return {};
+ if (cusProducts.length === 0) return {};
- const fullProducts = cusProducts.map((cp: FullCusProduct) =>
- cusProductToProduct({ cusProduct: cp })
- );
+ const fullProducts = cusProducts.map((cp: FullCusProduct) =>
+ cusProductToProduct({ cusProduct: cp }),
+ );
- const areAllProductsFree = checkIfAllProductsAreFree(fullProducts);
- const { startDates, endDates } = areAllProductsFree
- ? getDateRangesFromEntitlements(customer.customer_products)
- : getDateRangesFromSubscriptions(cusProducts, subscriptions);
+ const areAllProductsFree = checkIfAllProductsAreFree(fullProducts);
+ const { startDates, endDates } = areAllProductsFree
+ ? getDateRangesFromEntitlements(customer.customer_products)
+ : getDateRangesFromSubscriptions(cusProducts, subscriptions);
- if (startDates.length === 0 || endDates.length === 0) {
- return {};
- }
+ if (startDates.length === 0 || endDates.length === 0) {
+ return {};
+ }
- return calculateBillingCycleResult(startDates, endDates, intervalType);
+ return calculateBillingCycleResult(startDates, endDates, intervalType);
}
export function checkIfAllProductsAreFree(
- fullProducts: FullProduct[]
+ fullProducts: FullProduct[],
): boolean {
- return fullProducts.every((product: FullProduct) => {
- const isFree = isFreeProduct(product.prices);
+ return fullProducts.every((product: FullProduct) => {
+ const isFree = isFreeProduct(product.prices);
- return isFree;
- });
+ return isFree;
+ });
}
export function formatDateToString(date: Date): string {
- return date.toISOString().replace("T", " ").split(".")[0];
+ return date.toISOString().replace("T", " ").split(".")[0];
}
export function getDateRangesFromSubscriptions(
- customerProductsFiltered: FullCusProduct[],
- subscriptions: Subscription[]
+ customerProductsFiltered: FullCusProduct[],
+ subscriptions: Subscription[],
): { startDates: string[]; endDates: string[] } {
- const startDates: string[] = [];
- const endDates: string[] = [];
+ const startDates: string[] = [];
+ const endDates: string[] = [];
- customerProductsFiltered.forEach((product: FullCusProduct) => {
- product.subscription_ids?.forEach((subscriptionId: string) => {
- const subscription = subscriptions.find(
- (subscription: Subscription) =>
- subscription.stripe_id === subscriptionId
- );
+ customerProductsFiltered.forEach((product: FullCusProduct) => {
+ product.subscription_ids?.forEach((subscriptionId: string) => {
+ const subscription = subscriptions.find(
+ (subscription: Subscription) =>
+ subscription.stripe_id === subscriptionId,
+ );
- if (subscription) {
- startDates.push(
- formatDateToString(
- new Date((subscription.current_period_start ?? 0) * 1000)
- )
- );
- endDates.push(
- formatDateToString(
- new Date((subscription.current_period_end ?? 0) * 1000)
- )
- );
- }
- });
- });
+ if (subscription) {
+ startDates.push(
+ formatDateToString(
+ new Date((subscription.current_period_start ?? 0) * 1000),
+ ),
+ );
+ endDates.push(
+ formatDateToString(
+ new Date((subscription.current_period_end ?? 0) * 1000),
+ ),
+ );
+ }
+ });
+ });
- return { startDates, endDates };
+ return { startDates, endDates };
}
export function getDateRangesFromEntitlements(
- customerProducts?: FullCusProduct[]
+ customerProducts?: FullCusProduct[],
): { startDates: string[]; endDates: string[] } {
- const startDates: string[] = [];
- const endDates: string[] = [];
+ const startDates: string[] = [];
+ const endDates: string[] = [];
- console.log(
- "customer_entitlements",
- JSON.stringify(
- customerProducts?.map((x) => x.customer_entitlements),
- null,
- 4
- )
- );
+ console.log(
+ "customer_entitlements",
+ JSON.stringify(
+ customerProducts?.map((x) => x.customer_entitlements),
+ null,
+ 4,
+ ),
+ );
- if (!customerProducts || customerProducts.length < 1) {
- return { startDates, endDates };
- }
+ if (!customerProducts || customerProducts.length < 1) {
+ return { startDates, endDates };
+ }
- customerProducts.forEach((product: FullCusProduct) => {
- if (
- !product.customer_entitlements ||
- product.customer_entitlements.length < 1
- ) {
- return;
- }
+ customerProducts.forEach((product: FullCusProduct) => {
+ if (
+ !product.customer_entitlements ||
+ product.customer_entitlements.length < 1
+ ) {
+ return;
+ }
- product.customer_entitlements?.forEach(
- (entitlement: FullCustomerEntitlement) => {
- if (entitlement.next_reset_at) {
- endDates.push(
- formatDateToString(new Date(entitlement.next_reset_at))
- );
- }
+ product.customer_entitlements?.forEach(
+ (entitlement: FullCustomerEntitlement) => {
+ if (entitlement.next_reset_at) {
+ endDates.push(
+ formatDateToString(new Date(entitlement.next_reset_at)),
+ );
+ }
- const startDate = calculateStartDateFromInterval(
- entitlement.entitlement.interval,
- entitlement.next_reset_at,
- entitlement.created_at
- );
+ const startDate = calculateStartDateFromInterval(
+ entitlement.entitlement.interval,
+ entitlement.next_reset_at,
+ entitlement.created_at,
+ );
- if (startDate) {
- startDates.push(startDate);
- }
- }
- );
- });
+ if (startDate) {
+ startDates.push(startDate);
+ }
+ },
+ );
+ });
- return { startDates, endDates };
+ return { startDates, endDates };
}
export function calculateBillingCycleResult(
- startDates: string[],
- endDates: string[],
- intervalType: "1bc" | "3bc"
+ startDates: string[],
+ endDates: string[],
+ intervalType: "1bc" | "3bc",
) {
- const startDate = new Date(startDates[0]);
- const endDate = new Date(endDates[0]);
- const gap = endDate.getTime() - startDate.getTime();
- const gapDays = Math.floor(gap / (1000 * 60 * 60 * 24));
+ const startDate = new Date(startDates[0]);
+ const endDate = new Date(endDates[0]);
+ const gap = endDate.getTime() - startDate.getTime();
+ const gapDays = Math.floor(gap / (1000 * 60 * 60 * 24));
- return {
- startDate: startDates[0],
- endDate: endDates[0],
- gap: gapDays * (intervalType === "1bc" ? 1 : 3),
- };
+ return {
+ startDate: startDates[0],
+ endDate: endDates[0],
+ gap: gapDays * (intervalType === "1bc" ? 1 : 3),
+ };
}
export function calculateStartDateFromInterval(
- interval: EntInterval | null | undefined,
- nextResetAt: number | null | undefined,
- createdAt: number
+ interval: EntInterval | null | undefined,
+ nextResetAt: number | null | undefined,
+ createdAt: number,
): string | null {
- if (!nextResetAt && interval !== EntInterval.Lifetime) {
- return null;
- }
+ if (!nextResetAt && interval !== EntInterval.Lifetime) {
+ return null;
+ }
- switch (interval) {
- case EntInterval.Lifetime:
- return formatDateToString(new Date(createdAt));
- case EntInterval.Minute:
- return formatDateToString(new Date(nextResetAt! - 60 * 1000));
- case EntInterval.Hour:
- return formatDateToString(new Date(nextResetAt! - 60 * 60 * 1000));
- case EntInterval.Day:
- return formatDateToString(new Date(nextResetAt! - 24 * 60 * 60 * 1000));
- case EntInterval.Week:
- return formatDateToString(
- new Date(nextResetAt! - 7 * 24 * 60 * 60 * 1000)
- );
- case EntInterval.Month:
- const monthResetDate = new Date(nextResetAt!);
- monthResetDate.setMonth(monthResetDate.getMonth() - 1);
- return formatDateToString(monthResetDate);
- case EntInterval.Quarter:
- const quarterResetDate = new Date(nextResetAt!);
- quarterResetDate.setMonth(quarterResetDate.getMonth() - 3);
- return formatDateToString(quarterResetDate);
- case EntInterval.SemiAnnual:
- const semiAnnualResetDate = new Date(nextResetAt!);
- semiAnnualResetDate.setMonth(semiAnnualResetDate.getMonth() - 6);
- return formatDateToString(semiAnnualResetDate);
- case EntInterval.Year:
- const yearResetDate = new Date(nextResetAt!);
- yearResetDate.setFullYear(yearResetDate.getFullYear() - 1);
- return formatDateToString(yearResetDate);
- default:
- return null;
- }
+ switch (interval) {
+ case EntInterval.Lifetime:
+ return formatDateToString(new Date(createdAt));
+ case EntInterval.Minute:
+ return formatDateToString(new Date(nextResetAt! - 60 * 1000));
+ case EntInterval.Hour:
+ return formatDateToString(new Date(nextResetAt! - 60 * 60 * 1000));
+ case EntInterval.Day:
+ return formatDateToString(new Date(nextResetAt! - 24 * 60 * 60 * 1000));
+ case EntInterval.Week:
+ return formatDateToString(
+ new Date(nextResetAt! - 7 * 24 * 60 * 60 * 1000),
+ );
+ case EntInterval.Month:
+ const monthResetDate = new Date(nextResetAt!);
+ monthResetDate.setMonth(monthResetDate.getMonth() - 1);
+ return formatDateToString(monthResetDate);
+ case EntInterval.Quarter:
+ const quarterResetDate = new Date(nextResetAt!);
+ quarterResetDate.setMonth(quarterResetDate.getMonth() - 3);
+ return formatDateToString(quarterResetDate);
+ case EntInterval.SemiAnnual:
+ const semiAnnualResetDate = new Date(nextResetAt!);
+ semiAnnualResetDate.setMonth(semiAnnualResetDate.getMonth() - 6);
+ return formatDateToString(semiAnnualResetDate);
+ case EntInterval.Year:
+ const yearResetDate = new Date(nextResetAt!);
+ yearResetDate.setFullYear(yearResetDate.getFullYear() - 1);
+ return formatDateToString(yearResetDate);
+ default:
+ return null;
+ }
}
export function generateEventCountExpressions(
- eventNames: string[],
- noCount: boolean = false
+ eventNames: string[],
+ noCount: boolean = false,
): string {
- const expressions = eventNames.map((eventName) => {
- // Replicate ClickHouse's replaceAll(eventName, '''', '''''')
- const escapedEventName = eventName.replace(/'/g, "''");
- const columnName = noCount ? eventName : `${eventName}_count`;
- return `coalesce(sumIf(e.value, e.event_name = '${escapedEventName}'), 0) as \`${columnName}\``;
- });
+ const expressions = eventNames.map((eventName) => {
+ // Replicate ClickHouse's replaceAll(eventName, '''', '''''')
+ const escapedEventName = eventName.replace(/'/g, "''");
+ const columnName = noCount ? eventName : `${eventName}_count`;
+ return `coalesce(sumIf(e.value, e.event_name = '${escapedEventName}'), 0) as \`${columnName}\``;
+ });
- return expressions.join(",\n");
+ return expressions.join(",\n");
}
diff --git a/server/src/internal/analytics/handlers/handleCustomerCreated.ts b/server/src/internal/analytics/handlers/handleCustomerCreated.ts
index 236ef12cb..184995814 100644
--- a/server/src/internal/analytics/handlers/handleCustomerCreated.ts
+++ b/server/src/internal/analytics/handlers/handleCustomerCreated.ts
@@ -8,55 +8,55 @@ import { CusService } from "@/internal/customers/CusService.js";
import { ActionService } from "../ActionService.js";
export const addCustomerCreatedTask = async ({
- req,
- internalCustomerId,
- org,
- env,
+ req,
+ internalCustomerId,
+ org,
+ env,
}: {
- req: ExtendedRequest;
- internalCustomerId: string;
- org: Organization;
- env: AppEnv;
+ req: ExtendedRequest;
+ internalCustomerId: string;
+ org: Organization;
+ env: AppEnv;
}) => {
- await addTaskToQueue({
- jobName: JobName.HandleCustomerCreated,
- payload: {
- req: req ? parseReqForAction(req) : undefined,
- internalCustomerId,
- org,
- env,
- },
- });
+ await addTaskToQueue({
+ jobName: JobName.HandleCustomerCreated,
+ payload: {
+ req: req ? parseReqForAction(req) : undefined,
+ internalCustomerId,
+ org,
+ env,
+ },
+ });
};
export const handleCustomerCreated = async ({
- db,
- logger,
- data,
+ db,
+ logger,
+ data,
}: {
- db: DrizzleCli;
- logger: any;
- data: any;
+ db: DrizzleCli;
+ logger: any;
+ data: any;
}) => {
- const { req, internalCustomerId, org, env } = data;
+ const { req, internalCustomerId, org, env } = data;
- let customer = await CusService.getFull({
- db,
- idOrInternalId: internalCustomerId,
- orgId: org.id,
- env,
- });
+ let customer = await CusService.getFull({
+ db,
+ idOrInternalId: internalCustomerId,
+ orgId: org.id,
+ env,
+ });
- let action = constructAction({
- org,
- env,
- customer,
- type: ActionType.CustomerCreated,
- req,
- properties: {
- body: data.req.body,
- },
- });
+ let action = constructAction({
+ org,
+ env,
+ customer,
+ type: ActionType.CustomerCreated,
+ req,
+ properties: {
+ body: data.req.body,
+ },
+ });
- await ActionService.insert(db, action);
+ await ActionService.insert(db, action);
};
diff --git a/server/src/internal/analytics/handlers/handleProductsUpdated.ts b/server/src/internal/analytics/handlers/handleProductsUpdated.ts
index 6098162b5..362da3c5b 100644
--- a/server/src/internal/analytics/handlers/handleProductsUpdated.ts
+++ b/server/src/internal/analytics/handlers/handleProductsUpdated.ts
@@ -1,11 +1,11 @@
import {
- ActionType,
- AppEnv,
- AuthType,
- FullCusProduct,
- FullProduct,
- notNullish,
- Organization,
+ ActionType,
+ AppEnv,
+ AuthType,
+ FullCusProduct,
+ FullProduct,
+ notNullish,
+ Organization,
} from "@autumn/shared";
import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
@@ -27,203 +27,203 @@ import { cusProductToProduct } from "@autumn/shared";
import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js";
interface ActionDetails {
- request_id: string;
- method: string;
- path: string;
- timestamp: string;
- auth_type: AuthType;
- properties: any;
+ request_id: string;
+ method: string;
+ path: string;
+ timestamp: string;
+ auth_type: AuthType;
+ properties: any;
}
export const addProductsUpdatedWebhookTask = async ({
- req,
- org,
- env,
- customerId,
- internalCustomerId,
- cusProduct,
- scheduledCusProduct,
- deletedCusProduct,
- scenario,
- logger,
+ req,
+ org,
+ env,
+ customerId,
+ internalCustomerId,
+ cusProduct,
+ scheduledCusProduct,
+ deletedCusProduct,
+ scenario,
+ logger,
}: {
- req?: ExtendedRequest;
- org: Organization;
- env: AppEnv;
- customerId: string | null;
- internalCustomerId: string;
- cusProduct: FullCusProduct;
- scheduledCusProduct?: FullCusProduct;
- deletedCusProduct?: FullCusProduct;
- scenario: string;
- logger: any;
+ req?: ExtendedRequest;
+ org: Organization;
+ env: AppEnv;
+ customerId: string | null;
+ internalCustomerId: string;
+ cusProduct: FullCusProduct;
+ scheduledCusProduct?: FullCusProduct;
+ deletedCusProduct?: FullCusProduct;
+ scenario: string;
+ logger: any;
}) => {
- // Build action
+ // Build action
- try {
- await addTaskToQueue({
- jobName: JobName.HandleProductsUpdated,
- payload: {
- req: req ? parseReqForAction(req) : undefined,
- internalCustomerId,
- org,
- env,
- customerId,
- cusProduct,
- scheduledCusProduct,
- deletedCusProduct,
- scenario,
- },
- });
- } catch (error) {
- logger.error("Failed to add products updated webhook task to queue", {
- error,
- org_slug: org.slug,
- org_id: org.id,
- env,
- internalCustomerId,
- productId: cusProduct.product.id,
- cusProductId: cusProduct.id,
- // productId: product.id,
- });
- }
+ try {
+ await addTaskToQueue({
+ jobName: JobName.HandleProductsUpdated,
+ payload: {
+ req: req ? parseReqForAction(req) : undefined,
+ internalCustomerId,
+ org,
+ env,
+ customerId,
+ cusProduct,
+ scheduledCusProduct,
+ deletedCusProduct,
+ scenario,
+ },
+ });
+ } catch (error) {
+ logger.error("Failed to add products updated webhook task to queue", {
+ error,
+ org_slug: org.slug,
+ org_id: org.id,
+ env,
+ internalCustomerId,
+ productId: cusProduct.product.id,
+ cusProductId: cusProduct.id,
+ // productId: product.id,
+ });
+ }
};
export const handleProductsUpdated = async ({
- db,
- logger,
- data,
+ db,
+ logger,
+ data,
}: {
- db: DrizzleCli;
- logger: any;
- data: {
- req: Partial;
- actionDetails: ActionDetails;
- internalCustomerId: string;
- org: Organization;
- env: AppEnv;
- customerId: string;
- product: FullProduct;
- scenario: string;
- cusProduct: FullCusProduct;
- scheduledCusProduct?: FullCusProduct;
- deletedCusProduct?: FullCusProduct;
- };
+ db: DrizzleCli;
+ logger: any;
+ data: {
+ req: Partial;
+ actionDetails: ActionDetails;
+ internalCustomerId: string;
+ org: Organization;
+ env: AppEnv;
+ customerId: string;
+ product: FullProduct;
+ scenario: string;
+ cusProduct: FullCusProduct;
+ scheduledCusProduct?: FullCusProduct;
+ deletedCusProduct?: FullCusProduct;
+ };
}) => {
- const {
- req,
- org,
- env,
- scenario,
- cusProduct,
- scheduledCusProduct,
- deletedCusProduct,
- } = data;
+ const {
+ req,
+ org,
+ env,
+ scenario,
+ cusProduct,
+ scheduledCusProduct,
+ deletedCusProduct,
+ } = data;
- // Product:
- let product = cusProduct.product;
- let fullProduct: FullProduct = cusProductToProduct({ cusProduct });
+ // Product:
+ let product = cusProduct.product;
+ let fullProduct: FullProduct = cusProductToProduct({ cusProduct });
- let customer = await CusService.getFull({
- db,
- idOrInternalId: data.customerId || data.internalCustomerId,
- orgId: data.org.id,
- env: data.env,
- inStatuses: RELEVANT_STATUSES,
- entityId: cusProduct.internal_entity_id || undefined,
- });
+ let customer = await CusService.getFull({
+ db,
+ idOrInternalId: data.customerId || data.internalCustomerId,
+ orgId: data.org.id,
+ env: data.env,
+ inStatuses: RELEVANT_STATUSES,
+ entityId: cusProduct.internal_entity_id || undefined,
+ });
- const features = await FeatureService.list({
- db,
- orgId: org.id,
- env,
- });
+ const features = await FeatureService.list({
+ db,
+ orgId: org.id,
+ env,
+ });
- const cusDetails = await getCustomerDetails({
- db,
- customer: customer,
- org,
- env,
- features,
- logger,
- cusProducts: customer.customer_products,
- expand: [],
- });
+ const cusDetails = await getCustomerDetails({
+ db,
+ customer: customer,
+ org,
+ env,
+ features,
+ logger,
+ cusProducts: customer.customer_products,
+ expand: [],
+ });
- const productRes = await getProductResponse({
- product: fullProduct,
- features,
- });
+ const productRes = await getProductResponse({
+ product: fullProduct,
+ features,
+ });
- try {
- if (req) {
- let action = constructAction({
- org,
- env,
- customer,
- entity: customer.entity,
- type: ActionType.CustomerProductsUpdated,
- req,
- properties: {
- product_id: product.id,
- customer_product_id: cusProduct.id,
- scenario,
+ try {
+ if (req) {
+ let action = constructAction({
+ org,
+ env,
+ customer,
+ entity: customer.entity,
+ type: ActionType.CustomerProductsUpdated,
+ req,
+ properties: {
+ product_id: product.id,
+ customer_product_id: cusProduct.id,
+ scenario,
- deleted_product_id: deletedCusProduct?.product.id,
- scheduled_product_id: scheduledCusProduct?.product.id,
+ deleted_product_id: deletedCusProduct?.product.id,
+ scheduled_product_id: scheduledCusProduct?.product.id,
- body: req.body,
- },
- });
+ body: req.body,
+ },
+ });
- await ActionService.insert(db, action);
- } else {
- logger.warn(
- "products.updated, no req object found, skipping action insert"
- );
- }
- } catch (error: any) {
- // 23503 is for internal_customer_id not found
- if (error?.code !== "23503") {
- logger.error("Failed to log action to DB", {
- message: error.message,
- error: error,
- });
- }
- }
+ await ActionService.insert(db, action);
+ } else {
+ logger.warn(
+ "products.updated, no req object found, skipping action insert",
+ );
+ }
+ } catch (error: any) {
+ // 23503 is for internal_customer_id not found
+ if (error?.code !== "23503") {
+ logger.error("Failed to log action to DB", {
+ message: error.message,
+ error: error,
+ });
+ }
+ }
- let entityRes = null;
- if (notNullish(customer?.entity)) {
- entityRes = await getSingleEntityResponse({
- entityId: customer.entity!.id,
- org,
- env,
- fullCus: customer,
- entity: customer.entity!,
- features,
- });
- }
+ let entityRes = null;
+ if (notNullish(customer?.entity)) {
+ entityRes = await getSingleEntityResponse({
+ entityId: customer.entity!.id,
+ org,
+ env,
+ fullCus: customer,
+ entity: customer.entity!,
+ features,
+ });
+ }
- // console.log(`Sending svix event for customer ${customer.id}`);
- // console.log(
- // "Products:",
- // cusDetails.products.map((p) => ({
- // id: p.id,
- // status: p.status,
- // quantity: p.quantity,
- // }))
- // );
+ // console.log(`Sending svix event for customer ${customer.id}`);
+ // console.log(
+ // "Products:",
+ // cusDetails.products.map((p) => ({
+ // id: p.id,
+ // status: p.status,
+ // quantity: p.quantity,
+ // }))
+ // );
- // 2. Send Svix event
- await sendSvixEvent({
- org,
- env,
- eventType: "customer.products.updated",
- data: {
- scenario,
- customer: cusDetails,
- entity: entityRes,
- updated_product: productRes,
- },
- });
+ // 2. Send Svix event
+ await sendSvixEvent({
+ org,
+ env,
+ eventType: "customer.products.updated",
+ data: {
+ scenario,
+ customer: cusDetails,
+ entity: entityRes,
+ updated_product: productRes,
+ },
+ });
};
diff --git a/server/src/internal/analytics/internalAnalyticsRouter.ts b/server/src/internal/analytics/internalAnalyticsRouter.ts
index 6b7098d1e..44ed8a3be 100644
--- a/server/src/internal/analytics/internalAnalyticsRouter.ts
+++ b/server/src/internal/analytics/internalAnalyticsRouter.ts
@@ -3,13 +3,13 @@ import { CusService } from "../customers/CusService.js";
import { AnalyticsService } from "./AnalyticsService.js";
import { StatusCodes } from "http-status-codes";
import {
- AppEnv,
- ErrCode,
- Feature,
- FeatureType,
- FullCusProduct,
- FullCustomer,
- Organization,
+ AppEnv,
+ ErrCode,
+ Feature,
+ FeatureType,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
@@ -20,238 +20,238 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
export const analyticsRouter = Router();
analyticsRouter.get("/event_names", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "query event names",
- handler: async () => {
- const { db, org, env, features } = req;
- const { interval, event_names, customer_id } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "query event names",
+ handler: async () => {
+ const { db, org, env, features } = req;
+ const { interval, event_names, customer_id } = req.body;
- let result = await queryWithCache({
- action: CacheType.TopEvents,
- key: `${org.id}_${env}`,
- fn: async () => {
- const res = await AnalyticsService.getTopEventNames({
- req,
- });
+ let result = await queryWithCache({
+ action: CacheType.TopEvents,
+ key: `${org.id}_${env}`,
+ fn: async () => {
+ const res = await AnalyticsService.getTopEventNames({
+ req,
+ });
- return res?.eventNames;
- },
- });
+ return res?.eventNames;
+ },
+ });
- // const topEventNamesRes = await AnalyticsService.getTopEventNames({
- // req,
- // });
+ // const topEventNamesRes = await AnalyticsService.getTopEventNames({
+ // req,
+ // });
- // let result = topEventNamesRes?.eventNames;
+ // let result = topEventNamesRes?.eventNames;
- let featureIds: string[] = [];
- let eventNames: string[] = [];
+ let featureIds: string[] = [];
+ let eventNames: string[] = [];
- for (let i = 0; i < result.length; i++) {
- // Is an event name
- if (
- features.some(
- (feature: Feature) =>
- feature.type == FeatureType.Metered &&
- feature.config.filters?.[0]?.value.includes(result[i])
- )
- ) {
- eventNames.push(result[i]);
- } else if (
- features.some((feature: Feature) => feature.id === result[i])
- ) {
- featureIds.push(result[i]);
- }
+ for (let i = 0; i < result.length; i++) {
+ // Is an event name
+ if (
+ features.some(
+ (feature: Feature) =>
+ feature.type == FeatureType.Metered &&
+ feature.config.filters?.[0]?.value.includes(result[i]),
+ )
+ ) {
+ eventNames.push(result[i]);
+ } else if (
+ features.some((feature: Feature) => feature.id === result[i])
+ ) {
+ featureIds.push(result[i]);
+ }
- if (i >= 2) break;
- }
+ if (i >= 2) break;
+ }
- res.status(200).json({
- featureIds,
- eventNames,
- });
- },
- })
+ res.status(200).json({
+ featureIds,
+ eventNames,
+ });
+ },
+ }),
);
const getTopEvents = async ({ req }: { req: ExtendedRequest }) => {
- const { org, env, features } = req;
+ const { org, env, features } = req;
- // const result = await queryWithCache({
- // action: CacheType.TopEvents,
- // key: `${org.id}_${env}`,
- // fn: async () => {
- // const res = await AnalyticsService.getTopEventNames({
- // req,
- // });
+ // const result = await queryWithCache({
+ // action: CacheType.TopEvents,
+ // key: `${org.id}_${env}`,
+ // fn: async () => {
+ // const res = await AnalyticsService.getTopEventNames({
+ // req,
+ // });
- // return res?.eventNames;
- // },
- // });
+ // return res?.eventNames;
+ // },
+ // });
- const topEventNamesRes = await AnalyticsService.getTopEventNames({
- req,
- });
+ const topEventNamesRes = await AnalyticsService.getTopEventNames({
+ req,
+ });
- let result = topEventNamesRes?.eventNames;
+ let result = topEventNamesRes?.eventNames;
- let featureIds: string[] = [];
- let eventNames: string[] = [];
+ let featureIds: string[] = [];
+ let eventNames: string[] = [];
- for (let i = 0; i < result.length; i++) {
- // Is an event name
- if (
- features.some(
- (feature: Feature) =>
- feature.type == FeatureType.Metered &&
- feature.config.filters?.[0]?.value.includes(result[i])
- )
- ) {
- eventNames.push(result[i]);
- } else if (features.some((feature: Feature) => feature.id === result[i])) {
- featureIds.push(result[i]);
- }
+ for (let i = 0; i < result.length; i++) {
+ // Is an event name
+ if (
+ features.some(
+ (feature: Feature) =>
+ feature.type == FeatureType.Metered &&
+ feature.config.filters?.[0]?.value.includes(result[i]),
+ )
+ ) {
+ eventNames.push(result[i]);
+ } else if (features.some((feature: Feature) => feature.id === result[i])) {
+ featureIds.push(result[i]);
+ }
- if (i >= 2) break;
- }
+ if (i >= 2) break;
+ }
- return {
- featureIds,
- eventNames,
- };
+ return {
+ featureIds,
+ eventNames,
+ };
};
analyticsRouter.post("/events", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "query events by customer id",
- handler: async () => {
- const { db, org, env, features } = req;
- let { interval, event_names, customer_id } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "query events by customer id",
+ handler: async () => {
+ const { db, org, env, features } = req;
+ let { interval, event_names, customer_id } = req.body;
- let topEvents:
- | { featureIds: string[]; eventNames: string[] }
- | undefined = undefined;
+ let topEvents:
+ | { featureIds: string[]; eventNames: string[] }
+ | undefined = undefined;
- if (!event_names || event_names.length === 0) {
- topEvents = await getTopEvents({ req });
- event_names = [...topEvents.eventNames, ...topEvents.featureIds];
- }
+ if (!event_names || event_names.length === 0) {
+ topEvents = await getTopEvents({ req });
+ event_names = [...topEvents.eventNames, ...topEvents.featureIds];
+ }
- let aggregateAll = false;
- let customer: FullCustomer | undefined = undefined;
- let bcExclusionFlag = false;
+ let aggregateAll = false;
+ let customer: FullCustomer | undefined = undefined;
+ let bcExclusionFlag = false;
- if (!customer_id) {
- // No customer ID provided, set aggregateAll to true
- aggregateAll = true;
- } else {
- // Customer ID provided, fetch customer data
- customer = await CusService.getFull({
- db,
- idOrInternalId: customer_id,
- orgId: org.id,
- env,
- withSubs: true,
- });
+ if (!customer_id) {
+ // No customer ID provided, set aggregateAll to true
+ aggregateAll = true;
+ } else {
+ // Customer ID provided, fetch customer data
+ customer = await CusService.getFull({
+ db,
+ idOrInternalId: customer_id,
+ orgId: org.id,
+ env,
+ withSubs: true,
+ });
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- // Check for bcExclusionFlag only if we have a specific customer
- if (customer.customer_products) {
- customer.customer_products.forEach((product: FullCusProduct) => {
- if (product.product.is_default) {
- bcExclusionFlag = true;
- }
- });
- }
- }
+ // Check for bcExclusionFlag only if we have a specific customer
+ if (customer.customer_products) {
+ customer.customer_products.forEach((product: FullCusProduct) => {
+ if (product.product.is_default) {
+ bcExclusionFlag = true;
+ }
+ });
+ }
+ }
- if (event_names && Array.isArray(event_names)) {
- event_names = event_names.filter((name: string) => name !== "");
- }
+ if (event_names && Array.isArray(event_names)) {
+ event_names = event_names.filter((name: string) => name !== "");
+ }
- const events = await AnalyticsService.getTimeseriesEvents({
- req,
- params: {
- customer_id,
- interval,
- event_names,
- },
- customer,
- aggregateAll,
- });
+ const events = await AnalyticsService.getTimeseriesEvents({
+ req,
+ params: {
+ customer_id,
+ interval,
+ event_names,
+ },
+ customer,
+ aggregateAll,
+ });
- // console.log("events", events);
+ // console.log("events", events);
- res.status(200).json({
- customer,
- events,
- features,
- eventNames: event_names,
- topEvents,
- bcExclusionFlag,
- });
- },
- })
+ res.status(200).json({
+ customer,
+ events,
+ features,
+ eventNames: event_names,
+ topEvents,
+ bcExclusionFlag,
+ });
+ },
+ }),
);
analyticsRouter.post("/raw", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "query raw events by customer id",
- handler: async () => {
- const { db, org, env } = req;
- const { interval, customer_id } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "query raw events by customer id",
+ handler: async () => {
+ const { db, org, env } = req;
+ const { interval, customer_id } = req.body;
- let aggregateAll = false;
- let customer: FullCustomer | undefined = undefined;
+ let aggregateAll = false;
+ let customer: FullCustomer | undefined = undefined;
- if (!customer_id) {
- // No customer ID provided, set aggregateAll to true
- aggregateAll = true;
- } else {
- // Customer ID provided, fetch customer data
- customer = await CusService.getFull({
- db,
- idOrInternalId: customer_id,
- orgId: org.id,
- env,
- withSubs: true,
- });
+ if (!customer_id) {
+ // No customer ID provided, set aggregateAll to true
+ aggregateAll = true;
+ } else {
+ // Customer ID provided, fetch customer data
+ customer = await CusService.getFull({
+ db,
+ idOrInternalId: customer_id,
+ orgId: org.id,
+ env,
+ withSubs: true,
+ });
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
+ }
- const events = await AnalyticsService.getRawEvents({
- req,
- params: {
- // customer_id: customer?.internal_id,
- customer_id: customer?.id,
- interval,
- },
- customer,
- aggregateAll,
- });
+ const events = await AnalyticsService.getRawEvents({
+ req,
+ params: {
+ // customer_id: customer?.internal_id,
+ customer_id: customer?.id,
+ interval,
+ },
+ customer,
+ aggregateAll,
+ });
- res.status(200).json({
- rawEvents: events,
- });
- },
- })
+ res.status(200).json({
+ rawEvents: events,
+ });
+ },
+ }),
);
diff --git a/server/src/internal/analytics/runActionHandlerTask.ts b/server/src/internal/analytics/runActionHandlerTask.ts
index 619b3010c..498103bc2 100644
--- a/server/src/internal/analytics/runActionHandlerTask.ts
+++ b/server/src/internal/analytics/runActionHandlerTask.ts
@@ -7,49 +7,49 @@ import { handleProductsUpdated } from "./handlers/handleProductsUpdated.js";
import { handleCustomerCreated } from "./handlers/handleCustomerCreated.js";
export const runActionHandlerTask = async ({
- queue,
- job,
- logger,
- db,
- useBackup,
+ queue,
+ job,
+ logger,
+ db,
+ useBackup,
}: {
- queue: Queue;
- job: Job;
- logger: any;
- db: DrizzleCli;
- useBackup: boolean;
+ queue: Queue;
+ job: Job;
+ logger: any;
+ db: DrizzleCli;
+ useBackup: boolean;
}) => {
- let payload = job.data;
- let internalCustomerId = payload.internalCustomerId;
- let lockKey = `action:${internalCustomerId}`;
+ let payload = job.data;
+ let internalCustomerId = payload.internalCustomerId;
+ let lockKey = `action:${internalCustomerId}`;
- try {
- let lock = await getLock({ queue, job, lockKey, useBackup });
- if (!lock) return;
+ try {
+ let lock = await getLock({ queue, job, lockKey, useBackup });
+ if (!lock) return;
- switch (job.name) {
- case JobName.HandleProductsUpdated:
- await handleProductsUpdated({
- db,
- logger,
- data: payload,
- });
- break;
- case JobName.HandleCustomerCreated:
- await handleCustomerCreated({
- db,
- logger,
- data: payload,
- });
- break;
- }
- } catch (error: any) {
- logger.error("Error processing action handler job:", {
- // jobName: job.name,
- // payload,
- message: error.message,
- });
- } finally {
- await releaseLock({ lockKey, useBackup });
- }
+ switch (job.name) {
+ case JobName.HandleProductsUpdated:
+ await handleProductsUpdated({
+ db,
+ logger,
+ data: payload,
+ });
+ break;
+ case JobName.HandleCustomerCreated:
+ await handleCustomerCreated({
+ db,
+ logger,
+ data: payload,
+ });
+ break;
+ }
+ } catch (error: any) {
+ logger.error("Error processing action handler job:", {
+ // jobName: job.name,
+ // payload,
+ message: error.message,
+ });
+ } finally {
+ await releaseLock({ lockKey, useBackup });
+ }
};
diff --git a/server/src/internal/analytics/tracer/spanUtils.ts b/server/src/internal/analytics/tracer/spanUtils.ts
index daaf74332..e2fccf6a9 100644
--- a/server/src/internal/analytics/tracer/spanUtils.ts
+++ b/server/src/internal/analytics/tracer/spanUtils.ts
@@ -3,20 +3,20 @@ import { context, trace } from "@opentelemetry/api";
const tracer = trace.getTracer("express");
export const withSpan = ({
- name,
- attributes,
- fn,
+ name,
+ attributes,
+ fn,
}: {
- name: string;
- attributes: Record;
- fn: () => Promise;
+ name: string;
+ attributes: Record;
+ fn: () => Promise;
}) => {
- const span = tracer.startSpan(name);
- span.setAttributes(attributes);
+ const span = tracer.startSpan(name);
+ span.setAttributes(attributes);
- return context.with(trace.setSpan(context.active(), span), async () => {
- const result = await fn();
- span.end();
- return result;
- });
+ return context.with(trace.setSpan(context.active(), span), async () => {
+ const result = await fn();
+ span.end();
+ return result;
+ });
};
diff --git a/server/src/internal/api/batch/CusBatchService.ts b/server/src/internal/api/batch/CusBatchService.ts
index ab38cddaf..41069c89e 100644
--- a/server/src/internal/api/batch/CusBatchService.ts
+++ b/server/src/internal/api/batch/CusBatchService.ts
@@ -1,11 +1,11 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- AppEnv,
- Organization,
- CusProductStatus,
- FullCustomer,
- Feature,
- CusExpand,
+ AppEnv,
+ Organization,
+ CusProductStatus,
+ FullCustomer,
+ Feature,
+ CusExpand,
} from "@autumn/shared";
import type { NodeClickHouseClient } from "@clickhouse/client/dist/client.js";
@@ -14,165 +14,165 @@ import { getCustomerDetails } from "../../customers/cusUtils/getCustomerDetails.
import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
export class CusBatchService {
- static async getByInternalIds({
- db,
- org,
- env,
- internalCustomerIds,
- }: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- internalCustomerIds: string[];
- }) {
- let query = getPaginatedFullCusQuery({
- orgId: org.id,
- env,
- includeInvoices: true,
- withEntities: true,
- withTrialsUsed: false,
- withSubs: true,
- limit: 100,
- offset: 0,
- internalCustomerIds,
- });
- let results = await db.execute(query);
+ static async getByInternalIds({
+ db,
+ org,
+ env,
+ internalCustomerIds,
+ }: {
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ internalCustomerIds: string[];
+ }) {
+ let query = getPaginatedFullCusQuery({
+ orgId: org.id,
+ env,
+ includeInvoices: true,
+ withEntities: true,
+ withTrialsUsed: false,
+ withSubs: true,
+ limit: 100,
+ offset: 0,
+ internalCustomerIds,
+ });
+ let results = await db.execute(query);
- return results as unknown as FullCustomer[];
- }
+ return results as unknown as FullCustomer[];
+ }
- static async getPage({
- db,
- ch,
- org,
- env,
- limit,
- offset,
- features,
- statuses,
- expand = [],
- logger = console,
- reqApiVersion,
- }: {
- db: DrizzleCli;
- ch: NodeClickHouseClient;
- org: Organization;
- env: AppEnv;
- limit: number;
- offset: number;
- features: Feature[];
- statuses: CusProductStatus[];
- expand?: CusExpand[];
- logger?: any;
- reqApiVersion?: number;
- }) {
- if (!limit) limit = 10;
- if (!offset) offset = 0;
+ static async getPage({
+ db,
+ ch,
+ org,
+ env,
+ limit,
+ offset,
+ features,
+ statuses,
+ expand = [],
+ logger = console,
+ reqApiVersion,
+ }: {
+ db: DrizzleCli;
+ ch: NodeClickHouseClient;
+ org: Organization;
+ env: AppEnv;
+ limit: number;
+ offset: number;
+ features: Feature[];
+ statuses: CusProductStatus[];
+ expand?: CusExpand[];
+ logger?: any;
+ reqApiVersion?: number;
+ }) {
+ if (!limit) limit = 10;
+ if (!offset) offset = 0;
- if (!statuses) statuses = RELEVANT_STATUSES;
+ if (!statuses) statuses = RELEVANT_STATUSES;
- const includeInvoices = expand.includes(CusExpand.Invoices);
- const withEntities = expand.includes(CusExpand.Entities);
- const withTrialsUsed = expand.includes(CusExpand.TrialsUsed);
+ const includeInvoices = expand.includes(CusExpand.Invoices);
+ const withEntities = expand.includes(CusExpand.Entities);
+ const withTrialsUsed = expand.includes(CusExpand.TrialsUsed);
- let query = getPaginatedFullCusQuery({
- orgId: org.id,
- env,
- inStatuses: statuses,
- includeInvoices,
- withEntities,
- withTrialsUsed,
- withSubs: true,
- limit,
- offset,
- });
- let results = await db.execute(query);
- let finals = [];
- for (let result of results) {
- try {
- const normalizedCustomer = this.normalizeCustomerData(result);
- const customer = normalizedCustomer as FullCustomer;
- const cusProducts = customer.customer_products || [];
+ let query = getPaginatedFullCusQuery({
+ orgId: org.id,
+ env,
+ inStatuses: statuses,
+ includeInvoices,
+ withEntities,
+ withTrialsUsed,
+ withSubs: true,
+ limit,
+ offset,
+ });
+ let results = await db.execute(query);
+ let finals = [];
+ for (let result of results) {
+ try {
+ const normalizedCustomer = this.normalizeCustomerData(result);
+ const customer = normalizedCustomer as FullCustomer;
+ const cusProducts = customer.customer_products || [];
- const customerDetails = await getCustomerDetails({
- db,
- customer,
- features,
- org,
- env,
- params: {},
- logger: console,
- cusProducts,
- expand: expand,
- reqApiVersion: reqApiVersion,
- });
+ const customerDetails = await getCustomerDetails({
+ db,
+ customer,
+ features,
+ org,
+ env,
+ params: {},
+ logger: console,
+ cusProducts,
+ expand: expand,
+ reqApiVersion: reqApiVersion,
+ });
- finals.push(customerDetails);
- } catch (error) {
- console.error(`Failed to process customer ${result.id}:`, error);
- }
- }
+ finals.push(customerDetails);
+ } catch (error) {
+ console.error(`Failed to process customer ${result.id}:`, error);
+ }
+ }
- return finals;
- }
+ return finals;
+ }
- /**
- * Normalize customer data by converting string fields to numbers
- */
- private static normalizeCustomerData(rawCustomer: any): any {
- const normalizeTimestamp = (value: any): number => {
- if (typeof value === "string") {
- const parsed = parseInt(value, 10);
- return isNaN(parsed) ? Date.now() : parsed;
- }
- return typeof value === "number" ? value : Date.now();
- };
+ /**
+ * Normalize customer data by converting string fields to numbers
+ */
+ private static normalizeCustomerData(rawCustomer: any): any {
+ const normalizeTimestamp = (value: any): number => {
+ if (typeof value === "string") {
+ const parsed = parseInt(value, 10);
+ return isNaN(parsed) ? Date.now() : parsed;
+ }
+ return typeof value === "number" ? value : Date.now();
+ };
- const normalizedCustomer = {
- ...rawCustomer,
- created_at: normalizeTimestamp(rawCustomer.created_at),
- };
+ const normalizedCustomer = {
+ ...rawCustomer,
+ created_at: normalizeTimestamp(rawCustomer.created_at),
+ };
- // Normalize customer products
- if (
- rawCustomer.customer_products &&
- Array.isArray(rawCustomer.customer_products)
- ) {
- normalizedCustomer.customer_products = rawCustomer.customer_products.map(
- (cp: any) => ({
- ...cp,
- created_at: normalizeTimestamp(cp.created_at),
- starts_at: cp.starts_at
- ? normalizeTimestamp(cp.starts_at)
- : normalizeTimestamp(cp.created_at),
- canceled_at: cp.canceled_at
- ? normalizeTimestamp(cp.canceled_at)
- : null,
- ended_at: cp.ended_at ? normalizeTimestamp(cp.ended_at) : null,
- trial_ends_at: cp.trial_ends_at
- ? normalizeTimestamp(cp.trial_ends_at)
- : null,
- quantity: cp.quantity ? parseInt(cp.quantity, 10) || 1 : 1,
- options: cp.options || [],
- collection_method: cp.collection_method || "charge_automatically",
- subscription_ids: cp.subscription_ids || [],
- scheduled_ids: cp.scheduled_ids || [],
- // Normalize customer entitlements
- customer_entitlements: (cp.customer_entitlements || []).map(
- (ce: any) => ({
- ...ce,
- created_at: normalizeTimestamp(ce.created_at),
- next_reset_at: ce.next_reset_at
- ? normalizeTimestamp(ce.next_reset_at)
- : null,
- balance: ce.balance ? parseFloat(ce.balance) || 0 : 0,
- adjustment: ce.adjustment ? parseFloat(ce.adjustment) || 0 : 0,
- })
- ),
- })
- );
- }
+ // Normalize customer products
+ if (
+ rawCustomer.customer_products &&
+ Array.isArray(rawCustomer.customer_products)
+ ) {
+ normalizedCustomer.customer_products = rawCustomer.customer_products.map(
+ (cp: any) => ({
+ ...cp,
+ created_at: normalizeTimestamp(cp.created_at),
+ starts_at: cp.starts_at
+ ? normalizeTimestamp(cp.starts_at)
+ : normalizeTimestamp(cp.created_at),
+ canceled_at: cp.canceled_at
+ ? normalizeTimestamp(cp.canceled_at)
+ : null,
+ ended_at: cp.ended_at ? normalizeTimestamp(cp.ended_at) : null,
+ trial_ends_at: cp.trial_ends_at
+ ? normalizeTimestamp(cp.trial_ends_at)
+ : null,
+ quantity: cp.quantity ? parseInt(cp.quantity, 10) || 1 : 1,
+ options: cp.options || [],
+ collection_method: cp.collection_method || "charge_automatically",
+ subscription_ids: cp.subscription_ids || [],
+ scheduled_ids: cp.scheduled_ids || [],
+ // Normalize customer entitlements
+ customer_entitlements: (cp.customer_entitlements || []).map(
+ (ce: any) => ({
+ ...ce,
+ created_at: normalizeTimestamp(ce.created_at),
+ next_reset_at: ce.next_reset_at
+ ? normalizeTimestamp(ce.next_reset_at)
+ : null,
+ balance: ce.balance ? parseFloat(ce.balance) || 0 : 0,
+ adjustment: ce.adjustment ? parseFloat(ce.adjustment) || 0 : 0,
+ }),
+ ),
+ }),
+ );
+ }
- return normalizedCustomer;
- }
+ return normalizedCustomer;
+ }
}
diff --git a/server/src/internal/api/batch/batchRouter.ts b/server/src/internal/api/batch/batchRouter.ts
index 1dc1ab693..bd2125e82 100644
--- a/server/src/internal/api/batch/batchRouter.ts
+++ b/server/src/internal/api/batch/batchRouter.ts
@@ -3,4 +3,4 @@ import { handleBatchCustomers } from "./handlers/handleBatchCustomers.js";
export const batchRouter = Router();
-batchRouter.post("/customers", handleBatchCustomers);
\ No newline at end of file
+batchRouter.post("/customers", handleBatchCustomers);
diff --git a/server/src/internal/api/batch/handlers/handleBatchCustomers.ts b/server/src/internal/api/batch/handlers/handleBatchCustomers.ts
index 2f111e0ba..610459a31 100644
--- a/server/src/internal/api/batch/handlers/handleBatchCustomers.ts
+++ b/server/src/internal/api/batch/handlers/handleBatchCustomers.ts
@@ -7,89 +7,89 @@ import { CusProductStatus, ErrCode } from "@autumn/shared";
import z from "zod";
const schema = z.object({
- limit: z
- .number({
- required_error: "limit is required",
- invalid_type_error: "limit must be a number",
- })
- .int({ message: "limit must be an integer" })
- .min(10, { message: "limit must be at least 10" })
- .max(100, { message: "limit must be at most 100" })
- .default(10),
+ limit: z
+ .number({
+ required_error: "limit is required",
+ invalid_type_error: "limit must be a number",
+ })
+ .int({ message: "limit must be an integer" })
+ .min(10, { message: "limit must be at least 10" })
+ .max(100, { message: "limit must be at most 100" })
+ .default(10),
- offset: z
- .number({
- invalid_type_error: "offset must be a number",
- })
- .int({ message: "offset must be an integer" })
- .min(0, { message: "offset must be at least 0" })
- .optional()
- .default(0),
+ offset: z
+ .number({
+ invalid_type_error: "offset must be a number",
+ })
+ .int({ message: "offset must be an integer" })
+ .min(0, { message: "offset must be at least 0" })
+ .optional()
+ .default(0),
- statuses: z
- .array(
- z.nativeEnum(CusProductStatus, {
- errorMap: () => ({
- message: `status must be one of: ${Object.values(CusProductStatus).join(", ")}`,
- }),
- }),
- {
- invalid_type_error: "statuses must be an array of strings",
- }
- )
- .optional()
- .refine(
- (statuses) =>
- !statuses ||
- statuses.every((status) =>
- Object.values(CusProductStatus).includes(status)
- ),
- {
- message: `statuses must contain only valid values: ${Object.values(CusProductStatus).join(", ")}`,
- }
- ),
+ statuses: z
+ .array(
+ z.nativeEnum(CusProductStatus, {
+ errorMap: () => ({
+ message: `status must be one of: ${Object.values(CusProductStatus).join(", ")}`,
+ }),
+ }),
+ {
+ invalid_type_error: "statuses must be an array of strings",
+ },
+ )
+ .optional()
+ .refine(
+ (statuses) =>
+ !statuses ||
+ statuses.every((status) =>
+ Object.values(CusProductStatus).includes(status),
+ ),
+ {
+ message: `statuses must contain only valid values: ${Object.values(CusProductStatus).join(", ")}`,
+ },
+ ),
});
export const handleBatchCustomers = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "batch get customers",
- queryValidator: schema,
- loader: async ({
- query,
- req,
- }: {
- query: z.infer;
- req: ExtendedRequest;
- }) => {
- return await CusBatchService.getPage({
- db: req.db,
- ch: req.clickhouseClient,
- org: req.org,
- env: req.env,
- limit: query.limit,
- offset: query.offset,
- features: req.features,
- statuses: query.statuses ?? [],
- logger: req.logtail,
- reqApiVersion: req.apiVersion,
- });
- },
- handler: async (_, res: Response, data, query: z.infer) => {
- if (data) {
- res.status(200).json({
- list: data,
- total: data.length,
- limit: query.limit,
- offset: query.offset,
- });
- } else {
- throw new RecaseError({
- message: "No customers found",
- code: ErrCode.CustomersNotFound,
- });
- }
- },
- withSpan: true,
- });
+ routeHandler({
+ req,
+ res,
+ action: "batch get customers",
+ queryValidator: schema,
+ loader: async ({
+ query,
+ req,
+ }: {
+ query: z.infer;
+ req: ExtendedRequest;
+ }) => {
+ return await CusBatchService.getPage({
+ db: req.db,
+ ch: req.clickhouseClient,
+ org: req.org,
+ env: req.env,
+ limit: query.limit,
+ offset: query.offset,
+ features: req.features,
+ statuses: query.statuses ?? [],
+ logger: req.logtail,
+ reqApiVersion: req.apiVersion,
+ });
+ },
+ handler: async (_, res: Response, data, query: z.infer) => {
+ if (data) {
+ res.status(200).json({
+ list: data,
+ total: data.length,
+ limit: query.limit,
+ offset: query.offset,
+ });
+ } else {
+ throw new RecaseError({
+ message: "No customers found",
+ code: ErrCode.CustomersNotFound,
+ });
+ }
+ },
+ withSpan: true,
+ });
diff --git a/server/src/internal/api/components/componentRouter.ts b/server/src/internal/api/components/componentRouter.ts
index 699370069..149004fce 100644
--- a/server/src/internal/api/components/componentRouter.ts
+++ b/server/src/internal/api/components/componentRouter.ts
@@ -13,115 +13,115 @@ import { Router } from "express";
export const componentRouter: Router = Router();
componentRouter.get("/pricing_table", async (req: any, res) =>
- routeHandler({
- req,
- res,
- action: "get pricing table",
- handler: async () => {
- const { orgId, env, db } = req;
- let customerId = req.query.customer_id;
+ routeHandler({
+ req,
+ res,
+ action: "get pricing table",
+ handler: async () => {
+ const { orgId, env, db } = req;
+ let customerId = req.query.customer_id;
- const [org, features, products, customer] = await Promise.all([
- OrgService.getFromReq(req),
- FeatureService.getFromReq(req),
- ProductService.listFull({ db, orgId, env }),
- (async () => {
- if (!customerId) {
- return undefined;
- }
- return await CusService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: customerId,
- });
- })(),
- ]);
+ const [org, features, products, customer] = await Promise.all([
+ OrgService.getFromReq(req),
+ FeatureService.getFromReq(req),
+ ProductService.listFull({ db, orgId, env }),
+ (async () => {
+ if (!customerId) {
+ return undefined;
+ }
+ return await CusService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customerId,
+ });
+ })(),
+ ]);
- // Sort by add ons
- products.sort((a, b) => {
- return a.is_add_on ? 1 : -1;
- });
+ // Sort by add ons
+ products.sort((a, b) => {
+ return a.is_add_on ? 1 : -1;
+ });
- // 1. Sort products by price
- products.sort((a, b) => {
- let isUpgradeA = isProductUpgrade({
- prices1: a.prices,
- prices2: b.prices,
- usageAlwaysUpgrade: false,
- });
+ // 1. Sort products by price
+ products.sort((a, b) => {
+ let isUpgradeA = isProductUpgrade({
+ prices1: a.prices,
+ prices2: b.prices,
+ usageAlwaysUpgrade: false,
+ });
- if (isUpgradeA) {
- return -1;
- } else {
- return 1;
- }
- });
+ if (isUpgradeA) {
+ return -1;
+ } else {
+ return 1;
+ }
+ });
- let batchResponse = [];
- for (let p of products) {
- let prod = await getProductResponse({ product: p, features });
- let curMainProduct, curScheduledProduct;
+ let batchResponse = [];
+ for (let p of products) {
+ let prod = await getProductResponse({ product: p, features });
+ let curMainProduct, curScheduledProduct;
- if (customer) {
- let res = getExistingCusProducts({
- product: p,
- cusProducts: customer.customer_products,
- });
+ if (customer) {
+ let res = getExistingCusProducts({
+ product: p,
+ cusProducts: customer.customer_products,
+ });
- curMainProduct = res.curMainProduct;
- curScheduledProduct = res.curScheduledProduct;
- }
+ curMainProduct = res.curMainProduct;
+ curScheduledProduct = res.curScheduledProduct;
+ }
- batchResponse.push(
- toPricecnProduct({
- db,
- org,
- product: prod as ProductV2,
- fullProduct: p,
- features,
- curMainProduct,
- curScheduledProduct,
- otherProducts: products.filter((other) => other.id != p.id),
- fullCus: customer,
- }),
- );
- }
+ batchResponse.push(
+ toPricecnProduct({
+ db,
+ org,
+ product: prod as ProductV2,
+ fullProduct: p,
+ features,
+ curMainProduct,
+ curScheduledProduct,
+ otherProducts: products.filter((other) => other.id != p.id),
+ fullCus: customer,
+ }),
+ );
+ }
- let pricecnProds = await Promise.all(batchResponse);
+ let pricecnProds = await Promise.all(batchResponse);
- // let pricecnProds = await Promise.all(
- // products
- // // .filter((p) => !p.is_add_on)
- // .map(async (p) => {
- // let prod = getProductResponse({ product: p, features });
- // let curMainProduct, curScheduledProduct;
+ // let pricecnProds = await Promise.all(
+ // products
+ // // .filter((p) => !p.is_add_on)
+ // .map(async (p) => {
+ // let prod = getProductResponse({ product: p, features });
+ // let curMainProduct, curScheduledProduct;
- // if (cusProducts) {
- // let res = getExistingCusProducts({
- // product: p,
- // cusProducts: cusProducts,
- // });
+ // if (cusProducts) {
+ // let res = getExistingCusProducts({
+ // product: p,
+ // cusProducts: cusProducts,
+ // });
- // curMainProduct = res.curMainProduct;
- // curScheduledProduct = res.curScheduledProduct;
- // }
+ // curMainProduct = res.curMainProduct;
+ // curScheduledProduct = res.curScheduledProduct;
+ // }
- // return toPricecnProduct({
- // org,
- // product: prod as ProductV2,
- // fullProduct: p,
- // features,
- // curMainProduct,
- // curScheduledProduct,
- // otherProducts: products.filter((other) => other.id != p.id),
- // });
- // }),
- // );
+ // return toPricecnProduct({
+ // org,
+ // product: prod as ProductV2,
+ // fullProduct: p,
+ // features,
+ // curMainProduct,
+ // curScheduledProduct,
+ // otherProducts: products.filter((other) => other.id != p.id),
+ // });
+ // }),
+ // );
- res.status(200).json({
- list: pricecnProds,
- });
- },
- }),
+ res.status(200).json({
+ list: pricecnProds,
+ });
+ },
+ }),
);
diff --git a/server/src/internal/api/entities/EntityService.ts b/server/src/internal/api/entities/EntityService.ts
index cdc6f656b..34aa34e82 100644
--- a/server/src/internal/api/entities/EntityService.ts
+++ b/server/src/internal/api/entities/EntityService.ts
@@ -5,154 +5,154 @@ import { Entity, entities } from "@autumn/shared";
import { and, eq, inArray, sql } from "drizzle-orm";
export class EntityService {
- static async get({
- db,
- id,
- internalCustomerId,
- internalFeatureId,
- }: {
- db: DrizzleCli;
- id: string;
- internalCustomerId: string;
- internalFeatureId: string;
- }) {
- return await db.query.entities.findFirst({
- where: (entities, { eq, and }) =>
- and(
- eq(entities.id, id),
- eq(entities.internal_customer_id, internalCustomerId),
- eq(entities.internal_feature_id, internalFeatureId),
- ),
- });
- }
- static async getNull({
- db,
- orgId,
- env,
- internalCustomerId,
- internalFeatureId,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: string;
- internalCustomerId: string;
- internalFeatureId: string;
- }) {
- return await db.query.entities.findFirst({
- where: (entities, { eq, and }) =>
- and(
- eq(entities.id, sql`null`),
- eq(entities.org_id, orgId),
- eq(entities.env, env),
- eq(entities.internal_customer_id, internalCustomerId),
- eq(entities.internal_feature_id, internalFeatureId),
- ),
- });
- }
- static async insert({ db, data }: { db: DrizzleCli; data: any }) {
- if (data.length === 0) {
- return [];
- }
+ static async get({
+ db,
+ id,
+ internalCustomerId,
+ internalFeatureId,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ internalCustomerId: string;
+ internalFeatureId: string;
+ }) {
+ return await db.query.entities.findFirst({
+ where: (entities, { eq, and }) =>
+ and(
+ eq(entities.id, id),
+ eq(entities.internal_customer_id, internalCustomerId),
+ eq(entities.internal_feature_id, internalFeatureId),
+ ),
+ });
+ }
+ static async getNull({
+ db,
+ orgId,
+ env,
+ internalCustomerId,
+ internalFeatureId,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: string;
+ internalCustomerId: string;
+ internalFeatureId: string;
+ }) {
+ return await db.query.entities.findFirst({
+ where: (entities, { eq, and }) =>
+ and(
+ eq(entities.id, sql`null`),
+ eq(entities.org_id, orgId),
+ eq(entities.env, env),
+ eq(entities.internal_customer_id, internalCustomerId),
+ eq(entities.internal_feature_id, internalFeatureId),
+ ),
+ });
+ }
+ static async insert({ db, data }: { db: DrizzleCli; data: any }) {
+ if (data.length === 0) {
+ return [];
+ }
- const results = await db
- .insert(entities)
- .values(data as any)
- .returning();
+ const results = await db
+ .insert(entities)
+ .values(data as any)
+ .returning();
- return results as Entity[];
- }
+ return results as Entity[];
+ }
- static async getByInternalId({
- db,
- internalId,
- }: {
- db: DrizzleCli;
- internalId: string;
- }) {
- let entity = await db.query.entities.findFirst({
- where: (entities, { eq, and }) =>
- and(eq(entities.internal_id, internalId)),
- });
- if (!entity) {
- throw new RecaseError({
- message: `Entity not found for internal ID ${internalId}`,
- code: ErrCode.EntityNotFound,
- statusCode: 404,
- });
- }
+ static async getByInternalId({
+ db,
+ internalId,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ }) {
+ let entity = await db.query.entities.findFirst({
+ where: (entities, { eq, and }) =>
+ and(eq(entities.internal_id, internalId)),
+ });
+ if (!entity) {
+ throw new RecaseError({
+ message: `Entity not found for internal ID ${internalId}`,
+ code: ErrCode.EntityNotFound,
+ statusCode: 404,
+ });
+ }
- return entity as Entity;
- }
+ return entity as Entity;
+ }
- static async list({
- db,
- internalCustomerId,
- inFeatureIds,
- isDeleted,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- inFeatureIds?: string[];
- isDeleted?: boolean;
- }) {
- return (await db.query.entities.findMany({
- where: (entities, { eq }) =>
- and(
- eq(entities.internal_customer_id, internalCustomerId),
- inFeatureIds
- ? inArray(entities.internal_feature_id, inFeatureIds)
- : undefined,
- isDeleted ? eq(entities.deleted, isDeleted) : undefined,
- ),
- })) as Entity[];
- }
+ static async list({
+ db,
+ internalCustomerId,
+ inFeatureIds,
+ isDeleted,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ inFeatureIds?: string[];
+ isDeleted?: boolean;
+ }) {
+ return (await db.query.entities.findMany({
+ where: (entities, { eq }) =>
+ and(
+ eq(entities.internal_customer_id, internalCustomerId),
+ inFeatureIds
+ ? inArray(entities.internal_feature_id, inFeatureIds)
+ : undefined,
+ isDeleted ? eq(entities.deleted, isDeleted) : undefined,
+ ),
+ })) as Entity[];
+ }
- static async update({
- db,
- internalId,
- update,
- }: {
- db: DrizzleCli;
- internalId: string;
- update: any;
- }) {
- const results = await db
- .update(entities)
- .set(update)
- .where(eq(entities.internal_id, internalId))
- .returning();
+ static async update({
+ db,
+ internalId,
+ update,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ update: any;
+ }) {
+ const results = await db
+ .update(entities)
+ .set(update)
+ .where(eq(entities.internal_id, internalId))
+ .returning();
- if (results.length === 0) {
- throw new RecaseError({
- message: `Entity not found for internal ID ${internalId}`,
- code: ErrCode.EntityNotFound,
- statusCode: 404,
- });
- }
+ if (results.length === 0) {
+ throw new RecaseError({
+ message: `Entity not found for internal ID ${internalId}`,
+ code: ErrCode.EntityNotFound,
+ statusCode: 404,
+ });
+ }
- return results[0] as Entity;
- }
+ return results[0] as Entity;
+ }
- static async deleteInInternalIds({
- db,
- internalIds,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- internalIds: string[];
- orgId: string;
- env: string;
- }) {
- const _results = await db
- .delete(entities)
- .where(
- and(
- inArray(entities.internal_id, internalIds),
- eq(entities.org_id, orgId),
- eq(entities.env, env),
- ),
- )
- .returning();
- }
+ static async deleteInInternalIds({
+ db,
+ internalIds,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ internalIds: string[];
+ orgId: string;
+ env: string;
+ }) {
+ const _results = await db
+ .delete(entities)
+ .where(
+ and(
+ inArray(entities.internal_id, internalIds),
+ eq(entities.org_id, orgId),
+ eq(entities.env, env),
+ ),
+ )
+ .returning();
+ }
}
diff --git a/server/src/internal/api/entities/entityRelations.ts b/server/src/internal/api/entities/entityRelations.ts
index 5ce221c86..e288dfbe2 100644
--- a/server/src/internal/api/entities/entityRelations.ts
+++ b/server/src/internal/api/entities/entityRelations.ts
@@ -3,16 +3,16 @@ import { entities, customers, features, organizations } from "@autumn/shared";
import { relations } from "drizzle-orm";
export const entityRelations = relations(entities, ({ one }) => ({
- customer: one(customers, {
- fields: [entities.internal_customer_id],
- references: [customers.internal_id],
- }),
- feature: one(features, {
- fields: [entities.internal_feature_id],
- references: [features.internal_id],
- }),
- organization: one(organizations, {
- fields: [entities.org_id],
- references: [organizations.id],
- }),
+ customer: one(customers, {
+ fields: [entities.internal_customer_id],
+ references: [customers.internal_id],
+ }),
+ feature: one(features, {
+ fields: [entities.internal_feature_id],
+ references: [features.internal_id],
+ }),
+ organization: one(organizations, {
+ fields: [entities.org_id],
+ references: [organizations.id],
+ }),
}));
diff --git a/server/src/internal/api/entities/entityRouter.ts b/server/src/internal/api/entities/entityRouter.ts
index a5c3e9f11..bab88c220 100644
--- a/server/src/internal/api/entities/entityRouter.ts
+++ b/server/src/internal/api/entities/entityRouter.ts
@@ -10,27 +10,27 @@ export const entityRouter: Router = Router({ mergeParams: true });
// List entityes
entityRouter.get("", (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "listEntities",
- handler: async (req, res) => {
- const customerId = String(req.params.customer_id);
- let { orgId, env } = req;
+ routeHandler({
+ req,
+ res,
+ action: "listEntities",
+ handler: async (req, res) => {
+ const customerId = String(req.params.customer_id);
+ let { orgId, env } = req;
- let customer = await CusService.getFull({
- db: req.db,
- idOrInternalId: customerId,
- orgId,
- env,
- withEntities: true,
- });
+ let customer = await CusService.getFull({
+ db: req.db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ withEntities: true,
+ });
- res.status(200).json({
- data: customer.entities,
- });
- },
- }),
+ res.status(200).json({
+ data: customer.entities,
+ });
+ },
+ }),
);
// 1. Create entity
diff --git a/server/src/internal/api/entities/entityUtils.ts b/server/src/internal/api/entities/entityUtils.ts
index 38c8cdd09..72757a54e 100644
--- a/server/src/internal/api/entities/entityUtils.ts
+++ b/server/src/internal/api/entities/entityUtils.ts
@@ -3,180 +3,180 @@ import { submitUsageToStripe } from "@/external/stripe/stripeMeterUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
- getBillingType,
- roundUsage,
+ getBillingType,
+ roundUsage,
} from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- AppEnv,
- BillingType,
- Customer,
- Entitlement,
- Entity,
- EntityExpand,
- ErrCode,
- Feature,
- FullCustomerEntitlement,
- FullCustomerPrice,
- Organization,
- UsagePriceConfig,
+ AppEnv,
+ BillingType,
+ Customer,
+ Entitlement,
+ Entity,
+ EntityExpand,
+ ErrCode,
+ Feature,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ Organization,
+ UsagePriceConfig,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const getLinkedCusEnt = ({
- linkedFeature,
- cusEnts,
+ linkedFeature,
+ cusEnts,
}: {
- linkedFeature: any;
- cusEnts: any;
+ linkedFeature: any;
+ cusEnts: any;
}) => {
- // Get linked cus ent...
- let linkedCusEnt = cusEnts.find(
- (e: any) => e.entitlement.feature.id === linkedFeature.id,
- );
+ // Get linked cus ent...
+ let linkedCusEnt = cusEnts.find(
+ (e: any) => e.entitlement.feature.id === linkedFeature.id,
+ );
- if (!linkedCusEnt) {
- return null;
- }
+ if (!linkedCusEnt) {
+ return null;
+ }
- return linkedCusEnt;
+ return linkedCusEnt;
};
export const entityFeatureIdExists = ({
- cusEnt,
+ cusEnt,
}: {
- cusEnt: FullCustomerEntitlement;
+ cusEnt: FullCustomerEntitlement;
}) => {
- let ent = cusEnt.entitlement;
- return notNullish(ent.entity_feature_id);
+ let ent = cusEnt.entitlement;
+ return notNullish(ent.entity_feature_id);
};
export const entityMatchesFeature = ({
- feature,
- entity,
+ feature,
+ entity,
}: {
- feature: Feature;
- entity: Entity;
+ feature: Feature;
+ entity: Entity;
}) => {
- return feature.id == entity.feature_id;
+ return feature.id == entity.feature_id;
};
export const entitlementLinkedToEntity = ({
- entitlement,
- entity,
+ entitlement,
+ entity,
}: {
- entitlement: Entitlement;
- entity: Entity;
+ entitlement: Entitlement;
+ entity: Entity;
}) => {
- return entitlement.entity_feature_id == entity.feature_id;
+ return entitlement.entity_feature_id == entity.feature_id;
};
export const isLinkedToEntity = ({
- cusEnt,
- entity,
+ cusEnt,
+ entity,
}: {
- cusEnt: FullCustomerEntitlement;
- entity: Entity;
+ cusEnt: FullCustomerEntitlement;
+ entity: Entity;
}) => {
- return cusEnt.entitlement.entity_feature_id == entity.feature_id;
+ return cusEnt.entitlement.entity_feature_id == entity.feature_id;
};
export const removeEntityFromCusEnt = async ({
- db,
- cusEnt,
- entity,
- logger,
- cusPrice,
- customer,
- org,
- env,
+ db,
+ cusEnt,
+ entity,
+ logger,
+ cusPrice,
+ customer,
+ org,
+ env,
}: {
- db: DrizzleCli;
- cusEnt: FullCustomerEntitlement;
- entity: Entity;
- logger: any;
- cusPrice?: FullCustomerPrice;
- customer: Customer;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ cusEnt: FullCustomerEntitlement;
+ entity: Entity;
+ logger: any;
+ cusPrice?: FullCustomerPrice;
+ customer: Customer;
+ org: Organization;
+ env: AppEnv;
}) => {
- // isLinked
- let isLinked = isLinkedToEntity({
- cusEnt,
- entity,
- });
+ // isLinked
+ let isLinked = isLinkedToEntity({
+ cusEnt,
+ entity,
+ });
- if (!isLinked) {
- return;
- }
+ if (!isLinked) {
+ return;
+ }
- let entitlement = cusEnt.entitlement;
- console.log(
- `Linked cus ent: ${entitlement.feature.id}, isLinked: ${isLinked}`,
- );
+ let entitlement = cusEnt.entitlement;
+ console.log(
+ `Linked cus ent: ${entitlement.feature.id}, isLinked: ${isLinked}`,
+ );
- // Delete cus ent ids
- let newEntities = structuredClone(cusEnt.entities!);
+ // Delete cus ent ids
+ let newEntities = structuredClone(cusEnt.entities!);
- // TODO: Send usage to stripe if cus price exists
- let stripeCli = createStripeCli({
- org,
- env,
- });
- if (cusPrice) {
- let config = cusPrice.price.config as UsagePriceConfig;
- let billingType = getBillingType(config);
- if (billingType == BillingType.UsageInArrear) {
- let usage = -newEntities[entity.id]?.balance;
+ // TODO: Send usage to stripe if cus price exists
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ });
+ if (cusPrice) {
+ let config = cusPrice.price.config as UsagePriceConfig;
+ let billingType = getBillingType(config);
+ if (billingType == BillingType.UsageInArrear) {
+ let usage = -newEntities[entity.id]?.balance;
- usage = roundUsage({
- usage,
- billingUnits: config.billing_units!,
- });
+ usage = roundUsage({
+ usage,
+ billingUnits: config.billing_units!,
+ });
- await submitUsageToStripe({
- price: cusPrice.price,
- usage,
- customer,
- feature: entitlement.feature,
- logger,
- stripeCli,
- });
- }
- }
+ await submitUsageToStripe({
+ price: cusPrice.price,
+ usage,
+ customer,
+ feature: entitlement.feature,
+ logger,
+ stripeCli,
+ });
+ }
+ }
- delete newEntities[entity.id];
+ delete newEntities[entity.id];
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- entities: newEntities,
- },
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ entities: newEntities,
+ },
+ });
- logger.info(
- `Feature: ${entitlement.feature.id}, customer: ${cusEnt.customer_id}, deleted entities from cus ent`,
- );
+ logger.info(
+ `Feature: ${entitlement.feature.id}, customer: ${cusEnt.customer_id}, deleted entities from cus ent`,
+ );
};
export const parseEntityExpand = (expand: string): EntityExpand[] => {
- if (expand) {
- let options = expand.split(",");
- let result: EntityExpand[] = [];
- for (const option of options) {
- if (!Object.values(EntityExpand).includes(option as EntityExpand)) {
- throw new RecaseError({
- message: `Invalid expand option: ${option}`,
- code: ErrCode.InvalidExpand,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- result.push(option as EntityExpand);
- }
- return result;
- } else {
- return [];
- }
+ if (expand) {
+ let options = expand.split(",");
+ let result: EntityExpand[] = [];
+ for (const option of options) {
+ if (!Object.values(EntityExpand).includes(option as EntityExpand)) {
+ throw new RecaseError({
+ message: `Invalid expand option: ${option}`,
+ code: ErrCode.InvalidExpand,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ result.push(option as EntityExpand);
+ }
+ return result;
+ } else {
+ return [];
+ }
};
diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts
index 459363200..22e79b34f 100644
--- a/server/src/internal/api/entities/getEntityUtils.ts
+++ b/server/src/internal/api/entities/getEntityUtils.ts
@@ -6,179 +6,179 @@ import { processFullCusProducts } from "@/internal/customers/cusUtils/cusProduct
import RecaseError from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";
import {
- type AppEnv,
- Feature,
- type Entity,
- EntityExpand,
- type EntityResponse,
- ErrCode,
- type FullCusProduct,
- type Organization,
- type Subscription,
- CusProductResponse,
- notNullish,
- FullCustomer,
- APIVersion,
+ type AppEnv,
+ Feature,
+ type Entity,
+ EntityExpand,
+ type EntityResponse,
+ ErrCode,
+ type FullCusProduct,
+ type Organization,
+ type Subscription,
+ CusProductResponse,
+ notNullish,
+ FullCustomer,
+ APIVersion,
} from "@autumn/shared";
export const getSingleEntityResponse = async ({
- entityId,
- org,
- env,
- fullCus,
- entity,
- features,
- withAutumnId = false,
+ entityId,
+ org,
+ env,
+ fullCus,
+ entity,
+ features,
+ withAutumnId = false,
}: {
- entityId: string;
- org: Organization;
- env: AppEnv;
- fullCus: FullCustomer;
- entity: Entity;
- features: Feature[];
- withAutumnId?: boolean;
+ entityId: string;
+ org: Organization;
+ env: AppEnv;
+ fullCus: FullCustomer;
+ entity: Entity;
+ features: Feature[];
+ withAutumnId?: boolean;
}) => {
- const apiVersion = APIVersion.v1_2;
+ const apiVersion = APIVersion.v1_2;
- if (!entity) {
- throw new RecaseError({
- message: `Entity ${entityId} not found for customer ${fullCus.id}`,
- code: ErrCode.EntityNotFound,
- statusCode: 400,
- });
- }
+ if (!entity) {
+ throw new RecaseError({
+ message: `Entity ${entityId} not found for customer ${fullCus.id}`,
+ code: ErrCode.EntityNotFound,
+ statusCode: 400,
+ });
+ }
- const entityCusProducts = fullCus.customer_products.filter(
- (p: FullCusProduct) => {
- if (org.config.entity_product) {
- return (
- notNullish(p.internal_entity_id) &&
- p.internal_entity_id == entity.internal_id
- );
- }
+ const entityCusProducts = fullCus.customer_products.filter(
+ (p: FullCusProduct) => {
+ if (org.config.entity_product) {
+ return (
+ notNullish(p.internal_entity_id) &&
+ p.internal_entity_id == entity.internal_id
+ );
+ }
- return (
- p.internal_entity_id == entity.internal_id ||
- nullish(p.internal_entity_id)
- );
- }
- );
+ return (
+ p.internal_entity_id == entity.internal_id ||
+ nullish(p.internal_entity_id)
+ );
+ },
+ );
- let entitySubs = (fullCus.subscriptions || []).filter((s: Subscription) =>
- entityCusProducts.some((p: FullCusProduct) =>
- p.subscription_ids?.includes(s.stripe_id || "")
- )
- );
+ let entitySubs = (fullCus.subscriptions || []).filter((s: Subscription) =>
+ entityCusProducts.some((p: FullCusProduct) =>
+ p.subscription_ids?.includes(s.stripe_id || ""),
+ ),
+ );
- let { main, addOns } = await processFullCusProducts({
- fullCusProducts: entityCusProducts,
- entity,
- subs: entitySubs,
- org,
- apiVersion: APIVersion.v1_2,
- features,
- });
+ let { main, addOns } = await processFullCusProducts({
+ fullCusProducts: entityCusProducts,
+ entity,
+ subs: entitySubs,
+ org,
+ apiVersion: APIVersion.v1_2,
+ features,
+ });
- let products: CusProductResponse[] = [...main, ...addOns];
+ let products: CusProductResponse[] = [...main, ...addOns];
- let cusFeatures = await getCusFeaturesResponse({
- cusProducts: entityCusProducts,
- org,
- entity,
- apiVersion,
- });
+ let cusFeatures = await getCusFeaturesResponse({
+ cusProducts: entityCusProducts,
+ org,
+ entity,
+ apiVersion,
+ });
- return {
- ...(withAutumnId ? { autumn_id: entity.internal_id } : {}),
- id: entity.id,
- name: entity.name,
- created_at: entity.created_at,
- // feature_id: entity.feature_id,
- customer_id: fullCus.id || fullCus.internal_id,
- env,
- products,
- features: cusFeatures,
- };
+ return {
+ ...(withAutumnId ? { autumn_id: entity.internal_id } : {}),
+ id: entity.id,
+ name: entity.name,
+ created_at: entity.created_at,
+ // feature_id: entity.feature_id,
+ customer_id: fullCus.id || fullCus.internal_id,
+ env,
+ products,
+ features: cusFeatures,
+ };
};
export const getEntityResponse = async ({
- db,
- entityIds,
- org,
- env,
- customerId,
- expand,
- entityId,
- withAutumnId = false,
- apiVersion,
- features,
- logger,
- skipCache = false,
+ db,
+ entityIds,
+ org,
+ env,
+ customerId,
+ expand,
+ entityId,
+ withAutumnId = false,
+ apiVersion,
+ features,
+ logger,
+ skipCache = false,
}: {
- db: DrizzleCli;
- entityIds: string[];
- org: Organization;
- env: AppEnv;
- customerId: string;
- expand?: EntityExpand[];
- entityId?: string;
- withAutumnId?: boolean;
- apiVersion: number;
- features: Feature[];
- logger: any;
- skipCache?: boolean;
+ db: DrizzleCli;
+ entityIds: string[];
+ org: Organization;
+ env: AppEnv;
+ customerId: string;
+ expand?: EntityExpand[];
+ entityId?: string;
+ withAutumnId?: boolean;
+ apiVersion: number;
+ features: Feature[];
+ logger: any;
+ skipCache?: boolean;
}) => {
- let fullCus = await getCusWithCache({
- db,
- idOrInternalId: customerId,
- org,
- env,
- expand,
- entityId,
- logger,
- skipCache,
- });
+ let fullCus = await getCusWithCache({
+ db,
+ idOrInternalId: customerId,
+ org,
+ env,
+ expand,
+ entityId,
+ logger,
+ skipCache,
+ });
- if (!fullCus) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: 400,
- });
- }
+ if (!fullCus) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: 400,
+ });
+ }
- const entityResponses: EntityResponse[] = [];
+ const entityResponses: EntityResponse[] = [];
- for (const entityId of entityIds) {
- const entity = fullCus.entities.find(
- (e: Entity) => e.id == entityId || e.internal_id == entityId
- );
+ for (const entityId of entityIds) {
+ const entity = fullCus.entities.find(
+ (e: Entity) => e.id == entityId || e.internal_id == entityId,
+ );
- if (!entity) {
- throw new RecaseError({
- message: `Entity ${entityId} not found for customer ${fullCus.id}`,
- code: ErrCode.EntityNotFound,
- statusCode: 400,
- });
- }
+ if (!entity) {
+ throw new RecaseError({
+ message: `Entity ${entityId} not found for customer ${fullCus.id}`,
+ code: ErrCode.EntityNotFound,
+ statusCode: 400,
+ });
+ }
- let entityResponse = await getSingleEntityResponse({
- entityId,
- org,
- env,
- fullCus,
- entity,
- features,
- withAutumnId,
- });
+ let entityResponse = await getSingleEntityResponse({
+ entityId,
+ org,
+ env,
+ fullCus,
+ entity,
+ features,
+ withAutumnId,
+ });
- entityResponses.push(entityResponse);
- }
+ entityResponses.push(entityResponse);
+ }
- return {
- entities: entityResponses,
- customer: fullCus,
- fullEntities: fullCus.entities,
- invoices: fullCus.invoices,
- };
+ return {
+ entities: entityResponses,
+ customer: fullCus,
+ fullEntities: fullCus.entities,
+ invoices: fullCus.invoices,
+ };
};
diff --git a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts
index b7e8cc102..c77d31b1a 100644
--- a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts
+++ b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts
@@ -7,172 +7,172 @@ import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { handleCustomerRaceCondition } from "@/external/redis/redisUtils.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
- findLinkedCusEnts,
- findMainCusEntForFeature,
+ findLinkedCusEnts,
+ findMainCusEntForFeature,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import {
- deleteEntityFromCusEnt,
- replaceEntityInCusEnt,
+ deleteEntityFromCusEnt,
+ replaceEntityInCusEnt,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.js";
import { cancelSubsForEntity } from "@/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.js";
export const handleDeleteEntity = async (req: any, res: any) => {
- try {
- const { org, env, db, logtail: logger, features } = req;
- const { customer_id, entity_id } = req.params;
+ try {
+ const { org, env, db, logtail: logger, features } = req;
+ const { customer_id, entity_id } = req.params;
- await handleCustomerRaceCondition({
- action: "entity",
- customerId: customer_id,
- orgId: org.id,
- env,
- res,
- logger,
- });
+ await handleCustomerRaceCondition({
+ action: "entity",
+ customerId: customer_id,
+ orgId: org.id,
+ env,
+ res,
+ logger,
+ });
- const customer = await CusService.getFull({
- db,
- idOrInternalId: customer_id,
- orgId: req.orgId,
- env: req.env,
- withEntities: true,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- });
+ const customer = await CusService.getFull({
+ db,
+ idOrInternalId: customer_id,
+ orgId: req.orgId,
+ env: req.env,
+ withEntities: true,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ });
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customer_id} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customer_id} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const existingEntities = customer.entities;
- const cusProducts = customer.customer_products;
- const entity = existingEntities.find((e: any) => e.id === entity_id);
+ const existingEntities = customer.entities;
+ const cusProducts = customer.customer_products;
+ const entity = existingEntities.find((e: any) => e.id === entity_id);
- if (!entity) {
- throw new RecaseError({
- message: `Entity ${entity_id} not found`,
- code: ErrCode.EntityNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- } else if (entity.deleted) {
- throw new RecaseError({
- message: `Entity ${entity_id} already deleted`,
- code: ErrCode.EntityAlreadyDeleted,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!entity) {
+ throw new RecaseError({
+ message: `Entity ${entity_id} not found`,
+ code: ErrCode.EntityNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ } else if (entity.deleted) {
+ throw new RecaseError({
+ message: `Entity ${entity_id} already deleted`,
+ code: ErrCode.EntityAlreadyDeleted,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const feature = features.find((f: any) => f.id === entity?.feature_id);
+ const feature = features.find((f: any) => f.id === entity?.feature_id);
- for (const cusProduct of cusProducts) {
- let cusEnts = cusProduct.customer_entitlements;
+ for (const cusProduct of cusProducts) {
+ let cusEnts = cusProduct.customer_entitlements;
- let mainCusEnt = findMainCusEntForFeature({
- cusEnts,
- feature,
- });
+ let mainCusEnt = findMainCusEntForFeature({
+ cusEnts,
+ feature,
+ });
- if (!mainCusEnt) {
- continue;
- }
+ if (!mainCusEnt) {
+ continue;
+ }
- const { newReplaceables } = await adjustAllowance({
- db,
- env,
- org,
- cusPrices: cusProducts.flatMap((p: any) => p.customer_prices),
- customer,
- affectedFeature: mainCusEnt.entitlement.feature,
- cusEnt: { ...mainCusEnt, customer_product: cusProduct },
- originalBalance: mainCusEnt.balance!,
- newBalance: mainCusEnt.balance! + 1,
- logger,
- });
+ const { newReplaceables } = await adjustAllowance({
+ db,
+ env,
+ org,
+ cusPrices: cusProducts.flatMap((p: any) => p.customer_prices),
+ customer,
+ affectedFeature: mainCusEnt.entitlement.feature,
+ cusEnt: { ...mainCusEnt, customer_product: cusProduct },
+ originalBalance: mainCusEnt.balance!,
+ newBalance: mainCusEnt.balance! + 1,
+ logger,
+ });
- let linkedCusEnts = findLinkedCusEnts({
- cusEnts: cusProduct.customer_entitlements,
- feature: mainCusEnt.entitlement.feature,
- });
+ let linkedCusEnts = findLinkedCusEnts({
+ cusEnts: cusProduct.customer_entitlements,
+ feature: mainCusEnt.entitlement.feature,
+ });
- let replaceable =
- newReplaceables && newReplaceables.length > 0
- ? newReplaceables[0]
- : null;
+ let replaceable =
+ newReplaceables && newReplaceables.length > 0
+ ? newReplaceables[0]
+ : null;
- if (replaceable) {
- await RepService.update({
- db,
- id: replaceable.id,
- data: {
- from_entity_id: entity.id,
- },
- });
- }
+ if (replaceable) {
+ await RepService.update({
+ db,
+ id: replaceable.id,
+ data: {
+ from_entity_id: entity.id,
+ },
+ });
+ }
- // Update linked cus ents with replaceables...
- for (const linkedCusEnt of linkedCusEnts) {
- let newEntities;
- if (replaceable) {
- let { newEntities: newEntities_ } = replaceEntityInCusEnt({
- cusEnt: linkedCusEnt,
- entityId: entity.id,
- replaceable,
- });
- newEntities = newEntities_;
- } else {
- let { newEntities: newEntities_ } = deleteEntityFromCusEnt({
- cusEnt: linkedCusEnt,
- entityId: entity.id,
- });
- newEntities = newEntities_;
- }
+ // Update linked cus ents with replaceables...
+ for (const linkedCusEnt of linkedCusEnts) {
+ let newEntities;
+ if (replaceable) {
+ let { newEntities: newEntities_ } = replaceEntityInCusEnt({
+ cusEnt: linkedCusEnt,
+ entityId: entity.id,
+ replaceable,
+ });
+ newEntities = newEntities_;
+ } else {
+ let { newEntities: newEntities_ } = deleteEntityFromCusEnt({
+ cusEnt: linkedCusEnt,
+ entityId: entity.id,
+ });
+ newEntities = newEntities_;
+ }
- await CusEntService.update({
- db,
- id: linkedCusEnt.id,
- updates: {
- entities: newEntities,
- },
- });
- }
+ await CusEntService.update({
+ db,
+ id: linkedCusEnt.id,
+ updates: {
+ entities: newEntities,
+ },
+ });
+ }
- if (!replaceable) {
- await CusEntService.increment({
- db,
- id: mainCusEnt.id,
- amount: 1,
- });
- }
- }
+ if (!replaceable) {
+ await CusEntService.increment({
+ db,
+ id: mainCusEnt.id,
+ amount: 1,
+ });
+ }
+ }
- // Cancel any subs
- await cancelSubsForEntity({
- req,
- cusProducts,
- entity,
- });
+ // Cancel any subs
+ await cancelSubsForEntity({
+ req,
+ cusProducts,
+ entity,
+ });
- await EntityService.deleteInInternalIds({
- db,
- internalIds: [entity.internal_id],
- orgId: req.orgId,
- env: req.env,
- });
+ await EntityService.deleteInInternalIds({
+ db,
+ internalIds: [entity.internal_id],
+ orgId: req.orgId,
+ env: req.env,
+ });
- logger.info(` ✅ Finished deleting entity ${entity_id}`);
+ logger.info(` ✅ Finished deleting entity ${entity_id}`);
- return res.status(200).json({
- success: true,
- });
- } catch (error) {
- handleRequestError({ error, req, res, action: "delete entity" });
- }
+ return res.status(200).json({
+ success: true,
+ });
+ } catch (error) {
+ handleRequestError({ error, req, res, action: "delete entity" });
+ }
};
diff --git a/server/src/internal/api/entities/handlers/handleGetEntity.ts b/server/src/internal/api/entities/handlers/handleGetEntity.ts
index 2bd9fc30d..f149afa86 100644
--- a/server/src/internal/api/entities/handlers/handleGetEntity.ts
+++ b/server/src/internal/api/entities/handlers/handleGetEntity.ts
@@ -7,52 +7,52 @@ import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
import { orgToVersion } from "@/utils/versionUtils.js";
export const handleGetEntity = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "getEntity",
- handler: async (req, res) => {
- const entityId = req.params.entity_id as string;
- const customerId = req.params.customer_id as string;
- const expand = parseEntityExpand(req.query.expand);
+ routeHandler({
+ req,
+ res,
+ action: "getEntity",
+ handler: async (req, res) => {
+ const entityId = req.params.entity_id as string;
+ const customerId = req.params.customer_id as string;
+ const expand = parseEntityExpand(req.query.expand);
- let { orgId, env, db, logger, features } = req;
+ let { orgId, env, db, logger, features } = req;
- let org = await OrgService.getFromReq(req);
- let apiVersion = orgToVersion({
- org,
- reqApiVersion:
- req.apiVersion >= APIVersion.v1_1 ? req.apiVersion : APIVersion.v1_2,
- });
+ let org = await OrgService.getFromReq(req);
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion:
+ req.apiVersion >= APIVersion.v1_1 ? req.apiVersion : APIVersion.v1_2,
+ });
- // const start = performance.now();
- let { entities, customer, fullEntities, invoices } =
- await getEntityResponse({
- db,
- entityIds: [entityId],
- org,
- env,
- customerId,
- expand,
- entityId,
- apiVersion,
- features,
- logger,
- });
+ // const start = performance.now();
+ let { entities, customer, fullEntities, invoices } =
+ await getEntityResponse({
+ db,
+ entityIds: [entityId],
+ org,
+ env,
+ customerId,
+ expand,
+ entityId,
+ apiVersion,
+ features,
+ logger,
+ });
- let entity = entities[0];
- let withInvoices = expand.includes(EntityExpand.Invoices);
+ let entity = entities[0];
+ let withInvoices = expand.includes(EntityExpand.Invoices);
- res.status(200).json(
- EntityResponseSchema.parse({
- ...entity,
- invoices: withInvoices
- ? invoicesToResponse({
- invoices: invoices || [],
- logger,
- })
- : undefined,
- })
- );
- },
- });
+ res.status(200).json(
+ EntityResponseSchema.parse({
+ ...entity,
+ invoices: withInvoices
+ ? invoicesToResponse({
+ invoices: invoices || [],
+ logger,
+ })
+ : undefined,
+ }),
+ );
+ },
+ });
diff --git a/server/src/internal/api/entitled/checkRouter.ts b/server/src/internal/api/entitled/checkRouter.ts
index 3698c4715..8f95afaf5 100644
--- a/server/src/internal/api/entitled/checkRouter.ts
+++ b/server/src/internal/api/entitled/checkRouter.ts
@@ -18,192 +18,192 @@ import { getV2CheckResponse } from "./checkUtils/getV2CheckResponse.js";
export const checkRouter: Router = Router();
checkRouter.post("", async (req: any, res: any) => {
- try {
- let {
- customer_id,
- feature_id,
- product_id,
- required_quantity,
- required_balance,
- customer_data,
- send_event,
- event_data,
- entity_id,
- } = req.body;
+ try {
+ let {
+ customer_id,
+ feature_id,
+ product_id,
+ required_quantity,
+ required_balance,
+ customer_data,
+ send_event,
+ event_data,
+ entity_id,
+ } = req.body;
- const { logtail: logger, db } = req;
+ const { logtail: logger, db } = req;
- if (!customer_id) {
- throw new RecaseError({
- message: "`customer_id` is required",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!customer_id) {
+ throw new RecaseError({
+ message: "`customer_id` is required",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (!feature_id && !product_id) {
- throw new RecaseError({
- message: "`feature_id` or `product_id` is required",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!feature_id && !product_id) {
+ throw new RecaseError({
+ message: "`feature_id` or `product_id` is required",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (feature_id && product_id) {
- throw new RecaseError({
- message:
- "Provide either feature_id or product_id. Not allowed to provide both",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (feature_id && product_id) {
+ throw new RecaseError({
+ message:
+ "Provide either feature_id or product_id. Not allowed to provide both",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (product_id) {
- await handleProductCheck({ req, res });
- return;
- }
+ if (product_id) {
+ await handleProductCheck({ req, res });
+ return;
+ }
- const requiredBalance = notNullish(required_balance)
- ? required_balance
- : notNullish(required_quantity)
- ? required_quantity
- : null;
+ const requiredBalance = notNullish(required_balance)
+ ? required_balance
+ : notNullish(required_quantity)
+ ? required_quantity
+ : null;
- let quantity = 1;
- if (notNullish(requiredBalance)) {
- let floatQuantity = parseFloat(requiredBalance);
+ let quantity = 1;
+ if (notNullish(requiredBalance)) {
+ let floatQuantity = parseFloat(requiredBalance);
- if (isNaN(floatQuantity)) {
- throw new RecaseError({
- message: "Invalid required_balance",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- quantity = floatQuantity;
- }
+ if (isNaN(floatQuantity)) {
+ throw new RecaseError({
+ message: "Invalid required_balance",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ quantity = floatQuantity;
+ }
- const {
- fullCus,
- cusEnts,
- feature,
- creditSystems,
- org,
- cusProducts,
- allFeatures,
- } = await getCheckData({ req });
+ const {
+ fullCus,
+ cusEnts,
+ feature,
+ creditSystems,
+ org,
+ cusProducts,
+ allFeatures,
+ } = await getCheckData({ req });
- let apiVersion = orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- });
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ });
- // 2. If boolean, return true
- if (feature.type === FeatureType.Boolean) {
- return await getBooleanEntitledResult({
- db,
- fullCus,
- res,
- cusEnts,
- feature,
- apiVersion,
- withPreview: req.body.with_preview,
- cusProducts,
- allFeatures,
- });
- }
+ // 2. If boolean, return true
+ if (feature.type === FeatureType.Boolean) {
+ return await getBooleanEntitledResult({
+ db,
+ fullCus,
+ res,
+ cusEnts,
+ feature,
+ apiVersion,
+ withPreview: req.body.with_preview,
+ cusProducts,
+ allFeatures,
+ });
+ }
- const v1Response = getV1CheckResponse({
- originalFeature: feature,
- creditSystems,
- cusEnts: cusEnts!,
- quantity,
- entityId: entity_id,
- org,
- });
+ const v1Response = getV1CheckResponse({
+ originalFeature: feature,
+ creditSystems,
+ cusEnts: cusEnts!,
+ quantity,
+ entityId: entity_id,
+ org,
+ });
- const v2Response = await getV2CheckResponse({
- fullCus,
- cusEnts,
- feature,
- creditSystems,
- org,
- cusProducts,
- requiredBalance,
- apiVersion,
- });
+ const v2Response = await getV2CheckResponse({
+ fullCus,
+ cusEnts,
+ feature,
+ creditSystems,
+ org,
+ cusProducts,
+ requiredBalance,
+ apiVersion,
+ });
- const { allowed, balance } = v2Response;
- const featureToUse = allFeatures.find(
- (f: Feature) => f.id === v2Response.feature_id
- );
+ const { allowed, balance } = v2Response;
+ const featureToUse = allFeatures.find(
+ (f: Feature) => f.id === v2Response.feature_id,
+ );
- if (allowed && req.isPublic !== true) {
- if (send_event) {
- await handleEventSent({
- req: {
- ...req,
- body: {
- ...req.body,
- value: quantity,
- },
- },
- customer_id: customer_id,
- customer_data: customer_data,
- event_data: {
- customer_id: customer_id,
- feature_id: feature_id,
- value: quantity,
- entity_id: entity_id,
- },
- });
- } else if (notNullish(event_data)) {
- await handleEventSent({
- req,
- customer_id: customer_id,
- customer_data: customer_data,
- event_data: {
- customer_id: customer_id,
- feature_id: feature_id,
- ...event_data,
- },
- });
- }
- }
+ if (allowed && req.isPublic !== true) {
+ if (send_event) {
+ await handleEventSent({
+ req: {
+ ...req,
+ body: {
+ ...req.body,
+ value: quantity,
+ },
+ },
+ customer_id: customer_id,
+ customer_data: customer_data,
+ event_data: {
+ customer_id: customer_id,
+ feature_id: feature_id,
+ value: quantity,
+ entity_id: entity_id,
+ },
+ });
+ } else if (notNullish(event_data)) {
+ await handleEventSent({
+ req,
+ customer_id: customer_id,
+ customer_data: customer_data,
+ event_data: {
+ customer_id: customer_id,
+ feature_id: feature_id,
+ ...event_data,
+ },
+ });
+ }
+ }
- let preview = undefined;
- if (req.body.with_preview) {
- try {
- preview = await getCheckPreview({
- db,
- allowed,
- balance: notNullish(balance) ? balance : undefined,
- feature: featureToUse!,
- cusProducts,
- allFeatures,
- });
- } catch (error) {
- logger.error("Failed to get check preview", error);
- console.error(error);
- }
- }
+ let preview = undefined;
+ if (req.body.with_preview) {
+ try {
+ preview = await getCheckPreview({
+ db,
+ allowed,
+ balance: notNullish(balance) ? balance : undefined,
+ feature: featureToUse!,
+ cusProducts,
+ allFeatures,
+ });
+ } catch (error) {
+ logger.error("Failed to get check preview", error);
+ console.error(error);
+ }
+ }
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json({
- ...v2Response,
- preview,
- });
- } else {
- res.status(200).json({
- ...v1Response,
- preview,
- });
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json({
+ ...v2Response,
+ preview,
+ });
+ } else {
+ res.status(200).json({
+ ...v1Response,
+ preview,
+ });
+ }
- return;
- } catch (error) {
- handleRequestError({ req, error, res, action: "Failed to GET entitled" });
- }
+ return;
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "Failed to GET entitled" });
+ }
});
// let features = [feature, ...creditSystems];
diff --git a/server/src/internal/api/entitled/checkUtils.ts b/server/src/internal/api/entitled/checkUtils.ts
index 4a3e7b1a0..4527c951c 100644
--- a/server/src/internal/api/entitled/checkUtils.ts
+++ b/server/src/internal/api/entitled/checkUtils.ts
@@ -2,26 +2,26 @@ import { itemToPriceOrTiers } from "@/internal/products/product-items/productIte
import { isFeaturePriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
import {
- APIVersion,
- BillingInterval,
- type Feature,
- type FreeTrial,
- type FullCusProduct,
- type FullCustomer,
- type FullCustomerEntitlement,
- isTrialing,
- type ProductItem,
- SuccessCode,
- UsageModel,
+ APIVersion,
+ BillingInterval,
+ type Feature,
+ type FreeTrial,
+ type FullCusProduct,
+ type FullCustomer,
+ type FullCustomerEntitlement,
+ isTrialing,
+ type ProductItem,
+ SuccessCode,
+ UsageModel,
} from "@autumn/shared";
import { getCheckPreview } from "./getCheckPreview.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import {
- formatUnixToDate,
- formatUnixToDateTime,
- notNullish,
+ formatUnixToDate,
+ formatUnixToDateTime,
+ notNullish,
} from "@/utils/genUtils.js";
import { featureToCusPrice } from "@/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
@@ -29,177 +29,177 @@ import { Decimal } from "decimal.js";
import { isOneOffPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
export const getBooleanEntitledResult = async ({
- db,
- fullCus,
- cusEnts,
- res,
- feature,
- apiVersion,
- withPreview,
- cusProducts,
- allFeatures,
+ db,
+ fullCus,
+ cusEnts,
+ res,
+ feature,
+ apiVersion,
+ withPreview,
+ cusProducts,
+ allFeatures,
}: {
- db: DrizzleCli;
- fullCus: FullCustomer;
- cusEnts: FullCustomerEntitlement[];
- res: any;
- feature: Feature;
- apiVersion: number;
- withPreview: boolean;
- cusProducts: FullCusProduct[];
- allFeatures: Feature[];
+ db: DrizzleCli;
+ fullCus: FullCustomer;
+ cusEnts: FullCustomerEntitlement[];
+ res: any;
+ feature: Feature;
+ apiVersion: number;
+ withPreview: boolean;
+ cusProducts: FullCusProduct[];
+ allFeatures: Feature[];
}) => {
- const allowed = cusEnts.some((cusEnt) => {
- let featureMatch = cusEnt.internal_feature_id === feature.internal_id;
+ const allowed = cusEnts.some((cusEnt) => {
+ let featureMatch = cusEnt.internal_feature_id === feature.internal_id;
- let entityFeatureId = cusEnt.entitlement.entity_feature_id;
- let compareEntity =
- notNullish(entityFeatureId) && notNullish(fullCus.entity);
+ let entityFeatureId = cusEnt.entitlement.entity_feature_id;
+ let compareEntity =
+ notNullish(entityFeatureId) && notNullish(fullCus.entity);
- let entityMatch = compareEntity
- ? entityFeatureId === fullCus.entity!.feature_id
- : true;
+ let entityMatch = compareEntity
+ ? entityFeatureId === fullCus.entity!.feature_id
+ : true;
- return featureMatch && entityMatch;
- });
+ return featureMatch && entityMatch;
+ });
- if (apiVersion >= APIVersion.v1_1) {
- return res.status(200).json({
- customer_id: fullCus.id,
- feature_id: feature.id,
- code: SuccessCode.FeatureFound,
- allowed,
- preview: withPreview
- ? await getCheckPreview({
- db,
- allowed,
- balance: undefined,
- feature,
- cusProducts,
- allFeatures,
- })
- : undefined,
- });
- } else {
- return res.status(200).json({
- allowed,
- balances: allowed
- ? [
- {
- feature_id: feature.id,
- balance: null,
- },
- ]
- : [],
- });
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ return res.status(200).json({
+ customer_id: fullCus.id,
+ feature_id: feature.id,
+ code: SuccessCode.FeatureFound,
+ allowed,
+ preview: withPreview
+ ? await getCheckPreview({
+ db,
+ allowed,
+ balance: undefined,
+ feature,
+ cusProducts,
+ allFeatures,
+ })
+ : undefined,
+ });
+ } else {
+ return res.status(200).json({
+ allowed,
+ balances: allowed
+ ? [
+ {
+ feature_id: feature.id,
+ balance: null,
+ },
+ ]
+ : [],
+ });
+ }
};
export const getOptions = ({
- prodItems,
- features,
- anchor,
- proration,
- now,
- freeTrial,
- cusProduct,
+ prodItems,
+ features,
+ anchor,
+ proration,
+ now,
+ freeTrial,
+ cusProduct,
}: {
- prodItems: ProductItem[];
- features: Feature[];
- anchor?: number;
- proration?: {
- start: number;
- end: number;
- };
- now?: number;
- freeTrial?: FreeTrial | null;
- cusProduct?: FullCusProduct;
+ prodItems: ProductItem[];
+ features: Feature[];
+ anchor?: number;
+ proration?: {
+ start: number;
+ end: number;
+ };
+ now?: number;
+ freeTrial?: FreeTrial | null;
+ cusProduct?: FullCusProduct;
}) => {
- now = now || Date.now();
+ now = now || Date.now();
- return prodItems
- .filter((i) => isFeaturePriceItem(i) && i.usage_model == UsageModel.Prepaid)
- .map((i) => {
- const finalProration = getProration({
- anchor,
- proration,
- intervalConfig: {
- interval: (i.interval || BillingInterval.OneOff) as BillingInterval,
- intervalCount: i.interval_count || 1,
- },
- now,
- });
+ return prodItems
+ .filter((i) => isFeaturePriceItem(i) && i.usage_model == UsageModel.Prepaid)
+ .map((i) => {
+ const finalProration = getProration({
+ anchor,
+ proration,
+ intervalConfig: {
+ interval: (i.interval || BillingInterval.OneOff) as BillingInterval,
+ intervalCount: i.interval_count || 1,
+ },
+ now,
+ });
- let priceData = itemToPriceOrTiers({
- item: i,
- now,
- proration: finalProration,
- });
+ let priceData = itemToPriceOrTiers({
+ item: i,
+ now,
+ proration: finalProration,
+ });
- let actualPrice = itemToPriceOrTiers({
- item: i,
- });
+ let actualPrice = itemToPriceOrTiers({
+ item: i,
+ });
- if (
- (freeTrial || (cusProduct && isTrialing({ cusProduct, now }))) &&
- notNullish(i.interval)
- ) {
- priceData = {
- price: 0,
- tiers: undefined,
- };
- }
+ if (
+ (freeTrial || (cusProduct && isTrialing({ cusProduct, now }))) &&
+ notNullish(i.interval)
+ ) {
+ priceData = {
+ price: 0,
+ tiers: undefined,
+ };
+ }
- const currentOptions = cusProduct?.options.find(
- (o) => o.feature_id == i.feature_id
- );
+ const currentOptions = cusProduct?.options.find(
+ (o) => o.feature_id == i.feature_id,
+ );
- let currentQuantity = currentOptions?.quantity;
- let prorationAmount = 0;
+ let currentQuantity = currentOptions?.quantity;
+ let prorationAmount = 0;
- if (currentQuantity) {
- currentQuantity = currentQuantity * (i.billing_units || 1);
+ if (currentQuantity) {
+ currentQuantity = currentQuantity * (i.billing_units || 1);
- const curPrice = featureToCusPrice({
- internalFeatureId: currentOptions?.internal_feature_id!,
- cusPrices: cusProduct?.customer_prices!,
- })?.price;
+ const curPrice = featureToCusPrice({
+ internalFeatureId: currentOptions?.internal_feature_id!,
+ cusPrices: cusProduct?.customer_prices!,
+ })?.price;
- const curPriceAmount = priceToInvoiceAmount({
- price: curPrice!,
- quantity: currentQuantity,
- now,
- proration: finalProration,
- });
+ const curPriceAmount = priceToInvoiceAmount({
+ price: curPrice!,
+ quantity: currentQuantity,
+ now,
+ proration: finalProration,
+ });
- const newPriceAmount = priceToInvoiceAmount({
- item: i,
- quantity: currentQuantity,
- now,
- proration: finalProration,
- });
+ const newPriceAmount = priceToInvoiceAmount({
+ item: i,
+ quantity: currentQuantity,
+ now,
+ proration: finalProration,
+ });
- prorationAmount = new Decimal(newPriceAmount)
- .minus(curPriceAmount)
- .toNumber();
- }
+ prorationAmount = new Decimal(newPriceAmount)
+ .minus(curPriceAmount)
+ .toNumber();
+ }
- return {
- feature_id: i.feature_id,
- feature_name: features.find((f) => f.id == i.feature_id)?.name,
- billing_units: i.billing_units,
- included_usage: i.included_usage || 0,
- ...priceData,
+ return {
+ feature_id: i.feature_id,
+ feature_name: features.find((f) => f.id == i.feature_id)?.name,
+ billing_units: i.billing_units,
+ included_usage: i.included_usage || 0,
+ ...priceData,
- full_price: actualPrice?.price,
- full_tiers: actualPrice?.tiers,
+ full_price: actualPrice?.price,
+ full_tiers: actualPrice?.tiers,
- current_quantity: notNullish(currentQuantity)
- ? currentQuantity
- : undefined,
- proration_amount: prorationAmount,
- config: i.config,
- interval: i.interval,
- };
- });
+ current_quantity: notNullish(currentQuantity)
+ ? currentQuantity
+ : undefined,
+ proration_amount: prorationAmount,
+ config: i.config,
+ interval: i.interval,
+ };
+ });
};
diff --git a/server/src/internal/api/entitled/checkUtils/getCheckData.ts b/server/src/internal/api/entitled/checkUtils/getCheckData.ts
index e711e096b..a1e8c8e7d 100644
--- a/server/src/internal/api/entitled/checkUtils/getCheckData.ts
+++ b/server/src/internal/api/entitled/checkUtils/getCheckData.ts
@@ -4,99 +4,99 @@ import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCu
import { getCreditSystemsFromFeature } from "@/internal/features/creditSystemUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- CusProductStatus,
- ErrCode,
- Feature,
- FeatureType,
+ CusProductStatus,
+ ErrCode,
+ Feature,
+ FeatureType,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
// Main functions
const getFeatureAndCreditSystems = ({
- req,
- featureId,
+ req,
+ featureId,
}: {
- req: any;
- featureId: string;
+ req: any;
+ featureId: string;
}) => {
- const { features } = req;
+ const { features } = req;
- const feature: Feature | undefined = features.find(
- (feature: Feature) => feature.id === featureId
- );
+ const feature: Feature | undefined = features.find(
+ (feature: Feature) => feature.id === featureId,
+ );
- const creditSystems = getCreditSystemsFromFeature({
- featureId,
- features,
- });
+ const creditSystems = getCreditSystemsFromFeature({
+ featureId,
+ features,
+ });
- return { feature, creditSystems, allFeatures: features };
+ return { feature, creditSystems, allFeatures: features };
};
export const getCheckData = async ({ req }: { req: any }) => {
- let { customer_id, feature_id, customer_data, entity_id } = req.body;
+ let { customer_id, feature_id, customer_data, entity_id } = req.body;
- const { org, logger } = req;
+ const { org, logger } = req;
- const { feature, creditSystems, allFeatures } = getFeatureAndCreditSystems({
- req,
- featureId: feature_id,
- });
+ const { feature, creditSystems, allFeatures } = getFeatureAndCreditSystems({
+ req,
+ featureId: feature_id,
+ });
- // 1. Get org and features
- const startTime = Date.now();
+ // 1. Get org and features
+ const startTime = Date.now();
- logger.info(`running /check for org: ${org.slug}, feature: ${feature_id}`);
+ logger.info(`running /check for org: ${org.slug}, feature: ${feature_id}`);
- let inStatuses = org.config.include_past_due
- ? [CusProductStatus.Active, CusProductStatus.PastDue]
- : [CusProductStatus.Active];
+ let inStatuses = org.config.include_past_due
+ ? [CusProductStatus.Active, CusProductStatus.PastDue]
+ : [CusProductStatus.Active];
- const customer = await getOrCreateCustomer({
- req,
- customerId: customer_id,
- customerData: customer_data,
- inStatuses,
- entityId: entity_id,
- entityData: req.body.entity_data,
- withCache: true,
- });
+ const customer = await getOrCreateCustomer({
+ req,
+ customerId: customer_id,
+ customerData: customer_data,
+ inStatuses,
+ entityId: entity_id,
+ entityData: req.body.entity_data,
+ withCache: true,
+ });
- const duration = Date.now() - startTime;
- logger.info(`/check: fetched org, features & customer in ${duration}ms`);
+ const duration = Date.now() - startTime;
+ logger.info(`/check: fetched org, features & customer in ${duration}ms`);
- if (!feature) {
- throw new RecaseError({
- message: `feature with id ${feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `feature with id ${feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let cusProducts = customer.customer_products;
+ let cusProducts = customer.customer_products;
- let cusEnts = cusProductsToCusEnts({ cusProducts });
+ let cusEnts = cusProductsToCusEnts({ cusProducts });
- if (customer.entity) {
- cusEnts = cusEnts.filter((cusEnt) =>
- cusEntMatchesEntity({
- cusEnt,
- entity: customer.entity!,
- features: allFeatures,
- })
- );
- }
+ if (customer.entity) {
+ cusEnts = cusEnts.filter((cusEnt) =>
+ cusEntMatchesEntity({
+ cusEnt,
+ entity: customer.entity!,
+ features: allFeatures,
+ }),
+ );
+ }
- return {
- fullCus: customer,
- cusEnts,
- feature,
- creditSystems,
- org,
- cusProducts,
- allFeatures,
- entity: customer.entity,
- };
+ return {
+ fullCus: customer,
+ cusEnts,
+ feature,
+ creditSystems,
+ org,
+ cusProducts,
+ allFeatures,
+ entity: customer.entity,
+ };
};
// const creditSystems: Feature[] = features.filter((feature: Feature) => {
diff --git a/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts b/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts
index f9cb91382..35696d5bc 100644
--- a/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts
+++ b/server/src/internal/api/entitled/checkUtils/getV1CheckResponse.ts
@@ -1,150 +1,150 @@
import {
- getFeatureBalance,
- cusEntsContainFeature,
- getUnlimitedAndUsageAllowed,
- getPaidFeatureBalance,
+ getFeatureBalance,
+ cusEntsContainFeature,
+ getUnlimitedAndUsageAllowed,
+ getPaidFeatureBalance,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
import {
- FullCustomerEntitlement,
- Feature,
- FeatureType,
- Organization,
+ FullCustomerEntitlement,
+ Feature,
+ FeatureType,
+ Organization,
} from "@autumn/shared";
const getRequiredAndActualBalance = ({
- cusEnts,
- feature,
- originalFeatureId,
- required,
- entityId,
+ cusEnts,
+ feature,
+ originalFeatureId,
+ required,
+ entityId,
}: {
- cusEnts: FullCustomerEntitlement[];
- feature: Feature;
- originalFeatureId: string;
- required: number;
- entityId: string;
+ cusEnts: FullCustomerEntitlement[];
+ feature: Feature;
+ originalFeatureId: string;
+ required: number;
+ entityId: string;
}) => {
- let requiredBalance = required;
- if (
- feature.type === FeatureType.CreditSystem &&
- feature.id !== originalFeatureId
- ) {
- requiredBalance = featureToCreditSystem({
- featureId: originalFeatureId,
- creditSystem: feature,
- amount: required,
- });
- }
+ let requiredBalance = required;
+ if (
+ feature.type === FeatureType.CreditSystem &&
+ feature.id !== originalFeatureId
+ ) {
+ requiredBalance = featureToCreditSystem({
+ featureId: originalFeatureId,
+ creditSystem: feature,
+ amount: required,
+ });
+ }
- const actualBalance = getFeatureBalance({
- cusEnts,
- internalFeatureId: feature.internal_id!,
- entityId,
- });
+ const actualBalance = getFeatureBalance({
+ cusEnts,
+ internalFeatureId: feature.internal_id!,
+ entityId,
+ });
- return {
- required: requiredBalance,
- actual: actualBalance,
- entityId,
- };
+ return {
+ required: requiredBalance,
+ actual: actualBalance,
+ entityId,
+ };
};
export const getV1CheckResponse = ({
- originalFeature,
- creditSystems,
- cusEnts,
- quantity,
- entityId,
- org,
+ originalFeature,
+ creditSystems,
+ cusEnts,
+ quantity,
+ entityId,
+ org,
}: {
- originalFeature: Feature;
- creditSystems: Feature[];
- cusEnts: FullCustomerEntitlement[];
- quantity: number;
- entityId: string;
- org: Organization;
+ originalFeature: Feature;
+ creditSystems: Feature[];
+ cusEnts: FullCustomerEntitlement[];
+ quantity: number;
+ entityId: string;
+ org: Organization;
}) => {
- // If no entitlements -> return false
- if (!cusEnts || cusEnts.length === 0) {
- return {
- allowed: false,
- balances: [],
- };
- }
+ // If no entitlements -> return false
+ if (!cusEnts || cusEnts.length === 0) {
+ return {
+ allowed: false,
+ balances: [],
+ };
+ }
- let allowed = false;
- const balances = [];
+ let allowed = false;
+ const balances = [];
- for (const feature of [originalFeature, ...creditSystems]) {
- // 1. Skip if feature not among cusEnt
+ for (const feature of [originalFeature, ...creditSystems]) {
+ // 1. Skip if feature not among cusEnt
- if (!cusEntsContainFeature({ cusEnts, feature })) {
- continue;
- }
+ if (!cusEntsContainFeature({ cusEnts, feature })) {
+ continue;
+ }
- // 2. Handle unlimited / usage allowed features
- let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
- cusEnts,
- internalFeatureId: feature.internal_id!,
- });
+ // 2. Handle unlimited / usage allowed features
+ let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
+ cusEnts,
+ internalFeatureId: feature.internal_id!,
+ });
- if (unlimited || usageAllowed) {
- balances.push({
- feature_id: feature.id,
- unlimited,
- usage_allowed: usageAllowed,
- required: null,
- balance: unlimited
- ? null
- : getFeatureBalance({
- cusEnts,
- internalFeatureId: feature.internal_id!,
- entityId,
- }),
- });
- allowed = true;
- break;
- }
+ if (unlimited || usageAllowed) {
+ balances.push({
+ feature_id: feature.id,
+ unlimited,
+ usage_allowed: usageAllowed,
+ required: null,
+ balance: unlimited
+ ? null
+ : getFeatureBalance({
+ cusEnts,
+ internalFeatureId: feature.internal_id!,
+ entityId,
+ }),
+ });
+ allowed = true;
+ break;
+ }
- // 3. Get required and actual balance
- const { required, actual } = getRequiredAndActualBalance({
- cusEnts,
- feature,
- originalFeatureId: originalFeature.id,
- required: quantity,
- entityId,
- });
+ // 3. Get required and actual balance
+ const { required, actual } = getRequiredAndActualBalance({
+ cusEnts,
+ feature,
+ originalFeatureId: originalFeature.id,
+ required: quantity,
+ entityId,
+ });
- let totalPaidAllowance = getPaidFeatureBalance({
- cusEnts,
- internalFeatureId: feature.internal_id!,
- });
+ let totalPaidAllowance = getPaidFeatureBalance({
+ cusEnts,
+ internalFeatureId: feature.internal_id!,
+ });
- let newBalance: any = {
- feature_id: feature.id,
- required,
- balance: actual,
- };
+ let newBalance: any = {
+ feature_id: feature.id,
+ required,
+ balance: actual,
+ };
- if (entityId) {
- newBalance.entity_id = entityId;
- }
+ if (entityId) {
+ newBalance.entity_id = entityId;
+ }
- balances.push(newBalance);
+ balances.push(newBalance);
- // allowed = allowed && actual! >= required;
- allowed =
- (required && required < 0) ||
- actual! + (totalPaidAllowance || 0) >= required;
+ // allowed = allowed && actual! >= required;
+ allowed =
+ (required && required < 0) ||
+ actual! + (totalPaidAllowance || 0) >= required;
- if (allowed) {
- break;
- }
- }
+ if (allowed) {
+ break;
+ }
+ }
- return {
- allowed,
- balances,
- };
+ return {
+ allowed,
+ balances,
+ };
};
diff --git a/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts b/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts
index b5009df8b..8f3ac9ce8 100644
--- a/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts
+++ b/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts
@@ -3,141 +3,141 @@ import { cusEntMatchesFeature } from "@/internal/customers/cusProducts/cusEnts/c
import { getCusBalances } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.js";
import { balancesToFeatureResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.js";
import {
- CheckResponseSchema,
- Feature,
- FeatureType,
- FullCusEntWithFullCusProduct,
- FullCusProduct,
- FullCustomer,
- Organization,
- SuccessCode,
+ CheckResponseSchema,
+ Feature,
+ FeatureType,
+ FullCusEntWithFullCusProduct,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
+ SuccessCode,
} from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
export const getFeatureToUse = ({
- creditSystems,
- feature,
- cusEnts,
+ creditSystems,
+ feature,
+ cusEnts,
}: {
- creditSystems: Feature[];
- feature: Feature;
- cusEnts: FullCusEntWithFullCusProduct[];
+ creditSystems: Feature[];
+ feature: Feature;
+ cusEnts: FullCusEntWithFullCusProduct[];
}) => {
- // 1. If there's a credit system
- let featureCusEnts = cusEnts.filter((cusEnt) =>
- cusEntMatchesFeature({ cusEnt, feature })
- );
+ // 1. If there's a credit system
+ let featureCusEnts = cusEnts.filter((cusEnt) =>
+ cusEntMatchesFeature({ cusEnt, feature }),
+ );
- if (creditSystems.length > 0) {
- let creditCusEnts = cusEnts.filter((cusEnt) =>
- cusEntMatchesFeature({ cusEnt, feature: creditSystems[0] })
- );
+ if (creditSystems.length > 0) {
+ let creditCusEnts = cusEnts.filter((cusEnt) =>
+ cusEntMatchesFeature({ cusEnt, feature: creditSystems[0] }),
+ );
- if (creditCusEnts.length > 0) {
- return creditSystems[0];
- }
+ if (creditCusEnts.length > 0) {
+ return creditSystems[0];
+ }
- if (featureCusEnts.length > 0) {
- return feature;
- }
+ if (featureCusEnts.length > 0) {
+ return feature;
+ }
- return creditSystems[0];
- }
+ return creditSystems[0];
+ }
- return feature;
+ return feature;
};
export const getV2CheckResponse = async ({
- fullCus,
- cusEnts,
- feature,
- creditSystems,
- org,
- cusProducts,
- requiredBalance,
- apiVersion,
+ fullCus,
+ cusEnts,
+ feature,
+ creditSystems,
+ org,
+ cusProducts,
+ requiredBalance,
+ apiVersion,
}: {
- fullCus: FullCustomer;
- cusEnts: FullCusEntWithFullCusProduct[];
- feature: Feature;
- creditSystems: Feature[];
- org: Organization;
- cusProducts: FullCusProduct[];
- requiredBalance?: number;
- apiVersion: number;
+ fullCus: FullCustomer;
+ cusEnts: FullCusEntWithFullCusProduct[];
+ feature: Feature;
+ creditSystems: Feature[];
+ org: Organization;
+ cusProducts: FullCusProduct[];
+ requiredBalance?: number;
+ apiVersion: number;
}) => {
- // 1. Get the feature to use
- const featureToUse = getFeatureToUse({
- creditSystems,
- feature,
- cusEnts,
- });
+ // 1. Get the feature to use
+ const featureToUse = getFeatureToUse({
+ creditSystems,
+ feature,
+ cusEnts,
+ });
- const featureCusEnts = cusEnts.filter((cusEnt) =>
- cusEntMatchesFeature({ cusEnt, feature: featureToUse })
- );
+ const featureCusEnts = cusEnts.filter((cusEnt) =>
+ cusEntMatchesFeature({ cusEnt, feature: featureToUse }),
+ );
- const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
- cusEnts: featureCusEnts,
- internalFeatureId: featureToUse.internal_id!,
- });
+ const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
+ cusEnts: featureCusEnts,
+ internalFeatureId: featureToUse.internal_id!,
+ });
- const cusPrices = cusProducts.flatMap(
- (cusProduct) => cusProduct.customer_prices
- );
+ const cusPrices = cusProducts.flatMap(
+ (cusProduct) => cusProduct.customer_prices,
+ );
- const balances = await getCusBalances({
- cusEntsWithCusProduct: featureCusEnts,
- cusPrices,
- org,
- entity: fullCus.entity,
- apiVersion,
- });
+ const balances = await getCusBalances({
+ cusEntsWithCusProduct: featureCusEnts,
+ cusPrices,
+ org,
+ entity: fullCus.entity,
+ apiVersion,
+ });
- let cusFeatures = balancesToFeatureResponse({
- cusEnts: featureCusEnts,
- balances,
- });
+ let cusFeatures = balancesToFeatureResponse({
+ cusEnts: featureCusEnts,
+ balances,
+ });
- const cusFeature = cusFeatures[featureToUse.id] || {};
+ const cusFeature = cusFeatures[featureToUse.id] || {};
- let allowed = false;
+ let allowed = false;
- let totalPaidUsageAllowance = featureCusEnts.reduce((acc, ce) => {
- let ent = ce.entitlement;
- if (notNullish(ent.usage_limit)) {
- return acc + ent.usage_limit! - (ent.allowance || 0);
- }
- return acc;
- }, 0);
+ let totalPaidUsageAllowance = featureCusEnts.reduce((acc, ce) => {
+ let ent = ce.entitlement;
+ if (notNullish(ent.usage_limit)) {
+ return acc + ent.usage_limit! - (ent.allowance || 0);
+ }
+ return acc;
+ }, 0);
- if (
- (cusFeature && unlimited) ||
- usageAllowed ||
- (requiredBalance && requiredBalance < 0) ||
- cusFeature.balance + totalPaidUsageAllowance >= (requiredBalance || 1)
- ) {
- allowed = true;
- }
+ if (
+ (cusFeature && unlimited) ||
+ usageAllowed ||
+ (requiredBalance && requiredBalance < 0) ||
+ cusFeature.balance + totalPaidUsageAllowance >= (requiredBalance || 1)
+ ) {
+ allowed = true;
+ }
- let finalRequired = notNullish(requiredBalance) ? requiredBalance : 1;
- if (featureToUse.type == FeatureType.CreditSystem) {
- finalRequired = featureToCreditSystem({
- featureId: feature.id,
- creditSystem: featureToUse,
- amount: finalRequired!,
- });
- }
+ let finalRequired = notNullish(requiredBalance) ? requiredBalance : 1;
+ if (featureToUse.type == FeatureType.CreditSystem) {
+ finalRequired = featureToCreditSystem({
+ featureId: feature.id,
+ creditSystem: featureToUse,
+ amount: finalRequired!,
+ });
+ }
- return CheckResponseSchema.parse({
- customer_id: fullCus.id,
- feature_id: featureToUse.id,
- entity_id: fullCus.entity?.id,
- // required_balance: notNullish(requiredBalance) ? requiredBalance : 1,
- required_balance: finalRequired,
- code: SuccessCode.FeatureFound,
- allowed,
- ...cusFeature,
- });
+ return CheckResponseSchema.parse({
+ customer_id: fullCus.id,
+ feature_id: featureToUse.id,
+ entity_id: fullCus.entity?.id,
+ // required_balance: notNullish(requiredBalance) ? requiredBalance : 1,
+ required_balance: finalRequired,
+ code: SuccessCode.FeatureFound,
+ allowed,
+ ...cusFeature,
+ });
};
diff --git a/server/src/internal/api/entitled/getCheckPreview.ts b/server/src/internal/api/entitled/getCheckPreview.ts
index b0aa7d5ff..d1a11bae2 100644
--- a/server/src/internal/api/entitled/getCheckPreview.ts
+++ b/server/src/internal/api/entitled/getCheckPreview.ts
@@ -2,177 +2,177 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
import { fullCusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import {
- isOneOff,
- isProductUpgrade,
+ isOneOff,
+ isProductUpgrade,
} from "@/internal/products/productUtils.js";
import { sortProductsByPrice } from "@/internal/products/productUtils/sortProductUtils.js";
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- type Feature,
- FeaturePreviewScenario,
- type FullCusProduct,
- type FullProduct,
+ type Feature,
+ FeaturePreviewScenario,
+ type FullCusProduct,
+ type FullProduct,
} from "@autumn/shared";
export const getCheckPreview = async ({
- db,
- allowed,
- balance,
- feature,
- cusProducts,
- allFeatures,
+ db,
+ allowed,
+ balance,
+ feature,
+ cusProducts,
+ allFeatures,
}: {
- db: DrizzleCli;
- allowed: boolean;
- balance?: number | null;
- feature: Feature;
- cusProducts: FullCusProduct[];
- allFeatures: Feature[];
+ db: DrizzleCli;
+ allowed: boolean;
+ balance?: number | null;
+ feature: Feature;
+ cusProducts: FullCusProduct[];
+ allFeatures: Feature[];
}) => {
- if (allowed) {
- return null;
- }
+ if (allowed) {
+ return null;
+ }
- let mainCusProds = cusProducts.filter(
- (cp: FullCusProduct) => !cp.product.is_add_on,
- );
+ let mainCusProds = cusProducts.filter(
+ (cp: FullCusProduct) => !cp.product.is_add_on,
+ );
- let cusOwnedProducts = mainCusProds.map((cp: FullCusProduct) =>
- fullCusProductToProduct(cp),
- );
- sortProductsByPrice({ products: cusOwnedProducts });
- let highestTierProd =
- cusOwnedProducts.length > 0 ? cusOwnedProducts[0] : null;
+ let cusOwnedProducts = mainCusProds.map((cp: FullCusProduct) =>
+ fullCusProductToProduct(cp),
+ );
+ sortProductsByPrice({ products: cusOwnedProducts });
+ let highestTierProd =
+ cusOwnedProducts.length > 0 ? cusOwnedProducts[0] : null;
- let products: FullProduct[] = await ProductService.getByFeature({
- db,
- internalFeatureId: feature.internal_id!,
- });
+ let products: FullProduct[] = await ProductService.getByFeature({
+ db,
+ internalFeatureId: feature.internal_id!,
+ });
- // 1. Get add ons
- let addOns = [];
- for (let addOn of products) {
- if (addOn.is_add_on) {
- if (isOneOff(addOn.prices)) {
- addOns.push(addOn);
- } else if (
- !cusProducts.some((cp: FullCusProduct) => cp.product.id == addOn.id)
- ) {
- addOns.push(addOn);
- }
- }
- }
+ // 1. Get add ons
+ let addOns = [];
+ for (let addOn of products) {
+ if (addOn.is_add_on) {
+ if (isOneOff(addOn.prices)) {
+ addOns.push(addOn);
+ } else if (
+ !cusProducts.some((cp: FullCusProduct) => cp.product.id == addOn.id)
+ ) {
+ addOns.push(addOn);
+ }
+ }
+ }
- let mainProds: FullProduct[] = [];
- if (!highestTierProd) {
- mainProds = products.filter((product: FullProduct) => !product.is_add_on);
- } else {
- for (let prod of products) {
- if (prod.is_add_on) {
- continue;
- }
- if (mainCusProds.some((cp: FullCusProduct) => cp.product.id == prod.id)) {
- continue;
- } else if (
- isProductUpgrade({
- prices1: highestTierProd.prices,
- prices2: prod.prices,
- usageAlwaysUpgrade: false,
- })
- ) {
- mainProds.push(prod);
- }
- }
- }
+ let mainProds: FullProduct[] = [];
+ if (!highestTierProd) {
+ mainProds = products.filter((product: FullProduct) => !product.is_add_on);
+ } else {
+ for (let prod of products) {
+ if (prod.is_add_on) {
+ continue;
+ }
+ if (mainCusProds.some((cp: FullCusProduct) => cp.product.id == prod.id)) {
+ continue;
+ } else if (
+ isProductUpgrade({
+ prices1: highestTierProd.prices,
+ prices2: prod.prices,
+ usageAlwaysUpgrade: false,
+ })
+ ) {
+ mainProds.push(prod);
+ }
+ }
+ }
- let rawProducts = [...mainProds, ...addOns];
- for (let p of rawProducts) {
- p.entitlements = p.entitlements.map((e) => ({
- ...e,
- feature: allFeatures.find((f) => f.id == e.feature_id)!,
- }));
- }
+ let rawProducts = [...mainProds, ...addOns];
+ for (let p of rawProducts) {
+ p.entitlements = p.entitlements.map((e) => ({
+ ...e,
+ feature: allFeatures.find((f) => f.id == e.feature_id)!,
+ }));
+ }
- let v2Prods = await Promise.all(
- rawProducts.map((p) =>
- getProductResponse({ product: p, features: allFeatures }),
- ),
- );
+ let v2Prods = await Promise.all(
+ rawProducts.map((p) =>
+ getProductResponse({ product: p, features: allFeatures }),
+ ),
+ );
- let scenario = notNullish(balance)
- ? FeaturePreviewScenario.UsageLimit
- : FeaturePreviewScenario.FeatureFlag;
+ let scenario = notNullish(balance)
+ ? FeaturePreviewScenario.UsageLimit
+ : FeaturePreviewScenario.FeatureFlag;
- if (mainProds.length === 0 && addOns.length === 0) {
- return {
- scenario,
- title: `Feature Unavailable`,
- feature_id: feature.id,
- feature_name: feature.name,
- message:
- scenario == FeaturePreviewScenario.UsageLimit
- ? `You have reached the usage limit for ${feature.name}. Please contact us to increase your limit.`
- : `${feature.name} is not available for your account. Please contact us to enable it.`,
+ if (mainProds.length === 0 && addOns.length === 0) {
+ return {
+ scenario,
+ title: `Feature Unavailable`,
+ feature_id: feature.id,
+ feature_name: feature.name,
+ message:
+ scenario == FeaturePreviewScenario.UsageLimit
+ ? `You have reached the usage limit for ${feature.name}. Please contact us to increase your limit.`
+ : `${feature.name} is not available for your account. Please contact us to enable it.`,
- products: v2Prods,
- upgrade_product_id: null,
- };
- }
+ products: v2Prods,
+ upgrade_product_id: null,
+ };
+ }
- let nextProd = mainProds.length > 0 ? mainProds[0] : addOns[0];
+ let nextProd = mainProds.length > 0 ? mainProds[0] : addOns[0];
- let title = nextProd.free_trial
- ? `Start trial for ${nextProd.name}`
- : !nextProd.is_add_on
- ? `Upgrade to ${nextProd.name}`
- : `Purchase ${nextProd.name}`;
+ let title = nextProd.free_trial
+ ? `Start trial for ${nextProd.name}`
+ : !nextProd.is_add_on
+ ? `Upgrade to ${nextProd.name}`
+ : `Purchase ${nextProd.name}`;
- let msg = "";
+ let msg = "";
- if (notNullish(balance)) {
- msg = `You have reached the usage limit for ${feature.name.toLowerCase()}.`;
+ if (notNullish(balance)) {
+ msg = `You have reached the usage limit for ${feature.name.toLowerCase()}.`;
- if (mainProds.length > 0) {
- let prodString = `Please upgrade to ${mainProds[0].name} to continue using this feature.`;
- if (addOns.length > 0) {
- prodString += ` Alternatively, you can purchase the ${addOns[0].name} add on.`;
- }
- msg = `${msg} ${prodString}`;
- } else if (addOns.length > 0) {
- let prodString = `Please purchase the ${addOns[0].name} add on to continue using this feature.`;
- msg = `${msg} ${prodString}`;
- }
- }
- // If it will be a new feature...
- else {
- msg = `Your current plan does not include the ${feature.name} feature.`;
+ if (mainProds.length > 0) {
+ let prodString = `Please upgrade to ${mainProds[0].name} to continue using this feature.`;
+ if (addOns.length > 0) {
+ prodString += ` Alternatively, you can purchase the ${addOns[0].name} add on.`;
+ }
+ msg = `${msg} ${prodString}`;
+ } else if (addOns.length > 0) {
+ let prodString = `Please purchase the ${addOns[0].name} add on to continue using this feature.`;
+ msg = `${msg} ${prodString}`;
+ }
+ }
+ // If it will be a new feature...
+ else {
+ msg = `Your current plan does not include the ${feature.name} feature.`;
- if (mainProds.length > 0) {
- let prodString = `Please upgrade to ${mainProds[0].name} to use this feature.`;
- if (addOns.length > 0) {
- prodString += ` Alternatively, you can purchase the ${addOns[0].name} add on.`;
- }
- msg = `${msg} ${prodString}`;
- } else if (addOns.length > 0) {
- let prodString = `Please purchase the ${addOns[0].name} add on to use this feature.`;
- msg = `${msg} ${prodString}`;
- }
- }
+ if (mainProds.length > 0) {
+ let prodString = `Please upgrade to ${mainProds[0].name} to use this feature.`;
+ if (addOns.length > 0) {
+ prodString += ` Alternatively, you can purchase the ${addOns[0].name} add on.`;
+ }
+ msg = `${msg} ${prodString}`;
+ } else if (addOns.length > 0) {
+ let prodString = `Please purchase the ${addOns[0].name} add on to use this feature.`;
+ msg = `${msg} ${prodString}`;
+ }
+ }
- let nextTier =
- mainProds.length > 0 ? mainProds[0] : addOns.length > 0 ? addOns[0] : null;
+ let nextTier =
+ mainProds.length > 0 ? mainProds[0] : addOns.length > 0 ? addOns[0] : null;
- return {
- title,
- message: msg,
- scenario,
- feature_id: feature.id,
- feature_name: feature.name,
- products: v2Prods,
- // next_tier: nextTierResponse,
+ return {
+ title,
+ message: msg,
+ scenario,
+ feature_id: feature.id,
+ feature_name: feature.name,
+ products: v2Prods,
+ // next_tier: nextTierResponse,
- // Will depracate
- upgrade_product_id: nextTier?.id || null,
- };
+ // Will depracate
+ upgrade_product_id: nextTier?.id || null,
+ };
};
diff --git a/server/src/internal/api/entitled/handlers/attachToCheckPreview/getAttachScenario.ts b/server/src/internal/api/entitled/handlers/attachToCheckPreview/getAttachScenario.ts
index 88cd4ee02..8aa472fab 100644
--- a/server/src/internal/api/entitled/handlers/attachToCheckPreview/getAttachScenario.ts
+++ b/server/src/internal/api/entitled/handlers/attachToCheckPreview/getAttachScenario.ts
@@ -1,48 +1,48 @@
import { isFreeProduct } from "@/internal/products/productUtils.js";
import {
- AttachBranch,
- AttachPreview,
- AttachScenario,
- FullProduct,
+ AttachBranch,
+ AttachPreview,
+ AttachScenario,
+ FullProduct,
} from "@autumn/shared";
export const getAttachScenario = async ({
- preview,
- product,
+ preview,
+ product,
}: {
- preview: AttachPreview;
- product: FullProduct;
+ preview: AttachPreview;
+ product: FullProduct;
}) => {
- let branch = preview.branch;
+ let branch = preview.branch;
- if (
- branch == AttachBranch.New ||
- branch == AttachBranch.OneOff ||
- branch == AttachBranch.AddOn
- ) {
- return AttachScenario.New;
- }
+ if (
+ branch == AttachBranch.New ||
+ branch == AttachBranch.OneOff ||
+ branch == AttachBranch.AddOn
+ ) {
+ return AttachScenario.New;
+ }
- if (
- branch == AttachBranch.MainIsFree ||
- branch == AttachBranch.MainIsTrial ||
- branch == AttachBranch.Upgrade
- ) {
- return AttachScenario.Upgrade;
- }
+ if (
+ branch == AttachBranch.MainIsFree ||
+ branch == AttachBranch.MainIsTrial ||
+ branch == AttachBranch.Upgrade
+ ) {
+ return AttachScenario.Upgrade;
+ }
- if (branch == AttachBranch.Downgrade) {
- // return AttachScenario.Downgrade;
- if (isFreeProduct(product.prices)) {
- return AttachScenario.Cancel;
- } else {
- return AttachScenario.Downgrade;
- }
- }
+ if (branch == AttachBranch.Downgrade) {
+ // return AttachScenario.Downgrade;
+ if (isFreeProduct(product.prices)) {
+ return AttachScenario.Cancel;
+ } else {
+ return AttachScenario.Downgrade;
+ }
+ }
- if (branch == AttachBranch.Renew) {
- return AttachScenario.Renew;
- }
+ if (branch == AttachBranch.Renew) {
+ return AttachScenario.Renew;
+ }
- return AttachScenario.New;
+ return AttachScenario.New;
};
diff --git a/server/src/internal/api/entitled/handlers/getFeatureCheckPreview.ts b/server/src/internal/api/entitled/handlers/getFeatureCheckPreview.ts
index da77e56a1..54e7082ec 100644
--- a/server/src/internal/api/entitled/handlers/getFeatureCheckPreview.ts
+++ b/server/src/internal/api/entitled/handlers/getFeatureCheckPreview.ts
@@ -1,11 +1,11 @@
export interface FeatureCheckPreviewParams {
- customerId: string;
- featureId: string;
- quantity: number;
+ customerId: string;
+ featureId: string;
+ quantity: number;
}
export const getFeatureCheckPreview = async ({
- customerId,
- featureId,
- quantity,
+ customerId,
+ featureId,
+ quantity,
}: FeatureCheckPreviewParams) => {};
diff --git a/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts b/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts
index fe21f53df..efde1a413 100644
--- a/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts
+++ b/server/src/internal/api/entitled/handlers/getProductCheckPreview.ts
@@ -5,11 +5,11 @@ import { AttachBody } from "@autumn/shared";
import { attachParamsToPreview } from "@/internal/customers/attach/handleAttachPreview/attachParamsToPreview.js";
import {
- AttachFunction,
- AttachPreview,
- CheckProductPreview,
- Feature,
- Organization,
+ AttachFunction,
+ AttachPreview,
+ CheckProductPreview,
+ Feature,
+ Organization,
} from "@autumn/shared";
import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js";
@@ -22,157 +22,157 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const attachToCheckPreview = async ({
- preview,
- params,
- product,
- org,
- features,
- db,
- fullCus,
+ preview,
+ params,
+ product,
+ org,
+ features,
+ db,
+ fullCus,
}: {
- preview: AttachPreview;
- params: AttachParams;
- product: FullProduct;
- org: Organization;
- features: Feature[];
- db: DrizzleCli;
- fullCus: FullCustomer;
+ preview: AttachPreview;
+ params: AttachParams;
+ product: FullProduct;
+ org: Organization;
+ features: Feature[];
+ db: DrizzleCli;
+ fullCus: FullCustomer;
}) => {
- // 1. If check
- let attachFunc = preview.func;
+ // 1. If check
+ let attachFunc = preview.func;
- if (
- attachFunc == AttachFunction.AddProduct &&
- isFreeProduct(product.prices)
- ) {
- return null;
- }
+ if (
+ attachFunc == AttachFunction.AddProduct &&
+ isFreeProduct(product.prices)
+ ) {
+ return null;
+ }
- const noOptions = !preview.options || preview.options.length === 0;
- if (attachFunc == AttachFunction.CreateCheckout && noOptions) {
- return null;
- }
+ const noOptions = !preview.options || preview.options.length === 0;
+ if (attachFunc == AttachFunction.CreateCheckout && noOptions) {
+ return null;
+ }
- let scenario = await getAttachScenario({
- preview,
- product,
- });
+ let scenario = await getAttachScenario({
+ preview,
+ product,
+ });
- let items = preview.due_today?.line_items?.map((item) => {
- return {
- price: notNullish(item.amount)
- ? formatAmount({
- amount: item.amount!,
- org,
- minFractionDigits: 2,
- maxFractionDigits: 2,
- })
- : item.price,
- description: item.description,
- usage_model: item.usage_model,
- };
- });
+ let items = preview.due_today?.line_items?.map((item) => {
+ return {
+ price: notNullish(item.amount)
+ ? formatAmount({
+ amount: item.amount!,
+ org,
+ minFractionDigits: 2,
+ maxFractionDigits: 2,
+ })
+ : item.price,
+ description: item.description,
+ usage_model: item.usage_model,
+ };
+ });
- let options = preview.options?.map((option: any) => {
- return {
- ...option,
- price: new Decimal(option.price).toDecimalPlaces(2).toNumber(),
- };
- });
+ let options = preview.options?.map((option: any) => {
+ return {
+ ...option,
+ price: new Decimal(option.price).toDecimalPlaces(2).toNumber(),
+ };
+ });
- let due_today = preview.due_today
- ? {
- price: preview.due_today.total,
- currency: org.default_currency || "usd",
- }
- : undefined;
+ let due_today = preview.due_today
+ ? {
+ price: preview.due_today.total,
+ currency: org.default_currency || "usd",
+ }
+ : undefined;
- let due_next_cycle = undefined;
+ let due_next_cycle = undefined;
- if (preview.due_next_cycle) {
- due_next_cycle = {
- price: preview.due_next_cycle.line_items.reduce((acc, item) => {
- if (item.amount) {
- return acc + item.amount;
- }
- return acc;
- }, 0),
- currency: org.default_currency || "usd",
- };
- }
+ if (preview.due_next_cycle) {
+ due_next_cycle = {
+ price: preview.due_next_cycle.line_items.reduce((acc, item) => {
+ if (item.amount) {
+ return acc + item.amount;
+ }
+ return acc;
+ }, 0),
+ currency: org.default_currency || "usd",
+ };
+ }
- let checkPreview: CheckProductPreview = {
- // title: "Check",
- // message: "Check",
- scenario,
+ let checkPreview: CheckProductPreview = {
+ // title: "Check",
+ // message: "Check",
+ scenario,
- // Meta
- product_id: product.id,
- product_name: product.name,
- recurring: !isOneOff(product.prices),
- error_on_attach: false,
- next_cycle_at: preview.due_next_cycle?.due_at,
- current_product_name: preview.current_product?.name,
+ // Meta
+ product_id: product.id,
+ product_name: product.name,
+ recurring: !isOneOff(product.prices),
+ error_on_attach: false,
+ next_cycle_at: preview.due_next_cycle?.due_at,
+ current_product_name: preview.current_product?.name,
- // Otehrs
- options: options?.length > 0 ? options : undefined,
- items: items?.length > 0 ? items : undefined,
- due_today,
- due_next_cycle,
- product: await getProductResponse({
- product,
- features,
- db,
- fullCus,
- currency: org.default_currency || undefined,
- }),
- };
- return checkPreview;
+ // Otehrs
+ options: options?.length > 0 ? options : undefined,
+ items: items?.length > 0 ? items : undefined,
+ due_today,
+ due_next_cycle,
+ product: await getProductResponse({
+ product,
+ features,
+ db,
+ fullCus,
+ currency: org.default_currency || undefined,
+ }),
+ };
+ return checkPreview;
};
export const getProductCheckPreview = async ({
- req,
- customer,
- product,
- logger,
+ req,
+ customer,
+ product,
+ logger,
}: {
- req: ExtendedRequest;
- customer: FullCustomer;
- product: FullProduct;
- logger: any;
+ req: ExtendedRequest;
+ customer: FullCustomer;
+ product: FullProduct;
+ logger: any;
}) => {
- const { org, features, db } = req;
+ const { org, features, db } = req;
- // Build attach params
- const attachParams = await checkToAttachParams({
- req,
- customer,
- product,
- logger,
- });
+ // Build attach params
+ const attachParams = await checkToAttachParams({
+ req,
+ customer,
+ product,
+ logger,
+ });
- const attachBody: AttachBody = {
- customer_id: customer.id!,
- product_id: product.id,
- entity_id: customer.entity?.id,
- };
+ const attachBody: AttachBody = {
+ customer_id: customer.id!,
+ product_id: product.id,
+ entity_id: customer.entity?.id,
+ };
- const preview = await attachParamsToPreview({
- req,
- attachParams,
- attachBody,
- logger,
- });
+ const preview = await attachParamsToPreview({
+ req,
+ attachParams,
+ attachBody,
+ logger,
+ });
- const checkPreview = await attachToCheckPreview({
- preview,
- params: attachParams,
- product,
- org,
- features,
- db,
- fullCus: customer,
- });
+ const checkPreview = await attachToCheckPreview({
+ preview,
+ params: attachParams,
+ product,
+ org,
+ features,
+ db,
+ fullCus: customer,
+ });
- return checkPreview;
+ return checkPreview;
};
diff --git a/server/src/internal/api/entitled/handlers/handleProductCheck.ts b/server/src/internal/api/entitled/handlers/handleProductCheck.ts
index e371509fa..ccc5fbdd6 100644
--- a/server/src/internal/api/entitled/handlers/handleProductCheck.ts
+++ b/server/src/internal/api/entitled/handlers/handleProductCheck.ts
@@ -7,115 +7,115 @@ import { getOrgAndFeatures } from "@/internal/orgs/orgUtils.js";
import { getProductCheckPreview } from "./getProductCheckPreview.js";
export const handleProductCheck = async ({
- req,
- res,
+ req,
+ res,
}: {
- req: any;
- res: any;
+ req: any;
+ res: any;
}) => {
- const {
- customer_id,
- product_id,
- entity_id,
- customer_data,
- with_preview,
- entity_data,
- } = req.body;
- const { orgId, env, logtail: logger, db } = req;
+ const {
+ customer_id,
+ product_id,
+ entity_id,
+ customer_data,
+ with_preview,
+ entity_data,
+ } = req.body;
+ const { orgId, env, logtail: logger, db } = req;
- let { org, features } = await getOrgAndFeatures({ req });
+ let { org, features } = await getOrgAndFeatures({ req });
- // 1. Get customer and org
- let [customer, product] = await Promise.all([
- getOrCreateCustomer({
- req,
- customerId: customer_id,
- customerData: customer_data,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
+ // 1. Get customer and org
+ let [customer, product] = await Promise.all([
+ getOrCreateCustomer({
+ req,
+ customerId: customer_id,
+ customerData: customer_data,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
- entityId: entity_id,
- entityData: entity_data,
- withEntities: true,
- }),
- ProductService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: product_id,
- }),
- ]);
+ entityId: entity_id,
+ entityData: entity_data,
+ withEntities: true,
+ }),
+ ProductService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: product_id,
+ }),
+ ]);
- let cusProducts = customer.customer_products;
- if (customer.entity) {
- cusProducts = cusProducts.filter(
- (cusProduct: FullCusProduct) =>
- cusProduct.internal_entity_id == customer.entity!.internal_id,
- );
- }
+ let cusProducts = customer.customer_products;
+ if (customer.entity) {
+ cusProducts = cusProducts.filter(
+ (cusProduct: FullCusProduct) =>
+ cusProduct.internal_entity_id == customer.entity!.internal_id,
+ );
+ }
- let cusProduct: FullCusProduct | undefined = cusProducts.find(
- (cusProduct: FullCusProduct) => cusProduct.product.id === product_id,
- );
+ let cusProduct: FullCusProduct | undefined = cusProducts.find(
+ (cusProduct: FullCusProduct) => cusProduct.product.id === product_id,
+ );
- let preview = with_preview
- ? await getProductCheckPreview({
- req,
- customer,
- product,
- logger,
- })
- : undefined;
+ let preview = with_preview
+ ? await getProductCheckPreview({
+ req,
+ customer,
+ product,
+ logger,
+ })
+ : undefined;
- // let preview = with_preview
- // ? await getAttachPreview({
- // db,
- // customer,
- // org,
- // env,
- // product: product!,
- // cusProducts,
- // features,
- // logger,
- // shouldFormat: with_preview == "formatted",
- // })
- // : undefined;
+ // let preview = with_preview
+ // ? await getAttachPreview({
+ // db,
+ // customer,
+ // org,
+ // env,
+ // product: product!,
+ // cusProducts,
+ // features,
+ // logger,
+ // shouldFormat: with_preview == "formatted",
+ // })
+ // : undefined;
- if (!cusProduct) {
- res.status(200).json({
- customer_id,
- code: SuccessCode.ProductFound,
- product_id,
- allowed: false,
+ if (!cusProduct) {
+ res.status(200).json({
+ customer_id,
+ code: SuccessCode.ProductFound,
+ product_id,
+ allowed: false,
- preview,
- });
- return;
- }
+ preview,
+ });
+ return;
+ }
- let onTrial =
- notNullish(cusProduct.trial_ends_at) &&
- cusProduct.trial_ends_at! > Date.now();
+ let onTrial =
+ notNullish(cusProduct.trial_ends_at) &&
+ cusProduct.trial_ends_at! > Date.now();
- res.status(200).json({
- customer_id,
- code: SuccessCode.ProductFound,
- product_id,
- entity_id,
- allowed:
- cusProduct.status === CusProductStatus.Active ||
- cusProduct.status === CusProductStatus.PastDue,
- status: notNullish(cusProduct.canceled_at)
- ? "canceled"
- : onTrial
- ? "trialing"
- : cusProduct.status,
+ res.status(200).json({
+ customer_id,
+ code: SuccessCode.ProductFound,
+ product_id,
+ entity_id,
+ allowed:
+ cusProduct.status === CusProductStatus.Active ||
+ cusProduct.status === CusProductStatus.PastDue,
+ status: notNullish(cusProduct.canceled_at)
+ ? "canceled"
+ : onTrial
+ ? "trialing"
+ : cusProduct.status,
- preview,
- });
+ preview,
+ });
- return;
+ return;
};
diff --git a/server/src/internal/api/events/EventService.ts b/server/src/internal/api/events/EventService.ts
index b240562a8..0bee12324 100644
--- a/server/src/internal/api/events/EventService.ts
+++ b/server/src/internal/api/events/EventService.ts
@@ -6,72 +6,72 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
import { and, eq, desc } from "drizzle-orm";
export class EventService {
- static async insert({ db, event }: { db: DrizzleCli; event: EventInsert }) {
- try {
- const results = await db
- .insert(events)
- .values(event as any)
- .returning();
+ static async insert({ db, event }: { db: DrizzleCli; event: EventInsert }) {
+ try {
+ const results = await db
+ .insert(events)
+ .values(event as any)
+ .returning();
- if (results.length === 0) {
- throw new RecaseError({
- message: "Failed to insert event",
- code: ErrCode.CreateEventFailed,
- data: results,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (results.length === 0) {
+ throw new RecaseError({
+ message: "Failed to insert event",
+ code: ErrCode.CreateEventFailed,
+ data: results,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- return results[0];
- } catch (error: any) {
- if (error.code == "23505") {
- throw new RecaseError({
- message:
- "Event (event_name, customer_id, idempotency_key) already exists.",
- code: ErrCode.DuplicateEvent,
- // data: error,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- } else throw error;
- }
- }
+ return results[0];
+ } catch (error: any) {
+ if (error.code == "23505") {
+ throw new RecaseError({
+ message:
+ "Event (event_name, customer_id, idempotency_key) already exists.",
+ code: ErrCode.DuplicateEvent,
+ // data: error,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ } else throw error;
+ }
+ }
- static async getByCustomerId({
- db,
- orgId,
- internalCustomerId,
- env,
- limit = 10,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- orgId: string;
- env: string;
- limit?: number;
- }) {
- let results = await db
- .select({
- id: events.id,
- event_name: events.event_name,
- value: events.value,
- created_at: events.created_at,
- timestamp: events.timestamp,
- idempotency_key: events.idempotency_key,
- properties: events.properties,
- set_usage: events.set_usage,
- entity_id: events.entity_id,
- })
- .from(events)
- .where(
- and(
- eq(events.internal_customer_id, internalCustomerId),
- eq(events.org_id, orgId),
- eq(events.env, env)
- )
- )
- .orderBy(desc(events.created_at))
- .limit(limit);
+ static async getByCustomerId({
+ db,
+ orgId,
+ internalCustomerId,
+ env,
+ limit = 10,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ orgId: string;
+ env: string;
+ limit?: number;
+ }) {
+ let results = await db
+ .select({
+ id: events.id,
+ event_name: events.event_name,
+ value: events.value,
+ created_at: events.created_at,
+ timestamp: events.timestamp,
+ idempotency_key: events.idempotency_key,
+ properties: events.properties,
+ set_usage: events.set_usage,
+ entity_id: events.entity_id,
+ })
+ .from(events)
+ .where(
+ and(
+ eq(events.internal_customer_id, internalCustomerId),
+ eq(events.org_id, orgId),
+ eq(events.env, env),
+ ),
+ )
+ .orderBy(desc(events.created_at))
+ .limit(limit);
- return results;
- }
+ return results;
+ }
}
diff --git a/server/src/internal/api/events/eventRouter.ts b/server/src/internal/api/events/eventRouter.ts
index ebff76ef8..ea6f7b4d3 100644
--- a/server/src/internal/api/events/eventRouter.ts
+++ b/server/src/internal/api/events/eventRouter.ts
@@ -1,17 +1,17 @@
import { Router } from "express";
import {
- APIVersion,
- AppEnv,
- CreateEventSchema,
- CusProductStatus,
- EntityData,
- ErrCode,
- EventInsert,
- Feature,
- FeatureType,
- FullCustomer,
- Organization,
+ APIVersion,
+ AppEnv,
+ CreateEventSchema,
+ CusProductStatus,
+ EntityData,
+ ErrCode,
+ EventInsert,
+ Feature,
+ FeatureType,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
@@ -33,243 +33,243 @@ import { getEventTimestamp } from "./eventUtils.js";
export const eventsRouter: Router = Router();
const getEventAndCustomer = async ({
- req,
- db,
- org,
- env,
- features,
- customer_id,
- customer_data,
- event_data,
- logger,
- entityId,
- entityData,
+ req,
+ db,
+ org,
+ env,
+ features,
+ customer_id,
+ customer_data,
+ event_data,
+ logger,
+ entityId,
+ entityData,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- org: Organization;
- features: Feature[];
- env: AppEnv;
- customer_id: string;
- customer_data: any;
- event_data: any;
- entityId: string;
- logger: any;
- entityData?: EntityData;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ org: Organization;
+ features: Feature[];
+ env: AppEnv;
+ customer_id: string;
+ customer_data: any;
+ event_data: any;
+ entityId: string;
+ logger: any;
+ entityData?: EntityData;
}) => {
- if (!customer_id) {
- throw new RecaseError({
- message: "Customer ID is required",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!customer_id) {
+ throw new RecaseError({
+ message: "Customer ID is required",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- let customer: FullCustomer;
+ let customer: FullCustomer;
- // 2. Check if customer ID is valid
- customer = await getOrCreateCustomer({
- req,
- customerId: customer_id,
- customerData: customer_data,
- entityId,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- entityData,
- });
+ // 2. Check if customer ID is valid
+ customer = await getOrCreateCustomer({
+ req,
+ customerId: customer_id,
+ customerData: customer_data,
+ entityId,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ entityData,
+ });
- // 3. Insert event
- const parsedEvent = CreateEventSchema.parse(event_data);
- const timestamp = getEventTimestamp(parsedEvent.timestamp);
+ // 3. Insert event
+ const parsedEvent = CreateEventSchema.parse(event_data);
+ const timestamp = getEventTimestamp(parsedEvent.timestamp);
- let internalEntityId = null;
- if (event_data.entity_id) {
- internalEntityId = customer.entity?.internal_id;
- }
+ let internalEntityId = null;
+ if (event_data.entity_id) {
+ internalEntityId = customer.entity?.internal_id;
+ }
- const newEvent: EventInsert = {
- ...parsedEvent,
- properties: parsedEvent.properties || {},
- id: generateId("evt"),
- org_id: org.id,
- org_slug: org.slug,
- env: env,
- internal_customer_id: customer.internal_id,
- created_at: timestamp.getTime(),
- timestamp: timestamp,
- internal_entity_id: internalEntityId,
- };
+ const newEvent: EventInsert = {
+ ...parsedEvent,
+ properties: parsedEvent.properties || {},
+ id: generateId("evt"),
+ org_id: org.id,
+ org_slug: org.slug,
+ env: env,
+ internal_customer_id: customer.internal_id,
+ created_at: timestamp.getTime(),
+ timestamp: timestamp,
+ internal_entity_id: internalEntityId,
+ };
- let event = await EventService.insert({ db, event: newEvent });
+ let event = await EventService.insert({ db, event: newEvent });
- return { customer, event };
+ return { customer, event };
};
const getAffectedFeatures = async ({
- req,
- eventName,
+ req,
+ eventName,
}: {
- req: any;
- eventName: string;
+ req: any;
+ eventName: string;
}) => {
- let features = await FeatureService.getFromReq(req);
+ let features = await FeatureService.getFromReq(req);
- let featuresWithEvent = features.filter((feature) => {
- return (
- feature.type == FeatureType.Metered &&
- feature.config.filters.some((filter: any) => {
- return filter.value.includes(eventName);
- })
- );
- });
+ let featuresWithEvent = features.filter((feature) => {
+ return (
+ feature.type == FeatureType.Metered &&
+ feature.config.filters.some((filter: any) => {
+ return filter.value.includes(eventName);
+ })
+ );
+ });
- let creditSystems = features.filter((cs: Feature) => {
- return (
- cs.type == FeatureType.CreditSystem &&
- featuresWithEvent.some((f) =>
- creditSystemContainsFeature({
- creditSystem: cs,
- meteredFeatureId: f.id,
- })
- )
- );
- });
+ let creditSystems = features.filter((cs: Feature) => {
+ return (
+ cs.type == FeatureType.CreditSystem &&
+ featuresWithEvent.some((f) =>
+ creditSystemContainsFeature({
+ creditSystem: cs,
+ meteredFeatureId: f.id,
+ }),
+ )
+ );
+ });
- return [...featuresWithEvent, ...creditSystems];
+ return [...featuresWithEvent, ...creditSystems];
};
export const handleEventSent = async ({
- req,
- customer_id,
- customer_data,
- event_data,
+ req,
+ customer_id,
+ customer_data,
+ event_data,
}: {
- req: any;
- customer_id: string;
- customer_data: any;
- event_data: any;
+ req: any;
+ customer_id: string;
+ customer_data: any;
+ event_data: any;
}) => {
- if (event_data.feature_id) {
- return handleUsageEvent({
- req,
- });
- }
+ if (event_data.feature_id) {
+ return handleUsageEvent({
+ req,
+ });
+ }
- const { env, db } = req;
+ const { env, db } = req;
- let eventName = event_data.event_name;
+ let eventName = event_data.event_name;
- const org = await OrgService.getFromReq(req);
- const features = await FeatureService.getFromReq(req);
+ const org = await OrgService.getFromReq(req);
+ const features = await FeatureService.getFromReq(req);
- const affectedFeatures = await getAffectedFeatures({
- req,
- eventName,
- });
+ const affectedFeatures = await getAffectedFeatures({
+ req,
+ eventName,
+ });
- if (affectedFeatures.length == 0) {
- throw new RecaseError({
- message: `No features found for event_name ${event_data.event_name}`,
- code: ErrCode.InvalidEventName,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (affectedFeatures.length == 0) {
+ throw new RecaseError({
+ message: `No features found for event_name ${event_data.event_name}`,
+ code: ErrCode.InvalidEventName,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const { customer, event } = await getEventAndCustomer({
- req,
- db,
- org,
- env,
- customer_id,
- customer_data,
- event_data,
- logger: req.logtail,
- entityId: event_data.entity_id,
- entityData: event_data.entity_data,
- features,
- });
+ const { customer, event } = await getEventAndCustomer({
+ req,
+ db,
+ org,
+ env,
+ customer_id,
+ customer_data,
+ event_data,
+ logger: req.logtail,
+ entityId: event_data.entity_id,
+ entityData: event_data.entity_data,
+ features,
+ });
- const payload = {
- internalCustomerId: customer.internal_id,
- customerId: customer.id,
- entityId: event_data.entity_id,
- features: affectedFeatures,
- allFeatures: req.features,
- event,
- org,
- env,
- };
+ const payload = {
+ internalCustomerId: customer.internal_id,
+ customerId: customer.id,
+ entityId: event_data.entity_id,
+ features: affectedFeatures,
+ allFeatures: req.features,
+ event,
+ org,
+ env,
+ };
- await addTaskToQueue({
- jobName: JobName.UpdateBalance,
- payload,
- });
+ await addTaskToQueue({
+ jobName: JobName.UpdateBalance,
+ payload,
+ });
- return { event, affectedFeatures, org };
+ return { event, affectedFeatures, org };
};
eventsRouter.post("", async (req: any, res: any) => {
- try {
- const body = req.body;
+ try {
+ const body = req.body;
- if (!body.event_name && !body.feature_id) {
- throw new RecaseError({
- message: "event_name or feature_id is required",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!body.event_name && !body.feature_id) {
+ throw new RecaseError({
+ message: "event_name or feature_id is required",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (notNullish(body.event_name) && notNullish(body.feature_id)) {
- throw new RecaseError({
- message:
- "either `event_name` or `feature_id` should be provided, not both",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (notNullish(body.event_name) && notNullish(body.feature_id)) {
+ throw new RecaseError({
+ message:
+ "either `event_name` or `feature_id` should be provided, not both",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (!body.customer_id) {
- throw new RecaseError({
- message: "customer_id is required",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!body.customer_id) {
+ throw new RecaseError({
+ message: "customer_id is required",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- let { event, org }: any = await handleEventSent({
- req,
- customer_id: body.customer_id,
- customer_data: body.customer_data,
- event_data: body,
- });
+ let { event, org }: any = await handleEventSent({
+ req,
+ customer_id: body.customer_id,
+ customer_data: body.customer_data,
+ event_data: body,
+ });
- let apiVersion = orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- });
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ });
- let response: any = {
- id: event?.id,
- code: "event_received",
- customer_id: body.customer_id,
- entity_id: body.entity_id,
- };
+ let response: any = {
+ id: event?.id,
+ code: "event_received",
+ customer_id: body.customer_id,
+ entity_id: body.entity_id,
+ };
- if (body.feature_id) {
- response.feature_id = body.feature_id;
- } else {
- response.event_name = event.event_name;
- }
+ if (body.feature_id) {
+ response.feature_id = body.feature_id;
+ } else {
+ response.event_name = event.event_name;
+ }
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json(response);
- } else {
- res.status(200).json({ success: true });
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json(response);
+ } else {
+ res.status(200).json({ success: true });
+ }
- return;
- } catch (error) {
- handleRequestError({ req, res, error, action: "POST event failed" });
- return;
- }
+ return;
+ } catch (error) {
+ handleRequestError({ req, res, error, action: "POST event failed" });
+ return;
+ }
});
diff --git a/server/src/internal/api/events/eventUtils.ts b/server/src/internal/api/events/eventUtils.ts
index 48b5e5b1f..17cfb9d2e 100644
--- a/server/src/internal/api/events/eventUtils.ts
+++ b/server/src/internal/api/events/eventUtils.ts
@@ -4,38 +4,38 @@ import { differenceInMonths, differenceInYears } from "date-fns";
import { StatusCodes } from "http-status-codes";
export const getEventTimestamp = (timestamp?: number | null) => {
- // 1. If timestamp is not provided, return now
- if (!timestamp) {
- return new Date();
- }
+ // 1. If timestamp is not provided, return now
+ if (!timestamp) {
+ return new Date();
+ }
- try {
- let date = new Date(timestamp);
+ try {
+ let date = new Date(timestamp);
- if (differenceInYears(new Date(), date) >= 2) {
- throw new RecaseError({
- message: "Timestamp must be within the last 2 years",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- } else if (differenceInMonths(new Date(), date) <= -1) {
- throw new RecaseError({
- message: "Timestamp can only be up to 1 month in the future",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (differenceInYears(new Date(), date) >= 2) {
+ throw new RecaseError({
+ message: "Timestamp must be within the last 2 years",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ } else if (differenceInMonths(new Date(), date) <= -1) {
+ throw new RecaseError({
+ message: "Timestamp can only be up to 1 month in the future",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- return date;
- } catch (error) {
- if (error instanceof RecaseError) {
- throw error;
- }
+ return date;
+ } catch (error) {
+ if (error instanceof RecaseError) {
+ throw error;
+ }
- throw new RecaseError({
- message: "Invalid timestamp",
- code: ErrCode.InvalidInputs,
- data: error,
- });
- }
+ throw new RecaseError({
+ message: "Invalid timestamp",
+ code: ErrCode.InvalidInputs,
+ data: error,
+ });
+ }
};
diff --git a/server/src/internal/api/events/usageRouter.ts b/server/src/internal/api/events/usageRouter.ts
index b1f3f91f4..27e28fe8f 100644
--- a/server/src/internal/api/events/usageRouter.ts
+++ b/server/src/internal/api/events/usageRouter.ts
@@ -1,10 +1,10 @@
import { Router } from "express";
import {
- CusProductStatus,
- ErrCode,
- EventInsert,
- FeatureType,
- FullCustomer,
+ CusProductStatus,
+ ErrCode,
+ EventInsert,
+ FeatureType,
+ FullCustomer,
} from "@autumn/shared";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { generateId, nullish } from "@/utils/genUtils.js";
@@ -24,215 +24,215 @@ export const eventsRouter: Router = Router();
export const usageRouter: Router = Router();
const getCusFeatureAndOrg = async ({
- req,
- customerId,
- featureId,
- entityId,
- customerData,
+ req,
+ customerId,
+ featureId,
+ entityId,
+ customerData,
}: {
- req: ExtendedRequest;
- customerId: string;
- featureId: string;
- entityId: string;
- customerData: any;
+ req: ExtendedRequest;
+ customerId: string;
+ featureId: string;
+ entityId: string;
+ customerData: any;
}) => {
- // 1. Get customer
- const { org, features } = req;
+ // 1. Get customer
+ const { org, features } = req;
- let customer = await getOrCreateCustomer({
- req,
- customerId,
- customerData,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- entityId,
- entityData: req.body.entity_data,
- withEntities: true,
- });
+ let customer = await getOrCreateCustomer({
+ req,
+ customerId,
+ customerData,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ entityId,
+ entityData: req.body.entity_data,
+ withEntities: true,
+ });
- let feature = features.find((f) => f.id == featureId);
- let creditSystems = features.filter(
- (f) =>
- f.type == FeatureType.CreditSystem &&
- creditSystemContainsFeature({
- creditSystem: f,
- meteredFeatureId: featureId,
- })
- );
+ let feature = features.find((f) => f.id == featureId);
+ let creditSystems = features.filter(
+ (f) =>
+ f.type == FeatureType.CreditSystem &&
+ creditSystemContainsFeature({
+ creditSystem: f,
+ meteredFeatureId: featureId,
+ }),
+ );
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${featureId} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${featureId} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- return { customer, org, feature, creditSystems };
+ return { customer, org, feature, creditSystems };
};
const createAndInsertEvent = async ({
- req,
- customer,
- featureId,
- value,
- set_usage,
- properties,
- idempotencyKey,
+ req,
+ customer,
+ featureId,
+ value,
+ set_usage,
+ properties,
+ idempotencyKey,
}: {
- req: any;
- customer: FullCustomer;
- featureId: string;
- value?: number;
- set_usage?: boolean;
- properties: any;
- idempotencyKey?: string;
+ req: any;
+ customer: FullCustomer;
+ featureId: string;
+ value?: number;
+ set_usage?: boolean;
+ properties: any;
+ idempotencyKey?: string;
}) => {
- if (!customer.id) {
- throw new RecaseError({
- message: "Customer ID is required",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!customer.id) {
+ throw new RecaseError({
+ message: "Customer ID is required",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const timestamp = getEventTimestamp(req.body.timestamp);
+ const timestamp = getEventTimestamp(req.body.timestamp);
- const entityId = req.body.entity_id;
- let internalEntityId = null;
- if (entityId) {
- internalEntityId = customer.entity?.internal_id;
- }
+ const entityId = req.body.entity_id;
+ let internalEntityId = null;
+ if (entityId) {
+ internalEntityId = customer.entity?.internal_id;
+ }
- const newEvent: EventInsert = {
- id: generateId("evt"),
- org_id: req.orgId,
- org_slug: req.org.slug,
- env: req.env,
- internal_customer_id: customer.internal_id,
+ const newEvent: EventInsert = {
+ id: generateId("evt"),
+ org_id: req.orgId,
+ org_slug: req.org.slug,
+ env: req.env,
+ internal_customer_id: customer.internal_id,
- created_at: timestamp.getTime(),
- timestamp: timestamp,
+ created_at: timestamp.getTime(),
+ timestamp: timestamp,
- idempotency_key: idempotencyKey,
- customer_id: customer.id,
- event_name: featureId,
- properties,
- value,
- set_usage: set_usage || false,
- entity_id: req.body.entity_id,
- internal_entity_id: internalEntityId,
- };
+ idempotency_key: idempotencyKey,
+ customer_id: customer.id,
+ event_name: featureId,
+ properties,
+ value,
+ set_usage: set_usage || false,
+ entity_id: req.body.entity_id,
+ internal_entity_id: internalEntityId,
+ };
- return await EventService.insert({ db: req.db, event: newEvent });
+ return await EventService.insert({ db: req.db, event: newEvent });
};
export const handleUsageEvent = async ({
- req,
- setUsage = false,
+ req,
+ setUsage = false,
}: {
- req: any;
- setUsage?: boolean;
+ req: any;
+ setUsage?: boolean;
}) => {
- let {
- customer_id,
- customer_data,
- properties,
- feature_id,
- value,
- entity_id,
- idempotency_key,
- } = req.body;
- const { logtail: logger } = req;
+ let {
+ customer_id,
+ customer_data,
+ properties,
+ feature_id,
+ value,
+ entity_id,
+ idempotency_key,
+ } = req.body;
+ const { logtail: logger } = req;
- if (!customer_id || !feature_id) {
- throw new RecaseError({
- message: "customer_id and feature_id are required",
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!customer_id || !feature_id) {
+ throw new RecaseError({
+ message: "customer_id and feature_id are required",
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- properties = properties || {};
+ properties = properties || {};
- logger.info(`/track: customer ${customer_id}, feature ${feature_id}`);
- const startTime = Date.now();
- const { customer, org, feature, creditSystems } = await getCusFeatureAndOrg({
- req,
- customerId: customer_id,
- featureId: feature_id,
- customerData: customer_data,
- entityId: entity_id,
- });
- logger.info(`/track: get customer took ${Date.now() - startTime}ms`);
- const startTime2 = Date.now();
+ logger.info(`/track: customer ${customer_id}, feature ${feature_id}`);
+ const startTime = Date.now();
+ const { customer, org, feature, creditSystems } = await getCusFeatureAndOrg({
+ req,
+ customerId: customer_id,
+ featureId: feature_id,
+ customerData: customer_data,
+ entityId: entity_id,
+ });
+ logger.info(`/track: get customer took ${Date.now() - startTime}ms`);
+ const startTime2 = Date.now();
- let newEvent = await createAndInsertEvent({
- req,
- customer,
- featureId: feature_id,
- value,
- set_usage: setUsage,
- properties,
- idempotencyKey: idempotency_key,
- });
- logger.info(`/track: insert event took ${Date.now() - startTime2}ms`);
+ let newEvent = await createAndInsertEvent({
+ req,
+ customer,
+ featureId: feature_id,
+ value,
+ set_usage: setUsage,
+ properties,
+ idempotencyKey: idempotency_key,
+ });
+ logger.info(`/track: insert event took ${Date.now() - startTime2}ms`);
- const features = [feature, ...creditSystems];
+ const features = [feature, ...creditSystems];
- if (nullish(value) || isNaN(parseFloat(value))) {
- value = 1;
- } else {
- value = parseFloat(value);
- }
+ if (nullish(value) || isNaN(parseFloat(value))) {
+ value = 1;
+ } else {
+ value = parseFloat(value);
+ }
- const payload = {
- customerId: customer.id,
- internalCustomerId: customer.internal_id,
- eventId: newEvent.id,
- features,
- allFeatures: req.features,
- org,
- env: req.env,
- properties,
- value,
- set_usage: setUsage,
- entityId: entity_id,
- };
+ const payload = {
+ customerId: customer.id,
+ internalCustomerId: customer.internal_id,
+ eventId: newEvent.id,
+ features,
+ allFeatures: req.features,
+ org,
+ env: req.env,
+ properties,
+ value,
+ set_usage: setUsage,
+ entityId: entity_id,
+ };
- // console.log("Customer:", customer);
- // console.log(
- // "Is paid continuous use:",
- // isPaidContinuousUse({ feature, fullCus: customer })
- // );
+ // console.log("Customer:", customer);
+ // console.log(
+ // "Is paid continuous use:",
+ // isPaidContinuousUse({ feature, fullCus: customer })
+ // );
- if (isPaidContinuousUse({ feature, fullCus: customer })) {
- console.log(`Running update usage task synchronously`);
- await runUpdateUsageTask({
- payload,
- logger: console,
- db: req.db,
- throwError: true,
- });
- } else {
- await addTaskToQueue({
- jobName: JobName.UpdateUsage,
- payload,
- });
- }
+ if (isPaidContinuousUse({ feature, fullCus: customer })) {
+ console.log(`Running update usage task synchronously`);
+ await runUpdateUsageTask({
+ payload,
+ logger: console,
+ db: req.db,
+ throwError: true,
+ });
+ } else {
+ await addTaskToQueue({
+ jobName: JobName.UpdateUsage,
+ payload,
+ });
+ }
- return { event: newEvent, affectedFeatures: features, org };
+ return { event: newEvent, affectedFeatures: features, org };
};
usageRouter.post("", async (req: any, res: any) => {
- try {
- await handleUsageEvent({ req, setUsage: true });
- res.status(StatusCodes.OK).json({ success: true });
- } catch (error) {
- return handleRequestError({
- req,
- res,
- error,
- action: "handleUsageEvent",
- });
- }
+ try {
+ await handleUsageEvent({ req, setUsage: true });
+ res.status(StatusCodes.OK).json({ success: true });
+ } catch (error) {
+ return handleRequestError({
+ req,
+ res,
+ error,
+ action: "handleUsageEvent",
+ });
+ }
});
diff --git a/server/src/internal/api/invoiceRouter.ts b/server/src/internal/api/invoiceRouter.ts
index 73738f70f..3a9222ffc 100644
--- a/server/src/internal/api/invoiceRouter.ts
+++ b/server/src/internal/api/invoiceRouter.ts
@@ -7,17 +7,17 @@ import { Router } from "express";
export const invoiceRouter: Router = Router();
invoiceRouter.get("/:stripe_invoice_id/stripe", async (req: any, res: any) => {
- try {
- const org = await OrgService.getFromReq(req);
+ try {
+ const org = await OrgService.getFromReq(req);
- const stripeCli = createStripeCli({ org, env: req.env });
+ const stripeCli = createStripeCli({ org, env: req.env });
- const stripeInvoice = await stripeCli.invoices.retrieve(
- req.params.stripe_invoice_id,
- );
+ const stripeInvoice = await stripeCli.invoices.retrieve(
+ req.params.stripe_invoice_id,
+ );
- res.status(200).json(stripeInvoice);
- } catch (error) {
- handleRequestError({ req, error, res, action: "Get invoice" });
- }
+ res.status(200).json(stripeInvoice);
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "Get invoice" });
+ }
});
diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts
index 53f985e02..4f93b0360 100644
--- a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts
+++ b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts
@@ -1,8 +1,8 @@
import {
- ErrCode,
- RewardCategory,
- type RewardRedemption,
- RewardTriggerEvent,
+ ErrCode,
+ RewardCategory,
+ type RewardRedemption,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { parseReqForAction } from "@/internal/analytics/actionUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
@@ -19,187 +19,187 @@ import type { ExtendedRequest } from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
export default async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "redeem referral code",
- handler: async (req, res) => {
- const { orgId, env, logtail: logger, db } = req;
- const { code, customer_id: customerId } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "redeem referral code",
+ handler: async (req, res) => {
+ const { orgId, env, logtail: logger, db } = req;
+ const { code, customer_id: customerId } = req.body;
- // 1. Get redeemed by customer, and referral code
- const [customer, referralCode, org] = await Promise.all([
- CusService.get({
- db,
- orgId,
- env,
- idOrInternalId: customerId,
- }),
- RewardProgramService.getReferralCode({
- db,
- orgId,
- env,
- code,
- withRewardProgram: true,
- }),
- OrgService.getFromReq(req),
- ]);
+ // 1. Get redeemed by customer, and referral code
+ const [customer, referralCode, org] = await Promise.all([
+ CusService.get({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customerId,
+ }),
+ RewardProgramService.getReferralCode({
+ db,
+ orgId,
+ env,
+ code,
+ withRewardProgram: true,
+ }),
+ OrgService.getFromReq(req),
+ ]);
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- statusCode: 404,
- code: ErrCode.CustomerNotFound,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ statusCode: 404,
+ code: ErrCode.CustomerNotFound,
+ });
+ }
- // 2. Check that code has not reached max redemptions
- const redemptionCount = await RewardProgramService.getCodeRedemptionCount(
- {
- db,
- referralCodeId: referralCode.id,
- }
- );
+ // 2. Check that code has not reached max redemptions
+ const redemptionCount = await RewardProgramService.getCodeRedemptionCount(
+ {
+ db,
+ referralCodeId: referralCode.id,
+ },
+ );
- if (
- referralCode.reward_program.max_redemptions &&
- redemptionCount >= referralCode.reward_program.max_redemptions
- ) {
- throw new RecaseError({
- message: "Referral code has reached max redemptions",
- statusCode: 400,
- code: ErrCode.ReferralCodeMaxRedemptionsReached,
- });
- }
+ if (
+ referralCode.reward_program.max_redemptions &&
+ redemptionCount >= referralCode.reward_program.max_redemptions
+ ) {
+ throw new RecaseError({
+ message: "Referral code has reached max redemptions",
+ statusCode: 400,
+ code: ErrCode.ReferralCodeMaxRedemptionsReached,
+ });
+ }
- // 3. Check that customer has not already redeemed a code in this referral program
- const existingRedemptions = await RewardRedemptionService.getByCustomer({
- db,
- internalCustomerId: customer.internal_id,
- internalRewardProgramId: referralCode.internal_reward_program_id,
- });
+ // 3. Check that customer has not already redeemed a code in this referral program
+ const existingRedemptions = await RewardRedemptionService.getByCustomer({
+ db,
+ internalCustomerId: customer.internal_id,
+ internalRewardProgramId: referralCode.internal_reward_program_id,
+ });
- if (existingRedemptions.length > 0) {
- throw new RecaseError({
- message: `Customer ${customer.id} has already redeemed a code in this referral program`,
- statusCode: 400,
- code: ErrCode.CustomerAlreadyRedeemedReferralCode,
- });
- }
+ if (existingRedemptions.length > 0) {
+ throw new RecaseError({
+ message: `Customer ${customer.id} has already redeemed a code in this referral program`,
+ statusCode: 400,
+ code: ErrCode.CustomerAlreadyRedeemedReferralCode,
+ });
+ }
- // Don't let customer redeem their own code
- const codeCustomer = await CusService.getByInternalId({
- db: req.db,
- internalId: referralCode.internal_customer_id,
- });
+ // Don't let customer redeem their own code
+ const codeCustomer = await CusService.getByInternalId({
+ db: req.db,
+ internalId: referralCode.internal_customer_id,
+ });
- if (!codeCustomer) {
- throw new RecaseError({
- message: "Referral code customer not found",
- statusCode: 404,
- code: ErrCode.CustomerNotFound,
- });
- }
+ if (!codeCustomer) {
+ throw new RecaseError({
+ message: "Referral code customer not found",
+ statusCode: 404,
+ code: ErrCode.CustomerNotFound,
+ });
+ }
- if (
- codeCustomer.id === customer.id ||
- (notNullish(codeCustomer.fingerprint) &&
- codeCustomer.fingerprint === customer.fingerprint)
- ) {
- throw new RecaseError({
- message: "Customer cannot redeem their own code",
- statusCode: 400,
- code: ErrCode.CustomerCannotRedeemOwnCode,
- });
- }
+ if (
+ codeCustomer.id === customer.id ||
+ (notNullish(codeCustomer.fingerprint) &&
+ codeCustomer.fingerprint === customer.fingerprint)
+ ) {
+ throw new RecaseError({
+ message: "Customer cannot redeem their own code",
+ statusCode: 400,
+ code: ErrCode.CustomerCannotRedeemOwnCode,
+ });
+ }
- // 4. Insert redemption into db
- let redemption: RewardRedemption = {
- id: generateId("rr"),
- referral_code_id: referralCode.id,
- internal_customer_id: customer.internal_id, // redeemed by customer
- internal_reward_program_id: referralCode.internal_reward_program_id,
- created_at: Date.now(),
- triggered:
- referralCode.reward_program.when ===
- RewardTriggerEvent.CustomerCreation,
- applied: false,
- updated_at: Date.now(),
- redeemer_applied: false,
- };
+ // 4. Insert redemption into db
+ let redemption: RewardRedemption = {
+ id: generateId("rr"),
+ referral_code_id: referralCode.id,
+ internal_customer_id: customer.internal_id, // redeemed by customer
+ internal_reward_program_id: referralCode.internal_reward_program_id,
+ created_at: Date.now(),
+ triggered:
+ referralCode.reward_program.when ===
+ RewardTriggerEvent.CustomerCreation,
+ applied: false,
+ updated_at: Date.now(),
+ redeemer_applied: false,
+ };
- redemption = await RewardRedemptionService.insert({
- db,
- rewardRedemption: redemption,
- });
+ redemption = await RewardRedemptionService.insert({
+ db,
+ rewardRedemption: redemption,
+ });
- // 5. If reward trigger when is immediate:
- const { reward_program } = referralCode;
- const redeemRewardNow =
- referralCode.reward_program.when ===
- RewardTriggerEvent.CustomerCreation;
+ // 5. If reward trigger when is immediate:
+ const { reward_program } = referralCode;
+ const redeemRewardNow =
+ referralCode.reward_program.when ===
+ RewardTriggerEvent.CustomerCreation;
- if (redeemRewardNow) {
- const reward = await RewardService.get({
- db,
- orgId,
- env,
- idOrInternalId: reward_program.internal_reward_id,
- });
+ if (redeemRewardNow) {
+ const reward = await RewardService.get({
+ db,
+ orgId,
+ env,
+ idOrInternalId: reward_program.internal_reward_id,
+ });
- if (!reward) {
- throw new RecaseError({
- message: `Reward ${reward_program.internal_reward_id} not found`,
- statusCode: 404,
- code: ErrCode.RewardNotFound,
- });
- }
+ if (!reward) {
+ throw new RecaseError({
+ message: `Reward ${reward_program.internal_reward_id} not found`,
+ statusCode: 404,
+ code: ErrCode.RewardNotFound,
+ });
+ }
- const rewardCat = getRewardCat(reward);
- if (rewardCat === RewardCategory.FreeProduct) {
- await triggerFreeProduct({
- req: parseReqForAction(req) as ExtendedRequest,
- db,
- referralCode,
- redeemer: customer,
- rewardProgram: reward_program,
- org,
- env,
- logger,
- redemption,
- });
- } else {
- await triggerRedemption({
- db,
- referralCode,
- org,
- env,
- logger,
- reward,
- redemption,
- });
- }
- }
+ const rewardCat = getRewardCat(reward);
+ if (rewardCat === RewardCategory.FreeProduct) {
+ await triggerFreeProduct({
+ req: parseReqForAction(req) as ExtendedRequest,
+ db,
+ referralCode,
+ redeemer: customer,
+ rewardProgram: reward_program,
+ org,
+ env,
+ logger,
+ redemption,
+ });
+ } else {
+ await triggerRedemption({
+ db,
+ referralCode,
+ org,
+ env,
+ logger,
+ reward,
+ redemption,
+ });
+ }
+ }
- return res.status(200).json({
- id: redemption.id,
- customer_id: customer.id,
- reward_id: reward_program.reward.id,
- referrer: {
- id: codeCustomer.id,
- name: codeCustomer.name,
- email: codeCustomer.email,
- created_at: codeCustomer.created_at,
- },
- redeemer: {
- id: customer.id,
- name: customer.name,
- email: customer.email,
- created_at: customer.created_at,
- },
- });
- },
- });
+ return res.status(200).json({
+ id: redemption.id,
+ customer_id: customer.id,
+ reward_id: reward_program.reward.id,
+ referrer: {
+ id: codeCustomer.id,
+ name: codeCustomer.name,
+ email: codeCustomer.email,
+ created_at: codeCustomer.created_at,
+ },
+ redeemer: {
+ id: customer.id,
+ name: customer.name,
+ email: customer.email,
+ created_at: customer.created_at,
+ },
+ });
+ },
+ });
// res.status(200).json({
// id: redemption.id,
diff --git a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts
index 150d5f04d..00c281dd1 100644
--- a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts
+++ b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts
@@ -1,7 +1,7 @@
import {
- CreateRewardProgram,
- ErrCode,
- RewardTriggerEvent,
+ CreateRewardProgram,
+ ErrCode,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js";
diff --git a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts
index 722e21d64..b976a1298 100644
--- a/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts
+++ b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts
@@ -1,7 +1,7 @@
import {
- CreateRewardProgram,
- ErrCode,
- RewardTriggerEvent,
+ CreateRewardProgram,
+ ErrCode,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js";
diff --git a/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts
index 2f35e4645..2424e75f5 100644
--- a/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts
+++ b/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts
@@ -1,3 +1,3 @@
export { default as handleCreateRewardProgram } from "./handleCreateRewardProgram.js";
export { default as handleCreateTrigger } from "./handleCreateTrigger.js";
-export { default as handleDeleteRewardProgram } from "./handleDeleteRewardProgram.js";
\ No newline at end of file
+export { default as handleDeleteRewardProgram } from "./handleDeleteRewardProgram.js";
diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts
index c51b4948b..39f6c406e 100644
--- a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts
+++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts
@@ -1,7 +1,7 @@
import {
- CreateRewardSchema,
- isFixedPrice,
- RewardCategory,
+ CreateRewardSchema,
+ isFixedPrice,
+ RewardCategory,
} from "@autumn/shared";
import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -11,93 +11,93 @@ import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
import {
- constructReward,
- getRewardCat,
- initRewardStripePrices,
+ constructReward,
+ getRewardCat,
+ initRewardStripePrices,
} from "@/internal/rewards/rewardUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
export default async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "create coupon",
- handler: async (req, res) => {
- const { db, orgId, env, logtail: logger } = req;
- const rewardBody = req.body;
- const rewardData = CreateRewardSchema.parse(rewardBody);
+ routeHandler({
+ req,
+ res,
+ action: "create coupon",
+ handler: async (req, res) => {
+ const { db, orgId, env, logtail: logger } = req;
+ const rewardBody = req.body;
+ const rewardData = CreateRewardSchema.parse(rewardBody);
- const org = await OrgService.getFromReq(req);
+ const org = await OrgService.getFromReq(req);
- const newReward = constructReward({
- reward: rewardData,
- orgId,
- env,
- });
+ const newReward = constructReward({
+ reward: rewardData,
+ orgId,
+ env,
+ });
- if (getRewardCat(newReward) === RewardCategory.Discount) {
- const discountConfig = newReward.discount_config;
+ if (getRewardCat(newReward) === RewardCategory.Discount) {
+ const discountConfig = newReward.discount_config;
- // Get prices for coupon
- const [prices] = await Promise.all([
- PriceService.getInIds({
- db,
- ids: discountConfig!.price_ids || [],
- }),
- ]);
+ // Get prices for coupon
+ const [prices] = await Promise.all([
+ PriceService.getInIds({
+ db,
+ ids: discountConfig!.price_ids || [],
+ }),
+ ]);
- await initRewardStripePrices({
- db,
- prices,
- org,
- env,
- logger,
- });
+ await initRewardStripePrices({
+ db,
+ prices,
+ org,
+ env,
+ logger,
+ });
- await createStripeCoupon({
- reward: newReward,
- org,
- env,
- prices,
- logger,
- legacyVersion: req.query.legacyStripe === "true",
- });
- }
+ await createStripeCoupon({
+ reward: newReward,
+ org,
+ env,
+ prices,
+ logger,
+ legacyVersion: req.query.legacyStripe === "true",
+ });
+ }
- if (getRewardCat(newReward) === RewardCategory.FreeProduct) {
- // 1. Check if product is paid
- const fullProduct = await ProductService.getFull({
- db,
- idOrInternalId: newReward.free_product_id!,
- orgId: org.id,
- env,
- });
+ if (getRewardCat(newReward) === RewardCategory.FreeProduct) {
+ // 1. Check if product is paid
+ const fullProduct = await ProductService.getFull({
+ db,
+ idOrInternalId: newReward.free_product_id!,
+ orgId: org.id,
+ env,
+ });
- if (!isFreeProduct(fullProduct.prices)) {
- // For one-off products, include all prices; for recurring products, only fixed prices
- const isProductOneOff = pricesOnlyOneOff(fullProduct.prices);
- const relevantPrices = isProductOneOff
- ? fullProduct.prices // Include all prices for one-off products
- : fullProduct.prices.filter((price) => isFixedPrice({ price })); // Only fixed prices for recurring products
+ if (!isFreeProduct(fullProduct.prices)) {
+ // For one-off products, include all prices; for recurring products, only fixed prices
+ const isProductOneOff = pricesOnlyOneOff(fullProduct.prices);
+ const relevantPrices = isProductOneOff
+ ? fullProduct.prices // Include all prices for one-off products
+ : fullProduct.prices.filter((price) => isFixedPrice({ price })); // Only fixed prices for recurring products
- await createStripeCoupon({
- reward: newReward,
- org,
- env,
- prices: relevantPrices.map((price) => ({
- ...price,
- product: fullProduct,
- })),
- logger,
- });
- }
- }
+ await createStripeCoupon({
+ reward: newReward,
+ org,
+ env,
+ prices: relevantPrices.map((price) => ({
+ ...price,
+ product: fullProduct,
+ })),
+ logger,
+ });
+ }
+ }
- const insertedCoupon = await RewardService.insert({
- db,
- data: newReward,
- });
+ const insertedCoupon = await RewardService.insert({
+ db,
+ data: newReward,
+ });
- res.status(200).json(insertedCoupon);
- },
- });
+ res.status(200).json(insertedCoupon);
+ },
+ });
diff --git a/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts
index da02f9751..c22341b90 100644
--- a/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts
+++ b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts
@@ -37,7 +37,9 @@ export default async (req: any, res: any) =>
try {
await stripeCli.coupons.del(reward.id);
} catch (error) {
- console.log(`Failed to delete coupon from stripe: ${(error as { message: string }).message}`);
+ console.log(
+ `Failed to delete coupon from stripe: ${(error as { message: string }).message}`,
+ );
}
await RewardService.delete({
diff --git a/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts
index f4a694555..85fcc00fe 100644
--- a/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts
+++ b/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts
@@ -1,21 +1,22 @@
import { RewardService } from "@/internal/rewards/RewardService.js";
import { routeHandler } from "@/utils/routerUtils.js";
-export default async (req: any, res: any) => routeHandler({
- req,
- res,
- action: "get reward",
- handler: async (req, res) => {
- const { id } = req.params;
- const { orgId, env, db } = req;
+export default async (req: any, res: any) =>
+ routeHandler({
+ req,
+ res,
+ action: "get reward",
+ handler: async (req, res) => {
+ const { id } = req.params;
+ const { orgId, env, db } = req;
- const reward = await RewardService.get({
- db,
- idOrInternalId: id,
- orgId,
- env,
- });
+ const reward = await RewardService.get({
+ db,
+ idOrInternalId: id,
+ orgId,
+ env,
+ });
- res.status(200).json(reward);
- }
-});
\ No newline at end of file
+ res.status(200).json(reward);
+ },
+ });
diff --git a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts
index a664f8497..a5c9e05f3 100644
--- a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts
+++ b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts
@@ -107,4 +107,4 @@ export default async (req: any, res: any) =>
res.status(200).json(updatedCoupon);
},
- });
\ No newline at end of file
+ });
diff --git a/server/src/internal/api/rewards/handlers/rewards/index.ts b/server/src/internal/api/rewards/handlers/rewards/index.ts
index 29b1d2695..16a8490ef 100644
--- a/server/src/internal/api/rewards/handlers/rewards/index.ts
+++ b/server/src/internal/api/rewards/handlers/rewards/index.ts
@@ -3,4 +3,9 @@ import handleDeleteCoupon from "./handleDeleteCoupon.js";
import handleGetCoupon from "./handleGetCoupon.js";
import handleUpdateCoupon from "./handleUpdateCoupon.js";
-export { handleCreateCoupon, handleDeleteCoupon, handleGetCoupon, handleUpdateCoupon };
\ No newline at end of file
+export {
+ handleCreateCoupon,
+ handleDeleteCoupon,
+ handleGetCoupon,
+ handleUpdateCoupon,
+};
diff --git a/server/src/internal/api/rewards/referralRouter.ts b/server/src/internal/api/rewards/referralRouter.ts
index 98021711b..5e7774d12 100644
--- a/server/src/internal/api/rewards/referralRouter.ts
+++ b/server/src/internal/api/rewards/referralRouter.ts
@@ -1,8 +1,8 @@
import express, { type Router } from "express";
import {
- handleGetRedemption,
- handleGetReferralCode,
- handleRedeemReferral,
+ handleGetRedemption,
+ handleGetReferralCode,
+ handleRedeemReferral,
} from "./handlers/referrals/index.js";
export const referralRouter: Router = express.Router();
diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts
index 8f183eeca..9b80ae01a 100644
--- a/server/src/internal/api/rewards/rewardProgramRouter.ts
+++ b/server/src/internal/api/rewards/rewardProgramRouter.ts
@@ -1,15 +1,15 @@
import express, { type Router } from "express";
import {
- handleCreateRewardProgram,
- handleDeleteRewardProgram,
+ handleCreateRewardProgram,
+ handleDeleteRewardProgram,
} from "./handlers/rewardPrograms/index.js";
import { routeHandler } from "@/utils/routerUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- CreateRewardProgram,
- ErrCode,
- nullish,
- RewardTriggerEvent,
+ CreateRewardProgram,
+ ErrCode,
+ nullish,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js";
@@ -21,72 +21,72 @@ rewardProgramRouter.post("", handleCreateRewardProgram);
rewardProgramRouter.delete("/:id", handleDeleteRewardProgram);
rewardProgramRouter.put("/:id", (req, res) =>
- routeHandler({
- req,
- res,
- action: "update reward program",
- handler: async (req: any, res: any) => {
- const { orgId, env, db } = req;
- const { id } = req.params;
- const body = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "update reward program",
+ handler: async (req: any, res: any) => {
+ const { orgId, env, db } = req;
+ const { id } = req.params;
+ const body = req.body;
- if (!body.internal_reward_id) {
- throw new RecaseError({
- message: "Please select a reward to link this program to",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!body.internal_reward_id) {
+ throw new RecaseError({
+ message: "Please select a reward to link this program to",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- // Ensure program exists
- let existingProgram = await RewardProgramService.get({
- db,
- idOrInternalId: id,
- orgId,
- env,
- });
+ // Ensure program exists
+ let existingProgram = await RewardProgramService.get({
+ db,
+ idOrInternalId: id,
+ orgId,
+ env,
+ });
- if (!existingProgram) {
- throw new RecaseError({
- message: `Program with ID ${id} does not exist`,
- code: ErrCode.InvalidRequest,
- statusCode: 404,
- });
- }
+ if (!existingProgram) {
+ throw new RecaseError({
+ message: `Program with ID ${id} does not exist`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 404,
+ });
+ }
- const rewardProgram = constructRewardProgram({
- rewardProgramData: CreateRewardProgram.parse({
- ...body,
- id: existingProgram.id, // ID cannot be changed
- }),
- orgId,
- env,
- });
+ const rewardProgram = constructRewardProgram({
+ rewardProgramData: CreateRewardProgram.parse({
+ ...body,
+ id: existingProgram.id, // ID cannot be changed
+ }),
+ orgId,
+ env,
+ });
- // Update on existing redemptions? (should be none unless affecting stacked rewards...)
+ // Update on existing redemptions? (should be none unless affecting stacked rewards...)
- if (
- rewardProgram.when == RewardTriggerEvent.Checkout &&
- (nullish(rewardProgram.product_ids) ||
- rewardProgram.product_ids!.length == 0)
- ) {
- throw new RecaseError({
- message:
- "When `Redeem On` is set to `Checkout`, must specify at least one product",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (
+ rewardProgram.when == RewardTriggerEvent.Checkout &&
+ (nullish(rewardProgram.product_ids) ||
+ rewardProgram.product_ids!.length == 0)
+ ) {
+ throw new RecaseError({
+ message:
+ "When `Redeem On` is set to `Checkout`, must specify at least one product",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- let updatedRewardProgram = await RewardProgramService.update({
- db,
- idOrInternalId: id,
- orgId,
- env,
- data: rewardProgram,
- });
+ let updatedRewardProgram = await RewardProgramService.update({
+ db,
+ idOrInternalId: id,
+ orgId,
+ env,
+ data: rewardProgram,
+ });
- return res.status(200).json(updatedRewardProgram);
- },
- })
+ return res.status(200).json(updatedRewardProgram);
+ },
+ }),
);
diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts
index 86aff9652..dd52f0fa8 100644
--- a/server/src/internal/api/rewards/rewardRouter.ts
+++ b/server/src/internal/api/rewards/rewardRouter.ts
@@ -1,5 +1,10 @@
import express, { type Router } from "express";
-import { handleCreateCoupon, handleDeleteCoupon, handleGetCoupon, handleUpdateCoupon } from "./handlers/rewards/index.js";
+import {
+ handleCreateCoupon,
+ handleDeleteCoupon,
+ handleGetCoupon,
+ handleUpdateCoupon,
+} from "./handlers/rewards/index.js";
const rewardRouter: Router = express.Router();
@@ -11,4 +16,4 @@ rewardRouter.post("/:internalId", handleUpdateCoupon);
rewardRouter.get("/:id", handleGetCoupon);
-export default rewardRouter;
\ No newline at end of file
+export default rewardRouter;
diff --git a/server/src/internal/api/trmnl/trmnlRouter.ts b/server/src/internal/api/trmnl/trmnlRouter.ts
index c4833496c..944df0280 100644
--- a/server/src/internal/api/trmnl/trmnlRouter.ts
+++ b/server/src/internal/api/trmnl/trmnlRouter.ts
@@ -13,223 +13,223 @@ import { writeFile } from "fs/promises";
import { initUpstash } from "@/internal/customers/cusCache/upstashUtils.js";
const trmnlLimiter = rateLimit({
- windowMs: 60 * 1000 * 30,
- limit: process.env.NODE_ENV === "development" ? 1000 : 10,
- standardHeaders: "draft-8",
- legacyHeaders: false,
- validate: { xForwardedForHeader: false },
+ windowMs: 60 * 1000 * 30,
+ limit: process.env.NODE_ENV === "development" ? 1000 : 10,
+ standardHeaders: "draft-8",
+ legacyHeaders: false,
+ validate: { xForwardedForHeader: false },
});
const trmnlRouter = Router();
function numberWithCommas(x: number | string) {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
trmnlRouter.get("/device_id", withOrgAuth, async (req: any, res: any) => {
- routeHandler({
- req,
- res,
- action: "get trmnl data",
- handler: async () => {
- const upstash = await initUpstash();
+ routeHandler({
+ req,
+ res,
+ action: "get trmnl data",
+ handler: async () => {
+ const upstash = await initUpstash();
- if (!upstash)
- res.status(500).json({ error: "Failed to connect to upstash" });
+ if (!upstash)
+ res.status(500).json({ error: "Failed to connect to upstash" });
- const orgId = req.org.id;
- let trmnlConfig = await upstash?.get(`trmnl:org:${orgId}`);
+ const orgId = req.org.id;
+ let trmnlConfig = await upstash?.get(`trmnl:org:${orgId}`);
- res.status(200).json({ trmnlConfig });
- // let trmnlJson = await getTrmnlJson();
+ res.status(200).json({ trmnlConfig });
+ // let trmnlJson = await getTrmnlJson();
- // let deviceId = Object.keys(trmnlJson).find((deviceId: string) => {
- // return trmnlJson[deviceId] === req.org.id;
- // });
+ // let deviceId = Object.keys(trmnlJson).find((deviceId: string) => {
+ // return trmnlJson[deviceId] === req.org.id;
+ // });
- // res.status(200).json({ deviceId });
- },
- });
+ // res.status(200).json({ deviceId });
+ },
+ });
});
trmnlRouter.post("/device_id", withOrgAuth, async (req: any, res: any) => {
- routeHandler({
- req,
- res,
- action: "save trmnl device id",
- handler: async () => {
- let upstash = await initUpstash();
+ routeHandler({
+ req,
+ res,
+ action: "save trmnl device id",
+ handler: async () => {
+ let upstash = await initUpstash();
- if (!upstash)
- res.status(500).json({ error: "Failed to connect to upstash" });
+ if (!upstash)
+ res.status(500).json({ error: "Failed to connect to upstash" });
- // 1. Get upstash data
- upstash = upstash!;
+ // 1. Get upstash data
+ upstash = upstash!;
- const trmnlConfig = (await upstash.get(
- `trmnl:device:${req.body.deviceId}`
- )) as {
- orgId: string;
- hideRevenue: boolean;
- };
- if (trmnlConfig && trmnlConfig.orgId !== req.org.id) {
- return res.status(400).json({ error: "Device ID already taken" });
- }
+ const trmnlConfig = (await upstash.get(
+ `trmnl:device:${req.body.deviceId}`,
+ )) as {
+ orgId: string;
+ hideRevenue: boolean;
+ };
+ if (trmnlConfig && trmnlConfig.orgId !== req.org.id) {
+ return res.status(400).json({ error: "Device ID already taken" });
+ }
- // Get current device ID:
- const curTrmnlConfig = (await upstash.get(`trmnl:org:${req.org.id}`)) as {
- deviceId: string;
- hideRevenue: boolean;
- };
- if (curTrmnlConfig) {
- await upstash.del(`trmnl:device:${curTrmnlConfig.deviceId}`);
- }
+ // Get current device ID:
+ const curTrmnlConfig = (await upstash.get(`trmnl:org:${req.org.id}`)) as {
+ deviceId: string;
+ hideRevenue: boolean;
+ };
+ if (curTrmnlConfig) {
+ await upstash.del(`trmnl:device:${curTrmnlConfig.deviceId}`);
+ }
- await upstash.set(`trmnl:device:${req.body.deviceId}`, {
- orgId: req.org.id,
- hideRevenue: req.body.hideRevenue,
- });
+ await upstash.set(`trmnl:device:${req.body.deviceId}`, {
+ orgId: req.org.id,
+ hideRevenue: req.body.hideRevenue,
+ });
- await upstash.set(`trmnl:org:${req.org.id}`, {
- deviceId: req.body.deviceId,
- hideRevenue: req.body.hideRevenue,
- });
+ await upstash.set(`trmnl:org:${req.org.id}`, {
+ deviceId: req.body.deviceId,
+ hideRevenue: req.body.hideRevenue,
+ });
- // const trmnlJson = await upstash.get(`trmnl:${req.org.id}`);
+ // const trmnlJson = await upstash.get(`trmnl:${req.org.id}`);
- // if (!trmnlJson) {
- // res.status(400).json({ error: "Device ID not found" });
- // }
+ // if (!trmnlJson) {
+ // res.status(400).json({ error: "Device ID not found" });
+ // }
- // 2. Check if device ID is already taken
- // await upstash?.set(`trmnl:${req.org.id}`, req.body.deviceId);
+ // 2. Check if device ID is already taken
+ // await upstash?.set(`trmnl:${req.org.id}`, req.body.deviceId);
- // let trmnlJson = await getTrmnlJson();
+ // let trmnlJson = await getTrmnlJson();
- // let existingOrgId = trmnlJson[req.body.deviceId];
+ // let existingOrgId = trmnlJson[req.body.deviceId];
- // if (existingOrgId && existingOrgId !== req.org.id) {
- // return res.status(400).json({ error: "Device ID already taken" });
- // }
+ // if (existingOrgId && existingOrgId !== req.org.id) {
+ // return res.status(400).json({ error: "Device ID already taken" });
+ // }
- // trmnlJson[req.body.deviceId] = req.org.id;
- // // console.log("Trmnl JSON")
- // const sb = createSupabaseClient();
- // await sb.storage
- // .from("private")
- // .upload("trmnl.json", JSON.stringify(trmnlJson), {
- // upsert: true,
- // });
+ // trmnlJson[req.body.deviceId] = req.org.id;
+ // // console.log("Trmnl JSON")
+ // const sb = createSupabaseClient();
+ // await sb.storage
+ // .from("private")
+ // .upload("trmnl.json", JSON.stringify(trmnlJson), {
+ // upsert: true,
+ // });
- res.status(200).json({ message: "Device ID saved" });
- },
- });
+ res.status(200).json({ message: "Device ID saved" });
+ },
+ });
});
trmnlRouter.post(
- "/screen",
- trmnlLimiter,
- trmnlAuthMiddleware,
- async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "generate trmnl screen",
- handler: async () => {
- let { result }: any = await AnalyticsService.getTopEventNames({
- req,
- limit: 1,
- });
+ "/screen",
+ trmnlLimiter,
+ trmnlAuthMiddleware,
+ async (req: any, res: any) =>
+ routeHandler({
+ req,
+ res,
+ action: "generate trmnl screen",
+ handler: async () => {
+ let { result }: any = await AnalyticsService.getTopEventNames({
+ req,
+ limit: 1,
+ });
- let topEvent = "Unknown";
- if (result && result.data.length > 0) {
- topEvent = result.data[0].event_name;
- }
+ let topEvent = "Unknown";
+ if (result && result.data.length > 0) {
+ topEvent = result.data[0].event_name;
+ }
- if (topEvent === "Unknown") {
- res.status(500).json({ error: "No events found" });
- return;
- }
+ if (topEvent === "Unknown") {
+ res.status(500).json({ error: "No events found" });
+ return;
+ }
- let feature = req.features.find((feature: Feature) => {
- return (
- feature.id === topEvent ||
- (feature.config?.filters?.flatMap((x: { value: string[] }) => [
- ...x.value,
- ]) &&
- feature.config.filters
- .flatMap((x: { value: string[] }) => [...x.value])
- .includes(topEvent))
- );
- });
+ let feature = req.features.find((feature: Feature) => {
+ return (
+ feature.id === topEvent ||
+ (feature.config?.filters?.flatMap((x: { value: string[] }) => [
+ ...x.value,
+ ]) &&
+ feature.config.filters
+ .flatMap((x: { value: string[] }) => [...x.value])
+ .includes(topEvent))
+ );
+ });
- let featureName = getFeatureName({
- feature,
- plural: true,
- });
+ let featureName = getFeatureName({
+ feature,
+ plural: true,
+ });
- let totalEvents: number | string =
- await AnalyticsService.getTotalEvents({
- req,
- eventName: topEvent,
- });
+ let totalEvents: number | string =
+ await AnalyticsService.getTotalEvents({
+ req,
+ eventName: topEvent,
+ });
- if (!totalEvents) {
- totalEvents = "Unknown";
- }
+ if (!totalEvents) {
+ totalEvents = "Unknown";
+ }
- let monthlyRevenue:
- | { total_payment_volume: number; label: string }
- | string = await RevenueService.getMonthlyRevenue({
- req,
- });
+ let monthlyRevenue:
+ | { total_payment_volume: number; label: string }
+ | string = await RevenueService.getMonthlyRevenue({
+ req,
+ });
- if (!monthlyRevenue) {
- monthlyRevenue = {
- total_payment_volume: 0,
- label: "Unknown",
- };
- }
+ if (!monthlyRevenue) {
+ monthlyRevenue = {
+ total_payment_volume: 0,
+ label: "Unknown",
+ };
+ }
- let results = await AnalyticsService.getTimeseriesEvents({
- req,
- params: {
- event_names: [topEvent],
- interval: "30d",
- },
- aggregateAll: true,
- });
+ let results = await AnalyticsService.getTimeseriesEvents({
+ req,
+ params: {
+ event_names: [topEvent],
+ interval: "30d",
+ },
+ aggregateAll: true,
+ });
- let totalCustomers: number | string =
- await AnalyticsService.getTotalCustomers({ req });
+ let totalCustomers: number | string =
+ await AnalyticsService.getTotalCustomers({ req });
- if (!totalCustomers) {
- totalCustomers = "Unknown";
- }
+ if (!totalCustomers) {
+ totalCustomers = "Unknown";
+ }
- if (!results?.data) {
- results = {
- data: [],
- };
- }
+ if (!results?.data) {
+ results = {
+ data: [],
+ };
+ }
- // console.log({
- // rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent[0] + "_count"]}]`).join(",")}]`,
- // revenue: numberWithCommas(monthlyRevenue.total_payment_volume),
- // totalEvent: numberWithCommas(totalEvents),
- // totalCustomers: numberWithCommas(totalCustomers),
- // topEvent: topEvent[0],
- // });
- res.status(200).json({
- rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent + "_count"]}]`).join(",")}]`,
- revenue: numberWithCommas(monthlyRevenue.total_payment_volume),
- totalEvents: numberWithCommas(totalEvents),
- totalCustomers: numberWithCommas(totalCustomers),
- topEvent: featureName || "Unknown",
- hideRevenue: req.org.hideRevenue,
- });
- },
- })
+ // console.log({
+ // rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent[0] + "_count"]}]`).join(",")}]`,
+ // revenue: numberWithCommas(monthlyRevenue.total_payment_volume),
+ // totalEvent: numberWithCommas(totalEvents),
+ // totalCustomers: numberWithCommas(totalCustomers),
+ // topEvent: topEvent[0],
+ // });
+ res.status(200).json({
+ rowData: `[${results.data.map((row: any) => `['${row.period}', ${row[topEvent + "_count"]}]`).join(",")}]`,
+ revenue: numberWithCommas(monthlyRevenue.total_payment_volume),
+ totalEvents: numberWithCommas(totalEvents),
+ totalCustomers: numberWithCommas(totalCustomers),
+ topEvent: featureName || "Unknown",
+ hideRevenue: req.org.hideRevenue,
+ });
+ },
+ }),
);
export { trmnlRouter };
diff --git a/server/src/internal/api/trmnl/trmnlUtils.ts b/server/src/internal/api/trmnl/trmnlUtils.ts
index c06559b21..ca311d1f9 100644
--- a/server/src/internal/api/trmnl/trmnlUtils.ts
+++ b/server/src/internal/api/trmnl/trmnlUtils.ts
@@ -2,10 +2,10 @@ import { readFile } from "@/external/supabase/storageUtils.js";
import { createSupabaseClient } from "@/external/supabaseUtils.js";
export const getTrmnlJson = async () => {
- let sb = createSupabaseClient();
- const file = await readFile({ bucket: "private", path: "trmnl.json" });
- const fileString = await file.text();
- const fileJson = JSON.parse(fileString);
+ let sb = createSupabaseClient();
+ const file = await readFile({ bucket: "private", path: "trmnl.json" });
+ const fileString = await file.text();
+ const fileJson = JSON.parse(fileString);
- return fileJson as Record;
+ return fileJson as Record;
};
diff --git a/server/src/internal/customers/CusReadService.ts b/server/src/internal/customers/CusReadService.ts
index f41548692..5c41f93e0 100644
--- a/server/src/internal/customers/CusReadService.ts
+++ b/server/src/internal/customers/CusReadService.ts
@@ -3,17 +3,17 @@ import { Customer, customers } from "@autumn/shared";
import { inArray } from "drizzle-orm";
export class CusReadService {
- static async getInInternalIds({
- db,
- internalIds,
- }: {
- db: DrizzleCli;
- internalIds: string[];
- }) {
- const data = await db.query.customers.findMany({
- where: inArray(customers.internal_id, internalIds),
- });
+ static async getInInternalIds({
+ db,
+ internalIds,
+ }: {
+ db: DrizzleCli;
+ internalIds: string[];
+ }) {
+ const data = await db.query.customers.findMany({
+ where: inArray(customers.internal_id, internalIds),
+ });
- return data as Customer[];
- }
+ return data as Customer[];
+ }
}
diff --git a/server/src/internal/customers/CusSearchService.ts b/server/src/internal/customers/CusSearchService.ts
index 9398d8c19..7c1a4ed66 100644
--- a/server/src/internal/customers/CusSearchService.ts
+++ b/server/src/internal/customers/CusSearchService.ts
@@ -3,19 +3,19 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { AppEnv, customers, CusProductStatus } from "@autumn/shared";
import {
- and,
- desc,
- eq,
- ilike,
- or,
- lt,
- isNotNull,
- gt,
- sql,
- gte,
- isNull,
- inArray,
- notExists,
+ and,
+ desc,
+ eq,
+ ilike,
+ or,
+ lt,
+ isNotNull,
+ gt,
+ sql,
+ gte,
+ isNull,
+ inArray,
+ notExists,
} from "drizzle-orm";
import { customerProducts, products } from "@autumn/shared";
import { alias } from "drizzle-orm/pg-core";
@@ -24,623 +24,623 @@ import { alias } from "drizzle-orm/pg-core";
const customerProductsAlias = alias(customerProducts, "cp_alias");
const customerFields = {
- internal_id: customers.internal_id,
- id: customers.id,
- name: customers.name,
- email: customers.email,
- created_at: customers.created_at,
+ internal_id: customers.internal_id,
+ id: customers.id,
+ name: customers.name,
+ email: customers.email,
+ created_at: customers.created_at,
};
const customerProductFields = {
- id: customerProducts.id,
- internal_product_id: customerProducts.internal_product_id,
- product_id: customerProducts.product_id,
- canceled_at: customerProducts.canceled_at,
- status: customerProducts.status,
- trial_ends_at: customerProducts.trial_ends_at,
+ id: customerProducts.id,
+ internal_product_id: customerProducts.internal_product_id,
+ product_id: customerProducts.product_id,
+ canceled_at: customerProducts.canceled_at,
+ status: customerProducts.status,
+ trial_ends_at: customerProducts.trial_ends_at,
};
const productFields = {
- internal_id: products.internal_id,
- id: products.id,
- name: products.name,
- version: products.version,
+ internal_id: products.internal_id,
+ id: products.id,
+ name: products.name,
+ version: products.version,
};
interface SearchFilters {
- status?: string[];
- version?: string[];
- none?: string;
+ status?: string[];
+ version?: string[];
+ none?: string;
}
export class CusSearchService {
- static async searchByProduct({
- db,
- orgId,
- env,
- search,
- filters,
- pageSize = 50,
- pageNumber,
- // lastItem,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- search: string;
- filters: SearchFilters;
- pageSize?: number;
- pageNumber: number;
- // lastItem?: {
- // internal_id: string;
- // created_at?: string;
- // name?: string;
- // } | null;
- }) {
- // If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
- // let resolvedLastItem = lastItem;
- // if (lastItem && lastItem.internal_id && !lastItem.created_at) {
- // const customerData = await db
- // .select({
- // internal_id: customers.internal_id,
- // created_at: customers.created_at,
- // name: customers.name,
- // })
- // .from(customers)
- // .where(
- // and(
- // eq(customers.internal_id, lastItem.internal_id),
- // eq(customers.org_id, orgId),
- // eq(customers.env, env)
- // )
- // )
- // .limit(1);
+ static async searchByProduct({
+ db,
+ orgId,
+ env,
+ search,
+ filters,
+ pageSize = 50,
+ pageNumber,
+ // lastItem,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ search: string;
+ filters: SearchFilters;
+ pageSize?: number;
+ pageNumber: number;
+ // lastItem?: {
+ // internal_id: string;
+ // created_at?: string;
+ // name?: string;
+ // } | null;
+ }) {
+ // If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
+ // let resolvedLastItem = lastItem;
+ // if (lastItem && lastItem.internal_id && !lastItem.created_at) {
+ // const customerData = await db
+ // .select({
+ // internal_id: customers.internal_id,
+ // created_at: customers.created_at,
+ // name: customers.name,
+ // })
+ // .from(customers)
+ // .where(
+ // and(
+ // eq(customers.internal_id, lastItem.internal_id),
+ // eq(customers.org_id, orgId),
+ // eq(customers.env, env)
+ // )
+ // )
+ // .limit(1);
- // if (customerData.length > 0) {
- // resolvedLastItem = {
- // internal_id: customerData[0].internal_id,
- // created_at: customerData[0].created_at as any,
- // name: customerData[0].name || "",
- // };
- // } else {
- // // If customer not found, reset to no lastItem
- // resolvedLastItem = null;
- // }
- // }
+ // if (customerData.length > 0) {
+ // resolvedLastItem = {
+ // internal_id: customerData[0].internal_id,
+ // created_at: customerData[0].created_at as any,
+ // name: customerData[0].name || "",
+ // };
+ // } else {
+ // // If customer not found, reset to no lastItem
+ // resolvedLastItem = null;
+ // }
+ // }
- let statuses: string[] = [];
+ let statuses: string[] = [];
- // 1. Create base query to fetch all customerproducts
- let activeProdFilter = or(
- eq(customerProducts.status, CusProductStatus.Active),
- eq(customerProducts.status, CusProductStatus.PastDue)
- );
+ // 1. Create base query to fetch all customerproducts
+ let activeProdFilter = or(
+ eq(customerProducts.status, CusProductStatus.Active),
+ eq(customerProducts.status, CusProductStatus.PastDue),
+ );
- if (filters.status && filters.status.length > 0) {
- statuses = filters.status;
- } else {
- statuses = [];
- }
+ if (filters.status && filters.status.length > 0) {
+ statuses = filters.status;
+ } else {
+ statuses = [];
+ }
- // Handle product:version combinations
- let productVersionFilters: Array<{ productId: string; version: number }> =
- [];
+ // Handle product:version combinations
+ let productVersionFilters: Array<{ productId: string; version: number }> =
+ [];
- // Parse version field which now contains "productId:version,productId2:version2"
- if (filters.version && filters.version.length > 0) {
- const versionSelections = filters.version.filter(Boolean);
- productVersionFilters = versionSelections.map((selection) => {
- const [productId, version] = selection.split(":");
- return { productId, version: parseInt(version) };
- });
- }
+ // Parse version field which now contains "productId:version,productId2:version2"
+ if (filters.version && filters.version.length > 0) {
+ const versionSelections = filters.version.filter(Boolean);
+ productVersionFilters = versionSelections.map((selection) => {
+ const [productId, version] = selection.split(":");
+ return { productId, version: parseInt(version) };
+ });
+ }
- let filtersDrizzle = and(
- // New product:version filtering
- productVersionFilters.length > 0
- ? or(
- ...productVersionFilters.map((pv) =>
- and(
- eq(customerProducts.product_id, pv.productId),
- eq(products.version, pv.version)
- )
- )
- )
- : undefined,
- // Legacy product filtering (fallback)
- // productIds.length > 0 && productVersionFilters.length === 0
- // ? inArray(customerProducts.product_id, productIds)
- // : undefined,
- statuses.length > 0 && !statuses.includes("")
- ? or(
- ...statuses.map((status) => {
- switch (status) {
- case "canceled":
- return and(
- isNotNull(customerProducts.canceled_at),
- activeProdFilter
- );
- case "free_trial":
- return and(
- gt(customerProducts.trial_ends_at, Date.now()),
- isNotNull(customerProducts.free_trial_id),
- activeProdFilter
- );
- case CusProductStatus.Expired:
- return and(
- eq(customerProducts.status, CusProductStatus.Expired),
- isNull(customerProducts.canceled_at),
- notExists(
- db
- .select()
- .from(customerProductsAlias)
- .where(
- and(
- eq(
- customerProductsAlias.internal_customer_id,
- customerProducts.internal_customer_id
- ),
- eq(
- customerProductsAlias.product_id,
- customerProducts.product_id
- ),
- or(
- eq(
- customerProductsAlias.status,
- CusProductStatus.Active
- ),
- eq(
- customerProductsAlias.status,
- CusProductStatus.PastDue
- )
- )
- )
- )
- )
- );
- default:
- return eq(customerProducts.status, status);
- }
- })
- )
- : undefined
- );
+ let filtersDrizzle = and(
+ // New product:version filtering
+ productVersionFilters.length > 0
+ ? or(
+ ...productVersionFilters.map((pv) =>
+ and(
+ eq(customerProducts.product_id, pv.productId),
+ eq(products.version, pv.version),
+ ),
+ ),
+ )
+ : undefined,
+ // Legacy product filtering (fallback)
+ // productIds.length > 0 && productVersionFilters.length === 0
+ // ? inArray(customerProducts.product_id, productIds)
+ // : undefined,
+ statuses.length > 0 && !statuses.includes("")
+ ? or(
+ ...statuses.map((status) => {
+ switch (status) {
+ case "canceled":
+ return and(
+ isNotNull(customerProducts.canceled_at),
+ activeProdFilter,
+ );
+ case "free_trial":
+ return and(
+ gt(customerProducts.trial_ends_at, Date.now()),
+ isNotNull(customerProducts.free_trial_id),
+ activeProdFilter,
+ );
+ case CusProductStatus.Expired:
+ return and(
+ eq(customerProducts.status, CusProductStatus.Expired),
+ isNull(customerProducts.canceled_at),
+ notExists(
+ db
+ .select()
+ .from(customerProductsAlias)
+ .where(
+ and(
+ eq(
+ customerProductsAlias.internal_customer_id,
+ customerProducts.internal_customer_id,
+ ),
+ eq(
+ customerProductsAlias.product_id,
+ customerProducts.product_id,
+ ),
+ or(
+ eq(
+ customerProductsAlias.status,
+ CusProductStatus.Active,
+ ),
+ eq(
+ customerProductsAlias.status,
+ CusProductStatus.PastDue,
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ default:
+ return eq(customerProducts.status, status);
+ }
+ }),
+ )
+ : undefined,
+ );
- let cusFilter = and(
- eq(customers.org_id, orgId),
- eq(customers.env, env),
+ let cusFilter = and(
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
- search
- ? or(
- ilike(customers.id, `%${search}%`),
- ilike(customers.name, `%${search}%`),
- ilike(customers.email, `%${search}%`)
- )
- : undefined
- );
+ search
+ ? or(
+ ilike(customers.id, `%${search}%`),
+ ilike(customers.name, `%${search}%`),
+ ilike(customers.email, `%${search}%`),
+ )
+ : undefined,
+ );
- // Build the where clause
- // Apply active filter by default, unless user has selected non-active statuses
- const hasStatusFilters = statuses.length > 0 && !statuses.includes("");
- const hasNonActiveStatusFilters =
- hasStatusFilters &&
- statuses.some((status) => status !== "active" && status !== "");
- const shouldApplyActiveFilter =
- !hasStatusFilters ||
- (statuses.includes("active") && !hasNonActiveStatusFilters);
+ // Build the where clause
+ // Apply active filter by default, unless user has selected non-active statuses
+ const hasStatusFilters = statuses.length > 0 && !statuses.includes("");
+ const hasNonActiveStatusFilters =
+ hasStatusFilters &&
+ statuses.some((status) => status !== "active" && status !== "");
+ const shouldApplyActiveFilter =
+ !hasStatusFilters ||
+ (statuses.includes("active") && !hasNonActiveStatusFilters);
- const whereClause = and(
- shouldApplyActiveFilter ? activeProdFilter : undefined,
- filtersDrizzle,
- cusFilter
- // resolvedLastItem && resolvedLastItem.internal_id
- // ? lt(customers.internal_id, resolvedLastItem.internal_id)
- // : undefined
- );
+ const whereClause = and(
+ shouldApplyActiveFilter ? activeProdFilter : undefined,
+ filtersDrizzle,
+ cusFilter,
+ // resolvedLastItem && resolvedLastItem.internal_id
+ // ? lt(customers.internal_id, resolvedLastItem.internal_id)
+ // : undefined
+ );
- // Execute query with appropriate pagination
- const hasProductFilters = productVersionFilters.length > 0;
+ // Execute query with appropriate pagination
+ const hasProductFilters = productVersionFilters.length > 0;
- // Build the query based on pagination type
- const buildQuery = () => {
- const baseQuery = db
- .select({
- customer: customerFields,
- customerProduct: customerProductFields,
- product: productFields,
- })
- .from(customerProducts)
- .leftJoin(
- customers,
- eq(customerProducts.internal_customer_id, customers.internal_id)
- );
+ // Build the query based on pagination type
+ const buildQuery = () => {
+ const baseQuery = db
+ .select({
+ customer: customerFields,
+ customerProduct: customerProductFields,
+ product: productFields,
+ })
+ .from(customerProducts)
+ .leftJoin(
+ customers,
+ eq(customerProducts.internal_customer_id, customers.internal_id),
+ );
- if (hasProductFilters) {
- return baseQuery.innerJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- );
- } else {
- return baseQuery.leftJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- );
- }
- };
+ if (hasProductFilters) {
+ return baseQuery.innerJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ );
+ } else {
+ return baseQuery.leftJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ );
+ }
+ };
- let productQueryResult;
- if (pageNumber > 1) {
- // Use offset-based pagination
- const offset = (pageNumber - 1) * pageSize;
- productQueryResult = buildQuery()
- .where(whereClause)
- .orderBy(desc(customers.internal_id))
- .offset(offset)
- .limit(pageSize);
- } else {
- // Use cursor-based pagination
- productQueryResult = buildQuery()
- .where(whereClause)
- .orderBy(desc(customers.internal_id))
- .limit(pageSize);
- }
+ let productQueryResult;
+ if (pageNumber > 1) {
+ // Use offset-based pagination
+ const offset = (pageNumber - 1) * pageSize;
+ productQueryResult = buildQuery()
+ .where(whereClause)
+ .orderBy(desc(customers.internal_id))
+ .offset(offset)
+ .limit(pageSize);
+ } else {
+ // Use cursor-based pagination
+ productQueryResult = buildQuery()
+ .where(whereClause)
+ .orderBy(desc(customers.internal_id))
+ .limit(pageSize);
+ }
- // Build count query with same join logic
- const buildCountQuery = () => {
- const baseCountQuery = db
- .select({
- totalCount: sql`count(distinct ${customers.internal_id})`.as(
- "total_count"
- ),
- })
- .from(customerProducts)
- .leftJoin(
- customers,
- eq(customerProducts.internal_customer_id, customers.internal_id)
- );
+ // Build count query with same join logic
+ const buildCountQuery = () => {
+ const baseCountQuery = db
+ .select({
+ totalCount: sql`count(distinct ${customers.internal_id})`.as(
+ "total_count",
+ ),
+ })
+ .from(customerProducts)
+ .leftJoin(
+ customers,
+ eq(customerProducts.internal_customer_id, customers.internal_id),
+ );
- if (hasProductFilters) {
- return baseCountQuery.innerJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- );
- } else {
- return baseCountQuery.leftJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- );
- }
- };
+ if (hasProductFilters) {
+ return baseCountQuery.innerJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ );
+ } else {
+ return baseCountQuery.leftJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ );
+ }
+ };
- const [results, totalCountResult] = await Promise.all([
- productQueryResult,
- buildCountQuery().where(
- and(
- shouldApplyActiveFilter ? activeProdFilter : undefined,
- filtersDrizzle,
- cusFilter
- )
- ),
- ]);
+ const [results, totalCountResult] = await Promise.all([
+ productQueryResult,
+ buildCountQuery().where(
+ and(
+ shouldApplyActiveFilter ? activeProdFilter : undefined,
+ filtersDrizzle,
+ cusFilter,
+ ),
+ ),
+ ]);
- // Process the results to group customer products by customer
- const customerMap = new Map();
+ // Process the results to group customer products by customer
+ const customerMap = new Map();
- for (const row of results) {
- const customerId = row.customer?.internal_id;
- if (!customerId) continue;
+ for (const row of results) {
+ const customerId = row.customer?.internal_id;
+ if (!customerId) continue;
- if (!customerMap.has(customerId)) {
- customerMap.set(customerId, {
- ...row.customer,
- customer_products: [],
- });
- }
+ if (!customerMap.has(customerId)) {
+ customerMap.set(customerId, {
+ ...row.customer,
+ customer_products: [],
+ });
+ }
- if (row.customerProduct) {
- customerMap.get(customerId).customer_products.push({
- ...row.customerProduct,
- product: row.product,
- });
- }
- }
+ if (row.customerProduct) {
+ customerMap.get(customerId).customer_products.push({
+ ...row.customerProduct,
+ product: row.product,
+ });
+ }
+ }
- const processedData = Array.from(customerMap.values());
+ const processedData = Array.from(customerMap.values());
- const totalCount = totalCountResult[0]?.totalCount || 0;
+ const totalCount = totalCountResult[0]?.totalCount || 0;
- return { data: processedData, count: totalCount };
- }
+ return { data: processedData, count: totalCount };
+ }
- static async searchByNone({
- db,
- orgId,
- env,
- search,
- filters,
- pageSize = 50,
- pageNumber,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- search: string;
- filters: SearchFilters;
- pageSize?: number;
- pageNumber: number;
- }) {
- const noneFilter = notExists(
- db
- .select()
- .from(customerProducts)
- .where(
- and(
- eq(customerProducts.internal_customer_id, customers.internal_id),
- or(
- eq(customerProducts.status, CusProductStatus.Active),
- eq(customerProducts.status, CusProductStatus.PastDue),
- eq(customerProducts.status, CusProductStatus.Scheduled)
- )
- )
- )
- );
+ static async searchByNone({
+ db,
+ orgId,
+ env,
+ search,
+ filters,
+ pageSize = 50,
+ pageNumber,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ search: string;
+ filters: SearchFilters;
+ pageSize?: number;
+ pageNumber: number;
+ }) {
+ const noneFilter = notExists(
+ db
+ .select()
+ .from(customerProducts)
+ .where(
+ and(
+ eq(customerProducts.internal_customer_id, customers.internal_id),
+ or(
+ eq(customerProducts.status, CusProductStatus.Active),
+ eq(customerProducts.status, CusProductStatus.PastDue),
+ eq(customerProducts.status, CusProductStatus.Scheduled),
+ ),
+ ),
+ ),
+ );
- const baseWhereClause = and(
- eq(customers.org_id, orgId),
- eq(customers.env, env),
- search
- ? or(
- ilike(customers.id, `%${search}%`),
- ilike(customers.name, `%${search}%`),
- ilike(customers.email, `%${search}%`)
- )
- : undefined,
- noneFilter
- );
+ const baseWhereClause = and(
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ search
+ ? or(
+ ilike(customers.id, `%${search}%`),
+ ilike(customers.name, `%${search}%`),
+ ilike(customers.email, `%${search}%`),
+ )
+ : undefined,
+ noneFilter,
+ );
- let baseQuery;
- if (pageNumber > 1) {
- // Use offset-based pagination
- const offset = (pageNumber - 1) * pageSize;
- baseQuery = db
- .select(customerFields)
- .from(customers)
- .where(baseWhereClause)
- .orderBy(desc(customers.internal_id))
- .offset(offset)
- .limit(pageSize);
- } else {
- // Use cursor-based pagination
- baseQuery = db
- .select(customerFields)
- .from(customers)
- .where(baseWhereClause)
- .orderBy(desc(customers.internal_id))
- .limit(pageSize);
- }
+ let baseQuery;
+ if (pageNumber > 1) {
+ // Use offset-based pagination
+ const offset = (pageNumber - 1) * pageSize;
+ baseQuery = db
+ .select(customerFields)
+ .from(customers)
+ .where(baseWhereClause)
+ .orderBy(desc(customers.internal_id))
+ .offset(offset)
+ .limit(pageSize);
+ } else {
+ // Use cursor-based pagination
+ baseQuery = db
+ .select(customerFields)
+ .from(customers)
+ .where(baseWhereClause)
+ .orderBy(desc(customers.internal_id))
+ .limit(pageSize);
+ }
- const [results, totalCountResult] = await Promise.all([
- baseQuery,
- db
- .select({
- count: sql`count(*)`.as("count"),
- })
- .from(customers)
- .where(
- and(
- eq(customers.org_id, orgId),
- eq(customers.env, env),
- search
- ? or(
- ilike(customers.id, `%${search}%`),
- ilike(customers.name, `%${search}%`),
- ilike(customers.email, `%${search}%`)
- )
- : undefined,
- noneFilter
- )
- ),
- ]);
+ const [results, totalCountResult] = await Promise.all([
+ baseQuery,
+ db
+ .select({
+ count: sql`count(*)`.as("count"),
+ })
+ .from(customers)
+ .where(
+ and(
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ search
+ ? or(
+ ilike(customers.id, `%${search}%`),
+ ilike(customers.name, `%${search}%`),
+ ilike(customers.email, `%${search}%`),
+ )
+ : undefined,
+ noneFilter,
+ ),
+ ),
+ ]);
- return { data: results, count: totalCountResult[0]?.count || 0 };
- }
+ return { data: results, count: totalCountResult[0]?.count || 0 };
+ }
- static async search({
- db,
- orgId,
- env,
- search,
- pageSize = 50,
- filters,
- lastItem,
- pageNumber,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- search: string;
- lastItem?: {
- internal_id: string;
- created_at?: string;
- name?: string;
- } | null;
- filters?: SearchFilters;
- pageSize?: number;
- pageNumber: number;
- }) {
- // If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
- let resolvedLastItem = lastItem;
- if (lastItem && lastItem.internal_id && !lastItem.created_at) {
- const customerData = await db
- .select({
- internal_id: customers.internal_id,
- created_at: customers.created_at,
- name: customers.name,
- })
- .from(customers)
- .where(
- and(
- eq(customers.internal_id, lastItem.internal_id),
- eq(customers.org_id, orgId),
- eq(customers.env, env)
- )
- )
- .limit(1);
+ static async search({
+ db,
+ orgId,
+ env,
+ search,
+ pageSize = 50,
+ filters,
+ lastItem,
+ pageNumber,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ search: string;
+ lastItem?: {
+ internal_id: string;
+ created_at?: string;
+ name?: string;
+ } | null;
+ filters?: SearchFilters;
+ pageSize?: number;
+ pageNumber: number;
+ }) {
+ // If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
+ let resolvedLastItem = lastItem;
+ if (lastItem && lastItem.internal_id && !lastItem.created_at) {
+ const customerData = await db
+ .select({
+ internal_id: customers.internal_id,
+ created_at: customers.created_at,
+ name: customers.name,
+ })
+ .from(customers)
+ .where(
+ and(
+ eq(customers.internal_id, lastItem.internal_id),
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ ),
+ )
+ .limit(1);
- if (customerData.length > 0) {
- resolvedLastItem = {
- internal_id: customerData[0].internal_id,
- created_at: customerData[0].created_at as any,
- name: customerData[0].name || "",
- };
- } else {
- // If customer not found, reset to no lastItem (will show page 1)
- resolvedLastItem = null;
- }
- }
- const noneProducts = filters?.none === "true";
+ if (customerData.length > 0) {
+ resolvedLastItem = {
+ internal_id: customerData[0].internal_id,
+ created_at: customerData[0].created_at as any,
+ name: customerData[0].name || "",
+ };
+ } else {
+ // If customer not found, reset to no lastItem (will show page 1)
+ resolvedLastItem = null;
+ }
+ }
+ const noneProducts = filters?.none === "true";
- if (noneProducts) {
- return await this.searchByNone({
- db,
- orgId,
- env,
- search,
- filters,
- pageSize,
- // lastItem: resolvedLastItem,
- pageNumber,
- });
- }
+ if (noneProducts) {
+ return await this.searchByNone({
+ db,
+ orgId,
+ env,
+ search,
+ filters,
+ pageSize,
+ // lastItem: resolvedLastItem,
+ pageNumber,
+ });
+ }
- if (filters?.version && filters?.version.length > 0) {
- return await this.searchByProduct({
- db,
- orgId,
- env,
- search,
- filters,
- pageSize,
- // lastItem: resolvedLastItem,
- pageNumber,
- });
- }
+ if (filters?.version && filters?.version.length > 0) {
+ return await this.searchByProduct({
+ db,
+ orgId,
+ env,
+ search,
+ filters,
+ pageSize,
+ // lastItem: resolvedLastItem,
+ pageNumber,
+ });
+ }
- let filterClause = and(
- eq(customers.org_id, orgId),
- eq(customers.env, env),
- search
- ? or(
- ilike(customers.id, `%${search}%`),
- ilike(customers.name, `%${search}%`),
- ilike(customers.email, `%${search}%`)
- )
- : undefined
- );
+ let filterClause = and(
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ search
+ ? or(
+ ilike(customers.id, `%${search}%`),
+ ilike(customers.name, `%${search}%`),
+ ilike(customers.email, `%${search}%`),
+ )
+ : undefined,
+ );
- // Build the where clause for base query
- const baseWhereClause = and(
- filterClause,
- resolvedLastItem && resolvedLastItem.internal_id
- ? lt(customers.internal_id, resolvedLastItem.internal_id)
- : undefined
- );
+ // Build the where clause for base query
+ const baseWhereClause = and(
+ filterClause,
+ resolvedLastItem && resolvedLastItem.internal_id
+ ? lt(customers.internal_id, resolvedLastItem.internal_id)
+ : undefined,
+ );
- // Create the base customer query as a subquery with appropriate pagination
- let baseQuery;
- if (!resolvedLastItem && pageNumber > 1) {
- // Use offset-based pagination
- const offset = (pageNumber - 1) * pageSize;
- baseQuery = db
- .select(customerFields)
- .from(customers)
- .where(baseWhereClause)
- .orderBy(desc(customers.internal_id))
- .offset(offset)
- .limit(pageSize)
- .as("baseQuery");
- } else {
- // Use cursor-based pagination
- baseQuery = db
- .select(customerFields)
- .from(customers)
- .where(baseWhereClause)
- .orderBy(desc(customers.internal_id))
- .limit(pageSize)
- .as("baseQuery");
- }
+ // Create the base customer query as a subquery with appropriate pagination
+ let baseQuery;
+ if (!resolvedLastItem && pageNumber > 1) {
+ // Use offset-based pagination
+ const offset = (pageNumber - 1) * pageSize;
+ baseQuery = db
+ .select(customerFields)
+ .from(customers)
+ .where(baseWhereClause)
+ .orderBy(desc(customers.internal_id))
+ .offset(offset)
+ .limit(pageSize)
+ .as("baseQuery");
+ } else {
+ // Use cursor-based pagination
+ baseQuery = db
+ .select(customerFields)
+ .from(customers)
+ .where(baseWhereClause)
+ .orderBy(desc(customers.internal_id))
+ .limit(pageSize)
+ .as("baseQuery");
+ }
- // Get total count in parallel without pagination
- const totalCountQuery = db
- .select({
- count: sql`count(*)`.as("count"),
- })
- .from(customers)
- .where(filterClause);
+ // Get total count in parallel without pagination
+ const totalCountQuery = db
+ .select({
+ count: sql`count(*)`.as("count"),
+ })
+ .from(customers)
+ .where(filterClause);
- // Now join with customer products and products
- const [results, totalCountResult] = await Promise.all([
- db
- .select({
- // Customer fields
- customer: {
- internal_id: baseQuery.internal_id,
- id: baseQuery.id,
- name: baseQuery.name,
- email: baseQuery.email,
- created_at: baseQuery.created_at,
- },
- // Customer product fields
- customerProduct: customerProductFields,
- // Product fields
- product: productFields,
- })
- .from(baseQuery)
- .leftJoin(
- customerProducts,
- eq(baseQuery.internal_id, customerProducts.internal_customer_id)
- )
- .leftJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- )
- .orderBy(desc(baseQuery.internal_id)),
- totalCountQuery,
- ]);
+ // Now join with customer products and products
+ const [results, totalCountResult] = await Promise.all([
+ db
+ .select({
+ // Customer fields
+ customer: {
+ internal_id: baseQuery.internal_id,
+ id: baseQuery.id,
+ name: baseQuery.name,
+ email: baseQuery.email,
+ created_at: baseQuery.created_at,
+ },
+ // Customer product fields
+ customerProduct: customerProductFields,
+ // Product fields
+ product: productFields,
+ })
+ .from(baseQuery)
+ .leftJoin(
+ customerProducts,
+ eq(baseQuery.internal_id, customerProducts.internal_customer_id),
+ )
+ .leftJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ )
+ .orderBy(desc(baseQuery.internal_id)),
+ totalCountQuery,
+ ]);
- if (results.length === 0) {
- return { data: [], count: 0 };
- }
+ if (results.length === 0) {
+ return { data: [], count: 0 };
+ }
- const totalCount = totalCountResult[0]?.count || 0;
+ const totalCount = totalCountResult[0]?.count || 0;
- // Group the results by customer
- const customerMap = new Map();
+ // Group the results by customer
+ const customerMap = new Map();
- for (const row of results) {
- const customerId = row.customer.internal_id;
+ for (const row of results) {
+ const customerId = row.customer.internal_id;
- if (!customerMap.has(customerId)) {
- customerMap.set(customerId, {
- ...row.customer,
- created_at: Number(row.customer.created_at),
- customer_products: [],
- });
- }
+ if (!customerMap.has(customerId)) {
+ customerMap.set(customerId, {
+ ...row.customer,
+ created_at: Number(row.customer.created_at),
+ customer_products: [],
+ });
+ }
- // Add customer product if it exists
- if (row.customerProduct && row.customerProduct.id) {
- customerMap.get(customerId).customer_products.push({
- ...row.customerProduct,
- product: row.product,
- });
- }
- }
+ // Add customer product if it exists
+ if (row.customerProduct && row.customerProduct.id) {
+ customerMap.get(customerId).customer_products.push({
+ ...row.customerProduct,
+ product: row.product,
+ });
+ }
+ }
- const finalResults = Array.from(customerMap.values());
+ const finalResults = Array.from(customerMap.values());
- return { data: finalResults, count: totalCount };
- }
+ return { data: finalResults, count: totalCount };
+ }
}
// // Legacy support for product_id field (if still used)
diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts
index 87a5a11cf..a6778208c 100644
--- a/server/src/internal/customers/CusService.ts
+++ b/server/src/internal/customers/CusService.ts
@@ -1,14 +1,14 @@
import { SupabaseClient } from "@supabase/supabase-js";
import {
- AppEnv,
- CusExpand,
- CusProductStatus,
- Customer,
- customers,
- EntityExpand,
- FullCusProduct,
- FullCustomer,
- Organization,
+ AppEnv,
+ CusExpand,
+ CusProductStatus,
+ Customer,
+ customers,
+ EntityExpand,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js";
import { ErrCode } from "@/errors/errCodes.js";
@@ -23,281 +23,281 @@ import { RELEVANT_STATUSES } from "./cusProducts/CusProductService.js";
const tracer = trace.getTracer("express");
export class CusService {
- static async getFull({
- db,
- idOrInternalId,
- orgId,
- env,
- inStatuses = RELEVANT_STATUSES,
- withEntities = false,
- entityId,
- expand,
- withSubs = false,
- allowNotFound = false,
- withEvents = false,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: AppEnv;
- inStatuses?: CusProductStatus[];
- withEntities?: boolean;
- entityId?: string;
- expand?: (CusExpand | EntityExpand)[];
- withSubs?: boolean;
- allowNotFound?: boolean;
- withEvents?: boolean;
- }): Promise {
- const includeInvoices = expand?.includes(CusExpand.Invoices) || false;
- const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false;
+ static async getFull({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ inStatuses = RELEVANT_STATUSES,
+ withEntities = false,
+ entityId,
+ expand,
+ withSubs = false,
+ allowNotFound = false,
+ withEvents = false,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: AppEnv;
+ inStatuses?: CusProductStatus[];
+ withEntities?: boolean;
+ entityId?: string;
+ expand?: (CusExpand | EntityExpand)[];
+ withSubs?: boolean;
+ allowNotFound?: boolean;
+ withEvents?: boolean;
+ }): Promise {
+ const includeInvoices = expand?.includes(CusExpand.Invoices) || false;
+ const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false;
- return withSpan({
- name: "CusService.getFull",
- attributes: {
- idOrInternalId,
- entityId,
- orgId,
- env,
- inStatuses,
- withEntities,
- withSubs,
- },
- fn: async () => {
- const query = getFullCusQuery(
- idOrInternalId,
- orgId,
- env,
- inStatuses,
- includeInvoices,
- withEntities,
- withTrialsUsed,
- withSubs,
- withEvents,
- entityId
- );
+ return withSpan({
+ name: "CusService.getFull",
+ attributes: {
+ idOrInternalId,
+ entityId,
+ orgId,
+ env,
+ inStatuses,
+ withEntities,
+ withSubs,
+ },
+ fn: async () => {
+ const query = getFullCusQuery(
+ idOrInternalId,
+ orgId,
+ env,
+ inStatuses,
+ includeInvoices,
+ withEntities,
+ withTrialsUsed,
+ withSubs,
+ withEvents,
+ entityId,
+ );
- let result = await db.execute(query);
+ let result = await db.execute(query);
- if (!result || result.length == 0) {
- if (allowNotFound) {
- // @ts-ignore
- return null as FullCustomer;
- }
+ if (!result || result.length == 0) {
+ if (allowNotFound) {
+ // @ts-ignore
+ return null as FullCustomer;
+ }
- throw new RecaseError({
- message: `Customer ${idOrInternalId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ throw new RecaseError({
+ message: `Customer ${idOrInternalId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let data = result[0];
- data.created_at = Number(data.created_at);
+ let data = result[0];
+ data.created_at = Number(data.created_at);
- for (const product of data.customer_products as FullCusProduct[]) {
- if (!product.customer_prices) {
- product.customer_prices = [];
- }
+ for (const product of data.customer_products as FullCusProduct[]) {
+ if (!product.customer_prices) {
+ product.customer_prices = [];
+ }
- if (!product.customer_entitlements) {
- product.customer_entitlements = [];
- }
- }
+ if (!product.customer_entitlements) {
+ product.customer_entitlements = [];
+ }
+ }
- return data as FullCustomer;
- },
- });
- }
+ return data as FullCustomer;
+ },
+ });
+ }
- static async get({
- db,
- idOrInternalId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const customer = await db.query.customers.findFirst({
- where: and(
- or(
- eq(customers.id, idOrInternalId),
- eq(customers.internal_id, idOrInternalId)
- ),
- eq(customers.org_id, orgId),
- eq(customers.env, env)
- ),
- });
+ static async get({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const customer = await db.query.customers.findFirst({
+ where: and(
+ or(
+ eq(customers.id, idOrInternalId),
+ eq(customers.internal_id, idOrInternalId),
+ ),
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ ),
+ });
- if (!customer) {
- return null;
- }
+ if (!customer) {
+ return null;
+ }
- return customer as Customer;
- }
+ return customer as Customer;
+ }
- static async getByEmail({
- db,
- email,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- email: string;
- orgId: string;
- env: AppEnv;
- }) {
- const customer = await db.query.customers.findMany({
- where: and(
- ilike(customers.email, email),
- eq(customers.org_id, orgId),
- eq(customers.env, env)
- ),
- });
+ static async getByEmail({
+ db,
+ email,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ email: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const customer = await db.query.customers.findMany({
+ where: and(
+ ilike(customers.email, email),
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ ),
+ });
- return customer as Customer[];
- }
+ return customer as Customer[];
+ }
- static async getByInternalId({
- db,
- internalId,
- errorIfNotFound = true,
- withOrg = false,
- }: {
- db: DrizzleCli;
- internalId: string;
- errorIfNotFound?: boolean;
- withOrg?: boolean;
- }) {
- const customer = await db.query.customers.findFirst({
- where: eq(customers.internal_id, internalId),
- with: {
- org: withOrg ? true : undefined,
- },
- });
+ static async getByInternalId({
+ db,
+ internalId,
+ errorIfNotFound = true,
+ withOrg = false,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ errorIfNotFound?: boolean;
+ withOrg?: boolean;
+ }) {
+ const customer = await db.query.customers.findFirst({
+ where: eq(customers.internal_id, internalId),
+ with: {
+ org: withOrg ? true : undefined,
+ },
+ });
- if (errorIfNotFound && !customer) {
- throw new RecaseError({
- message: `Customer ${internalId} not found`,
- statusCode: 404,
- code: ErrCode.CustomerNotFound,
- });
- } else if (!customer) {
- return null;
- }
+ if (errorIfNotFound && !customer) {
+ throw new RecaseError({
+ message: `Customer ${internalId} not found`,
+ statusCode: 404,
+ code: ErrCode.CustomerNotFound,
+ });
+ } else if (!customer) {
+ return null;
+ }
- return customer as Customer & { org?: Organization };
- }
+ return customer as Customer & { org?: Organization };
+ }
- static async getByStripeId({
- db,
- stripeId,
- }: {
- db: DrizzleCli;
- stripeId: string;
- }) {
- const customer = await db.query.customers.findFirst({
- where: eq(sql`processor->>'id'`, stripeId),
- });
+ static async getByStripeId({
+ db,
+ stripeId,
+ }: {
+ db: DrizzleCli;
+ stripeId: string;
+ }) {
+ const customer = await db.query.customers.findFirst({
+ where: eq(sql`processor->>'id'`, stripeId),
+ });
- if (!customer) {
- return null;
- }
+ if (!customer) {
+ return null;
+ }
- return customer as Customer;
- }
+ return customer as Customer;
+ }
- static async insert({ db, data }: { db: DrizzleCli; data: Customer }) {
- try {
- const results = await db
- .insert(customers)
- .values(data as any)
- .returning();
- if (results && results.length > 0) {
- return results[0] as Customer;
- } else {
- return null;
- }
- } catch (error: any) {
- if (error.code === "23505") {
- throw new RecaseError({
- code: ErrCode.DuplicateCustomerId,
- message: "Customer ID already exists",
- statusCode: StatusCodes.BAD_REQUEST,
- data: error,
- });
- }
- throw error;
- }
- }
+ static async insert({ db, data }: { db: DrizzleCli; data: Customer }) {
+ try {
+ const results = await db
+ .insert(customers)
+ .values(data as any)
+ .returning();
+ if (results && results.length > 0) {
+ return results[0] as Customer;
+ } else {
+ return null;
+ }
+ } catch (error: any) {
+ if (error.code === "23505") {
+ throw new RecaseError({
+ code: ErrCode.DuplicateCustomerId,
+ message: "Customer ID already exists",
+ statusCode: StatusCodes.BAD_REQUEST,
+ data: error,
+ });
+ }
+ throw error;
+ }
+ }
- static async update({
- db,
- internalCusId,
- update,
- }: {
- db: DrizzleCli;
- internalCusId: string;
- update: any;
- }) {
- try {
- const results = await db
- .update(customers)
- .set(update)
- .where(eq(customers.internal_id, internalCusId))
- .returning();
+ static async update({
+ db,
+ internalCusId,
+ update,
+ }: {
+ db: DrizzleCli;
+ internalCusId: string;
+ update: any;
+ }) {
+ try {
+ const results = await db
+ .update(customers)
+ .set(update)
+ .where(eq(customers.internal_id, internalCusId))
+ .returning();
- if (results && results.length > 0) {
- return results[0] as Customer;
- } else {
- return null;
- }
- } catch (error) {
- throw error;
- }
- }
+ if (results && results.length > 0) {
+ return results[0] as Customer;
+ } else {
+ return null;
+ }
+ } catch (error) {
+ throw error;
+ }
+ }
- static async deleteByInternalId({
- db,
- internalId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- internalId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const results = await db
- .delete(customers)
- .where(
- and(
- eq(customers.internal_id, internalId),
- eq(customers.org_id, orgId),
- eq(customers.env, env)
- )
- )
- .returning();
+ static async deleteByInternalId({
+ db,
+ internalId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const results = await db
+ .delete(customers)
+ .where(
+ and(
+ eq(customers.internal_id, internalId),
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ ),
+ )
+ .returning();
- return results;
- }
+ return results;
+ }
- static async deleteByOrgId({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- const results = await db
- .delete(customers)
- .where(and(eq(customers.org_id, orgId), eq(customers.env, env)))
- .returning();
+ static async deleteByOrgId({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const results = await db
+ .delete(customers)
+ .where(and(eq(customers.org_id, orgId), eq(customers.env, env)))
+ .returning();
- return results;
- }
+ return results;
+ }
}
diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts
index 09f8468e1..92fa6cae6 100644
--- a/server/src/internal/customers/add-product/createFullCusProduct.ts
+++ b/server/src/internal/customers/add-product/createFullCusProduct.ts
@@ -1,19 +1,19 @@
import {
- type APIVersion,
- CollectionMethod,
- type CusProduct,
- CusProductStatus,
- type Customer,
- type CustomerEntitlement,
- type CustomerPrice,
- type FeatureOptions,
- type FreeTrial,
- type FullCusProduct,
- type FullProduct,
- type InsertReplaceable,
- type Price,
- ProcessorType,
- type ProductOptions,
+ type APIVersion,
+ CollectionMethod,
+ type CusProduct,
+ CusProductStatus,
+ type Customer,
+ type CustomerEntitlement,
+ type CustomerPrice,
+ type FeatureOptions,
+ type FreeTrial,
+ type FullCusProduct,
+ type FullProduct,
+ type InsertReplaceable,
+ type Price,
+ ProcessorType,
+ type ProductOptions,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
@@ -36,522 +36,522 @@ import { updateOneTimeCusProduct } from "./createOneTimeCusProduct.js";
import { initCusEntitlement } from "./initCusEnt.js";
export const initCusPrice = ({
- price,
- customer,
- cusProductId,
+ price,
+ customer,
+ cusProductId,
}: {
- price: Price;
- customer: Customer;
- cusProductId: string;
+ price: Price;
+ customer: Customer;
+ cusProductId: string;
}) => {
- const cusPrice: CustomerPrice = {
- id: generateId("cus_price"),
- internal_customer_id: customer.internal_id,
- customer_product_id: cusProductId,
- created_at: Date.now(),
+ const cusPrice: CustomerPrice = {
+ id: generateId("cus_price"),
+ internal_customer_id: customer.internal_id,
+ customer_product_id: cusProductId,
+ created_at: Date.now(),
- price_id: price.id || null,
- };
+ price_id: price.id || null,
+ };
- return cusPrice;
+ return cusPrice;
};
export const initCusProduct = ({
- customer,
- product,
- // subscriptionId,
- // subscriptionScheduleId,
- // lastInvoiceId,
- cusProdId,
- startsAt,
- optionsList,
- freeTrial,
- trialEndsAt,
- subscriptionStatus,
- canceledAt,
- createdAt,
- collectionMethod,
- subscriptionIds,
- subscriptionScheduleIds,
- isCustom,
- entityId,
- internalEntityId,
- apiVersion,
- quantity,
+ customer,
+ product,
+ // subscriptionId,
+ // subscriptionScheduleId,
+ // lastInvoiceId,
+ cusProdId,
+ startsAt,
+ optionsList,
+ freeTrial,
+ trialEndsAt,
+ subscriptionStatus,
+ canceledAt,
+ createdAt,
+ collectionMethod,
+ subscriptionIds,
+ subscriptionScheduleIds,
+ isCustom,
+ entityId,
+ internalEntityId,
+ apiVersion,
+ quantity,
}: {
- customer: Customer;
- product: FullProduct;
- // subscriptionId: string | undefined | null;
- // subscriptionScheduleId?: string | null;
- // lastInvoiceId?: string | null;
- cusProdId: string;
- startsAt?: number;
- optionsList: FeatureOptions[];
- freeTrial: FreeTrial | null;
- trialEndsAt?: number | null;
- subscriptionStatus?: CusProductStatus;
- canceledAt?: number | null;
- createdAt?: number | null;
- collectionMethod?: CollectionMethod;
- subscriptionIds?: string[];
- subscriptionScheduleIds?: string[];
- isCustom?: boolean;
- entityId?: string;
- internalEntityId?: string;
- apiVersion?: APIVersion;
- quantity?: number;
+ customer: Customer;
+ product: FullProduct;
+ // subscriptionId: string | undefined | null;
+ // subscriptionScheduleId?: string | null;
+ // lastInvoiceId?: string | null;
+ cusProdId: string;
+ startsAt?: number;
+ optionsList: FeatureOptions[];
+ freeTrial: FreeTrial | null;
+ trialEndsAt?: number | null;
+ subscriptionStatus?: CusProductStatus;
+ canceledAt?: number | null;
+ createdAt?: number | null;
+ collectionMethod?: CollectionMethod;
+ subscriptionIds?: string[];
+ subscriptionScheduleIds?: string[];
+ isCustom?: boolean;
+ entityId?: string;
+ internalEntityId?: string;
+ apiVersion?: APIVersion;
+ quantity?: number;
}) => {
- const isFuture = startsAt && startsAt > Date.now();
+ const isFuture = startsAt && startsAt > Date.now();
- let trialEnds = trialEndsAt;
- if (!trialEndsAt && freeTrial) {
- trialEnds = freeTrialToStripeTimestamp({ freeTrial })! * 1000;
- }
+ let trialEnds = trialEndsAt;
+ if (!trialEndsAt && freeTrial) {
+ trialEnds = freeTrialToStripeTimestamp({ freeTrial })! * 1000;
+ }
- return {
- id: cusProdId,
- internal_customer_id: customer.internal_id,
- customer_id: customer.id,
- internal_product_id: product.internal_id,
- product_id: product.id,
- created_at: createdAt || Date.now(),
- canceled: notNullish(canceledAt) ? true : false,
+ return {
+ id: cusProdId,
+ internal_customer_id: customer.internal_id,
+ customer_id: customer.id,
+ internal_product_id: product.internal_id,
+ product_id: product.id,
+ created_at: createdAt || Date.now(),
+ canceled: notNullish(canceledAt) ? true : false,
- status: subscriptionStatus
- ? subscriptionStatus
- : isFuture
- ? CusProductStatus.Scheduled
- : CusProductStatus.Active,
+ status: subscriptionStatus
+ ? subscriptionStatus
+ : isFuture
+ ? CusProductStatus.Scheduled
+ : CusProductStatus.Active,
- processor: {
- type: ProcessorType.Stripe,
- // subscription_id: subscriptionId,
- // subscription_schedule_id: subscriptionScheduleId,
- // last_invoice_id: lastInvoiceId,
- },
+ processor: {
+ type: ProcessorType.Stripe,
+ // subscription_id: subscriptionId,
+ // subscription_schedule_id: subscriptionScheduleId,
+ // last_invoice_id: lastInvoiceId,
+ },
- starts_at: startsAt || Date.now(),
- trial_ends_at: trialEnds,
- options: optionsList || [],
- free_trial_id: freeTrial?.id || null,
- canceled_at: canceledAt,
- collection_method: collectionMethod || CollectionMethod.ChargeAutomatically,
- subscription_ids: subscriptionIds,
- scheduled_ids: subscriptionScheduleIds,
- is_custom: isCustom || false,
- quantity: quantity || 1,
- internal_entity_id: internalEntityId,
- entity_id: entityId,
- api_version: apiVersion,
- };
+ starts_at: startsAt || Date.now(),
+ trial_ends_at: trialEnds,
+ options: optionsList || [],
+ free_trial_id: freeTrial?.id || null,
+ canceled_at: canceledAt,
+ collection_method: collectionMethod || CollectionMethod.ChargeAutomatically,
+ subscription_ids: subscriptionIds,
+ scheduled_ids: subscriptionScheduleIds,
+ is_custom: isCustom || false,
+ quantity: quantity || 1,
+ internal_entity_id: internalEntityId,
+ entity_id: entityId,
+ api_version: apiVersion,
+ };
};
export const insertFullCusProduct = async ({
- db,
- cusProd,
- cusEnts,
- cusPrices,
- replaceables,
+ db,
+ cusProd,
+ cusEnts,
+ cusPrices,
+ replaceables,
}: {
- db: DrizzleCli;
- cusProd: CusProduct;
- cusEnts: CustomerEntitlement[];
- cusPrices: CustomerPrice[];
- replaceables: InsertReplaceable[];
+ db: DrizzleCli;
+ cusProd: CusProduct;
+ cusEnts: CustomerEntitlement[];
+ cusPrices: CustomerPrice[];
+ replaceables: InsertReplaceable[];
}) => {
- await CusProductService.insert({
- db,
- data: cusProd,
- });
+ await CusProductService.insert({
+ db,
+ data: cusProd,
+ });
- await CusEntService.insert({
- db,
- data: cusEnts,
- });
+ await CusEntService.insert({
+ db,
+ data: cusEnts,
+ });
- await CusPriceService.insert({
- db,
- data: cusPrices,
- });
+ await CusPriceService.insert({
+ db,
+ data: cusPrices,
+ });
- await RepService.insert({
- db,
- data: replaceables,
- });
+ await RepService.insert({
+ db,
+ data: replaceables,
+ });
};
export const expireOrDeleteCusProduct = async ({
- db,
- startsAt,
- product,
- cusProducts,
- internalEntityId,
+ db,
+ startsAt,
+ product,
+ cusProducts,
+ internalEntityId,
}: {
- db: DrizzleCli;
- startsAt?: number;
- product: FullProduct;
- cusProducts?: FullCusProduct[];
- internalEntityId?: string;
+ db: DrizzleCli;
+ startsAt?: number;
+ product: FullProduct;
+ cusProducts?: FullCusProduct[];
+ internalEntityId?: string;
}) => {
- // 1. If startsAt
- if (startsAt && startsAt > Date.now()) {
- const curScheduledProduct = cusProducts?.find(
- (cp) =>
- cp.product.group === product.group &&
- cp.status === CusProductStatus.Scheduled &&
- (internalEntityId
- ? cp.internal_entity_id === internalEntityId
- : nullish(cp.internal_entity_id))
- );
+ // 1. If startsAt
+ if (startsAt && startsAt > Date.now()) {
+ const curScheduledProduct = cusProducts?.find(
+ (cp) =>
+ cp.product.group === product.group &&
+ cp.status === CusProductStatus.Scheduled &&
+ (internalEntityId
+ ? cp.internal_entity_id === internalEntityId
+ : nullish(cp.internal_entity_id)),
+ );
- if (curScheduledProduct) {
- await CusProductService.delete({
- db,
- cusProductId: curScheduledProduct.id,
- });
- }
- } else {
- const { curMainProduct } = getExistingCusProducts({
- product,
- cusProducts: cusProducts as FullCusProduct[],
- internalEntityId,
- });
+ if (curScheduledProduct) {
+ await CusProductService.delete({
+ db,
+ cusProductId: curScheduledProduct.id,
+ });
+ }
+ } else {
+ const { curMainProduct } = getExistingCusProducts({
+ product,
+ cusProducts: cusProducts as FullCusProduct[],
+ internalEntityId,
+ });
- if (curMainProduct) {
- await CusProductService.update({
- db,
- cusProductId: curMainProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- },
- });
- }
- }
+ if (curMainProduct) {
+ await CusProductService.update({
+ db,
+ cusProductId: curMainProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ },
+ });
+ }
+ }
};
export const getExistingCusProduct = async ({
- db,
- cusProducts,
- product,
- internalCustomerId,
- internalEntityId,
+ db,
+ cusProducts,
+ product,
+ internalCustomerId,
+ internalEntityId,
}: {
- db: DrizzleCli;
+ db: DrizzleCli;
- cusProducts?: FullCusProduct[];
- product: FullProduct;
- internalCustomerId: string;
- internalEntityId?: string;
+ cusProducts?: FullCusProduct[];
+ product: FullProduct;
+ internalCustomerId: string;
+ internalEntityId?: string;
}) => {
- if (!cusProducts) {
- cusProducts = await CusProductService.list({
- db,
- internalCustomerId,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- });
- }
+ if (!cusProducts) {
+ cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ });
+ }
- const { curMainProduct } = getExistingCusProducts({
- product,
- cusProducts: cusProducts as FullCusProduct[],
- internalEntityId,
- });
+ const { curMainProduct } = getExistingCusProducts({
+ product,
+ cusProducts: cusProducts as FullCusProduct[],
+ internalEntityId,
+ });
- return curMainProduct;
+ return curMainProduct;
};
export const createFullCusProduct = async ({
- db,
- attachParams,
- startsAt,
- // subscriptionId,
- nextResetAt,
- disableFreeTrial = false,
- productOptions,
- trialEndsAt,
- subscriptionStatus,
- canceledAt = null,
- createdAt = null,
- subscriptionIds = [],
- subscriptionScheduleIds = [],
- // keepResetIntervals = false,
- anchorToUnix,
- carryExistingUsages = false,
- carryOverTrial = false,
- isDowngrade = false,
- scenario = "default",
- sendWebhook = true,
- logger,
+ db,
+ attachParams,
+ startsAt,
+ // subscriptionId,
+ nextResetAt,
+ disableFreeTrial = false,
+ productOptions,
+ trialEndsAt,
+ subscriptionStatus,
+ canceledAt = null,
+ createdAt = null,
+ subscriptionIds = [],
+ subscriptionScheduleIds = [],
+ // keepResetIntervals = false,
+ anchorToUnix,
+ carryExistingUsages = false,
+ carryOverTrial = false,
+ isDowngrade = false,
+ scenario = "default",
+ sendWebhook = true,
+ logger,
}: {
- db: DrizzleCli;
- attachParams: InsertCusProductParams;
- startsAt?: number;
- // subscriptionId?: string;
- nextResetAt?: number;
- billLaterOnly?: boolean;
- disableFreeTrial?: boolean;
- productOptions?: ProductOptions;
- trialEndsAt?: number;
- subscriptionStatus?: CusProductStatus;
- canceledAt?: number | null;
- createdAt?: number | null;
- subscriptionIds?: string[];
- subscriptionScheduleIds?: string[];
- keepResetIntervals?: boolean;
- anchorToUnix?: number;
- carryExistingUsages?: boolean;
- carryOverTrial?: boolean;
- isDowngrade?: boolean;
- scenario?: string;
- sendWebhook?: boolean;
- logger: any;
+ db: DrizzleCli;
+ attachParams: InsertCusProductParams;
+ startsAt?: number;
+ // subscriptionId?: string;
+ nextResetAt?: number;
+ billLaterOnly?: boolean;
+ disableFreeTrial?: boolean;
+ productOptions?: ProductOptions;
+ trialEndsAt?: number;
+ subscriptionStatus?: CusProductStatus;
+ canceledAt?: number | null;
+ createdAt?: number | null;
+ subscriptionIds?: string[];
+ subscriptionScheduleIds?: string[];
+ keepResetIntervals?: boolean;
+ anchorToUnix?: number;
+ carryExistingUsages?: boolean;
+ carryOverTrial?: boolean;
+ isDowngrade?: boolean;
+ scenario?: string;
+ sendWebhook?: boolean;
+ logger: any;
}) => {
- disableFreeTrial = attachParams.disableFreeTrial || disableFreeTrial;
+ disableFreeTrial = attachParams.disableFreeTrial || disableFreeTrial;
- let { customer, product, prices, entitlements, optionsList, org, freeTrial } =
- attachParams;
+ let { customer, product, prices, entitlements, optionsList, org, freeTrial } =
+ attachParams;
- // Try to get current cus product or set to null...
- const curCusProduct = await getExistingCusProduct({
- db,
- cusProducts: attachParams.cusProducts,
- product,
- internalCustomerId: customer.internal_id,
- internalEntityId: attachParams.internalEntityId,
- });
+ // Try to get current cus product or set to null...
+ const curCusProduct = await getExistingCusProduct({
+ db,
+ cusProducts: attachParams.cusProducts,
+ product,
+ internalCustomerId: customer.internal_id,
+ internalEntityId: attachParams.internalEntityId,
+ });
- freeTrial = disableFreeTrial ? null : freeTrial;
+ freeTrial = disableFreeTrial ? null : freeTrial;
- if (carryOverTrial && curCusProduct?.free_trial) {
- freeTrial = curCusProduct.free_trial;
- trialEndsAt = curCusProduct.trial_ends_at || undefined;
- }
+ if (carryOverTrial && curCusProduct?.free_trial) {
+ freeTrial = curCusProduct.free_trial;
+ trialEndsAt = curCusProduct.trial_ends_at || undefined;
+ }
- const attachReplaceables = attachParams.replaceables || [];
+ const attachReplaceables = attachParams.replaceables || [];
- const existingCusProduct = searchCusProducts({
- internalProductId: product.internal_id,
- cusProducts: attachParams.cusProducts!,
- status: CusProductStatus.Active,
- });
+ const existingCusProduct = searchCusProducts({
+ internalProductId: product.internal_id,
+ cusProducts: attachParams.cusProducts!,
+ status: CusProductStatus.Active,
+ });
- if (
- (isOneOff(prices) || (isFreeProduct(prices) && product.is_add_on)) &&
- notNullish(existingCusProduct) &&
- !attachParams.isCustom
- ) {
- await updateOneTimeCusProduct({
- db,
- attachParams,
- logger,
- });
- return;
- }
+ if (
+ (isOneOff(prices) || (isFreeProduct(prices) && product.is_add_on)) &&
+ notNullish(existingCusProduct) &&
+ !attachParams.isCustom
+ ) {
+ await updateOneTimeCusProduct({
+ db,
+ attachParams,
+ logger,
+ });
+ return;
+ }
- const cusProdId = generateId("cus_prod");
- logger.info(
- `Inserting cus product ${product.id} for ${customer.name}, cus product ID: ${cusProdId}`
- );
- logger.info(productOptions);
+ const cusProdId = generateId("cus_prod");
+ logger.info(
+ `Inserting cus product ${product.id} for ${customer.name}, cus product ID: ${cusProdId}`,
+ );
+ logger.info(productOptions);
- // 1. create customer entitlements
- const cusEnts: CustomerEntitlement[] = [];
- const newReplaceables: InsertReplaceable[] = [];
+ // 1. create customer entitlements
+ const cusEnts: CustomerEntitlement[] = [];
+ const newReplaceables: InsertReplaceable[] = [];
- for (const entitlement of entitlements) {
- const options = getEntOptions(optionsList, entitlement);
- const relatedPrice = getEntRelatedPrice(entitlement, prices);
+ for (const entitlement of entitlements) {
+ const options = getEntOptions(optionsList, entitlement);
+ const relatedPrice = getEntRelatedPrice(entitlement, prices);
- const cusEnt: any = initCusEntitlement({
- entitlement,
- customer,
- cusProductId: cusProdId,
- options: options || undefined,
- nextResetAt,
- freeTrial,
- relatedPrice,
- trialEndsAt,
- anchorToUnix,
- entities: attachParams.entities || [],
- carryExistingUsages,
- curCusProduct: curCusProduct as FullCusProduct,
- replaceables: attachReplaceables,
- now: attachParams.now,
- productOptions: productOptions || undefined,
- });
+ const cusEnt: any = initCusEntitlement({
+ entitlement,
+ customer,
+ cusProductId: cusProdId,
+ options: options || undefined,
+ nextResetAt,
+ freeTrial,
+ relatedPrice,
+ trialEndsAt,
+ anchorToUnix,
+ entities: attachParams.entities || [],
+ carryExistingUsages,
+ curCusProduct: curCusProduct as FullCusProduct,
+ replaceables: attachReplaceables,
+ now: attachParams.now,
+ productOptions: productOptions || undefined,
+ });
- cusEnts.push(cusEnt);
+ cusEnts.push(cusEnt);
- const newReplaceables_ = attachReplaceables
- .filter((r) => r.ent.id === entitlement.id)
- .map((r) => ({
- ...r,
- cus_ent_id: cusEnt.id,
- }));
+ const newReplaceables_ = attachReplaceables
+ .filter((r) => r.ent.id === entitlement.id)
+ .map((r) => ({
+ ...r,
+ cus_ent_id: cusEnt.id,
+ }));
- newReplaceables.push(...newReplaceables_);
- }
+ newReplaceables.push(...newReplaceables_);
+ }
- // 3. Deduct existing usages
+ // 3. Deduct existing usages
- let deductedCusEnts = addExistingUsagesToCusEnts({
- cusEnts: cusEnts,
- entitlements: entitlements,
- curCusProduct: curCusProduct as FullCusProduct,
- carryExistingUsages,
- isDowngrade,
- entities: attachParams.entities,
- features: attachParams.features,
- });
+ let deductedCusEnts = addExistingUsagesToCusEnts({
+ cusEnts: cusEnts,
+ entitlements: entitlements,
+ curCusProduct: curCusProduct as FullCusProduct,
+ carryExistingUsages,
+ isDowngrade,
+ entities: attachParams.entities,
+ features: attachParams.features,
+ });
- // 4. Get new rollovers
- const rolloverOps = await getNewProductRollovers({
- db,
- curCusProduct: curCusProduct as FullCusProduct,
- cusEnts,
- entitlements,
- logger,
- });
+ // 4. Get new rollovers
+ const rolloverOps = await getNewProductRollovers({
+ db,
+ curCusProduct: curCusProduct as FullCusProduct,
+ cusEnts,
+ entitlements,
+ logger,
+ });
- // 4. create customer prices
- const cusPrices: CustomerPrice[] = [];
+ // 4. create customer prices
+ const cusPrices: CustomerPrice[] = [];
- for (const price of prices) {
- const cusPrice: CustomerPrice = initCusPrice({
- price,
- customer,
- cusProductId: cusProdId,
- });
+ for (const price of prices) {
+ const cusPrice: CustomerPrice = initCusPrice({
+ price,
+ customer,
+ cusProductId: cusProdId,
+ });
- cusPrices.push(cusPrice);
- }
+ cusPrices.push(cusPrice);
+ }
- // let entityId = customer.entity?.id;
- const cusProd = initCusProduct({
- cusProdId,
- customer,
- product,
- startsAt,
- optionsList,
- freeTrial: disableFreeTrial ? null : freeTrial,
- trialEndsAt,
- subscriptionStatus,
- canceledAt,
- createdAt,
- collectionMethod: attachParams.invoiceOnly
- ? CollectionMethod.SendInvoice
- : CollectionMethod.ChargeAutomatically,
- subscriptionIds,
- subscriptionScheduleIds,
- isCustom: attachParams.isCustom || false,
- internalEntityId: attachParams.internalEntityId,
- entityId: attachParams.entityId,
- apiVersion: attachParams.apiVersion,
- quantity: productOptions?.quantity ?? undefined,
- });
+ // let entityId = customer.entity?.id;
+ const cusProd = initCusProduct({
+ cusProdId,
+ customer,
+ product,
+ startsAt,
+ optionsList,
+ freeTrial: disableFreeTrial ? null : freeTrial,
+ trialEndsAt,
+ subscriptionStatus,
+ canceledAt,
+ createdAt,
+ collectionMethod: attachParams.invoiceOnly
+ ? CollectionMethod.SendInvoice
+ : CollectionMethod.ChargeAutomatically,
+ subscriptionIds,
+ subscriptionScheduleIds,
+ isCustom: attachParams.isCustom || false,
+ internalEntityId: attachParams.internalEntityId,
+ entityId: attachParams.entityId,
+ apiVersion: attachParams.apiVersion,
+ quantity: productOptions?.quantity ?? undefined,
+ });
- // Expire previous product if not one off and add on...?
- if (!isOneOff(prices) && product.is_add_on) {
- const { curSameProduct } = getExistingCusProducts({
- product,
- cusProducts: attachParams.cusProducts!,
- internalEntityId: attachParams.internalEntityId,
- });
+ // Expire previous product if not one off and add on...?
+ if (!isOneOff(prices) && product.is_add_on) {
+ const { curSameProduct } = getExistingCusProducts({
+ product,
+ cusProducts: attachParams.cusProducts!,
+ internalEntityId: attachParams.internalEntityId,
+ });
- if (curSameProduct) {
- await CusProductService.update({
- db,
- cusProductId: curSameProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- },
- });
- }
- }
+ if (curSameProduct) {
+ await CusProductService.update({
+ db,
+ cusProductId: curSameProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ },
+ });
+ }
+ }
- if (!isOneOff(prices) && !product.is_add_on) {
- await expireOrDeleteCusProduct({
- db,
- startsAt,
- product,
- cusProducts: attachParams.cusProducts,
- internalEntityId: attachParams.internalEntityId,
- });
- }
+ if (!isOneOff(prices) && !product.is_add_on) {
+ await expireOrDeleteCusProduct({
+ db,
+ startsAt,
+ product,
+ cusProducts: attachParams.cusProducts,
+ internalEntityId: attachParams.internalEntityId,
+ });
+ }
- await insertFullCusProduct({
- db,
- cusProd,
- cusEnts: deductedCusEnts,
- cusPrices,
- replaceables: newReplaceables,
- });
+ await insertFullCusProduct({
+ db,
+ cusProd,
+ cusEnts: deductedCusEnts,
+ cusPrices,
+ replaceables: newReplaceables,
+ });
- const rolloverInserts: any = [];
+ const rolloverInserts: any = [];
- for (const operation of rolloverOps) {
- rolloverInserts.push(
- RolloverService.insert({
- db,
- rows: operation.toInsert,
- fullCusEnt: operation.cusEnt,
- })
- );
- }
+ for (const operation of rolloverOps) {
+ rolloverInserts.push(
+ RolloverService.insert({
+ db,
+ rows: operation.toInsert,
+ fullCusEnt: operation.cusEnt,
+ }),
+ );
+ }
- const finalRollovers = (await Promise.all(rolloverInserts)).flatMap((r) => r);
+ const finalRollovers = (await Promise.all(rolloverInserts)).flatMap((r) => r);
- // Get rollovers for each entitlement
- const cusEntsWithRollovers = await Promise.all(
- cusEnts.map(async (ce) => ({
- ...ce,
- entitlement: entitlements.find((e) => e.id === ce.entitlement_id)!,
- replaceables: newReplaceables
- .filter((r) => r.cus_ent_id === ce.id)
- .map((r) => ({
- ...r,
- delete_next_cycle: r.delete_next_cycle || false,
- })),
- rollovers: finalRollovers.filter((r) => r.cus_ent_id === ce.id),
- // await RolloverService.getCurrentRollovers({
- // db,
- // cusEntID: ce.id,
- // }),
- }))
- );
+ // Get rollovers for each entitlement
+ const cusEntsWithRollovers = await Promise.all(
+ cusEnts.map(async (ce) => ({
+ ...ce,
+ entitlement: entitlements.find((e) => e.id === ce.entitlement_id)!,
+ replaceables: newReplaceables
+ .filter((r) => r.cus_ent_id === ce.id)
+ .map((r) => ({
+ ...r,
+ delete_next_cycle: r.delete_next_cycle || false,
+ })),
+ rollovers: finalRollovers.filter((r) => r.cus_ent_id === ce.id),
+ // await RolloverService.getCurrentRollovers({
+ // db,
+ // cusEntID: ce.id,
+ // }),
+ })),
+ );
- const fullCusProduct = {
- ...cusProd,
- product,
- customer_entitlements: cusEntsWithRollovers,
- customer_prices: cusPrices.map((cp) => ({
- ...cp,
- price: prices.find((p) => p.id === cp.price_id)!,
- })),
- };
+ const fullCusProduct = {
+ ...cusProd,
+ product,
+ customer_entitlements: cusEntsWithRollovers,
+ customer_prices: cusPrices.map((cp) => ({
+ ...cp,
+ price: prices.find((p) => p.id === cp.price_id)!,
+ })),
+ };
- try {
- if (sendWebhook && !attachParams.fromMigration) {
- // Maybe send two for downgrade? (one for scheduled, one for active)
- await addProductsUpdatedWebhookTask({
- req: attachParams.req,
- internalCustomerId: customer.internal_id,
- org,
- env: customer.env,
- customerId: customer.id || null,
- cusProduct: isDowngrade ? curCusProduct! : fullCusProduct,
- scheduledCusProduct: isDowngrade ? fullCusProduct : undefined,
- scenario,
- logger,
- });
- }
- } catch (error) {
- logger.error("Failed to add products updated webhook task to queue");
- }
+ try {
+ if (sendWebhook && !attachParams.fromMigration) {
+ // Maybe send two for downgrade? (one for scheduled, one for active)
+ await addProductsUpdatedWebhookTask({
+ req: attachParams.req,
+ internalCustomerId: customer.internal_id,
+ org,
+ env: customer.env,
+ customerId: customer.id || null,
+ cusProduct: isDowngrade ? curCusProduct! : fullCusProduct,
+ scheduledCusProduct: isDowngrade ? fullCusProduct : undefined,
+ scenario,
+ logger,
+ });
+ }
+ } catch (error) {
+ logger.error("Failed to add products updated webhook task to queue");
+ }
- return fullCusProduct;
+ return fullCusProduct;
};
diff --git a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts
index 36560e427..0b33e4715 100644
--- a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts
+++ b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts
@@ -1,13 +1,13 @@
import { InsertCusProductParams } from "../cusProducts/AttachParams.js";
import {
- AllowanceType,
- AppEnv,
- CusProductStatus,
- EntitlementWithFeature,
- FeatureOptions,
- FullCustomerEntitlement,
- Organization,
- Price,
+ AllowanceType,
+ AppEnv,
+ CusProductStatus,
+ EntitlementWithFeature,
+ FeatureOptions,
+ FullCustomerEntitlement,
+ Organization,
+ Price,
} from "@autumn/shared";
import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js";
import { initCusEntitlement } from "./initCusEnt.js";
@@ -19,148 +19,148 @@ import { nullish } from "@/utils/genUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
const updateOneOffExistingEntitlement = async ({
- db,
- cusEnt,
- entitlement,
- org,
- env,
- options,
- relatedPrice,
- logger,
+ db,
+ cusEnt,
+ entitlement,
+ org,
+ env,
+ options,
+ relatedPrice,
+ logger,
}: {
- db: DrizzleCli;
- cusEnt: FullCustomerEntitlement;
- entitlement: EntitlementWithFeature;
- org: Organization;
- env: AppEnv;
- options?: FeatureOptions;
- relatedPrice?: Price;
- logger: any;
+ db: DrizzleCli;
+ cusEnt: FullCustomerEntitlement;
+ entitlement: EntitlementWithFeature;
+ org: Organization;
+ env: AppEnv;
+ options?: FeatureOptions;
+ relatedPrice?: Price;
+ logger: any;
}) => {
- if (entitlement.allowance_type === AllowanceType.Unlimited) {
- return;
- }
+ if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ return;
+ }
- // Fetch to get latest entitlement
- const updatedCusEnt = await CusEntService.getStrict({
- db,
- id: cusEnt.id,
- orgId: org.id,
- env: env,
- });
+ // Fetch to get latest entitlement
+ const updatedCusEnt = await CusEntService.getStrict({
+ db,
+ id: cusEnt.id,
+ orgId: org.id,
+ env: env,
+ });
- const resetBalance = getResetBalance({
- entitlement,
- options,
- relatedPrice,
- });
+ const resetBalance = getResetBalance({
+ entitlement,
+ options,
+ relatedPrice,
+ });
- if (nullish(resetBalance)) {
- logger.warn(
- "Tried updating one off entitlement, no reset balance, entitlement: ",
- );
- logger.warn(entitlement);
- return;
- }
+ if (nullish(resetBalance)) {
+ logger.warn(
+ "Tried updating one off entitlement, no reset balance, entitlement: ",
+ );
+ logger.warn(entitlement);
+ return;
+ }
- await CusEntService.update({
- db,
- id: updatedCusEnt.id,
- updates: {
- balance: updatedCusEnt.balance! + resetBalance!,
- },
- });
+ await CusEntService.update({
+ db,
+ id: updatedCusEnt.id,
+ updates: {
+ balance: updatedCusEnt.balance! + resetBalance!,
+ },
+ });
- return;
+ return;
};
export const updateOneTimeCusProduct = async ({
- db,
- attachParams,
- logger,
+ db,
+ attachParams,
+ logger,
}: {
- db: DrizzleCli;
- attachParams: InsertCusProductParams;
- logger: any;
+ db: DrizzleCli;
+ attachParams: InsertCusProductParams;
+ logger: any;
}) => {
- // 1. Sort cus products by created_at
- attachParams.cusProducts?.sort((a, b) => b.created_at - a.created_at);
+ // 1. Sort cus products by created_at
+ attachParams.cusProducts?.sort((a, b) => b.created_at - a.created_at);
- // 2. Get existing same cus product and customer entitlements
- let existingCusProduct = attachParams.cusProducts?.find(
- (cp) =>
- cp.product.internal_id === attachParams.product.internal_id &&
- cp.status === CusProductStatus.Active,
- )!;
+ // 2. Get existing same cus product and customer entitlements
+ let existingCusProduct = attachParams.cusProducts?.find(
+ (cp) =>
+ cp.product.internal_id === attachParams.product.internal_id &&
+ cp.status === CusProductStatus.Active,
+ )!;
- let existingCusEnts = existingCusProduct.customer_entitlements;
+ let existingCusEnts = existingCusProduct.customer_entitlements;
- // 3. Update existing entitlements
- for (const entitlement of attachParams.entitlements) {
- const existingCusEnt = existingCusEnts.find(
- (ce) => ce.internal_feature_id === entitlement.internal_feature_id,
- );
+ // 3. Update existing entitlements
+ for (const entitlement of attachParams.entitlements) {
+ const existingCusEnt = existingCusEnts.find(
+ (ce) => ce.internal_feature_id === entitlement.internal_feature_id,
+ );
- let relatedPrice = getEntRelatedPrice(entitlement, attachParams.prices);
- const options = getEntOptions(attachParams.optionsList, entitlement);
+ let relatedPrice = getEntRelatedPrice(entitlement, attachParams.prices);
+ const options = getEntOptions(attachParams.optionsList, entitlement);
- if (existingCusEnt) {
- await updateOneOffExistingEntitlement({
- db,
- cusEnt: existingCusEnt,
- entitlement,
- org: attachParams.org,
- env: attachParams.customer.env,
- options: options || undefined,
- relatedPrice,
- logger,
- });
- } else {
- let newCusEnt = initCusEntitlement({
- entitlement,
- customer: attachParams.customer,
- cusProductId: existingCusProduct.id,
- options: undefined,
- nextResetAt: undefined,
- freeTrial: null,
- relatedPrice,
- replaceables: [],
- entities: attachParams.entities || [],
- });
+ if (existingCusEnt) {
+ await updateOneOffExistingEntitlement({
+ db,
+ cusEnt: existingCusEnt,
+ entitlement,
+ org: attachParams.org,
+ env: attachParams.customer.env,
+ options: options || undefined,
+ relatedPrice,
+ logger,
+ });
+ } else {
+ let newCusEnt = initCusEntitlement({
+ entitlement,
+ customer: attachParams.customer,
+ cusProductId: existingCusProduct.id,
+ options: undefined,
+ nextResetAt: undefined,
+ freeTrial: null,
+ relatedPrice,
+ replaceables: [],
+ entities: attachParams.entities || [],
+ });
- console.log("Inserting new cus ent");
- await CusEntService.insert({
- db,
- data: [newCusEnt as any],
- });
- }
- }
+ console.log("Inserting new cus ent");
+ await CusEntService.insert({
+ db,
+ data: [newCusEnt as any],
+ });
+ }
+ }
- // Update options on full cus product
- let newOptionsList = [...attachParams.optionsList];
+ // Update options on full cus product
+ let newOptionsList = [...attachParams.optionsList];
- for (const curOptions of existingCusProduct.options) {
- // Find the option in the new options list
- const newOptionIndex = newOptionsList.findIndex(
- (o) => o.internal_feature_id === curOptions.internal_feature_id,
- );
+ for (const curOptions of existingCusProduct.options) {
+ // Find the option in the new options list
+ const newOptionIndex = newOptionsList.findIndex(
+ (o) => o.internal_feature_id === curOptions.internal_feature_id,
+ );
- if (newOptionIndex !== -1) {
- newOptionsList[newOptionIndex] = {
- ...newOptionsList[newOptionIndex],
- quantity:
- (newOptionsList[newOptionIndex].quantity || 0) +
- (curOptions.quantity || 0),
- };
- }
- }
+ if (newOptionIndex !== -1) {
+ newOptionsList[newOptionIndex] = {
+ ...newOptionsList[newOptionIndex],
+ quantity:
+ (newOptionsList[newOptionIndex].quantity || 0) +
+ (curOptions.quantity || 0),
+ };
+ }
+ }
- await CusProductService.update({
- db,
- cusProductId: existingCusProduct.id,
- updates: {
- options: newOptionsList,
- quantity: existingCusProduct.quantity + 1,
- },
- });
+ await CusProductService.update({
+ db,
+ cusProductId: existingCusProduct.id,
+ updates: {
+ options: newOptionsList,
+ quantity: existingCusProduct.quantity + 1,
+ },
+ });
};
diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts
index 2b1b40d9a..bad33a055 100644
--- a/server/src/internal/customers/add-product/handleCreateCheckout.ts
+++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts
@@ -1,7 +1,7 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "../cusProducts/AttachParams.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { pricesContainRecurring } from "@/internal/products/prices/priceUtils.js";
@@ -18,193 +18,193 @@ import Stripe from "stripe";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
export const handleCreateCheckout = async ({
- req,
- res,
- attachParams,
- config,
- returnCheckout = false,
+ req,
+ res,
+ attachParams,
+ config,
+ returnCheckout = false,
}: {
- req: any;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
- returnCheckout?: boolean;
+ req: any;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ returnCheckout?: boolean;
}) => {
- const { db, logtail: logger } = req;
+ const { db, logtail: logger } = req;
- const { customer, org, freeTrial, successUrl, rewards } = attachParams;
+ const { customer, org, freeTrial, successUrl, rewards } = attachParams;
- const stripeCli = createStripeCli({
- org,
- env: customer.env,
- legacyVersion: true,
- });
+ const stripeCli = createStripeCli({
+ org,
+ env: customer.env,
+ legacyVersion: true,
+ });
- const itemSets = await getStripeSubItems({
- attachParams,
- isCheckout: true,
- });
+ const itemSets = await getStripeSubItems({
+ attachParams,
+ isCheckout: true,
+ });
- if (itemSets.length === 0) {
- throw new RecaseError({
- code: ErrCode.ProductHasNoPrices,
- message: "Product has no prices",
- });
- }
+ if (itemSets.length === 0) {
+ throw new RecaseError({
+ code: ErrCode.ProductHasNoPrices,
+ message: "Product has no prices",
+ });
+ }
- const { items } = itemSets[0];
+ const { items } = itemSets[0];
- attachParams.itemSets = itemSets;
+ attachParams.itemSets = itemSets;
- const isRecurring = pricesContainRecurring(attachParams.prices);
+ const isRecurring = pricesContainRecurring(attachParams.prices);
- // Insert metadata
- const metaId = await createCheckoutMetadata({
- db,
- attachParams,
- });
+ // Insert metadata
+ const metaId = await createCheckoutMetadata({
+ db,
+ attachParams,
+ });
- let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month
- ? Math.floor(
- getNextStartOfMonthUnix({
- interval: itemSets[0].interval,
- intervalCount: itemSets[0].intervalCount,
- }) / 1000
- )
- : undefined;
+ let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month
+ ? Math.floor(
+ getNextStartOfMonthUnix({
+ interval: itemSets[0].interval,
+ intervalCount: itemSets[0].intervalCount,
+ }) / 1000,
+ )
+ : undefined;
- if (attachParams.billingAnchor) {
- billingCycleAnchorUnixSeconds = Math.floor(
- attachParams.billingAnchor / 1000
- );
- }
+ if (attachParams.billingAnchor) {
+ billingCycleAnchorUnixSeconds = Math.floor(
+ attachParams.billingAnchor / 1000,
+ );
+ }
- const subscriptionData:
- | Stripe.Checkout.SessionCreateParams.SubscriptionData
- | undefined = isRecurring
- ? {
- trial_end:
- freeTrial && !attachParams.disableFreeTrial
- ? freeTrialToStripeTimestamp({ freeTrial })
- : undefined,
- trial_settings:
- freeTrial && !attachParams.disableFreeTrial && freeTrial.card_required
- ? {
- end_behavior: {
- missing_payment_method: "cancel",
- },
- }
- : undefined,
- billing_cycle_anchor: billingCycleAnchorUnixSeconds,
- }
- : undefined;
+ const subscriptionData:
+ | Stripe.Checkout.SessionCreateParams.SubscriptionData
+ | undefined = isRecurring
+ ? {
+ trial_end:
+ freeTrial && !attachParams.disableFreeTrial
+ ? freeTrialToStripeTimestamp({ freeTrial })
+ : undefined,
+ trial_settings:
+ freeTrial && !attachParams.disableFreeTrial && freeTrial.card_required
+ ? {
+ end_behavior: {
+ missing_payment_method: "cancel",
+ },
+ }
+ : undefined,
+ billing_cycle_anchor: billingCycleAnchorUnixSeconds,
+ }
+ : undefined;
- let checkoutParams = attachParams.checkoutSessionParams || {};
- let allowPromotionCodes =
- notNullish(checkoutParams.discounts) || notNullish(rewards)
- ? undefined
- : checkoutParams.allow_promotion_codes || true;
+ let checkoutParams = attachParams.checkoutSessionParams || {};
+ let allowPromotionCodes =
+ notNullish(checkoutParams.discounts) || notNullish(rewards)
+ ? undefined
+ : checkoutParams.allow_promotion_codes || true;
- let rewardData = {};
- if (rewards) {
- rewardData = {
- discounts: rewards.map((r) => ({ coupon: r.id })),
- };
- }
+ let rewardData = {};
+ if (rewards) {
+ rewardData = {
+ discounts: rewards.map((r) => ({ coupon: r.id })),
+ };
+ }
- // Prepare checkout session parameters
- let checkout;
+ // Prepare checkout session parameters
+ let checkout;
- let paymentMethodSet =
- notNullish(checkoutParams.payment_method_types) ||
- notNullish(checkoutParams.payment_method_configuration);
+ let paymentMethodSet =
+ notNullish(checkoutParams.payment_method_types) ||
+ notNullish(checkoutParams.payment_method_configuration);
- let sessionParams = {
- customer: customer.processor.id,
- line_items: items,
- subscription_data: subscriptionData,
- mode: isRecurring ? "subscription" : "payment",
- currency: org.default_currency,
- success_url: successUrl || toSuccessUrl({ org, env: customer.env }),
+ let sessionParams = {
+ customer: customer.processor.id,
+ line_items: items,
+ subscription_data: subscriptionData,
+ mode: isRecurring ? "subscription" : "payment",
+ currency: org.default_currency,
+ success_url: successUrl || toSuccessUrl({ org, env: customer.env }),
- allow_promotion_codes: allowPromotionCodes,
- invoice_creation: !isRecurring ? { enabled: true } : undefined,
- saved_payment_method_options: { payment_method_save: "enabled" },
- ...rewardData,
- ...(attachParams.checkoutSessionParams || {}),
- metadata: {
- ...(attachParams.metadata ? attachParams.metadata : {}),
- ...(attachParams.checkoutSessionParams?.metadata || {}),
- autumn_metadata_id: metaId,
- },
- payment_method_collection:
- freeTrial &&
- !attachParams.disableFreeTrial &&
- freeTrial.card_required === false
- ? "if_required"
- : undefined,
- } satisfies Stripe.Checkout.SessionCreateParams;
+ allow_promotion_codes: allowPromotionCodes,
+ invoice_creation: !isRecurring ? { enabled: true } : undefined,
+ saved_payment_method_options: { payment_method_save: "enabled" },
+ ...rewardData,
+ ...(attachParams.checkoutSessionParams || {}),
+ metadata: {
+ ...(attachParams.metadata ? attachParams.metadata : {}),
+ ...(attachParams.checkoutSessionParams?.metadata || {}),
+ autumn_metadata_id: metaId,
+ },
+ payment_method_collection:
+ freeTrial &&
+ !attachParams.disableFreeTrial &&
+ freeTrial.card_required === false
+ ? "if_required"
+ : undefined,
+ } satisfies Stripe.Checkout.SessionCreateParams;
- if (attachParams.setupPayment) {
- sessionParams = {
- customer: customer.processor?.id,
- mode: "setup",
- success_url: successUrl || toSuccessUrl({ org, env: customer.env }),
- currency: org.default_currency || "usd",
- ...(checkoutParams as any),
- metadata: {
- ...(attachParams.checkoutSessionParams?.metadata || {}),
- autumn_metadata_id: metaId,
- },
- };
- }
+ if (attachParams.setupPayment) {
+ sessionParams = {
+ customer: customer.processor?.id,
+ mode: "setup",
+ success_url: successUrl || toSuccessUrl({ org, env: customer.env }),
+ currency: org.default_currency || "usd",
+ ...(checkoutParams as any),
+ metadata: {
+ ...(attachParams.checkoutSessionParams?.metadata || {}),
+ autumn_metadata_id: metaId,
+ },
+ };
+ }
- try {
- checkout = await stripeCli.checkout.sessions.create(sessionParams);
- logger.info(
- `✅ Successfully created checkout for customer ${customer.id || customer.internal_id}`
- );
- } catch (error: any) {
- let msg = error.message;
- if (
- msg &&
- msg.includes("No valid payment method types") &&
- !paymentMethodSet
- ) {
- checkout = await stripeCli.checkout.sessions.create({
- ...sessionParams,
- payment_method_types: ["card"],
- });
+ try {
+ checkout = await stripeCli.checkout.sessions.create(sessionParams);
+ logger.info(
+ `✅ Successfully created checkout for customer ${customer.id || customer.internal_id}`,
+ );
+ } catch (error: any) {
+ let msg = error.message;
+ if (
+ msg &&
+ msg.includes("No valid payment method types") &&
+ !paymentMethodSet
+ ) {
+ checkout = await stripeCli.checkout.sessions.create({
+ ...sessionParams,
+ payment_method_types: ["card"],
+ });
- logger.info(
- `✅ Created fallback checkout session with card payment method for customer ${customer.id || customer.internal_id}`
- );
- } else {
- throw error;
- }
- }
+ logger.info(
+ `✅ Created fallback checkout session with card payment method for customer ${customer.id || customer.internal_id}`,
+ );
+ } else {
+ throw error;
+ }
+ }
- if (returnCheckout) {
- return checkout;
- }
+ if (returnCheckout) {
+ return checkout;
+ }
- let apiVersion = attachParams.apiVersion || APIVersion.v1;
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json(
- AttachResultSchema.parse({
- checkout_url: checkout.url,
- code: SuccessCode.CheckoutCreated,
- message: `Successfully created checkout for customer ${
- customer.id || customer.internal_id
- }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
- product_ids: attachParams.products.map((p) => p.id),
- customer_id: customer.id || customer.internal_id,
- })
- );
- } else {
- res.status(200).json({
- checkout_url: checkout.url,
- });
- }
- return;
+ let apiVersion = attachParams.apiVersion || APIVersion.v1;
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ checkout_url: checkout.url,
+ code: SuccessCode.CheckoutCreated,
+ message: `Successfully created checkout for customer ${
+ customer.id || customer.internal_id
+ }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
+ product_ids: attachParams.products.map((p) => p.id),
+ customer_id: customer.id || customer.internal_id,
+ }),
+ );
+ } else {
+ res.status(200).json({
+ checkout_url: checkout.url,
+ });
+ }
+ return;
};
diff --git a/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts b/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts
index 6ea666c62..a4db8ed78 100644
--- a/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts
+++ b/server/src/internal/customers/add-product/handleCreateInvoiceCheckout.ts
@@ -1,6 +1,6 @@
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "../cusProducts/AttachParams.js";
import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js";
@@ -9,109 +9,109 @@ import { isOneOff } from "@/internal/products/productUtils.js";
import { handlePaidProduct } from "../attach/attachFunctions/addProductFlow/handlePaidProduct.js";
import {
- AttachBody,
- AttachBranch,
- AttachConfig,
- SuccessCode,
+ AttachBody,
+ AttachBranch,
+ AttachConfig,
+ SuccessCode,
} from "@autumn/shared";
import Stripe from "stripe";
import { handleOneOffFunction } from "../attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
import { handleMultiAttachFlow } from "../attach/attachFunctions/multiAttach/handleMultiAttachFlow.js";
export const handleCreateInvoiceCheckout = async ({
- req,
- res,
- attachParams,
- attachBody,
- config,
- branch,
+ req,
+ res,
+ attachParams,
+ attachBody,
+ config,
+ branch,
}: {
- req: any;
- res?: any;
- attachParams: AttachParams;
- attachBody: AttachBody;
- config: AttachConfig;
- branch: AttachBranch;
+ req: any;
+ res?: any;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ config: AttachConfig;
+ branch: AttachBranch;
}) => {
- // if one off
- const { stripeCli } = attachParams;
+ // if one off
+ const { stripeCli } = attachParams;
- let invoiceResult;
+ let invoiceResult;
- if (attachParams.productsList) {
- invoiceResult = await handleMultiAttachFlow({
- req,
- res,
- attachParams,
- attachBody,
- branch,
- config,
- });
- } else if (isOneOff(attachParams.prices)) {
- invoiceResult = await handleOneOffFunction({
- req,
- res,
- attachParams,
- config,
- });
- } else {
- invoiceResult = await handlePaidProduct({
- req,
- res,
- attachParams,
- config,
- });
- }
+ if (attachParams.productsList) {
+ invoiceResult = await handleMultiAttachFlow({
+ req,
+ res,
+ attachParams,
+ attachBody,
+ branch,
+ config,
+ });
+ } else if (isOneOff(attachParams.prices)) {
+ invoiceResult = await handleOneOffFunction({
+ req,
+ res,
+ attachParams,
+ config,
+ });
+ } else {
+ invoiceResult = await handlePaidProduct({
+ req,
+ res,
+ attachParams,
+ config,
+ });
+ }
- const { invoices, anchorToUnix, subs }: any = invoiceResult;
+ const { invoices, anchorToUnix, subs }: any = invoiceResult;
- const metadataId = await createCheckoutMetadata({
- db: req.db,
- attachParams: {
- ...attachParams,
- anchorToUnix,
- subIds: subs.map((s: Stripe.Subscription) => s.id),
- config,
- } as any,
- });
+ const metadataId = await createCheckoutMetadata({
+ db: req.db,
+ attachParams: {
+ ...attachParams,
+ anchorToUnix,
+ subIds: subs.map((s: Stripe.Subscription) => s.id),
+ config,
+ } as any,
+ });
- for (const invoice of invoices) {
- await stripeCli.invoices.update(invoice.id, {
- metadata: {
- autumn_metadata_id: metadataId,
- },
- });
- }
+ for (const invoice of invoices) {
+ await stripeCli.invoices.update(invoice.id, {
+ metadata: {
+ autumn_metadata_id: metadataId,
+ },
+ });
+ }
- if (res) {
- if (!config.finalizeInvoice) {
- res.status(200).json(
- AttachResultSchema.parse({
- invoice: invoices[0],
- code: SuccessCode.CheckoutCreated,
- message: `Successfully created invoice for customer ${
- attachParams.customer.id || attachParams.customer.internal_id
- }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
- product_ids: attachParams.products.map((p) => p.id),
- customer_id:
- attachParams.customer.id || attachParams.customer.internal_id,
- })
- );
- return;
- }
- res.status(200).json(
- AttachResultSchema.parse({
- checkout_url: invoices[0].hosted_invoice_url,
- code: SuccessCode.CheckoutCreated,
- message: `Successfully created invoice checkout for customer ${
- attachParams.customer.id || attachParams.customer.internal_id
- }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
- product_ids: attachParams.products.map((p) => p.id),
- customer_id:
- attachParams.customer.id || attachParams.customer.internal_id,
- })
- );
- }
+ if (res) {
+ if (!config.finalizeInvoice) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ invoice: invoices[0],
+ code: SuccessCode.CheckoutCreated,
+ message: `Successfully created invoice for customer ${
+ attachParams.customer.id || attachParams.customer.internal_id
+ }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
+ product_ids: attachParams.products.map((p) => p.id),
+ customer_id:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ }),
+ );
+ return;
+ }
+ res.status(200).json(
+ AttachResultSchema.parse({
+ checkout_url: invoices[0].hosted_invoice_url,
+ code: SuccessCode.CheckoutCreated,
+ message: `Successfully created invoice checkout for customer ${
+ attachParams.customer.id || attachParams.customer.internal_id
+ }, product(s) ${attachParams.products.map((p) => p.name).join(", ")}`,
+ product_ids: attachParams.products.map((p) => p.id),
+ customer_id:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ }),
+ );
+ }
- return { invoices };
+ return { invoices };
};
diff --git a/server/src/internal/customers/add-product/initCusEnt.ts b/server/src/internal/customers/add-product/initCusEnt.ts
index 0907809d5..5837402ec 100644
--- a/server/src/internal/customers/add-product/initCusEnt.ts
+++ b/server/src/internal/customers/add-product/initCusEnt.ts
@@ -1,16 +1,16 @@
import {
- AllowanceType,
- AttachReplaceable,
- BillingType,
- Customer,
- Entity,
- EntityBalance,
- FeatureType,
- FreeTrial,
- FullCusProduct,
- FullCustomerEntitlement,
- Price,
- ProductOptions,
+ AllowanceType,
+ AttachReplaceable,
+ BillingType,
+ Customer,
+ Entity,
+ EntityBalance,
+ FeatureType,
+ FreeTrial,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ Price,
+ ProductOptions,
} from "@autumn/shared";
import { FeatureOptions } from "@autumn/shared";
@@ -24,187 +24,187 @@ import { initNextResetAt } from "../cusProducts/insertCusProduct/initCusEnt/init
import { Decimal } from "decimal.js";
export const initCusEntEntities = ({
- entitlement,
- entities,
- existingCusEnt,
- resetBalance,
+ entitlement,
+ entities,
+ existingCusEnt,
+ resetBalance,
}: {
- entitlement: EntitlementWithFeature;
- entities: Entity[];
- existingCusEnt?: FullCustomerEntitlement;
- resetBalance?: number | null;
+ entitlement: EntitlementWithFeature;
+ entities: Entity[];
+ existingCusEnt?: FullCustomerEntitlement;
+ resetBalance?: number | null;
}) => {
- let newEntities: Record | null = notNullish(
- entitlement.entity_feature_id
- )
- ? {}
- : null;
+ let newEntities: Record | null = notNullish(
+ entitlement.entity_feature_id,
+ )
+ ? {}
+ : null;
- for (const entity of entities) {
- if (!entitlementLinkedToEntity({ entitlement, entity })) {
- continue;
- }
+ for (const entity of entities) {
+ if (!entitlementLinkedToEntity({ entitlement, entity })) {
+ continue;
+ }
- if (
- existingCusEnt &&
- existingCusEnt.entities &&
- existingCusEnt.entities[entity.id]
- ) {
- continue;
- }
+ if (
+ existingCusEnt &&
+ existingCusEnt.entities &&
+ existingCusEnt.entities[entity.id]
+ ) {
+ continue;
+ }
- if (!newEntities) {
- newEntities = {};
- }
+ if (!newEntities) {
+ newEntities = {};
+ }
- newEntities[entity.id] = {
- id: entity.id,
- balance: resetBalance || 0,
- adjustment: 0,
- };
- }
+ newEntities[entity.id] = {
+ id: entity.id,
+ balance: resetBalance || 0,
+ adjustment: 0,
+ };
+ }
- return newEntities;
+ return newEntities;
};
const initCusEntBalance = ({
- entitlement,
- curCusProduct,
+ entitlement,
+ curCusProduct,
- options,
- relatedPrice,
- // existingCusEnt,
- entities,
- carryExistingUsages = false,
+ options,
+ relatedPrice,
+ // existingCusEnt,
+ entities,
+ carryExistingUsages = false,
}: {
- entitlement: EntitlementWithFeature;
- curCusProduct?: FullCusProduct;
+ entitlement: EntitlementWithFeature;
+ curCusProduct?: FullCusProduct;
- options?: FeatureOptions;
- relatedPrice?: Price;
- // existingCusEnt?: FullCustomerEntitlement;
- entities: Entity[];
- carryExistingUsages?: boolean;
+ options?: FeatureOptions;
+ relatedPrice?: Price;
+ // existingCusEnt?: FullCustomerEntitlement;
+ entities: Entity[];
+ carryExistingUsages?: boolean;
}) => {
- if (entitlement.feature.type === FeatureType.Boolean) {
- return { newBalance: null, newEntities: null };
- }
+ if (entitlement.feature.type === FeatureType.Boolean) {
+ return { newBalance: null, newEntities: null };
+ }
- const resetBalance = getResetBalance({
- entitlement,
- options,
- relatedPrice,
- });
+ const resetBalance = getResetBalance({
+ entitlement,
+ options,
+ relatedPrice,
+ });
- let newEntities: Record | null = initCusEntEntities({
- entitlement,
- entities,
- resetBalance,
- });
+ let newEntities: Record | null = initCusEntEntities({
+ entitlement,
+ entities,
+ resetBalance,
+ });
- return { newBalance: resetBalance, newEntities };
+ return { newBalance: resetBalance, newEntities };
};
// MAIN FUNCTION
export const initCusEntitlement = ({
- entitlement,
- customer,
- cusProductId,
- freeTrial,
- options,
- nextResetAt,
- relatedPrice,
- // existingCusEnt,
- // keepResetIntervals = false,
- trialEndsAt,
- anchorToUnix,
- entities,
- carryExistingUsages = false,
- curCusProduct,
- replaceables,
- now,
- productOptions,
+ entitlement,
+ customer,
+ cusProductId,
+ freeTrial,
+ options,
+ nextResetAt,
+ relatedPrice,
+ // existingCusEnt,
+ // keepResetIntervals = false,
+ trialEndsAt,
+ anchorToUnix,
+ entities,
+ carryExistingUsages = false,
+ curCusProduct,
+ replaceables,
+ now,
+ productOptions,
}: {
- entitlement: EntitlementWithFeature;
- customer: Customer;
- cusProductId: string;
- freeTrial: FreeTrial | null;
- options?: FeatureOptions;
- nextResetAt?: number;
- relatedPrice?: Price;
- // existingCusEnt?: FullCustomerEntitlement;
- // keepResetIntervals?: boolean;
- trialEndsAt?: number;
- anchorToUnix?: number;
- entities: Entity[];
- carryExistingUsages?: boolean;
- curCusProduct?: FullCusProduct;
- replaceables: AttachReplaceable[];
- now?: number;
- productOptions?: ProductOptions;
+ entitlement: EntitlementWithFeature;
+ customer: Customer;
+ cusProductId: string;
+ freeTrial: FreeTrial | null;
+ options?: FeatureOptions;
+ nextResetAt?: number;
+ relatedPrice?: Price;
+ // existingCusEnt?: FullCustomerEntitlement;
+ // keepResetIntervals?: boolean;
+ trialEndsAt?: number;
+ anchorToUnix?: number;
+ entities: Entity[];
+ carryExistingUsages?: boolean;
+ curCusProduct?: FullCusProduct;
+ replaceables: AttachReplaceable[];
+ now?: number;
+ productOptions?: ProductOptions;
}) => {
- now = now || Date.now();
- let { newBalance, newEntities } = initCusEntBalance({
- entitlement,
- options,
- relatedPrice,
- entities,
- carryExistingUsages,
- curCusProduct,
- });
+ now = now || Date.now();
+ let { newBalance, newEntities } = initCusEntBalance({
+ entitlement,
+ options,
+ relatedPrice,
+ entities,
+ carryExistingUsages,
+ curCusProduct,
+ });
- newBalance =
- (newBalance || 0) -
- replaceables.filter((r) => r.ent.id === entitlement.id).length;
+ newBalance =
+ (newBalance || 0) -
+ replaceables.filter((r) => r.ent.id === entitlement.id).length;
- let nextResetAtValue = initNextResetAt({
- entitlement,
- nextResetAt,
- // keepResetIntervals,
- // existingCusEnt,
- trialEndsAt,
- freeTrial,
- anchorToUnix,
- now,
- });
+ let nextResetAtValue = initNextResetAt({
+ entitlement,
+ nextResetAt,
+ // keepResetIntervals,
+ // existingCusEnt,
+ trialEndsAt,
+ freeTrial,
+ anchorToUnix,
+ now,
+ });
- // 3. Define expires at (TODO next time...)
- let isBooleanFeature = entitlement.feature.type === FeatureType.Boolean;
- let usageAllowed = false;
+ // 3. Define expires at (TODO next time...)
+ let isBooleanFeature = entitlement.feature.type === FeatureType.Boolean;
+ let usageAllowed = false;
- if (
- relatedPrice &&
- (getBillingType(relatedPrice.config!) === BillingType.UsageInArrear ||
- getBillingType(relatedPrice.config!) === BillingType.InArrearProrated)
- ) {
- usageAllowed = true;
- }
+ if (
+ relatedPrice &&
+ (getBillingType(relatedPrice.config!) === BillingType.UsageInArrear ||
+ getBillingType(relatedPrice.config!) === BillingType.InArrearProrated)
+ ) {
+ usageAllowed = true;
+ }
- if (notNullish(productOptions?.quantity) && notNullish(newBalance)) {
- newBalance = new Decimal(newBalance!)
- .mul(productOptions?.quantity!)
- .toNumber();
- }
+ if (notNullish(productOptions?.quantity) && notNullish(newBalance)) {
+ newBalance = new Decimal(newBalance!)
+ .mul(productOptions?.quantity!)
+ .toNumber();
+ }
- return {
- id: generateId("cus_ent"),
- internal_customer_id: customer.internal_id,
- internal_feature_id: entitlement.internal_feature_id,
- feature_id: entitlement.feature_id,
- customer_id: customer.id,
+ return {
+ id: generateId("cus_ent"),
+ internal_customer_id: customer.internal_id,
+ internal_feature_id: entitlement.internal_feature_id,
+ feature_id: entitlement.feature_id,
+ customer_id: customer.id,
- // Foreign keys
- entitlement_id: entitlement.id,
- customer_product_id: cusProductId,
- created_at: Date.now(),
+ // Foreign keys
+ entitlement_id: entitlement.id,
+ customer_product_id: cusProductId,
+ created_at: Date.now(),
- // Entitlement fields
- unlimited: isBooleanFeature
- ? null
- : entitlement.allowance_type === AllowanceType.Unlimited,
- balance: newBalance || 0,
- entities: newEntities,
- usage_allowed: usageAllowed,
- next_reset_at: nextResetAtValue,
- };
+ // Entitlement fields
+ unlimited: isBooleanFeature
+ ? null
+ : entitlement.allowance_type === AllowanceType.Unlimited,
+ balance: newBalance || 0,
+ entities: newEntities,
+ usage_allowed: usageAllowed,
+ next_reset_at: nextResetAtValue,
+ };
};
diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts
index d4da5b36e..678c83110 100644
--- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts
+++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts
@@ -3,15 +3,15 @@ import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import {
- getEarliestPeriodEnd,
- getLatestPeriodStart,
+ getEarliestPeriodEnd,
+ getLatestPeriodStart,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { buildInvoiceMemoFromEntitlements } from "@/internal/invoices/invoiceMemoUtils.js";
import {
- freeTrialToStripeTimestamp,
- rewardTrialToStripeTimestamp,
+ freeTrialToStripeTimestamp,
+ rewardTrialToStripeTimestamp,
} from "@/internal/products/free-trials/freeTrialUtils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import RecaseError from "@/utils/errorUtils.js";
@@ -21,160 +21,160 @@ import type { ItemSet } from "@/utils/models/ItemSet.js";
// Get payment method
export const createStripeSub2 = async ({
- db,
- stripeCli,
- attachParams,
- config,
- billingCycleAnchorUnix,
- itemSet,
- logger,
+ db,
+ stripeCli,
+ attachParams,
+ config,
+ billingCycleAnchorUnix,
+ itemSet,
+ logger,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- attachParams: AttachParams;
- config: AttachConfig;
- billingCycleAnchorUnix?: number;
- itemSet: ItemSet;
- logger: any;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ billingCycleAnchorUnix?: number;
+ itemSet: ItemSet;
+ logger: any;
}) => {
- const { customer, invoiceOnly, freeTrial, org, now, rewards, rewardTrial } =
- attachParams;
- // const isDefaultTrial = freeTrial && !freeTrial.card_required;
- // let shouldErrorIfNoPm = !invoiceOnly;
- // if (isDefaultTrial) shouldErrorIfNoPm = false;
- // if (rewardTrial) shouldErrorIfNoPm = false;
+ const { customer, invoiceOnly, freeTrial, org, now, rewards, rewardTrial } =
+ attachParams;
+ // const isDefaultTrial = freeTrial && !freeTrial.card_required;
+ // let shouldErrorIfNoPm = !invoiceOnly;
+ // if (isDefaultTrial) shouldErrorIfNoPm = false;
+ // if (rewardTrial) shouldErrorIfNoPm = false;
- const paymentMethod = await getCusPaymentMethod({
- stripeCli,
- stripeId: customer.processor.id,
- errorIfNone: config.requirePaymentMethod,
- });
+ const paymentMethod = await getCusPaymentMethod({
+ stripeCli,
+ stripeId: customer.processor.id,
+ errorIfNone: config.requirePaymentMethod,
+ });
- let paymentMethodData = {};
- if (paymentMethod) {
- paymentMethodData = {
- default_payment_method: paymentMethod.id,
- };
- }
+ let paymentMethodData = {};
+ if (paymentMethod) {
+ paymentMethodData = {
+ default_payment_method: paymentMethod.id,
+ };
+ }
- const { subItems, invoiceItems, usageFeatures } = itemSet;
+ const { subItems, invoiceItems, usageFeatures } = itemSet;
- const discounts = rewards
- ? rewards.map((reward) => ({ coupon: reward.id }))
- : undefined;
+ const discounts = rewards
+ ? rewards.map((reward) => ({ coupon: reward.id }))
+ : undefined;
- try {
- const subscription = await stripeCli.subscriptions.create({
- ...paymentMethodData,
- customer: customer.processor.id,
- items: sanitizeSubItems(subItems),
+ try {
+ const subscription = await stripeCli.subscriptions.create({
+ ...paymentMethodData,
+ customer: customer.processor.id,
+ items: sanitizeSubItems(subItems),
- billing_mode: { type: "flexible" },
- payment_behavior: "error_if_incomplete",
- add_invoice_items: invoiceItems,
- collection_method: invoiceOnly ? "send_invoice" : "charge_automatically",
- days_until_due: invoiceOnly ? 30 : undefined,
- billing_cycle_anchor: billingCycleAnchorUnix
- ? Math.floor(billingCycleAnchorUnix / 1000)
- : undefined,
+ billing_mode: { type: "flexible" },
+ payment_behavior: "error_if_incomplete",
+ add_invoice_items: invoiceItems,
+ collection_method: invoiceOnly ? "send_invoice" : "charge_automatically",
+ days_until_due: invoiceOnly ? 30 : undefined,
+ billing_cycle_anchor: billingCycleAnchorUnix
+ ? Math.floor(billingCycleAnchorUnix / 1000)
+ : undefined,
- discounts,
- expand: ["latest_invoice"],
+ discounts,
+ expand: ["latest_invoice"],
- ...{
- trial_settings:
- freeTrial && !freeTrial.card_required
- ? {
- end_behavior: {
- missing_payment_method: "cancel",
- },
- }
- : undefined,
+ ...{
+ trial_settings:
+ freeTrial && !freeTrial.card_required
+ ? {
+ end_behavior: {
+ missing_payment_method: "cancel",
+ },
+ }
+ : undefined,
- trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
- },
+ trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
+ },
- // ...{
- // trial_settings: rewardTrial
- // ? {
- // end_behavior: {
- // missing_payment_method: "cancel",
- // },
- // }
- // : undefined,
+ // ...{
+ // trial_settings: rewardTrial
+ // ? {
+ // end_behavior: {
+ // missing_payment_method: "cancel",
+ // },
+ // }
+ // : undefined,
- // trial_end: rewardTrialToStripeTimestamp({ rewardTrial, now }),
- // },
- });
+ // trial_end: rewardTrialToStripeTimestamp({ rewardTrial, now }),
+ // },
+ });
- const latestInvoice = subscription.latest_invoice as Stripe.Invoice;
+ const latestInvoice = subscription.latest_invoice as Stripe.Invoice;
- if (
- invoiceOnly &&
- org.config.invoice_memos &&
- latestInvoice &&
- latestInvoice.status === "draft"
- ) {
- try {
- const desc = await buildInvoiceMemoFromEntitlements({
- org,
- entitlements: attachParams.entitlements,
- features: attachParams.features,
- prices: attachParams.prices,
- logger,
- });
- await stripeCli.invoices.update(latestInvoice.id!, {
- description: desc,
- });
- } catch (error) {
- logger.error("CREATE STRIPE SUB: error adding invoice memo", { error });
- }
- }
+ if (
+ invoiceOnly &&
+ org.config.invoice_memos &&
+ latestInvoice &&
+ latestInvoice.status === "draft"
+ ) {
+ try {
+ const desc = await buildInvoiceMemoFromEntitlements({
+ org,
+ entitlements: attachParams.entitlements,
+ features: attachParams.features,
+ prices: attachParams.prices,
+ logger,
+ });
+ await stripeCli.invoices.update(latestInvoice.id!, {
+ description: desc,
+ });
+ } catch (error) {
+ logger.error("CREATE STRIPE SUB: error adding invoice memo", { error });
+ }
+ }
- if (
- invoiceOnly &&
- config.invoiceCheckout &&
- config.finalizeInvoice &&
- latestInvoice &&
- latestInvoice.status === "draft"
- ) {
- subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice(
- (subscription.latest_invoice as Stripe.Invoice).id!
- );
- }
+ if (
+ invoiceOnly &&
+ config.invoiceCheckout &&
+ config.finalizeInvoice &&
+ latestInvoice &&
+ latestInvoice.status === "draft"
+ ) {
+ subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice(
+ (subscription.latest_invoice as Stripe.Invoice).id!,
+ );
+ }
- // Store
- const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
- const currentPeriodStart = getLatestPeriodStart({ sub: subscription });
+ // Store
+ const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription });
+ const currentPeriodStart = getLatestPeriodStart({ sub: subscription });
- await SubService.createSub({
- db,
- sub: {
- id: generateId("sub"),
- stripe_id: subscription.id,
- stripe_schedule_id: subscription.schedule as string,
- created_at: subscription.created * 1000,
- usage_features: usageFeatures,
- org_id: org.id,
- env: customer.env,
- current_period_start: currentPeriodStart,
- current_period_end: earliestPeriodEnd,
- },
- });
+ await SubService.createSub({
+ db,
+ sub: {
+ id: generateId("sub"),
+ stripe_id: subscription.id,
+ stripe_schedule_id: subscription.schedule as string,
+ created_at: subscription.created * 1000,
+ usage_features: usageFeatures,
+ org_id: org.id,
+ env: customer.env,
+ current_period_start: currentPeriodStart,
+ current_period_end: earliestPeriodEnd,
+ },
+ });
- return subscription;
- } catch (error: any) {
- console.log("Warning: Failed to create stripe subscription");
- console.log("Error code:", error.code);
- console.log("Message:", error.message);
- console.log("Decline code:", error.decline_code);
+ return subscription;
+ } catch (error: any) {
+ console.log("Warning: Failed to create stripe subscription");
+ console.log("Error code:", error.code);
+ console.log("Message:", error.message);
+ console.log("Decline code:", error.decline_code);
- throw new RecaseError({
- code: ErrCode.CreateStripeSubscriptionFailed,
- message: `Create stripe subscription failed ${
- error.code ? `(${error.code})` : ""
- }: ${error.message || ""}`,
- statusCode: 500,
- });
- }
+ throw new RecaseError({
+ code: ErrCode.CreateStripeSubscriptionFailed,
+ message: `Create stripe subscription failed ${
+ error.code ? `(${error.code})` : ""
+ }: ${error.message || ""}`,
+ statusCode: 500,
+ });
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.ts
index d4bc00129..54f0cf5a3 100644
--- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.ts
+++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.ts
@@ -5,34 +5,34 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachConfig, FullProduct, Product, products } from "@autumn/shared";
export const getMergeCusProduct = async ({
- attachParams,
- products,
- config,
+ attachParams,
+ products,
+ config,
}: {
- attachParams: AttachParams;
- products: FullProduct[];
- config: AttachConfig;
+ attachParams: AttachParams;
+ products: FullProduct[];
+ config: AttachConfig;
}) => {
- const { stripeCli, cusProducts, freeTrial } = attachParams;
+ const { stripeCli, cusProducts, freeTrial } = attachParams;
- let mergeCusProduct = undefined;
- if (!config.disableMerge && !freeTrial) {
- mergeCusProduct = cusProducts?.find((cp) =>
- products.some((p) => p.group == cp.product.group)
- );
- }
+ let mergeCusProduct = undefined;
+ if (!config.disableMerge && !freeTrial) {
+ mergeCusProduct = cusProducts?.find((cp) =>
+ products.some((p) => p.group == cp.product.group),
+ );
+ }
- const mergeSub = await cusProductToSub({
- cusProduct: mergeCusProduct,
- stripeCli,
- });
- // let mergeSubs = await getStripeSubs({
- // stripeCli,
- // subIds: mergeCusProduct?.subscription_ids,
- // });
+ const mergeSub = await cusProductToSub({
+ cusProduct: mergeCusProduct,
+ stripeCli,
+ });
+ // let mergeSubs = await getStripeSubs({
+ // stripeCli,
+ // subIds: mergeCusProduct?.subscription_ids,
+ // });
- return {
- mergeCusProduct,
- mergeSub: mergeSub || undefined,
- };
+ return {
+ mergeCusProduct,
+ mergeSub: mergeSub || undefined,
+ };
};
diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts
index b0063a390..77b721a74 100644
--- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts
+++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts
@@ -1,16 +1,16 @@
import {
- APIVersion,
- AttachBranch,
- type AttachConfig,
- SuccessCode,
+ APIVersion,
+ AttachBranch,
+ type AttachConfig,
+ SuccessCode,
} from "@autumn/shared";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { createFullCusProduct } from "../../../add-product/createFullCusProduct.js";
import {
- type AttachParams,
- AttachResultSchema,
+ type AttachParams,
+ AttachResultSchema,
} from "../../../cusProducts/AttachParams.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { getDefaultAttachConfig } from "../../attachUtils/getAttachConfig.js";
diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts
index 89ffaa414..0c16258e9 100644
--- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts
+++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.ts
@@ -13,220 +13,220 @@ import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/pric
import { AttachConfig } from "@autumn/shared";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import {
- attachToInvoiceResponse,
- insertInvoiceFromAttach,
+ attachToInvoiceResponse,
+ insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { Decimal } from "decimal.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { buildInvoiceMemoFromEntitlements } from "@/internal/invoices/invoiceMemoUtils.js";
export const handleOneOffFunction = async ({
- req,
- attachParams,
- config,
- res,
+ req,
+ attachParams,
+ config,
+ res,
}: {
- req: any;
- attachParams: AttachParams;
- config: AttachConfig;
- res: any;
+ req: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ res: any;
}) => {
- const logger = req.logtail;
- logger.info("Scenario 4A: One-off prices");
+ const logger = req.logtail;
+ logger.info("Scenario 4A: One-off prices");
- const {
- stripeCli,
- paymentMethod,
- org,
- customer,
- products,
- prices,
- entitlements,
- optionsList,
- rewards,
- } = attachParams;
+ const {
+ stripeCli,
+ paymentMethod,
+ org,
+ customer,
+ products,
+ prices,
+ entitlements,
+ optionsList,
+ rewards,
+ } = attachParams;
- const { invoiceOnly } = config;
+ const { invoiceOnly } = config;
- let invoiceItems = [];
+ let invoiceItems = [];
- for (const price of prices) {
- const options = getPriceOptions(price, optionsList);
- let quantity = options?.quantity;
+ for (const price of prices) {
+ const options = getPriceOptions(price, optionsList);
+ let quantity = options?.quantity;
- if (quantity) {
- let config = price.config as UsagePriceConfig;
- quantity = new Decimal(quantity)
- .mul(config.billing_units || 1)
- .toNumber();
- }
+ if (quantity) {
+ let config = price.config as UsagePriceConfig;
+ quantity = new Decimal(quantity)
+ .mul(config.billing_units || 1)
+ .toNumber();
+ }
- let invoiceItemData = {};
- if (isFixedPrice({ price })) {
- quantity = 1;
+ let invoiceItemData = {};
+ if (isFixedPrice({ price })) {
+ quantity = 1;
- invoiceItemData = {
- pricing: {
- price: price.config.stripe_price_id,
- },
- quantity: 1,
- };
- } else {
- const amount = priceToInvoiceAmount({
- price,
- quantity,
- });
+ invoiceItemData = {
+ pricing: {
+ price: price.config.stripe_price_id,
+ },
+ quantity: 1,
+ };
+ } else {
+ const amount = priceToInvoiceAmount({
+ price,
+ quantity,
+ });
- const product = priceToProduct({
- price,
- products,
- });
+ const product = priceToProduct({
+ price,
+ products,
+ });
- const description = newPriceToInvoiceDescription({
- org,
- price,
- product: product!,
- ents: entitlements,
- quantity: options?.quantity,
- withProductPrefix: true,
- });
+ const description = newPriceToInvoiceDescription({
+ org,
+ price,
+ product: product!,
+ ents: entitlements,
+ quantity: options?.quantity,
+ withProductPrefix: true,
+ });
- invoiceItemData = {
- description,
- price_data: {
- unit_amount: new Decimal(amount).mul(100).round().toNumber(),
- currency: org.default_currency,
- product: price.config?.stripe_product_id || product?.processor?.id!,
- },
- };
- }
+ invoiceItemData = {
+ description,
+ price_data: {
+ unit_amount: new Decimal(amount).mul(100).round().toNumber(),
+ currency: org.default_currency,
+ product: price.config?.stripe_product_id || product?.processor?.id!,
+ },
+ };
+ }
- invoiceItems.push({
- ...invoiceItemData,
- quantity: 1,
- });
- }
+ invoiceItems.push({
+ ...invoiceItemData,
+ quantity: 1,
+ });
+ }
- let shouldMemo = false;
- let invoiceMemo = "";
- try {
- shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
- invoiceMemo = shouldMemo
- ? await buildInvoiceMemoFromEntitlements({
- org: attachParams.org,
- entitlements: attachParams.entitlements,
- features: attachParams.features,
- prices: attachParams.prices,
- logger,
- })
- : "";
- } catch (error) {
- logger.error("ONE OFF FUNCTION: error adding invoice memo", {
- error,
- });
- }
+ let shouldMemo = false;
+ let invoiceMemo = "";
+ try {
+ shouldMemo = attachParams.org.config.invoice_memos && invoiceOnly;
+ invoiceMemo = shouldMemo
+ ? await buildInvoiceMemoFromEntitlements({
+ org: attachParams.org,
+ entitlements: attachParams.entitlements,
+ features: attachParams.features,
+ prices: attachParams.prices,
+ logger,
+ })
+ : "";
+ } catch (error) {
+ logger.error("ONE OFF FUNCTION: error adding invoice memo", {
+ error,
+ });
+ }
- // Create invoice
- logger.info("1. Creating invoice");
- let stripeInvoice = await stripeCli.invoices.create({
- customer: customer.processor.id!,
- auto_advance: false,
- currency: org.default_currency!,
- discounts: rewards ? rewards.map((r) => ({ coupon: r.id })) : undefined,
- collection_method: attachParams.invoiceOnly ? "send_invoice" : undefined,
- days_until_due: attachParams.invoiceOnly ? 30 : undefined,
- ...(shouldMemo ? { description: invoiceMemo } : {}),
- });
+ // Create invoice
+ logger.info("1. Creating invoice");
+ let stripeInvoice = await stripeCli.invoices.create({
+ customer: customer.processor.id!,
+ auto_advance: false,
+ currency: org.default_currency!,
+ discounts: rewards ? rewards.map((r) => ({ coupon: r.id })) : undefined,
+ collection_method: attachParams.invoiceOnly ? "send_invoice" : undefined,
+ days_until_due: attachParams.invoiceOnly ? 30 : undefined,
+ ...(shouldMemo ? { description: invoiceMemo } : {}),
+ });
- logger.info("2. Creating invoice items");
- for (const invoiceItem of invoiceItems) {
- await stripeCli.invoiceItems.create({
- ...invoiceItem,
- customer: customer.processor.id!,
- invoice: stripeInvoice.id,
- } as any);
- }
+ logger.info("2. Creating invoice items");
+ for (const invoiceItem of invoiceItems) {
+ await stripeCli.invoiceItems.create({
+ ...invoiceItem,
+ customer: customer.processor.id!,
+ invoice: stripeInvoice.id,
+ } as any);
+ }
- if (config.invoiceCheckout) {
- if (stripeInvoice.status === "draft" && config.finalizeInvoice) {
- stripeInvoice = await stripeCli.invoices.finalizeInvoice(
- stripeInvoice.id!
- );
- }
+ if (config.invoiceCheckout) {
+ if (stripeInvoice.status === "draft" && config.finalizeInvoice) {
+ stripeInvoice = await stripeCli.invoices.finalizeInvoice(
+ stripeInvoice.id!,
+ );
+ }
- await insertInvoiceFromAttach({
- db: req.db,
- attachParams,
- invoiceId: stripeInvoice.id,
- logger,
- });
+ await insertInvoiceFromAttach({
+ db: req.db,
+ attachParams,
+ invoiceId: stripeInvoice.id,
+ logger,
+ });
- return { invoices: [stripeInvoice], subs: [], anchorToUnix: undefined };
- }
+ return { invoices: [stripeInvoice], subs: [], anchorToUnix: undefined };
+ }
- // Create invoice items
- if (!invoiceOnly) {
- await stripeCli.invoices.finalizeInvoice(stripeInvoice.id!);
+ // Create invoice items
+ if (!invoiceOnly) {
+ await stripeCli.invoices.finalizeInvoice(stripeInvoice.id!);
- logger.info("3. Paying invoice");
- const { paid, error } = await payForInvoice({
- stripeCli,
- invoiceId: stripeInvoice.id!,
- paymentMethod,
- logger,
- errorOnFail: false,
- voidIfFailed: true,
- });
+ logger.info("3. Paying invoice");
+ const { paid, error } = await payForInvoice({
+ stripeCli,
+ invoiceId: stripeInvoice.id!,
+ paymentMethod,
+ logger,
+ errorOnFail: false,
+ voidIfFailed: true,
+ });
- if (!paid) {
- if (org.config.checkout_on_failed_payment) {
- return await handleCreateCheckout({
- req,
- res,
- attachParams,
- config,
- });
- }
- throw error;
- }
- }
+ if (!paid) {
+ if (org.config.checkout_on_failed_payment) {
+ return await handleCreateCheckout({
+ req,
+ res,
+ attachParams,
+ config,
+ });
+ }
+ throw error;
+ }
+ }
- logger.info("4. Creating full customer product");
- const batchInsert = [];
- for (const product of products) {
- batchInsert.push(
- createFullCusProduct({
- db: req.db,
- attachParams: attachToInsertParams(attachParams, product),
- logger,
- })
- );
- }
- await Promise.all(batchInsert);
+ logger.info("4. Creating full customer product");
+ const batchInsert = [];
+ for (const product of products) {
+ batchInsert.push(
+ createFullCusProduct({
+ db: req.db,
+ attachParams: attachToInsertParams(attachParams, product),
+ logger,
+ }),
+ );
+ }
+ await Promise.all(batchInsert);
- logger.info("5. Creating invoice from stripe");
- await insertInvoiceFromAttach({
- db: req.db,
- attachParams,
- invoiceId: stripeInvoice.id,
- logger,
- });
+ logger.info("5. Creating invoice from stripe");
+ await insertInvoiceFromAttach({
+ db: req.db,
+ attachParams,
+ invoiceId: stripeInvoice.id,
+ logger,
+ });
- if (res) {
- const productNames = products.map((p) => p.name).join(", ");
- const customerName = customer.name || customer.email || customer.id;
- res.status(200).json(
- AttachResultSchema.parse({
- success: true,
- message: `Successfully purchased ${productNames} and attached to ${customerName}`,
- invoice: invoiceOnly
- ? attachToInvoiceResponse({ invoice: stripeInvoice })
- : undefined,
- code: SuccessCode.OneOffProductAttached,
- product_ids: products.map((p) => p.id),
- customer_id: customer.id || customer.internal_id,
- scenario: AttachScenario.New,
- })
- );
- }
+ if (res) {
+ const productNames = products.map((p) => p.name).join(", ");
+ const customerName = customer.name || customer.email || customer.id;
+ res.status(200).json(
+ AttachResultSchema.parse({
+ success: true,
+ message: `Successfully purchased ${productNames} and attached to ${customerName}`,
+ invoice: invoiceOnly
+ ? attachToInvoiceResponse({ invoice: stripeInvoice })
+ : undefined,
+ code: SuccessCode.OneOffProductAttached,
+ product_ids: products.map((p) => p.id),
+ customer_id: customer.id || customer.internal_id,
+ scenario: AttachScenario.New,
+ }),
+ );
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts
index 6c2b0fa1f..342af726d 100644
--- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts
+++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts
@@ -1,10 +1,10 @@
import {
- APIVersion,
- type AttachConfig,
- AttachScenario,
- ErrCode,
- isTrialing,
- SuccessCode,
+ APIVersion,
+ type AttachConfig,
+ AttachScenario,
+ ErrCode,
+ isTrialing,
+ SuccessCode,
} from "@autumn/shared";
import type Stripe from "stripe";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
@@ -12,12 +12,12 @@ import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSu
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
import {
- type AttachParams,
- AttachResultSchema,
+ type AttachParams,
+ AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
- attachToInvoiceResponse,
- insertInvoiceFromAttach,
+ attachToInvoiceResponse,
+ insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
@@ -25,9 +25,9 @@ import { createStripeSub2 } from "./createStripeSub2.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import {
- getCustomerSchedule,
- getCustomerSub,
- paramsToCurSubSchedule,
+ getCustomerSchedule,
+ getCustomerSub,
+ paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js";
@@ -40,248 +40,248 @@ import { rewardTrialToStripeTimestamp } from "@/internal/products/free-trials/fr
import RecaseError from "@/utils/errorUtils.js";
export const handlePaidProduct = async ({
- req,
- res,
- attachParams,
- config,
+ req,
+ res,
+ attachParams,
+ config,
}: {
- req: ExtendedRequest;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
+ req: ExtendedRequest;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- const logger = req.logtail;
+ const logger = req.logtail;
- const {
- org,
- customer,
- products,
+ const {
+ org,
+ customer,
+ products,
- invoiceOnly,
+ invoiceOnly,
- stripeCli,
- } = attachParams;
+ stripeCli,
+ } = attachParams;
- if (config.disableTrial) {
- attachParams.freeTrial = null;
- }
+ if (config.disableTrial) {
+ attachParams.freeTrial = null;
+ }
- const itemSet = await getStripeSubItems2({
- attachParams,
- config,
- });
+ const itemSet = await getStripeSubItems2({
+ attachParams,
+ config,
+ });
- const subscriptions: Stripe.Subscription[] = [];
+ const subscriptions: Stripe.Subscription[] = [];
- const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
- attachParams,
- });
+ const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
+ attachParams,
+ });
- let sub: Stripe.Subscription | null = null;
- let schedule: Stripe.SubscriptionSchedule | null | undefined = null;
- let invoice: Stripe.Invoice | undefined;
- let trialEndsAt = undefined;
+ let sub: Stripe.Subscription | null = null;
+ let schedule: Stripe.SubscriptionSchedule | null | undefined = null;
+ let invoice: Stripe.Invoice | undefined;
+ let trialEndsAt = undefined;
- // 1. If merge sub
+ // 1. If merge sub
- if (mergeSub && !config.disableMerge) {
- if (mergeCusProduct?.free_trial) {
- trialEndsAt = isTrialing({
- cusProduct: mergeCusProduct,
- now: attachParams.now,
- })
- ? mergeCusProduct.trial_ends_at
- : undefined;
- }
- attachParams.freeTrial = null;
- // 1. If merged sub is canceled, also add to current schedule
- const newItemSet = await paramsToSubItems({
- req,
- sub: mergeSub,
- attachParams,
- config,
- });
+ if (mergeSub && !config.disableMerge) {
+ if (mergeCusProduct?.free_trial) {
+ trialEndsAt = isTrialing({
+ cusProduct: mergeCusProduct,
+ now: attachParams.now,
+ })
+ ? mergeCusProduct.trial_ends_at
+ : undefined;
+ }
+ attachParams.freeTrial = null;
+ // 1. If merged sub is canceled, also add to current schedule
+ const newItemSet = await paramsToSubItems({
+ req,
+ sub: mergeSub,
+ attachParams,
+ config,
+ });
- const { updatedSub, latestInvoice } = await updateStripeSub2({
- req,
- attachParams,
- curSub: mergeSub,
- itemSet: newItemSet,
- config,
- fromCreate: true,
- });
+ const { updatedSub, latestInvoice } = await updateStripeSub2({
+ req,
+ attachParams,
+ curSub: mergeSub,
+ itemSet: newItemSet,
+ config,
+ fromCreate: true,
+ });
- sub = updatedSub;
+ sub = updatedSub;
- if (latestInvoice) {
- invoice = await insertInvoiceFromAttach({
- db: req.db,
- stripeInvoice: latestInvoice,
- attachParams,
- logger,
- });
- }
- if (subIsCanceled({ sub: mergeSub })) {
- logger.info("ADD PRODUCT FLOW, CREATING NEW SCHEDULE");
- schedule = await subToNewSchedule({
- req,
- sub: mergeSub,
- attachParams,
- config,
- endOfBillingPeriod: mergeSub.cancel_at!,
- removeCusProducts: attachParams.cusProducts.filter((cp) => cp.canceled),
- });
- } else {
- const res = await getCustomerSchedule({
- attachParams,
- subId: mergeSub.id,
- logger,
- });
- schedule = res.schedule;
- logger.info(`ADD PRODUCT FLOW, SCHEDULE ID: ${schedule?.id}`);
- if (schedule) {
- await handleUpgradeFlowSchedule({
- req,
- logger,
- attachParams,
- config,
- schedule,
- curSub: mergeSub,
- removeCusProducts: [],
- fromAddProduct: true,
- });
- }
- }
- } else {
- let billingCycleAnchorUnix = undefined;
- const smallestInterval = getSmallestInterval({
- prices: attachParams.prices,
- });
+ if (latestInvoice) {
+ invoice = await insertInvoiceFromAttach({
+ db: req.db,
+ stripeInvoice: latestInvoice,
+ attachParams,
+ logger,
+ });
+ }
+ if (subIsCanceled({ sub: mergeSub })) {
+ logger.info("ADD PRODUCT FLOW, CREATING NEW SCHEDULE");
+ schedule = await subToNewSchedule({
+ req,
+ sub: mergeSub,
+ attachParams,
+ config,
+ endOfBillingPeriod: mergeSub.cancel_at!,
+ removeCusProducts: attachParams.cusProducts.filter((cp) => cp.canceled),
+ });
+ } else {
+ const res = await getCustomerSchedule({
+ attachParams,
+ subId: mergeSub.id,
+ logger,
+ });
+ schedule = res.schedule;
+ logger.info(`ADD PRODUCT FLOW, SCHEDULE ID: ${schedule?.id}`);
+ if (schedule) {
+ await handleUpgradeFlowSchedule({
+ req,
+ logger,
+ attachParams,
+ config,
+ schedule,
+ curSub: mergeSub,
+ removeCusProducts: [],
+ fromAddProduct: true,
+ });
+ }
+ }
+ } else {
+ let billingCycleAnchorUnix = undefined;
+ const smallestInterval = getSmallestInterval({
+ prices: attachParams.prices,
+ });
- // 1. If anchor to start of month, get next month anchor
- if (org.config.anchor_start_of_month) {
- billingCycleAnchorUnix = getNextStartOfMonthUnix({
- interval: smallestInterval!.interval,
- intervalCount: smallestInterval!.intervalCount,
- });
- }
+ // 1. If anchor to start of month, get next month anchor
+ if (org.config.anchor_start_of_month) {
+ billingCycleAnchorUnix = getNextStartOfMonthUnix({
+ interval: smallestInterval!.interval,
+ intervalCount: smallestInterval!.intervalCount,
+ });
+ }
- // 2. If merge sub anchor, use it
- if (mergeSub && !config.disableMerge) {
- billingCycleAnchorUnix = addIntervalToAnchor({
- anchorUnix: mergeSub.billing_cycle_anchor * 1000,
- intervalConfig: smallestInterval!,
- now: attachParams.now,
- });
- }
+ // 2. If merge sub anchor, use it
+ if (mergeSub && !config.disableMerge) {
+ billingCycleAnchorUnix = addIntervalToAnchor({
+ anchorUnix: mergeSub.billing_cycle_anchor * 1000,
+ intervalConfig: smallestInterval!,
+ now: attachParams.now,
+ });
+ }
- // 3. If billing cycle anchor, just use it
- if (attachParams.billingAnchor) {
- billingCycleAnchorUnix = attachParams.billingAnchor;
- }
+ // 3. If billing cycle anchor, just use it
+ if (attachParams.billingAnchor) {
+ billingCycleAnchorUnix = attachParams.billingAnchor;
+ }
- // console.log("Item set: ", itemSet);
- try {
- sub = await createStripeSub2({
- db: req.db,
- stripeCli,
- attachParams,
- itemSet,
- billingCycleAnchorUnix,
- config,
- logger,
- });
+ // console.log("Item set: ", itemSet);
+ try {
+ sub = await createStripeSub2({
+ db: req.db,
+ stripeCli,
+ attachParams,
+ itemSet,
+ billingCycleAnchorUnix,
+ config,
+ logger,
+ });
- if (sub?.latest_invoice) {
- invoice = await insertInvoiceFromAttach({
- db: req.db,
- stripeInvoice: sub.latest_invoice as Stripe.Invoice,
- attachParams,
- logger,
- });
- }
- } catch (error: any) {
- if (
- error instanceof RecaseError &&
- !invoiceOnly &&
- error.code == ErrCode.CreateStripeSubscriptionFailed
- ) {
- return await handleCreateCheckout({
- req,
- res,
- attachParams,
- config,
- });
- }
+ if (sub?.latest_invoice) {
+ invoice = await insertInvoiceFromAttach({
+ db: req.db,
+ stripeInvoice: sub.latest_invoice as Stripe.Invoice,
+ attachParams,
+ logger,
+ });
+ }
+ } catch (error: any) {
+ if (
+ error instanceof RecaseError &&
+ !invoiceOnly &&
+ error.code == ErrCode.CreateStripeSubscriptionFailed
+ ) {
+ return await handleCreateCheckout({
+ req,
+ res,
+ attachParams,
+ config,
+ });
+ }
- throw error;
- }
- }
+ throw error;
+ }
+ }
- subscriptions.push(sub);
+ subscriptions.push(sub);
- const anchorToUnix = getEarliestPeriodEnd({ sub }) * 1000;
+ const anchorToUnix = getEarliestPeriodEnd({ sub }) * 1000;
- if (config.invoiceCheckout) {
- return {
- invoices: subscriptions.map((s) => s.latest_invoice as Stripe.Invoice),
- subs: subscriptions,
- anchorToUnix,
- config,
- };
- }
+ if (config.invoiceCheckout) {
+ return {
+ invoices: subscriptions.map((s) => s.latest_invoice as Stripe.Invoice),
+ subs: subscriptions,
+ anchorToUnix,
+ config,
+ };
+ }
- // Add product and entitlements to customer
- const batchInsert = [];
+ // Add product and entitlements to customer
+ const batchInsert = [];
- for (const product of products) {
- batchInsert.push(
- createFullCusProduct({
- db: req.db,
- attachParams: attachToInsertParams(attachParams, product),
- subscriptionIds: subscriptions.map((s) => s.id),
- subscriptionScheduleIds: schedule ? [schedule.id] : undefined,
- anchorToUnix,
- carryExistingUsages: config.carryUsage,
- scenario: AttachScenario.New,
- trialEndsAt:
- trialEndsAt ||
- (attachParams.rewardTrial
- ? (rewardTrialToStripeTimestamp({
- rewardTrial: attachParams.rewardTrial,
- now: attachParams.now,
- }) || 0) * 1000
- : undefined),
- logger,
- })
- );
- }
- await Promise.all(batchInsert);
+ for (const product of products) {
+ batchInsert.push(
+ createFullCusProduct({
+ db: req.db,
+ attachParams: attachToInsertParams(attachParams, product),
+ subscriptionIds: subscriptions.map((s) => s.id),
+ subscriptionScheduleIds: schedule ? [schedule.id] : undefined,
+ anchorToUnix,
+ carryExistingUsages: config.carryUsage,
+ scenario: AttachScenario.New,
+ trialEndsAt:
+ trialEndsAt ||
+ (attachParams.rewardTrial
+ ? (rewardTrialToStripeTimestamp({
+ rewardTrial: attachParams.rewardTrial,
+ now: attachParams.now,
+ }) || 0) * 1000
+ : undefined),
+ logger,
+ }),
+ );
+ }
+ await Promise.all(batchInsert);
- if (res) {
- const apiVersion = attachParams.apiVersion || APIVersion.v1;
- const productNames = products.map((p) => p.name).join(", ");
- const customerName = customer.name || customer.email || customer.id;
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json(
- AttachResultSchema.parse({
- message: `Successfully created subscriptions and attached ${productNames} to ${customerName}`,
- code: SuccessCode.NewProductAttached,
- product_ids: products.map((p) => p.id),
- customer_id: customer.id || customer.internal_id,
- invoice: invoiceOnly
- ? attachToInvoiceResponse({ invoice })
- : undefined,
- })
- );
- } else {
- res.status(200).json({
- success: true,
- message: `Successfully created subscriptions and attached ${products
- .map((p) => p.name)
- .join(", ")} to ${customer.name}`,
- invoice: invoiceOnly ? invoice : undefined,
- });
- }
- }
+ if (res) {
+ const apiVersion = attachParams.apiVersion || APIVersion.v1;
+ const productNames = products.map((p) => p.name).join(", ");
+ const customerName = customer.name || customer.email || customer.id;
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ message: `Successfully created subscriptions and attached ${productNames} to ${customerName}`,
+ code: SuccessCode.NewProductAttached,
+ product_ids: products.map((p) => p.id),
+ customer_id: customer.id || customer.internal_id,
+ invoice: invoiceOnly
+ ? attachToInvoiceResponse({ invoice })
+ : undefined,
+ }),
+ );
+ } else {
+ res.status(200).json({
+ success: true,
+ message: `Successfully created subscriptions and attached ${products
+ .map((p) => p.name)
+ .join(", ")} to ${customer.name}`,
+ invoice: invoiceOnly ? invoice : undefined,
+ });
+ }
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts
index fafa0eec3..0cb34adf5 100644
--- a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts
+++ b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts
@@ -1,19 +1,19 @@
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
- attachParamsToCurCusProduct,
- attachParamToCusProducts,
- getSubForAttach,
- paramsToCurSub,
+ attachParamsToCurCusProduct,
+ attachParamToCusProducts,
+ getSubForAttach,
+ paramsToCurSub,
} from "../attachUtils/convertAttachParams.js";
import { paramsToScheduleItems } from "../mergeUtils/paramsToScheduleItems.js";
import { AttachConfig, AttachScenario, SuccessCode } from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- cusProductToSchedule,
- cusProductToSub,
+ cusProductToSchedule,
+ cusProductToSub,
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { subToNewSchedule } from "../mergeUtils/subToNewSchedule.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
@@ -23,217 +23,217 @@ import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/han
import { addSubIdToCache } from "../../cusCache/subCacheUtils.js";
export const handleRenewProduct = async ({
- req,
- res,
- attachParams,
- config,
+ req,
+ res,
+ attachParams,
+ config,
}: {
- req: any;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
+ req: any;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- const logger = req.logtail;
- const { stripeCli, customer: fullCus } = attachParams;
- const { curScheduledProduct } = attachParamToCusProducts({ attachParams });
+ const logger = req.logtail;
+ const { stripeCli, customer: fullCus } = attachParams;
+ const { curScheduledProduct } = attachParamToCusProducts({ attachParams });
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const product = attachParams.products[0];
- const cusProducts = attachParams.customer.customer_products;
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const product = attachParams.products[0];
+ const cusProducts = attachParams.customer.customer_products;
- const schedule = await cusProductToSchedule({
- cusProduct: curCusProduct!,
- stripeCli,
- });
+ const schedule = await cusProductToSchedule({
+ cusProduct: curCusProduct!,
+ stripeCli,
+ });
- // If not add on
- if (product.is_add_on) {
- res.status(200).json(
- AttachResultSchema.parse({
- code: SuccessCode.RenewedProduct,
- message: `Successfully renewed product ${product.name}`,
- product_ids: [product.id],
- })
- );
- return;
- }
+ // If not add on
+ if (product.is_add_on) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ code: SuccessCode.RenewedProduct,
+ message: `Successfully renewed product ${product.name}`,
+ product_ids: [product.id],
+ }),
+ );
+ return;
+ }
- const curSubId = curCusProduct?.subscription_ids?.[0];
- const otherCanceled = cusProducts.some(
- (cp) =>
- cp.subscription_ids?.includes(curSubId!) &&
- cp.canceled &&
- cp.id !== curCusProduct?.id
- );
+ const curSubId = curCusProduct?.subscription_ids?.[0];
+ const otherCanceled = cusProducts.some(
+ (cp) =>
+ cp.subscription_ids?.includes(curSubId!) &&
+ cp.canceled &&
+ cp.id !== curCusProduct?.id,
+ );
- let expectedEnd = undefined;
- if (curSubId) {
- const curSub = await getSubForAttach({
- stripeCli,
- subId: curSubId,
- });
- const subItems = curSub?.items.data.filter((item) =>
- subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item })
- );
- expectedEnd = getLatestPeriodEnd({ subItems });
- }
+ let expectedEnd = undefined;
+ if (curSubId) {
+ const curSub = await getSubForAttach({
+ stripeCli,
+ subId: curSubId,
+ });
+ const subItems = curSub?.items.data.filter((item) =>
+ subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item }),
+ );
+ expectedEnd = getLatestPeriodEnd({ subItems });
+ }
- if (!otherCanceled) {
- if (schedule) {
- logger.info(`RENEW FLOW: releasing schedule ${schedule.id}`);
- await stripeCli.subscriptionSchedules.release(schedule.id);
+ if (!otherCanceled) {
+ if (schedule) {
+ logger.info(`RENEW FLOW: releasing schedule ${schedule.id}`);
+ await stripeCli.subscriptionSchedules.release(schedule.id);
- await CusProductService.updateByStripeScheduledId({
- db: req.db,
- stripeScheduledId: schedule.id,
- updates: {
- scheduled_ids: [],
- },
- });
- }
+ await CusProductService.updateByStripeScheduledId({
+ db: req.db,
+ stripeScheduledId: schedule.id,
+ updates: {
+ scheduled_ids: [],
+ },
+ });
+ }
- if (curSubId) {
- // Add sub id to upstash
- await addSubIdToCache({
- subId: curSubId,
- scenario: AttachScenario.Renew,
- });
- await stripeCli.subscriptions.update(curSubId, {
- cancel_at: null,
- });
- }
+ if (curSubId) {
+ // Add sub id to upstash
+ await addSubIdToCache({
+ subId: curSubId,
+ scenario: AttachScenario.Renew,
+ });
+ await stripeCli.subscriptions.update(curSubId, {
+ cancel_at: null,
+ });
+ }
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- canceled: false,
- canceled_at: null,
- },
- });
- } else {
- const scheduledProduct = curScheduledProduct;
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ canceled: false,
+ canceled_at: null,
+ },
+ });
+ } else {
+ const scheduledProduct = curScheduledProduct;
- // Case 1: Add current cus product back to schedule and remove scheduled product from schedule
- if (schedule) {
- logger.info(
- `RENEW FLOW: adding cur cus product back to schedule ${schedule.id}`
- );
- const newItems = await paramsToScheduleItems({
- req,
- attachParams,
- config,
- schedule,
- removeCusProducts: scheduledProduct ? [scheduledProduct] : [],
- billingPeriodEnd: expectedEnd,
- });
+ // Case 1: Add current cus product back to schedule and remove scheduled product from schedule
+ if (schedule) {
+ logger.info(
+ `RENEW FLOW: adding cur cus product back to schedule ${schedule.id}`,
+ );
+ const newItems = await paramsToScheduleItems({
+ req,
+ attachParams,
+ config,
+ schedule,
+ removeCusProducts: scheduledProduct ? [scheduledProduct] : [],
+ billingPeriodEnd: expectedEnd,
+ });
- if (newItems.phases.length > 1) {
- const curSub = await paramsToCurSub({ attachParams });
- await updateCurSchedule({
- req,
- attachParams,
- schedule,
- newPhases: newItems.phases,
- sub: curSub!,
- });
+ if (newItems.phases.length > 1) {
+ const curSub = await paramsToCurSub({ attachParams });
+ await updateCurSchedule({
+ req,
+ attachParams,
+ schedule,
+ newPhases: newItems.phases,
+ sub: curSub!,
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- scheduled_ids: [schedule!.id],
- canceled: false,
- canceled_at: null,
- },
- });
- } else {
- logger.info(
- `RENEW FLOW: no new schedule items, releasing schedule ${schedule.id}`
- );
- await stripeCli.subscriptionSchedules.release(schedule.id);
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ scheduled_ids: [schedule!.id],
+ canceled: false,
+ canceled_at: null,
+ },
+ });
+ } else {
+ logger.info(
+ `RENEW FLOW: no new schedule items, releasing schedule ${schedule.id}`,
+ );
+ await stripeCli.subscriptionSchedules.release(schedule.id);
- await CusProductService.updateByStripeScheduledId({
- db: req.db,
- stripeScheduledId: schedule.id,
- updates: {
- scheduled_ids: [],
- },
- });
+ await CusProductService.updateByStripeScheduledId({
+ db: req.db,
+ stripeScheduledId: schedule.id,
+ updates: {
+ scheduled_ids: [],
+ },
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- canceled: false,
- canceled_at: null,
- },
- });
- }
- }
- // Case 2: Create new schedule for current cus product
- // Example scenario: Premium 1, Premium 2, Free 1, Free 2, Premium 1
- else {
- logger.info(`RENEW FLOW: creating new schedule`);
- const curSub = await cusProductToSub({
- cusProduct: curCusProduct!,
- stripeCli,
- });
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ canceled: false,
+ canceled_at: null,
+ },
+ });
+ }
+ }
+ // Case 2: Create new schedule for current cus product
+ // Example scenario: Premium 1, Premium 2, Free 1, Free 2, Premium 1
+ else {
+ logger.info(`RENEW FLOW: creating new schedule`);
+ const curSub = await cusProductToSub({
+ cusProduct: curCusProduct!,
+ stripeCli,
+ });
- const periodEnd = getLatestPeriodEnd({ sub: curSub! });
- await subToNewSchedule({
- req,
- sub: curSub!,
- attachParams,
- config,
- endOfBillingPeriod: periodEnd,
- });
+ const periodEnd = getLatestPeriodEnd({ sub: curSub! });
+ await subToNewSchedule({
+ req,
+ sub: curSub!,
+ attachParams,
+ config,
+ endOfBillingPeriod: periodEnd,
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- canceled: false,
- canceled_at: null,
- },
- });
- }
- }
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ canceled: false,
+ canceled_at: null,
+ },
+ });
+ }
+ }
- if (curCusProduct) {
- try {
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: curCusProduct.internal_customer_id,
- org: attachParams.org,
- env: attachParams.customer.env,
- customerId:
- attachParams.customer.id || attachParams.customer.internal_id,
- scenario: AttachScenario.Renew,
- cusProduct: curCusProduct,
- logger,
- });
- } catch (error) {
- logger.error("RENEW FLOW: failed to add to webhook queue", { error });
- }
- }
+ if (curCusProduct) {
+ try {
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: curCusProduct.internal_customer_id,
+ org: attachParams.org,
+ env: attachParams.customer.env,
+ customerId:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ scenario: AttachScenario.Renew,
+ cusProduct: curCusProduct,
+ logger,
+ });
+ } catch (error) {
+ logger.error("RENEW FLOW: failed to add to webhook queue", { error });
+ }
+ }
- if (curScheduledProduct) {
- await CusProductService.delete({
- db: req.db,
- cusProductId: curScheduledProduct!.id,
- });
- }
+ if (curScheduledProduct) {
+ await CusProductService.delete({
+ db: req.db,
+ cusProductId: curScheduledProduct!.id,
+ });
+ }
- if (res) {
- res.status(200).json(
- AttachResultSchema.parse({
- code: SuccessCode.RenewedProduct,
- message: `Successfully renewed product ${product.name}`,
- product_ids: [product.id],
- customer_id:
- attachParams.customer.id || attachParams.customer.internal_id,
- })
- );
- }
+ if (res) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ code: SuccessCode.RenewedProduct,
+ message: `Successfully renewed product ${product.name}`,
+ product_ids: [product.id],
+ customer_id:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ }),
+ );
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.ts b/server/src/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.ts
index e4dda8c14..28934b74a 100644
--- a/server/src/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.ts
+++ b/server/src/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.ts
@@ -8,90 +8,90 @@ import { AppEnv, AttachScenario, Organization } from "@autumn/shared";
import Stripe from "stripe";
export const handleInvoiceCheckoutPaid = async ({
- req,
- org,
- env,
- db,
- stripeCli,
- invoice,
+ req,
+ org,
+ env,
+ db,
+ stripeCli,
+ invoice,
}: {
- req: ExtendedRequest;
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
- stripeCli: Stripe;
- invoice: Stripe.Invoice;
+ req: ExtendedRequest;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ invoice: Stripe.Invoice;
}) => {
- const { logger } = req;
- const metadataId = invoice.metadata?.autumn_metadata_id!;
+ const { logger } = req;
+ const metadataId = invoice.metadata?.autumn_metadata_id!;
- const metadata = await MetadataService.get({
- db,
- id: metadataId,
- });
+ const metadata = await MetadataService.get({
+ db,
+ id: metadataId,
+ });
- const { subIds, anchorToUnix, config, ...rest } = metadata?.data;
- const attachParams = rest as AttachParams;
+ const { subIds, anchorToUnix, config, ...rest } = metadata?.data;
+ const attachParams = rest as AttachParams;
- if (!attachParams) {
- return;
- }
+ if (!attachParams) {
+ return;
+ }
- const reqMatch =
- attachParams.org.id === org.id && attachParams.customer.env === env;
+ const reqMatch =
+ attachParams.org.id === org.id && attachParams.customer.env === env;
- if (!reqMatch) return;
+ if (!reqMatch) return;
- if (attachParams.productsList) {
- console.log("Inserting products list");
- for (const productOptions of attachParams.productsList) {
- const product = attachParams.products.find(
- (p) => p.id === productOptions.product_id
- );
+ if (attachParams.productsList) {
+ console.log("Inserting products list");
+ for (const productOptions of attachParams.productsList) {
+ const product = attachParams.products.find(
+ (p) => p.id === productOptions.product_id,
+ );
- if (!product) {
- logger.error(
- `checkout.completed: product not found for productOptions: ${JSON.stringify(
- productOptions
- )}`
- );
- continue;
- }
+ if (!product) {
+ logger.error(
+ `checkout.completed: product not found for productOptions: ${JSON.stringify(
+ productOptions,
+ )}`,
+ );
+ continue;
+ }
- await createFullCusProduct({
- db,
- attachParams: attachToInsertParams(
- attachParams,
- product,
- productOptions.entity_id || undefined
- ),
- subscriptionIds: subIds,
- anchorToUnix,
- scenario: AttachScenario.New,
- logger,
- productOptions,
- });
- }
- } else {
- const batchInsert = [];
- for (const product of attachParams.products) {
- batchInsert.push(
- createFullCusProduct({
- db,
- attachParams: attachToInsertParams(attachParams, product),
- subscriptionIds: subIds,
- anchorToUnix,
- carryExistingUsages: config.carryUsage,
- scenario: AttachScenario.New,
- logger: req.logger,
- })
- );
- }
+ await createFullCusProduct({
+ db,
+ attachParams: attachToInsertParams(
+ attachParams,
+ product,
+ productOptions.entity_id || undefined,
+ ),
+ subscriptionIds: subIds,
+ anchorToUnix,
+ scenario: AttachScenario.New,
+ logger,
+ productOptions,
+ });
+ }
+ } else {
+ const batchInsert = [];
+ for (const product of attachParams.products) {
+ batchInsert.push(
+ createFullCusProduct({
+ db,
+ attachParams: attachToInsertParams(attachParams, product),
+ subscriptionIds: subIds,
+ anchorToUnix,
+ carryExistingUsages: config.carryUsage,
+ scenario: AttachScenario.New,
+ logger: req.logger,
+ }),
+ );
+ }
- await Promise.all(batchInsert);
- }
+ await Promise.all(batchInsert);
+ }
- req.logger.info(
- `✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`
- );
+ req.logger.info(
+ `✅ invoice.paid, successfully inserted cus products: ${attachParams.products.map((p) => p.id).join(", ")}`,
+ );
};
diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts
index a980f544e..81156f0ed 100644
--- a/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts
+++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/getAddAndRemoveProducts.ts
@@ -1,103 +1,103 @@
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import {
- AttachConfig,
- FullCusProduct,
- getCusProductMinQuantity,
- ProductOptions,
+ AttachConfig,
+ FullCusProduct,
+ getCusProductMinQuantity,
+ ProductOptions,
} from "@autumn/shared";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { mergeItemSets } from "./mergeItemSets.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
export const getProdListWithoutEntities = ({
- attachParams,
- productsList,
+ attachParams,
+ productsList,
}: {
- attachParams: AttachParams;
- productsList: ProductOptions[];
+ attachParams: AttachParams;
+ productsList: ProductOptions[];
}) => {
- const newProdList = structuredClone(productsList);
- for (let i = 0; i < newProdList.length; i++) {
- let productOptions = newProdList[i];
- newProdList[i] = {
- ...productOptions,
- quantity:
- (productOptions.quantity || 1) -
- getCusProductMinQuantity({
- cusProducts: attachParams.customer.customer_products,
- productId: productOptions.product_id,
- }),
- };
- }
- return newProdList;
+ const newProdList = structuredClone(productsList);
+ for (let i = 0; i < newProdList.length; i++) {
+ let productOptions = newProdList[i];
+ newProdList[i] = {
+ ...productOptions,
+ quantity:
+ (productOptions.quantity || 1) -
+ getCusProductMinQuantity({
+ cusProducts: attachParams.customer.customer_products,
+ productId: productOptions.product_id,
+ }),
+ };
+ }
+ return newProdList;
};
export const getAddAndRemoveProducts = async ({
- attachParams,
- config,
+ attachParams,
+ config,
}: {
- attachParams: AttachParams;
- config: AttachConfig;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- const productsList = attachParams.productsList!;
- const removeCusProducts: FullCusProduct[] = [];
- let itemSet: ItemSet = {
- subItems: [],
- invoiceItems: [],
- usageFeatures: [],
- };
- const expireCusProducts: FullCusProduct[] = [];
+ const productsList = attachParams.productsList!;
+ const removeCusProducts: FullCusProduct[] = [];
+ let itemSet: ItemSet = {
+ subItems: [],
+ invoiceItems: [],
+ usageFeatures: [],
+ };
+ const expireCusProducts: FullCusProduct[] = [];
- const newProdList = getProdListWithoutEntities({
- attachParams,
- productsList,
- });
+ const newProdList = getProdListWithoutEntities({
+ attachParams,
+ productsList,
+ });
- for (const productOptions of productsList) {
- const product = attachParams.products.find(
- (p) => p.id === productOptions.product_id
- );
+ for (const productOptions of productsList) {
+ const product = attachParams.products.find(
+ (p) => p.id === productOptions.product_id,
+ );
- const entity = attachParams.customer.entities.find(
- (e) => e.id === productOptions.entity_id
- );
+ const entity = attachParams.customer.entities.find(
+ (e) => e.id === productOptions.entity_id,
+ );
- const { curSameProduct, curScheduledProduct } = getExistingCusProducts({
- product: product!,
- cusProducts: attachParams.customer.customer_products,
- internalEntityId: entity?.internal_id,
- });
+ const { curSameProduct, curScheduledProduct } = getExistingCusProducts({
+ product: product!,
+ cusProducts: attachParams.customer.customer_products,
+ internalEntityId: entity?.internal_id,
+ });
- if (curSameProduct) {
- expireCusProducts.push(curSameProduct);
- if (!curSameProduct.product.is_add_on && curScheduledProduct) {
- removeCusProducts.push(curScheduledProduct);
- } else {
- removeCusProducts.push(curSameProduct);
- }
- }
+ if (curSameProduct) {
+ expireCusProducts.push(curSameProduct);
+ if (!curSameProduct.product.is_add_on && curScheduledProduct) {
+ removeCusProducts.push(curScheduledProduct);
+ } else {
+ removeCusProducts.push(curSameProduct);
+ }
+ }
- const newItemSet = await getStripeSubItems2({
- attachParams: {
- ...attachParams,
- products: [product!],
- prices: product?.prices || [],
- entitlements: product?.entitlements || [],
- productsList: newProdList,
- },
- config,
- });
+ const newItemSet = await getStripeSubItems2({
+ attachParams: {
+ ...attachParams,
+ products: [product!],
+ prices: product?.prices || [],
+ entitlements: product?.entitlements || [],
+ productsList: newProdList,
+ },
+ config,
+ });
- itemSet = mergeItemSets({
- curItemSet: itemSet,
- newItemSet,
- });
- }
+ itemSet = mergeItemSets({
+ curItemSet: itemSet,
+ newItemSet,
+ });
+ }
- return {
- removeCusProducts,
- expireCusProducts,
- itemSet,
- };
+ return {
+ removeCusProducts,
+ expireCusProducts,
+ itemSet,
+ };
};
diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts
index 29e5436bf..12b695790 100644
--- a/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts
+++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/handleMultiAttachFlow.ts
@@ -1,21 +1,21 @@
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import {
- AttachBody,
- AttachBranch,
- AttachConfig,
- AttachScenario,
- CusProductStatus,
- SuccessCode,
+ AttachBody,
+ AttachBranch,
+ AttachConfig,
+ AttachScenario,
+ CusProductStatus,
+ SuccessCode,
} from "@autumn/shared";
import {
- getCustomerSub,
- paramsToCurSubSchedule,
+ getCustomerSub,
+ paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
@@ -24,198 +24,198 @@ import Stripe from "stripe";
import { createStripeSub2 } from "../addProductFlow/createStripeSub2.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import {
- attachToInvoiceResponse,
- insertInvoiceFromAttach,
+ attachToInvoiceResponse,
+ insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
import {
- getAddAndRemoveProducts,
- getProdListWithoutEntities,
+ getAddAndRemoveProducts,
+ getProdListWithoutEntities,
} from "./getAddAndRemoveProducts.js";
import { handleUpgradeFlowSchedule } from "../upgradeFlow/handleUpgradeFlowSchedule.js";
import { isTrialing } from "@autumn/shared";
import { handleMultiAttachErrors } from "../../attachUtils/handleAttachErrors/handleMultiAttachErrors.js";
export const handleMultiAttachFlow = async ({
- req,
- res,
- attachParams,
- attachBody,
- branch,
- config,
+ req,
+ res,
+ attachParams,
+ attachBody,
+ branch,
+ config,
}: {
- req: ExtendedRequest;
- res: ExtendedResponse;
- attachParams: AttachParams;
- attachBody: AttachBody;
- branch: AttachBranch;
- config: AttachConfig;
+ req: ExtendedRequest;
+ res: ExtendedResponse;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ branch: AttachBranch;
+ config: AttachConfig;
}) => {
- await handleMultiAttachErrors({ attachParams, attachBody, branch });
- const { db, logger } = req;
- const { stripeCli } = attachParams;
- const productsList = attachParams.productsList!;
+ await handleMultiAttachErrors({ attachParams, attachBody, branch });
+ const { db, logger } = req;
+ const { stripeCli } = attachParams;
+ const productsList = attachParams.productsList!;
- let { sub: curSub, cusProduct: mergeCusProduct } = await getCustomerSub({
- attachParams,
- });
- let latestInvoice: Stripe.Invoice | null = null;
+ let { sub: curSub, cusProduct: mergeCusProduct } = await getCustomerSub({
+ attachParams,
+ });
+ let latestInvoice: Stripe.Invoice | null = null;
- const { removeCusProducts, itemSet, expireCusProducts } =
- await getAddAndRemoveProducts({
- attachParams,
- config,
- });
+ const { removeCusProducts, itemSet, expireCusProducts } =
+ await getAddAndRemoveProducts({
+ attachParams,
+ config,
+ });
- const mergedItemSet = await paramsToSubItems({
- req,
- attachParams,
- config,
- removeCusProducts,
- addItemSet: itemSet,
- sub: curSub,
- });
+ const mergedItemSet = await paramsToSubItems({
+ req,
+ attachParams,
+ config,
+ removeCusProducts,
+ addItemSet: itemSet,
+ sub: curSub,
+ });
- itemSet.subItems = mergedItemSet.subItems;
+ itemSet.subItems = mergedItemSet.subItems;
- if (!curSub) {
- console.log("MULTI ATTACH FLOW, NO SUB, CREATING NEW");
- const newSub = await createStripeSub2({
- db,
- attachParams,
- config,
- stripeCli,
- itemSet,
- logger,
- });
+ if (!curSub) {
+ console.log("MULTI ATTACH FLOW, NO SUB, CREATING NEW");
+ const newSub = await createStripeSub2({
+ db,
+ attachParams,
+ config,
+ stripeCli,
+ itemSet,
+ logger,
+ });
- if (config?.invoiceCheckout) {
- return {
- invoices: [newSub.latest_invoice as Stripe.Invoice],
- subs: [newSub],
- anchorToUnix: getLatestPeriodEnd({ sub: newSub }) * 1000,
- config,
- };
- }
- curSub = newSub;
- latestInvoice = newSub.latest_invoice as Stripe.Invoice;
- // Do something about current sub...
- } else if (itemSet.subItems.length > 0) {
- console.log(`MULTI ATTACH FLOW, UPDATING SUB ${curSub!.id}`);
- config.disableTrial = true;
+ if (config?.invoiceCheckout) {
+ return {
+ invoices: [newSub.latest_invoice as Stripe.Invoice],
+ subs: [newSub],
+ anchorToUnix: getLatestPeriodEnd({ sub: newSub }) * 1000,
+ config,
+ };
+ }
+ curSub = newSub;
+ latestInvoice = newSub.latest_invoice as Stripe.Invoice;
+ // Do something about current sub...
+ } else if (itemSet.subItems.length > 0) {
+ console.log(`MULTI ATTACH FLOW, UPDATING SUB ${curSub!.id}`);
+ config.disableTrial = true;
- const updateResult = await updateStripeSub2({
- req,
- attachParams,
- config,
- curSub: curSub!,
- itemSet,
- // fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product...
- fromCreate: true, // just for now, if no products, it comes from cancel product...
- });
+ const updateResult = await updateStripeSub2({
+ req,
+ attachParams,
+ config,
+ curSub: curSub!,
+ itemSet,
+ // fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product...
+ fromCreate: true, // just for now, if no products, it comes from cancel product...
+ });
- // TODO: Add these missing functions or remove if not needed
- const schedule = await paramsToCurSubSchedule({ attachParams });
- if (schedule) {
- await handleUpgradeFlowSchedule({
- req,
- attachParams,
- config,
- schedule,
- curSub,
- removeCusProducts,
- logger,
- });
- }
+ // TODO: Add these missing functions or remove if not needed
+ const schedule = await paramsToCurSubSchedule({ attachParams });
+ if (schedule) {
+ await handleUpgradeFlowSchedule({
+ req,
+ attachParams,
+ config,
+ schedule,
+ curSub,
+ removeCusProducts,
+ logger,
+ });
+ }
- attachParams.replaceables = updateResult.replaceables || [];
- curSub = updateResult.updatedSub;
- latestInvoice = updateResult.latestInvoice;
- }
+ attachParams.replaceables = updateResult.replaceables || [];
+ curSub = updateResult.updatedSub;
+ latestInvoice = updateResult.latestInvoice;
+ }
- for (const cusProduct of removeCusProducts) {
- if (cusProduct.status === CusProductStatus.Scheduled) {
- await CusProductService.delete({
- db,
- cusProductId: cusProduct.id,
- });
- }
- }
+ for (const cusProduct of removeCusProducts) {
+ if (cusProduct.status === CusProductStatus.Scheduled) {
+ await CusProductService.delete({
+ db,
+ cusProductId: cusProduct.id,
+ });
+ }
+ }
- for (const cusProduct of expireCusProducts) {
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- },
- });
- }
+ for (const cusProduct of expireCusProducts) {
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ },
+ });
+ }
- if (latestInvoice) {
- await insertInvoiceFromAttach({
- db,
- attachParams,
- stripeInvoice: latestInvoice,
- logger,
- });
- }
+ if (latestInvoice) {
+ await insertInvoiceFromAttach({
+ db,
+ attachParams,
+ stripeInvoice: latestInvoice,
+ logger,
+ });
+ }
- // Expire all existing cus products at the customer level
- const batchInsert: any[] = [];
- const newProdList = getProdListWithoutEntities({
- attachParams,
- productsList,
- });
- for (const productOptions of newProdList) {
- const product = attachParams.products.find(
- (p) => p.id === productOptions.product_id
- )!;
+ // Expire all existing cus products at the customer level
+ const batchInsert: any[] = [];
+ const newProdList = getProdListWithoutEntities({
+ attachParams,
+ productsList,
+ });
+ for (const productOptions of newProdList) {
+ const product = attachParams.products.find(
+ (p) => p.id === productOptions.product_id,
+ )!;
- if (productOptions.quantity === 0) continue;
+ if (productOptions.quantity === 0) continue;
- const anchorToUnix = curSub
- ? getLatestPeriodEnd({ sub: curSub! }) * 1000
- : undefined;
+ const anchorToUnix = curSub
+ ? getLatestPeriodEnd({ sub: curSub! }) * 1000
+ : undefined;
- batchInsert.push(
- createFullCusProduct({
- db,
- attachParams: attachToInsertParams(
- attachParams,
- product,
- productOptions.entity_id || undefined
- ),
- subscriptionIds: curSub ? [curSub?.id!] : undefined,
- anchorToUnix,
- scenario: AttachScenario.New,
- logger,
- productOptions,
- trialEndsAt:
- mergeCusProduct && isTrialing({ cusProduct: mergeCusProduct })
- ? mergeCusProduct?.trial_ends_at!
- : undefined,
- })
- );
- }
+ batchInsert.push(
+ createFullCusProduct({
+ db,
+ attachParams: attachToInsertParams(
+ attachParams,
+ product,
+ productOptions.entity_id || undefined,
+ ),
+ subscriptionIds: curSub ? [curSub?.id!] : undefined,
+ anchorToUnix,
+ scenario: AttachScenario.New,
+ logger,
+ productOptions,
+ trialEndsAt:
+ mergeCusProduct && isTrialing({ cusProduct: mergeCusProduct })
+ ? mergeCusProduct?.trial_ends_at!
+ : undefined,
+ }),
+ );
+ }
- console.log("Running multi attach flow!");
- if (res) {
- const invoice = latestInvoice;
- res.status(200).json(
- AttachResultSchema.parse(
- AttachResultSchema.parse({
- message: `Successfully created subscriptions and attached ${attachParams.products.map((p) => p.name).join(", ")} to ${attachParams.customer.name}`,
- code: SuccessCode.NewProductAttached,
- product_ids: attachParams.products.map((p) => p.id),
- customer_id:
- attachParams.customer.id || attachParams.customer.internal_id,
- invoice: attachParams.invoiceOnly
- ? attachToInvoiceResponse({ invoice })
- : undefined,
- })
- )
- );
- }
+ console.log("Running multi attach flow!");
+ if (res) {
+ const invoice = latestInvoice;
+ res.status(200).json(
+ AttachResultSchema.parse(
+ AttachResultSchema.parse({
+ message: `Successfully created subscriptions and attached ${attachParams.products.map((p) => p.name).join(", ")} to ${attachParams.customer.name}`,
+ code: SuccessCode.NewProductAttached,
+ product_ids: attachParams.products.map((p) => p.id),
+ customer_id:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ invoice: attachParams.invoiceOnly
+ ? attachToInvoiceResponse({ invoice })
+ : undefined,
+ }),
+ ),
+ );
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts b/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts
index c20f1221e..0368af672 100644
--- a/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts
+++ b/server/src/internal/customers/attach/attachFunctions/multiAttach/mergeItemSets.ts
@@ -1,42 +1,42 @@
import { ItemSet } from "@/utils/models/ItemSet.js";
export const mergeItemSets = ({
- curItemSet,
- newItemSet,
+ curItemSet,
+ newItemSet,
}: {
- curItemSet: ItemSet;
- newItemSet: ItemSet;
+ curItemSet: ItemSet;
+ newItemSet: ItemSet;
}) => {
- const curSetItems = curItemSet.subItems;
- for (const item of newItemSet.subItems) {
- const priceIndex = curSetItems.findIndex((i) => i.price === item.price);
- if (priceIndex !== -1) {
- curSetItems[priceIndex].quantity =
- (curSetItems[priceIndex].quantity || 0) + (item.quantity || 0);
- } else {
- curSetItems.push(item);
- }
- }
- const curInvoiceItems = curItemSet.invoiceItems;
- for (const item of newItemSet.invoiceItems) {
- const priceIndex = curInvoiceItems.findIndex((i) => i.price === item.price);
- if (priceIndex !== -1) {
- curInvoiceItems[priceIndex].quantity =
- (curInvoiceItems[priceIndex].quantity || 0) + (item.quantity || 0);
- } else {
- curInvoiceItems.push(item);
- }
- }
- const curUsageFeatures = curItemSet.usageFeatures;
- for (const feature of newItemSet.usageFeatures) {
- if (!curUsageFeatures.includes(feature)) {
- curUsageFeatures.push(feature);
- }
- }
+ const curSetItems = curItemSet.subItems;
+ for (const item of newItemSet.subItems) {
+ const priceIndex = curSetItems.findIndex((i) => i.price === item.price);
+ if (priceIndex !== -1) {
+ curSetItems[priceIndex].quantity =
+ (curSetItems[priceIndex].quantity || 0) + (item.quantity || 0);
+ } else {
+ curSetItems.push(item);
+ }
+ }
+ const curInvoiceItems = curItemSet.invoiceItems;
+ for (const item of newItemSet.invoiceItems) {
+ const priceIndex = curInvoiceItems.findIndex((i) => i.price === item.price);
+ if (priceIndex !== -1) {
+ curInvoiceItems[priceIndex].quantity =
+ (curInvoiceItems[priceIndex].quantity || 0) + (item.quantity || 0);
+ } else {
+ curInvoiceItems.push(item);
+ }
+ }
+ const curUsageFeatures = curItemSet.usageFeatures;
+ for (const feature of newItemSet.usageFeatures) {
+ if (!curUsageFeatures.includes(feature)) {
+ curUsageFeatures.push(feature);
+ }
+ }
- return {
- subItems: curSetItems,
- invoiceItems: curInvoiceItems,
- usageFeatures: curUsageFeatures,
- };
+ return {
+ subItems: curSetItems,
+ invoiceItems: curInvoiceItems,
+ usageFeatures: curUsageFeatures,
+ };
};
diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/createSubSchedule.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/createSubSchedule.ts
index b27370e7c..1fddc511a 100644
--- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/createSubSchedule.ts
+++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/createSubSchedule.ts
@@ -6,59 +6,59 @@ import { generateId } from "@/utils/genUtils.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
export const createSubSchedule = async ({
- db,
- attachParams,
- itemSet,
- endOfBillingPeriod,
+ db,
+ attachParams,
+ itemSet,
+ endOfBillingPeriod,
}: {
- db: DrizzleCli;
- attachParams: AttachParams;
- itemSet: ItemSet;
- endOfBillingPeriod: number;
+ db: DrizzleCli;
+ attachParams: AttachParams;
+ itemSet: ItemSet;
+ endOfBillingPeriod: number;
}) => {
- const { org, customer, paymentMethod } = attachParams;
+ const { org, customer, paymentMethod } = attachParams;
- const { stripeCli } = attachParams;
+ const { stripeCli } = attachParams;
- // let subItems = items.filter(
- // (item: any, index: number) =>
- // index >= prices.length ||
- // prices[index].config!.interval !== BillingInterval.OneOff
- // );
- // let oneOffItems = items.filter(
- // (item: any, index: number) =>
- // index < prices.length &&
- // prices[index].config!.interval === BillingInterval.OneOff
- // );
- const { subItems, invoiceItems, usageFeatures } = itemSet;
+ // let subItems = items.filter(
+ // (item: any, index: number) =>
+ // index >= prices.length ||
+ // prices[index].config!.interval !== BillingInterval.OneOff
+ // );
+ // let oneOffItems = items.filter(
+ // (item: any, index: number) =>
+ // index < prices.length &&
+ // prices[index].config!.interval === BillingInterval.OneOff
+ // );
+ const { subItems, invoiceItems, usageFeatures } = itemSet;
- const newSubscriptionSchedule = await stripeCli.subscriptionSchedules.create({
- customer: customer.processor.id,
- start_date: endOfBillingPeriod,
- billing_mode: { type: "flexible" },
- phases: [
- {
- items: sanitizeSubItems(subItems),
- default_payment_method: paymentMethod?.id,
- add_invoice_items: invoiceItems,
- },
- ],
- });
+ const newSubscriptionSchedule = await stripeCli.subscriptionSchedules.create({
+ customer: customer.processor.id,
+ start_date: endOfBillingPeriod,
+ billing_mode: { type: "flexible" },
+ phases: [
+ {
+ items: sanitizeSubItems(subItems),
+ default_payment_method: paymentMethod?.id,
+ add_invoice_items: invoiceItems,
+ },
+ ],
+ });
- await SubService.createSub({
- db,
- sub: {
- id: generateId("sub"),
- stripe_id: null,
- stripe_schedule_id: newSubscriptionSchedule.id,
- created_at: Date.now(),
- usage_features: usageFeatures,
- org_id: org.id,
- env: customer.env,
- current_period_start: null,
- current_period_end: null,
- },
- });
+ await SubService.createSub({
+ db,
+ sub: {
+ id: generateId("sub"),
+ stripe_id: null,
+ stripe_schedule_id: newSubscriptionSchedule.id,
+ created_at: Date.now(),
+ usage_features: usageFeatures,
+ org_id: org.id,
+ env: customer.env,
+ current_period_start: null,
+ current_period_end: null,
+ },
+ });
- return newSubscriptionSchedule;
+ return newSubscriptionSchedule;
};
diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts
index 1fa90b73d..64a07d4a4 100644
--- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts
+++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts
@@ -1,24 +1,24 @@
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
- APIVersion,
- AttachConfig,
- AttachScenario,
- SuccessCode,
+ APIVersion,
+ AttachConfig,
+ AttachScenario,
+ SuccessCode,
} from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- attachToInsertParams,
- isFreeProduct,
+ attachToInsertParams,
+ isFreeProduct,
} from "@/internal/products/productUtils.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import {
- attachParamsToCurCusProduct,
- paramsToCurSub,
- paramsToCurSubSchedule,
+ attachParamsToCurCusProduct,
+ paramsToCurSub,
+ paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
@@ -29,178 +29,178 @@ import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js"
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
export const handleScheduleFunction2 = async ({
- req,
- res,
- attachParams,
- config,
- skipInsertCusProduct = false,
+ req,
+ res,
+ attachParams,
+ config,
+ skipInsertCusProduct = false,
}: {
- req: any;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
- skipInsertCusProduct?: boolean;
+ req: any;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ skipInsertCusProduct?: boolean;
}) => {
- const logger = req.logtail;
- const product = attachParams.products[0];
- const { stripeCli } = attachParams;
+ const logger = req.logtail;
+ const product = attachParams.products[0];
+ const { stripeCli } = attachParams;
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const curSub = await paramsToCurSub({ attachParams });
- const subItems = curSub?.items.data.filter((item) =>
- subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item })
- );
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const curSub = await paramsToCurSub({ attachParams });
+ const subItems = curSub?.items.data.filter((item) =>
+ subItemInCusProduct({ cusProduct: curCusProduct!, subItem: item }),
+ );
- const expectedEnd = getLatestPeriodEnd({ subItems });
+ const expectedEnd = getLatestPeriodEnd({ subItems });
- // 1. Cancel current subscription and fetch items from other cus products...?
- let schedule = await paramsToCurSubSchedule({ attachParams });
+ // 1. Cancel current subscription and fetch items from other cus products...?
+ let schedule = await paramsToCurSubSchedule({ attachParams });
- const newProductFree = isFreeProduct(attachParams.prices);
+ const newProductFree = isFreeProduct(attachParams.prices);
- if (schedule) {
- const newItems = await paramsToScheduleItems({
- req,
- schedule: schedule!,
- attachParams,
- config,
- billingPeriodEnd: expectedEnd!,
- });
+ if (schedule) {
+ const newItems = await paramsToScheduleItems({
+ req,
+ schedule: schedule!,
+ attachParams,
+ config,
+ billingPeriodEnd: expectedEnd!,
+ });
- const currentPhaseIndex = getCurrentPhaseIndex({
- schedule: { phases: newItems.phases } as any,
- now: attachParams.now,
- });
+ const currentPhaseIndex = getCurrentPhaseIndex({
+ schedule: { phases: newItems.phases } as any,
+ now: attachParams.now,
+ });
- if (currentPhaseIndex == newItems.phases.length - 1) {
- logger.info(
- `SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`
- );
- await stripeCli.subscriptionSchedules.release(schedule!.id);
- await CusProductService.updateByStripeScheduledId({
- db: req.db,
- stripeScheduledId: schedule!.id,
- updates: { scheduled_ids: [] },
- });
+ if (currentPhaseIndex == newItems.phases.length - 1) {
+ logger.info(
+ `SCHEDULE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`,
+ );
+ await stripeCli.subscriptionSchedules.release(schedule!.id);
+ await CusProductService.updateByStripeScheduledId({
+ db: req.db,
+ stripeScheduledId: schedule!.id,
+ updates: { scheduled_ids: [] },
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- canceled: true,
- canceled_at: Date.now(),
- ended_at: expectedEnd * 1000,
- },
- });
- schedule = undefined;
- } else {
- logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`);
- schedule = await updateCurSchedule({
- req,
- attachParams,
- schedule,
- newPhases: newItems.phases || [],
- sub: curSub!,
- });
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ canceled: true,
+ canceled_at: Date.now(),
+ ended_at: expectedEnd * 1000,
+ },
+ });
+ schedule = undefined;
+ } else {
+ logger.info(`SCHEDULE FLOW: updating schedule ${schedule?.id}`);
+ schedule = await updateCurSchedule({
+ req,
+ attachParams,
+ schedule,
+ newPhases: newItems.phases || [],
+ sub: curSub!,
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- scheduled_ids: [schedule!.id],
- canceled_at: Date.now(),
- canceled: true,
- ended_at: expectedEnd * 1000,
- },
- });
- }
- } else {
- logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
- schedule = await subToNewSchedule({
- req,
- sub: curSub!,
- attachParams,
- config,
- endOfBillingPeriod: expectedEnd!,
- });
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ scheduled_ids: [schedule!.id],
+ canceled_at: Date.now(),
+ canceled: true,
+ ended_at: expectedEnd * 1000,
+ },
+ });
+ }
+ } else {
+ logger.info(`SCHEDULE FLOW: no schedule, creating new schedule`);
+ schedule = await subToNewSchedule({
+ req,
+ sub: curSub!,
+ attachParams,
+ config,
+ endOfBillingPeriod: expectedEnd!,
+ });
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- canceled: true,
- canceled_at: Date.now(),
- ended_at: expectedEnd * 1000,
- },
- });
- }
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ canceled: true,
+ canceled_at: Date.now(),
+ ended_at: expectedEnd * 1000,
+ },
+ });
+ }
- if (!schedule) {
- logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`);
- await stripeCli.subscriptions.update(curSub!.id, {
- cancel_at: expectedEnd!,
- cancellation_details: {
- comment: "autumn_downgrade",
- },
- });
- }
+ if (!schedule) {
+ logger.info(`SCHEDULE FLOW: no schedule, canceling sub ${curSub?.id}`);
+ await stripeCli.subscriptions.update(curSub!.id, {
+ cancel_at: expectedEnd!,
+ cancellation_details: {
+ comment: "autumn_downgrade",
+ },
+ });
+ }
- if (!skipInsertCusProduct) {
- await createFullCusProduct({
- db: req.db,
- attachParams: attachToInsertParams(attachParams, product),
- startsAt: expectedEnd! * 1000,
- subscriptionScheduleIds: schedule ? [schedule.id] : [],
- nextResetAt: expectedEnd! * 1000,
- disableFreeTrial: true,
- isDowngrade: true,
- sendWebhook: false,
- // scenario: newProductFree
- // ? AttachScenario.Cancel
- // : AttachScenario.Downgrade,
- logger,
- });
- }
+ if (!skipInsertCusProduct) {
+ await createFullCusProduct({
+ db: req.db,
+ attachParams: attachToInsertParams(attachParams, product),
+ startsAt: expectedEnd! * 1000,
+ subscriptionScheduleIds: schedule ? [schedule.id] : [],
+ nextResetAt: expectedEnd! * 1000,
+ disableFreeTrial: true,
+ isDowngrade: true,
+ sendWebhook: false,
+ // scenario: newProductFree
+ // ? AttachScenario.Cancel
+ // : AttachScenario.Downgrade,
+ logger,
+ });
+ }
- if (curCusProduct) {
- try {
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: curCusProduct.internal_customer_id,
- org: attachParams.org,
- env: attachParams.customer.env,
- customerId:
- attachParams.customer.id || attachParams.customer.internal_id,
+ if (curCusProduct) {
+ try {
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: curCusProduct.internal_customer_id,
+ org: attachParams.org,
+ env: attachParams.customer.env,
+ customerId:
+ attachParams.customer.id || attachParams.customer.internal_id,
- scenario: isFreeProduct(attachParams.prices)
- ? AttachScenario.Cancel
- : AttachScenario.Downgrade,
+ scenario: isFreeProduct(attachParams.prices)
+ ? AttachScenario.Cancel
+ : AttachScenario.Downgrade,
- cusProduct: curCusProduct,
- logger,
- });
- } catch (error) {
- logger.error("SCHEDULE FLOW: failed to add to webhook queue", { error });
- }
- }
+ cusProduct: curCusProduct,
+ logger,
+ });
+ } catch (error) {
+ logger.error("SCHEDULE FLOW: failed to add to webhook queue", { error });
+ }
+ }
- let apiVersion = attachParams.apiVersion || APIVersion.v1;
+ let apiVersion = attachParams.apiVersion || APIVersion.v1;
- if (res) {
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json(
- AttachResultSchema.parse({
- code: SuccessCode.DowngradeScheduled,
- message: `Successfully downgraded from ${curCusProduct!.product.name} to ${product.name}`,
- product_ids: [product.id],
- customer_id:
- attachParams.customer.id || attachParams.customer.internal_id,
- })
- );
- } else {
- res.status(200).json({
- success: true,
- });
- }
- }
+ if (res) {
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ code: SuccessCode.DowngradeScheduled,
+ message: `Successfully downgraded from ${curCusProduct!.product.name} to ${product.name}`,
+ product_ids: [product.id],
+ customer_id:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ }),
+ );
+ } else {
+ res.status(200).json({
+ success: true,
+ });
+ }
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts
index 30a7e9a1f..7e9475064 100644
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts
+++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityDowngrade.ts
@@ -1,20 +1,20 @@
import {
- calculateProrationAmount,
- Feature,
- FeatureOptions,
- FullCusProduct,
- getFeatureInvoiceDescription,
- OnDecrease,
- priceToInvoiceAmount,
- UsagePriceConfig,
+ calculateProrationAmount,
+ Feature,
+ FeatureOptions,
+ FullCusProduct,
+ getFeatureInvoiceDescription,
+ OnDecrease,
+ priceToInvoiceAmount,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Stripe } from "stripe";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { featureToCusPrice } from "@/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.js";
import {
- shouldBillNow,
- shouldProrate,
+ shouldBillNow,
+ shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { Decimal } from "decimal.js";
@@ -29,186 +29,186 @@ import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/creat
import { notNullish } from "@/utils/genUtils.js";
const onDecreaseToStripeProration: Record = {
- [OnDecrease.ProrateImmediately]: "always_invoice",
- [OnDecrease.ProrateNextCycle]: "create_prorations",
- [OnDecrease.Prorate]: "create_prorations",
- [OnDecrease.None]: "none",
- [OnDecrease.NoProrations]: "none",
+ [OnDecrease.ProrateImmediately]: "always_invoice",
+ [OnDecrease.ProrateNextCycle]: "create_prorations",
+ [OnDecrease.Prorate]: "create_prorations",
+ [OnDecrease.None]: "none",
+ [OnDecrease.NoProrations]: "none",
};
export const handleQuantityDowngrade = async ({
- req,
- attachParams,
- cusProduct,
- stripeSub,
- oldOptions,
- newOptions,
- subItem,
+ req,
+ attachParams,
+ cusProduct,
+ stripeSub,
+ oldOptions,
+ newOptions,
+ subItem,
}: {
- req: any;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- stripeSub: Stripe.Subscription;
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
- subItem: Stripe.SubscriptionItem;
+ req: any;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ stripeSub: Stripe.Subscription;
+ oldOptions: FeatureOptions;
+ newOptions: FeatureOptions;
+ subItem: Stripe.SubscriptionItem;
}) => {
- const { db, logger, org, features } = req;
- const { stripeCli, paymentMethod } = attachParams;
+ const { db, logger, org, features } = req;
+ const { stripeCli, paymentMethod } = attachParams;
- const cusPrice = featureToCusPrice({
- internalFeatureId: newOptions.internal_feature_id!,
- cusPrices: cusProduct.customer_prices,
- })!;
+ const cusPrice = featureToCusPrice({
+ internalFeatureId: newOptions.internal_feature_id!,
+ cusPrices: cusProduct.customer_prices,
+ })!;
- const onDecrease =
- cusPrice.price.proration_config?.on_decrease ||
- OnDecrease.ProrateImmediately;
+ const onDecrease =
+ cusPrice.price.proration_config?.on_decrease ||
+ OnDecrease.ProrateImmediately;
- const difference = new Decimal(newOptions.quantity)
- .minus(oldOptions.quantity)
- .toNumber();
+ 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 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 billingUnits =
+ (cusPrice.price.config as UsagePriceConfig).billing_units || 1;
- // const diffWithBillingUnits = new Decimal(difference)
- // .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1)
- // .toNumber();
+ // const diffWithBillingUnits = new Decimal(difference)
+ // .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1)
+ // .toNumber();
- const newSubItemQuantity = new Decimal(subItem.quantity || 0)
- .plus(subItemDifference)
- .toNumber();
+ const newSubItemQuantity = new Decimal(subItem.quantity || 0)
+ .plus(subItemDifference)
+ .toNumber();
- const stripeProration = onDecreaseToStripeProration[
- onDecrease
- ] as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
+ const stripeProration = onDecreaseToStripeProration[
+ onDecrease
+ ] as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
- const createDowngradeInvoice = async () => {
- const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
+ const createDowngradeInvoice = async () => {
+ const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
- const prevAmount = priceToInvoiceAmount({
- price: cusPrice.price,
- quantity: new Decimal(oldOptions.quantity).mul(billingUnits!).toNumber(),
- });
+ 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(),
- });
+ const newAmount = priceToInvoiceAmount({
+ price: cusPrice.price,
+ quantity: new Decimal(newOptions.quantity).mul(billingUnits!).toNumber(),
+ });
- let amount = new Decimal(newAmount).minus(prevAmount).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,
- });
+ amount = calculateProrationAmount({
+ periodEnd: end * 1000,
+ periodStart: start * 1000,
+ now: attachParams.now || Date.now(),
+ amount,
+ allowNegative: true,
+ });
- const product = cusProductToProduct({ cusProduct });
- const feature = req.features.find(
- (f: Feature) => f.internal_id == newOptions.internal_feature_id
- )!;
- const invoiceItem = constructStripeInvoiceItem({
- product,
- amount: amount,
- org: req.org,
- 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),
- });
+ const product = cusProductToProduct({ cusProduct });
+ const feature = req.features.find(
+ (f: Feature) => f.internal_id == newOptions.internal_feature_id,
+ )!;
+ const invoiceItem = constructStripeInvoiceItem({
+ product,
+ amount: amount,
+ org: req.org,
+ 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}`
- );
+ logger.info(
+ `🔥 Creating downgrade prepaid invoice item: ${invoiceItem.description} - ${amount}`,
+ );
- await stripeCli.invoiceItems.create(invoiceItem);
+ 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,
- logger,
- });
+ if (shouldBillNow(onDecrease)) {
+ const { invoice: finalInvoice } = await createAndFinalizeInvoice({
+ stripeCli,
+ stripeCusId: stripeSub.customer as string,
+ stripeSubId: stripeSub.id,
+ paymentMethod: paymentMethod || null,
+ logger,
+ });
- try {
- const invoiceItems = await getInvoiceItems({
- stripeInvoice: finalInvoice,
- prices: [cusPrice.price],
- 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}`);
- }
- }
- };
+ 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",
- });
+ 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;
- }
+ if (!shouldProrate(onDecrease)) {
+ newOptions.upcoming_quantity = newOptions.quantity;
+ newOptions.quantity = oldOptions.quantity;
+ return;
+ }
- await createDowngradeInvoice();
+ await createDowngradeInvoice();
- const cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts: cusProduct.customer_entitlements,
- });
+ const cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts: cusProduct.customer_entitlements,
+ });
- if (cusEnt) {
- const config = cusPrice.price.config as UsagePriceConfig;
- let decrementBy = new Decimal(oldOptions.quantity)
- .minus(new Decimal(newOptions.quantity))
- .mul(billingUnits)
- .toNumber();
+ if (cusEnt) {
+ const config = cusPrice.price.config as UsagePriceConfig;
+ let decrementBy = new Decimal(oldOptions.quantity)
+ .minus(new Decimal(newOptions.quantity))
+ .mul(billingUnits)
+ .toNumber();
- await CusEntService.decrement({
- db,
- id: cusEnt.id,
- amount: decrementBy,
- });
- }
+ await CusEntService.decrement({
+ db,
+ 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
index 0677f5afe..5d602a899 100644
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts
+++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/handleQuantityUpgrade.ts
@@ -1,21 +1,21 @@
import {
- calculateProrationAmount,
- Feature,
- FeatureOptions,
- FullCusProduct,
- FullCustomerPrice,
- getAmountForQuantity,
- getFeatureInvoiceDescription,
- OnIncrease,
- UsagePriceConfig,
+ calculateProrationAmount,
+ Feature,
+ FeatureOptions,
+ FullCusProduct,
+ FullCustomerPrice,
+ getAmountForQuantity,
+ getFeatureInvoiceDescription,
+ OnIncrease,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Stripe } from "stripe";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- shouldBillNow,
- shouldProrate,
+ shouldBillNow,
+ shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
@@ -30,174 +30,174 @@ import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const handleQuantityUpgrade = async ({
- req,
- attachParams,
- cusProduct,
- stripeSubs,
- oldOptions,
- newOptions,
- cusPrice,
- stripeSub,
- subItem,
+ req,
+ attachParams,
+ cusProduct,
+ stripeSubs,
+ oldOptions,
+ newOptions,
+ cusPrice,
+ stripeSub,
+ subItem,
}: {
- req: any;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- stripeSubs: Stripe.Subscription[];
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
- cusPrice: FullCustomerPrice;
- stripeSub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
+ req: any;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ stripeSubs: Stripe.Subscription[];
+ oldOptions: FeatureOptions;
+ newOptions: FeatureOptions;
+ cusPrice: FullCustomerPrice;
+ stripeSub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
}) => {
- // Manually calculate prorations...
- const { features, org, logger, db } = req;
- const { stripeCli, now, paymentMethod } = attachParams;
+ // Manually calculate prorations...
+ const { features, org, logger, db } = req;
+ const { stripeCli, now, paymentMethod } = attachParams;
- const difference = new Decimal(newOptions.quantity)
- .minus(oldOptions.quantity)
- .toNumber();
+ 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 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 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;
+ const prorate = shouldProrate(onIncrease);
+ const config = cusPrice.price.config as UsagePriceConfig;
+ const billingUnits = config.billing_units || 1;
- const diffWithBillingUnits = new Decimal(difference)
- .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1)
- .toNumber();
+ const diffWithBillingUnits = new Decimal(difference)
+ .mul((cusPrice.price.config as UsagePriceConfig).billing_units || 1)
+ .toNumber();
- if (prorate && stripeSub?.status !== "trialing") {
- const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
+ 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 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(),
- });
+ 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,
- });
- }
+ let amount = new Decimal(newAmount).minus(prevAmount).toNumber();
+ if (prorate) {
+ amount = calculateProrationAmount({
+ periodEnd: end * 1000,
+ periodStart: start * 1000,
+ now: now || Date.now(),
+ amount,
+ });
+ }
- // const amount = priceToInvoiceAmount({
- // price: cusPrice.price,
- // quantity: diffWithBillingUnits,
- // proration: prorate
- // ? {
- // start: start * 1000,
- // end: end * 1000,
- // }
- // : undefined,
- // now,
- // });
+ // const amount = priceToInvoiceAmount({
+ // price: cusPrice.price,
+ // quantity: diffWithBillingUnits,
+ // proration: prorate
+ // ? {
+ // start: start * 1000,
+ // end: end * 1000,
+ // }
+ // : undefined,
+ // now,
+ // });
- const feature = features.find(
- (f: Feature) => f.internal_id == newOptions.internal_feature_id
- )!;
+ const feature = features.find(
+ (f: Feature) => f.internal_id == newOptions.internal_feature_id,
+ )!;
- const product = cusProductToProduct({ cusProduct });
- const invoiceItem = constructStripeInvoiceItem({
- product,
- amount: amount,
- org: org,
- 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),
- });
+ const product = cusProductToProduct({ cusProduct });
+ const invoiceItem = constructStripeInvoiceItem({
+ product,
+ amount: amount,
+ org: org,
+ 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}`
- );
+ logger.info(
+ `🔥 Creating prepaid invoice item: ${invoiceItem.description} - ${amount}`,
+ );
- await stripeCli.invoiceItems.create(invoiceItem);
+ 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,
- logger,
- });
+ if (shouldBillNow(onIncrease)) {
+ const { invoice: finalInvoice } = await createAndFinalizeInvoice({
+ stripeCli,
+ stripeCusId: stripeSub.customer as string,
+ stripeSubId: stripeSub.id,
+ paymentMethod: paymentMethod || null,
+ logger,
+ });
- try {
- const invoiceItems = await getInvoiceItems({
- stripeInvoice: finalInvoice,
- prices: [cusPrice.price],
- 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}`);
- }
- }
- }
+ 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: newOptions.quantity,
- quantity: (subItem.quantity || 0) + subItemDifference,
- proration_behavior: "none",
- });
+ await stripeCli.subscriptionItems.update(subItem.id, {
+ // quantity: newOptions.quantity,
+ quantity: (subItem.quantity || 0) + subItemDifference,
+ proration_behavior: "none",
+ });
- // Update cus ent
+ // Update cus ent
- let cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts: cusProduct.customer_entitlements,
- });
+ let cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts: 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({
- db,
- id: cusEnt.id,
- amount: incrementBy,
- });
- }
+ if (cusEnt) {
+ const incrementBy = new Decimal(difference).mul(billingUnits).toNumber();
+ logger.info(
+ `🔥 Incrementing feature ${cusEnt.entitlement.feature.id} balance by ${incrementBy}`,
+ );
+ await CusEntService.increment({
+ db,
+ id: cusEnt.id,
+ amount: incrementBy,
+ });
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts
index 368d25a0f..989453b6c 100644
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts
+++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateFeatureQuantity.ts
@@ -3,10 +3,10 @@ import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeS
import RecaseError from "@/utils/errorUtils.js";
import {
- ErrCode,
- Feature,
- FeatureOptions,
- FullCusProduct,
+ ErrCode,
+ Feature,
+ FeatureOptions,
+ FullCusProduct,
} from "@autumn/shared";
import { Stripe } from "stripe";
@@ -16,108 +16,108 @@ import { handleQuantityUpgrade } from "./handleQuantityUpgrade.js";
import { handleQuantityDowngrade } from "./handleQuantityDowngrade.js";
export const handleUpdateFeatureQuantity = async ({
- req,
- attachParams,
- cusProduct,
- stripeSubs,
- oldOptions,
- newOptions,
+ req,
+ attachParams,
+ cusProduct,
+ stripeSubs,
+ oldOptions,
+ newOptions,
}: {
- req: any;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- stripeSubs: Stripe.Subscription[];
- oldOptions: FeatureOptions;
- newOptions: FeatureOptions;
+ req: any;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ stripeSubs: Stripe.Subscription[];
+ oldOptions: FeatureOptions;
+ newOptions: FeatureOptions;
}) => {
- const { db, logger } = req;
- const { stripeCli } = attachParams;
+ const { db, logger } = req;
+ const { stripeCli } = attachParams;
- const prorationBehavior = "always_invoice";
+ const prorationBehavior = "always_invoice";
- const subToUpdate = stripeSubs?.[0];
- // const subToUpdate = await getUsageBasedSub({
- // db,
- // stripeCli: stripeCli,
- // subIds: cusProduct.subscription_ids || [],
- // feature: {
- // internal_id: newOptions.internal_feature_id,
- // id: newOptions.feature_id,
- // } as Feature,
- // stripeSubs: stripeSubs,
- // });
+ const subToUpdate = stripeSubs?.[0];
+ // const subToUpdate = await getUsageBasedSub({
+ // db,
+ // stripeCli: stripeCli,
+ // subIds: cusProduct.subscription_ids || [],
+ // feature: {
+ // internal_id: newOptions.internal_feature_id,
+ // id: newOptions.feature_id,
+ // } as Feature,
+ // stripeSubs: stripeSubs,
+ // });
- const cusPrice = featureToCusPrice({
- internalFeatureId: newOptions.internal_feature_id!,
- cusPrices: cusProduct.customer_prices,
- })!;
+ const cusPrice = featureToCusPrice({
+ internalFeatureId: newOptions.internal_feature_id!,
+ cusPrices: cusProduct.customer_prices,
+ })!;
- const price = cusPrice.price;
+ 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,
- });
- }
+ if (!subToUpdate) {
+ throw new RecaseError({
+ message: `Failed to update prepaid quantity for ${newOptions.feature_id} because no subscription found`,
+ code: ErrCode.InternalError,
+ statusCode: 500,
+ });
+ }
- let subItem = findStripeItemForPrice({
- price: price!,
- stripeItems: subToUpdate.items.data,
- }) as Stripe.SubscriptionItem;
+ let subItem = findStripeItemForPrice({
+ price: price!,
+ stripeItems: subToUpdate.items.data,
+ }) as Stripe.SubscriptionItem;
- if (newOptions.quantity < oldOptions.quantity) {
- return await handleQuantityDowngrade({
- req,
- attachParams,
- cusProduct,
- stripeSub: subToUpdate,
- oldOptions,
- newOptions,
- subItem,
- });
- } else {
- return await handleQuantityUpgrade({
- req,
- attachParams,
- cusProduct,
- stripeSubs,
- oldOptions,
- newOptions,
- cusPrice,
- stripeSub: subToUpdate,
- subItem,
- });
- }
+ if (newOptions.quantity < oldOptions.quantity) {
+ return await handleQuantityDowngrade({
+ req,
+ attachParams,
+ cusProduct,
+ stripeSub: subToUpdate,
+ oldOptions,
+ newOptions,
+ subItem,
+ });
+ } else {
+ return await handleQuantityUpgrade({
+ req,
+ attachParams,
+ cusProduct,
+ stripeSubs,
+ oldOptions,
+ newOptions,
+ cusPrice,
+ stripeSub: subToUpdate,
+ subItem,
+ });
+ }
- // if (!price) {
- // throw new RecaseError({
- // message: `updateFeatureQuantity: No price found for feature ${newOptions.feature_id}`,
- // code: ErrCode.PriceNotFound,
- // });
- // }
+ // if (!price) {
+ // throw new RecaseError({
+ // message: `updateFeatureQuantity: No price found for feature ${newOptions.feature_id}`,
+ // code: ErrCode.PriceNotFound,
+ // });
+ // }
- // if (!subItem) {
- // subItem = await stripeCli.subscriptionItems.create({
- // subscription: subToUpdate.id,
- // price: price.config.stripe_price_id as string,
- // quantity: newOptions.quantity,
- // proration_behavior: prorationBehavior,
- // payment_behavior: "error_if_incomplete",
- // });
+ // if (!subItem) {
+ // subItem = await stripeCli.subscriptionItems.create({
+ // subscription: subToUpdate.id,
+ // price: price.config.stripe_price_id as string,
+ // quantity: newOptions.quantity,
+ // proration_behavior: prorationBehavior,
+ // payment_behavior: "error_if_incomplete",
+ // });
- // logger.info(
- // `updateFeatureQuantity: Successfully created sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`,
- // );
- // } else {
- // await stripeCli.subscriptionItems.update(subItem.id, {
- // quantity: newOptions.quantity,
- // proration_behavior: prorationBehavior,
- // payment_behavior: "error_if_incomplete",
- // });
- // logger.info(
- // `updateFeatureQuantity: Successfully updated sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`,
- // );
- // }
+ // logger.info(
+ // `updateFeatureQuantity: Successfully created sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`,
+ // );
+ // } else {
+ // await stripeCli.subscriptionItems.update(subItem.id, {
+ // quantity: newOptions.quantity,
+ // proration_behavior: prorationBehavior,
+ // payment_behavior: "error_if_incomplete",
+ // });
+ // logger.info(
+ // `updateFeatureQuantity: Successfully updated sub item for feature ${newOptions.feature_id}: ${newOptions.quantity}`,
+ // );
+ // }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts
index 4f8a77484..77105ef1d 100644
--- a/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts
+++ b/server/src/internal/customers/attach/attachFunctions/updateQuantityFlow/updateQuantityFlow.ts
@@ -1,7 +1,7 @@
import { SuccessCode } from "@autumn/shared";
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "../../../cusProducts/AttachParams.js";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
import { handleUpdateFeatureQuantity } from "./updateFeatureQuantity.js";
@@ -10,52 +10,52 @@ import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
export const handleUpdateQuantityFunction = async ({
- req,
- res,
- attachParams,
- config,
+ req,
+ res,
+ attachParams,
+ config,
}: {
- req: any;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
+ req: any;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- // 2. Update quantities
- const optionsToUpdate = attachParams.optionsToUpdate!;
- const { customer } = attachParams;
- const { curSameProduct } = attachParamToCusProducts({ attachParams });
+ // 2. Update quantities
+ const optionsToUpdate = attachParams.optionsToUpdate!;
+ const { customer } = attachParams;
+ const { curSameProduct } = attachParamToCusProducts({ attachParams });
- // Check balance of each option to update...?
- const stripeCli = attachParams.stripeCli;
- const cusProduct = curSameProduct!;
- const stripeSubs = await getStripeSubs({
- stripeCli: stripeCli,
- subIds: cusProduct.subscription_ids || [],
- });
+ // Check balance of each option to update...?
+ const stripeCli = attachParams.stripeCli;
+ const cusProduct = curSameProduct!;
+ const stripeSubs = await getStripeSubs({
+ stripeCli: stripeCli,
+ subIds: cusProduct.subscription_ids || [],
+ });
- for (const options of optionsToUpdate) {
- await handleUpdateFeatureQuantity({
- req,
- attachParams,
- cusProduct,
- stripeSubs,
- oldOptions: options.old,
- newOptions: options.new,
- });
- }
+ for (const options of optionsToUpdate) {
+ await handleUpdateFeatureQuantity({
+ req,
+ attachParams,
+ cusProduct,
+ stripeSubs,
+ oldOptions: options.old,
+ newOptions: options.new,
+ });
+ }
- await CusProductService.update({
- db: req.db,
- cusProductId: cusProduct.id,
- updates: { options: optionsToUpdate.map((o) => o.new) },
- });
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: cusProduct.id,
+ updates: { options: optionsToUpdate.map((o) => o.new) },
+ });
- res.status(200).json(
- AttachResultSchema.parse({
- customer_id: customer.id || customer.internal_id,
- product_ids: attachParams.products.map((p) => p.id),
- code: SuccessCode.FeaturesUpdated,
- message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
- }),
- );
+ res.status(200).json(
+ AttachResultSchema.parse({
+ customer_id: customer.id || customer.internal_id,
+ product_ids: attachParams.products.map((p) => p.id),
+ code: SuccessCode.FeaturesUpdated,
+ message: `Successfully updated quantity for features: ${optionsToUpdate.map((o) => o.new.feature_id).join(", ")}`,
+ }),
+ );
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoice.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoice.ts
index 6e9be5713..54ead4441 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoice.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoice.ts
@@ -3,77 +3,77 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { FullCusProduct } from "@autumn/shared";
import Stripe from "stripe";
import {
- createUsageInvoiceItems,
- resetUsageBalances,
+ createUsageInvoiceItems,
+ resetUsageBalances,
} from "./createUsageInvoiceItems.js";
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
export const createUsageInvoice = async ({
- db,
- attachParams,
- cusProduct,
- sub,
- logger,
+ db,
+ attachParams,
+ cusProduct,
+ sub,
+ logger,
}: {
- db: DrizzleCli;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- sub: Stripe.Subscription;
- logger: any;
+ db: DrizzleCli;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ sub: Stripe.Subscription;
+ logger: any;
}) => {
- const { stripeCli, paymentMethod } = attachParams;
- const customer = cusProduct.customer!;
- const invoice = await stripeCli.invoices.create({
- customer: customer.processor.id,
- auto_advance: false,
- });
+ const { stripeCli, paymentMethod } = attachParams;
+ const customer = cusProduct.customer!;
+ const invoice = await stripeCli.invoices.create({
+ customer: customer.processor.id,
+ auto_advance: false,
+ });
- const { cusEntIds } = await createUsageInvoiceItems({
- db,
- attachParams,
- cusProduct,
- sub,
- invoiceId: invoice.id,
- logger,
- });
+ const { cusEntIds } = await createUsageInvoiceItems({
+ db,
+ attachParams,
+ cusProduct,
+ sub,
+ invoiceId: invoice.id,
+ logger,
+ });
- await stripeCli.invoices.finalizeInvoice(invoice.id!, {
- auto_advance: false,
- });
+ await stripeCli.invoices.finalizeInvoice(invoice.id!, {
+ auto_advance: false,
+ });
- const {
- paid,
- error,
- invoice: latestInvoice,
- } = await payForInvoice({
- stripeCli,
- paymentMethod,
- invoiceId: invoice.id!,
- logger,
- errorOnFail: false,
- });
+ const {
+ paid,
+ error,
+ invoice: latestInvoice,
+ } = await payForInvoice({
+ stripeCli,
+ paymentMethod,
+ invoiceId: invoice.id!,
+ logger,
+ errorOnFail: false,
+ });
- if (latestInvoice) {
- await resetUsageBalances({
- db,
- cusEntIds,
- cusProduct,
- });
+ if (latestInvoice) {
+ await resetUsageBalances({
+ db,
+ cusEntIds,
+ cusProduct,
+ });
- await insertInvoiceFromAttach({
- db,
- invoiceId: latestInvoice.id,
- attachParams,
- logger,
- });
- }
+ await insertInvoiceFromAttach({
+ db,
+ invoiceId: latestInvoice.id,
+ attachParams,
+ logger,
+ });
+ }
- if (!paid) {
- logger.error(`sub.deleted, failed to pay invoice: ${invoice.id}`, {
- error,
- });
- }
+ if (!paid) {
+ logger.error(`sub.deleted, failed to pay invoice: ${invoice.id}`, {
+ error,
+ });
+ }
- return invoice;
+ return invoice;
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts
index e5e8239ec..94e4bb262 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/createUsageInvoiceItems.ts
@@ -4,203 +4,203 @@ import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
- getCusPriceUsage,
- getRelatedCusEnt,
+ getCusPriceUsage,
+ getRelatedCusEnt,
} from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
import {
- formatPrice,
- getBillingType,
+ formatPrice,
+ getBillingType,
} from "@/internal/products/prices/priceUtils.js";
import {
- FullCusProduct,
- UsagePriceConfig,
- BillingType,
- BillingInterval,
- intervalsDifferent,
- CusProductStatus,
+ FullCusProduct,
+ UsagePriceConfig,
+ BillingType,
+ BillingInterval,
+ intervalsDifferent,
+ CusProductStatus,
} from "@autumn/shared";
import Stripe from "stripe";
export const getUsageInvoiceItems = async ({
- db,
- logger,
- attachParams,
- cusProduct,
- // stripeSubs,
- sub,
- interval,
- intervalCount,
+ db,
+ logger,
+ attachParams,
+ cusProduct,
+ // stripeSubs,
+ sub,
+ interval,
+ intervalCount,
}: {
- db: DrizzleCli;
- logger: any;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- // stripeSubs: Stripe.Subscription[];
- sub: Stripe.Subscription;
- interval?: BillingInterval;
- intervalCount?: number;
+ db: DrizzleCli;
+ logger: any;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ // stripeSubs: Stripe.Subscription[];
+ sub: Stripe.Subscription;
+ interval?: BillingInterval;
+ intervalCount?: number;
}) => {
- const { stripeCli, org } = attachParams;
+ const { stripeCli, org } = attachParams;
- const cusPrices = cusProductsToCusPrices({
- cusProducts: [cusProduct],
- });
- // const ents = cusProductToEnts({ cusProduct });
- const cusEnts = cusProductsToCusEnts({
- cusProducts: [cusProduct],
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.Expired,
- CusProductStatus.PastDue,
- ],
- });
+ const cusPrices = cusProductsToCusPrices({
+ cusProducts: [cusProduct],
+ });
+ // const ents = cusProductToEnts({ cusProduct });
+ const cusEnts = cusProductsToCusEnts({
+ cusProducts: [cusProduct],
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.Expired,
+ CusProductStatus.PastDue,
+ ],
+ });
- const invoiceItems: any[] = [];
- const cusEntIds: string[] = [];
+ const invoiceItems: any[] = [];
+ const cusEntIds: string[] = [];
- for (const cusPrice of cusPrices) {
- const config = cusPrice.price.config! as UsagePriceConfig;
- const billingType = getBillingType(config);
+ for (const cusPrice of cusPrices) {
+ const config = cusPrice.price.config! as UsagePriceConfig;
+ const billingType = getBillingType(config);
- if (billingType !== BillingType.UsageInArrear) continue;
+ if (billingType !== BillingType.UsageInArrear) continue;
- const { description, amount } = getCusPriceUsage({
- cusPrice,
- cusProduct,
- logger,
- });
+ const { description, amount } = getCusPriceUsage({
+ cusPrice,
+ cusProduct,
+ logger,
+ });
- if (amount <= 0) continue;
+ if (amount <= 0) continue;
- const cusEnt = getRelatedCusEnt({ cusPrice, cusEnts })!;
+ const cusEnt = getRelatedCusEnt({ cusPrice, cusEnts })!;
- if (!cusEnt) {
- console.log("Price:", formatPrice({ price: cusPrice.price }));
- console.log("Cus ents:", cusEnts);
- console.log("NO CUS ENT FOUND");
- }
+ if (!cusEnt) {
+ console.log("Price:", formatPrice({ price: cusPrice.price }));
+ console.log("Cus ents:", cusEnts);
+ console.log("NO CUS ENT FOUND");
+ }
- if (
- interval &&
- intervalsDifferent({
- intervalA: { interval, intervalCount },
- intervalB: subToAutumnInterval(sub),
- })
- )
- continue;
+ if (
+ interval &&
+ intervalsDifferent({
+ intervalA: { interval, intervalCount },
+ intervalB: subToAutumnInterval(sub),
+ })
+ )
+ continue;
- cusEntIds.push(cusEnt.id);
+ cusEntIds.push(cusEnt.id);
- let invoiceItem = {
- description,
- price_data: {
- product: config.stripe_product_id!,
- unit_amount: Math.round(amount * 100),
- currency: org.default_currency || "usd",
- },
- period: {
- start: sub.items.data[0].current_period_start,
- end: Math.floor((attachParams.now || Date.now()) / 1000),
- },
- };
+ let invoiceItem = {
+ description,
+ price_data: {
+ product: config.stripe_product_id!,
+ unit_amount: Math.round(amount * 100),
+ currency: org.default_currency || "usd",
+ },
+ period: {
+ start: sub.items.data[0].current_period_start,
+ end: Math.floor((attachParams.now || Date.now()) / 1000),
+ },
+ };
- invoiceItems.push(invoiceItem);
- }
+ invoiceItems.push(invoiceItem);
+ }
- return {
- invoiceItems,
- cusEntIds,
- };
+ return {
+ invoiceItems,
+ cusEntIds,
+ };
};
export const createUsageInvoiceItems = async ({
- db,
- attachParams,
- cusProduct,
- // stripeSubs,
- sub,
- invoiceId,
- logger,
- interval,
- intervalCount,
+ db,
+ attachParams,
+ cusProduct,
+ // stripeSubs,
+ sub,
+ invoiceId,
+ logger,
+ interval,
+ intervalCount,
}: {
- db: DrizzleCli;
- attachParams: AttachParams;
- cusProduct: FullCusProduct;
- // stripeSubs: Stripe.Subscription[];
- sub: Stripe.Subscription;
- invoiceId?: string;
- logger: any;
- interval?: BillingInterval;
- intervalCount?: number;
+ db: DrizzleCli;
+ attachParams: AttachParams;
+ cusProduct: FullCusProduct;
+ // stripeSubs: Stripe.Subscription[];
+ sub: Stripe.Subscription;
+ invoiceId?: string;
+ logger: any;
+ interval?: BillingInterval;
+ intervalCount?: number;
}) => {
- const { stripeCli } = attachParams;
+ const { stripeCli } = attachParams;
- const { invoiceItems, cusEntIds } = await getUsageInvoiceItems({
- db,
- attachParams,
- cusProduct,
- // stripeSubs,
- sub,
- interval,
- intervalCount,
- logger,
- });
+ const { invoiceItems, cusEntIds } = await getUsageInvoiceItems({
+ db,
+ attachParams,
+ cusProduct,
+ // stripeSubs,
+ sub,
+ interval,
+ intervalCount,
+ logger,
+ });
- const batchCreate = [];
+ const batchCreate = [];
- for (let i = 0; i < invoiceItems.length; i++) {
- const invoiceItem = invoiceItems[i];
- const createInvoiceItem = async () => {
- logger.info(
- `🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}`
- );
+ for (let i = 0; i < invoiceItems.length; i++) {
+ const invoiceItem = invoiceItems[i];
+ const createInvoiceItem = async () => {
+ logger.info(
+ `🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}`,
+ );
- await stripeCli.invoiceItems.create({
- ...invoiceItem,
- invoice: invoiceId ? invoiceId : undefined,
- subscription: invoiceId ? undefined : sub.id,
- customer: attachParams.customer.processor.id,
- });
- };
+ await stripeCli.invoiceItems.create({
+ ...invoiceItem,
+ invoice: invoiceId ? invoiceId : undefined,
+ subscription: invoiceId ? undefined : sub.id,
+ customer: attachParams.customer.processor.id,
+ });
+ };
- batchCreate.push(createInvoiceItem());
- }
- await Promise.all(batchCreate);
+ batchCreate.push(createInvoiceItem());
+ }
+ await Promise.all(batchCreate);
- return {
- invoiceItems,
- cusEntIds,
- };
+ return {
+ invoiceItems,
+ cusEntIds,
+ };
};
export const resetUsageBalances = async ({
- db,
- cusEntIds,
- cusProduct,
+ db,
+ cusEntIds,
+ cusProduct,
}: {
- db: DrizzleCli;
- cusEntIds: string[];
- cusProduct: FullCusProduct;
+ db: DrizzleCli;
+ cusEntIds: string[];
+ cusProduct: FullCusProduct;
}) => {
- for (const cusEntId of cusEntIds) {
- await CusEntService.update({
- db,
- id: cusEntId,
- updates: {
- balance: 0,
- },
- });
+ for (const cusEntId of cusEntIds) {
+ await CusEntService.update({
+ db,
+ id: cusEntId,
+ updates: {
+ balance: 0,
+ },
+ });
- let index = cusProduct.customer_entitlements.findIndex(
- (ce) => ce.id === cusEntId
- );
+ let index = cusProduct.customer_entitlements.findIndex(
+ (ce) => ce.id === cusEntId,
+ );
- cusProduct.customer_entitlements[index] = {
- ...cusProduct.customer_entitlements[index],
- balance: 0,
- };
- }
+ cusProduct.customer_entitlements[index] = {
+ ...cusProduct.customer_entitlements[index],
+ balance: 0,
+ };
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts
index eb20fd08e..235d8beb3 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/upgradeUtils.ts
@@ -2,7 +2,7 @@ import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSub
import Stripe from "stripe";
export const getNextCycle = (stripeSubs: Stripe.Subscription[]) => {
- const { end } = subToPeriodStartEnd({ sub: stripeSubs[0] });
- const nextCycle = end * 1000;
- return nextCycle;
+ const { end } = subToPeriodStartEnd({ sub: stripeSubs[0] });
+ const nextCycle = end * 1000;
+ return nextCycle;
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts
index 25744e4f6..675f867bf 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts
@@ -1,31 +1,31 @@
import {
- AttachParams,
- AttachResultSchema,
+ AttachParams,
+ AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import {
- attachParamsToCurCusProduct,
- attachParamsToProduct,
- paramsToCurSub,
- paramsToCurSubSchedule,
+ attachParamsToCurCusProduct,
+ attachParamsToProduct,
+ paramsToCurSub,
+ paramsToCurSubSchedule,
} from "../../attachUtils/convertAttachParams.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import {
- APIVersion,
- AttachBranch,
- AttachConfig,
- AttachScenario,
- CusProductStatus,
- cusProductToProduct,
- logCusProducts,
- ProrationBehavior,
+ APIVersion,
+ AttachBranch,
+ AttachConfig,
+ AttachScenario,
+ CusProductStatus,
+ cusProductToProduct,
+ logCusProducts,
+ ProrationBehavior,
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- attachToInvoiceResponse,
- insertInvoiceFromAttach,
+ attachToInvoiceResponse,
+ insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { updateStripeSub2 } from "./updateStripeSub2.js";
@@ -37,204 +37,204 @@ import { handleUpgradeFlowSchedule } from "./handleUpgradeFlowSchedule.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
export const handleUpgradeFlow = async ({
- req,
- res,
- attachParams,
- config,
- branch,
+ req,
+ res,
+ attachParams,
+ config,
+ branch,
}: {
- req: ExtendedRequest;
- res?: any;
- attachParams: AttachParams;
- config: AttachConfig;
- branch: AttachBranch;
+ req: ExtendedRequest;
+ res?: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ branch: AttachBranch;
}) => {
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const curSub = await paramsToCurSub({ attachParams });
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const curSub = await paramsToCurSub({ attachParams });
- const logger = req.logtail;
+ const logger = req.logtail;
- if (curCusProduct?.api_version) {
- attachParams.apiVersion = curCusProduct.api_version;
- }
+ if (curCusProduct?.api_version) {
+ attachParams.apiVersion = curCusProduct.api_version;
+ }
- let sub = curSub;
- let latestInvoice = undefined;
+ let sub = curSub;
+ let latestInvoice = undefined;
- const itemSet = await getStripeSubItems2({
- attachParams,
- config,
- });
+ const itemSet = await getStripeSubItems2({
+ attachParams,
+ config,
+ });
- const newItemSet = await paramsToSubItems({
- req,
- sub: curSub,
- attachParams,
- config,
- });
+ const newItemSet = await paramsToSubItems({
+ req,
+ sub: curSub,
+ attachParams,
+ config,
+ });
- const { subItems } = newItemSet;
+ const { subItems } = newItemSet;
- const products = attachParams.fromCancel
- ? [cusProductToProduct({ cusProduct: attachParams.cusProduct! })]
- : attachParams.products;
+ const products = attachParams.fromCancel
+ ? [cusProductToProduct({ cusProduct: attachParams.cusProduct! })]
+ : attachParams.products;
- for (const product of products) {
- if (product.is_add_on) continue;
+ for (const product of products) {
+ if (product.is_add_on) continue;
- const { curScheduledProduct } = getExistingCusProducts({
- product,
- cusProducts: attachParams.cusProducts,
- internalEntityId: attachParams.internalEntityId,
- });
+ const { curScheduledProduct } = getExistingCusProducts({
+ product,
+ cusProducts: attachParams.cusProducts,
+ internalEntityId: attachParams.internalEntityId,
+ });
- if (curScheduledProduct) {
- await CusProductService.delete({
- db: req.db,
- cusProductId: curScheduledProduct.id,
- });
- }
- }
+ if (curScheduledProduct) {
+ await CusProductService.delete({
+ db: req.db,
+ cusProductId: curScheduledProduct.id,
+ });
+ }
+ }
- let canceled = false;
- // SCENARIO 1, NO SUB:
+ let canceled = false;
+ // SCENARIO 1, NO SUB:
- // Don't really need this...
- if (branch == AttachBranch.SameCustomEnts) {
- config.proration = ProrationBehavior.None;
- }
+ // Don't really need this...
+ if (branch == AttachBranch.SameCustomEnts) {
+ config.proration = ProrationBehavior.None;
+ }
- if (!curSub) {
- logger.info("UPGRADE FLOW: no sub (from cancel maybe...?)");
- // Do something about current sub...
- } else if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) {
- logger.info(
- `UPGRADE FLOW: canceling sub ${curSub!.id}, proration: ${config.proration}`
- );
- canceled = true;
- const { stripeCli } = attachParams;
- await stripeCli.subscriptions.cancel(curSub!.id, {
- prorate: config.proration == ProrationBehavior.Immediately,
- invoice_now: config.proration == ProrationBehavior.Immediately,
- cancellation_details: {
- comment: "autumn_cancel",
- },
- });
- } else if (subItems.length > 0) {
- logger.info(`UPGRADE FLOW, updating sub ${curSub!.id}`);
- itemSet.subItems = subItems;
+ if (!curSub) {
+ logger.info("UPGRADE FLOW: no sub (from cancel maybe...?)");
+ // Do something about current sub...
+ } else if (shouldCancelSub({ sub: curSub!, newSubItems: subItems })) {
+ logger.info(
+ `UPGRADE FLOW: canceling sub ${curSub!.id}, proration: ${config.proration}`,
+ );
+ canceled = true;
+ const { stripeCli } = attachParams;
+ await stripeCli.subscriptions.cancel(curSub!.id, {
+ prorate: config.proration == ProrationBehavior.Immediately,
+ invoice_now: config.proration == ProrationBehavior.Immediately,
+ cancellation_details: {
+ comment: "autumn_cancel",
+ },
+ });
+ } else if (subItems.length > 0) {
+ logger.info(`UPGRADE FLOW, updating sub ${curSub!.id}`);
+ itemSet.subItems = subItems;
- // await logPhaseItems({
- // db: req.db,
- // items: itemSet.subItems,
- // });
+ // await logPhaseItems({
+ // db: req.db,
+ // items: itemSet.subItems,
+ // });
- const res = await updateStripeSub2({
- req,
- attachParams,
- config,
- curSub: curSub!,
- itemSet,
- fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product...
- });
+ const res = await updateStripeSub2({
+ req,
+ attachParams,
+ config,
+ curSub: curSub!,
+ itemSet,
+ fromCreate: attachParams.products.length === 0, // just for now, if no products, it comes from cancel product...
+ });
- if (res?.latestInvoice) {
- logger.info(`UPGRADE FLOW: inserting invoice ${res.latestInvoice.id}`);
- await insertInvoiceFromAttach({
- db: req.db,
- attachParams,
- stripeInvoice: res.latestInvoice,
- logger,
- });
- }
+ if (res?.latestInvoice) {
+ logger.info(`UPGRADE FLOW: inserting invoice ${res.latestInvoice.id}`);
+ await insertInvoiceFromAttach({
+ db: req.db,
+ attachParams,
+ stripeInvoice: res.latestInvoice,
+ logger,
+ });
+ }
- const schedule = await paramsToCurSubSchedule({ attachParams });
+ const schedule = await paramsToCurSubSchedule({ attachParams });
- if (schedule) {
- await handleUpgradeFlowSchedule({
- req,
- logger,
- attachParams,
- config,
- schedule,
- curSub,
- });
- }
+ if (schedule) {
+ await handleUpgradeFlowSchedule({
+ req,
+ logger,
+ attachParams,
+ config,
+ schedule,
+ curSub,
+ });
+ }
- attachParams.replaceables = res.replaceables || [];
- sub = res.updatedSub;
- latestInvoice = res.latestInvoice;
- }
+ attachParams.replaceables = res.replaceables || [];
+ sub = res.updatedSub;
+ latestInvoice = res.latestInvoice;
+ }
- if (curCusProduct) {
- logger.info(`UPGRADE FLOW: expiring previous cus product`);
- await CusProductService.update({
- db: req.db,
- cusProductId: curCusProduct!.id,
- updates: {
- subscription_ids: canceled ? undefined : [],
- status: CusProductStatus.Expired,
- },
- });
+ if (curCusProduct) {
+ logger.info(`UPGRADE FLOW: expiring previous cus product`);
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: curCusProduct!.id,
+ updates: {
+ subscription_ids: canceled ? undefined : [],
+ status: CusProductStatus.Expired,
+ },
+ });
- try {
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: curCusProduct.internal_customer_id,
- org: attachParams.org,
- env: attachParams.customer.env,
- customerId:
- attachParams.customer.id || attachParams.customer.internal_id,
- scenario: AttachScenario.Expired,
- cusProduct: curCusProduct,
- logger,
- });
- } catch (error) {
- logger.error("UPGRADE FLOW: failed to add to webhook queue", { error });
- }
- }
+ try {
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: curCusProduct.internal_customer_id,
+ org: attachParams.org,
+ env: attachParams.customer.env,
+ customerId:
+ attachParams.customer.id || attachParams.customer.internal_id,
+ scenario: AttachScenario.Expired,
+ cusProduct: curCusProduct,
+ logger,
+ });
+ } catch (error) {
+ logger.error("UPGRADE FLOW: failed to add to webhook queue", { error });
+ }
+ }
- if (attachParams.products.length > 0) {
- logger.info(`UPGRADE FLOW: creating new cus product`);
- const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined;
- await createFullCusProduct({
- db: req.db,
- attachParams: attachToInsertParams(
- attachParams,
- attachParams.products[0]
- ),
- subscriptionIds: curCusProduct!.subscription_ids || [],
- disableFreeTrial: config.disableTrial,
- carryExistingUsages: config.carryUsage,
- carryOverTrial: config.carryTrial,
- anchorToUnix: anchorToUnix,
- scenario: AttachScenario.Upgrade,
- logger,
- });
- }
+ if (attachParams.products.length > 0) {
+ logger.info(`UPGRADE FLOW: creating new cus product`);
+ const anchorToUnix = sub ? getEarliestPeriodEnd({ sub }) * 1000 : undefined;
+ await createFullCusProduct({
+ db: req.db,
+ attachParams: attachToInsertParams(
+ attachParams,
+ attachParams.products[0],
+ ),
+ subscriptionIds: curCusProduct!.subscription_ids || [],
+ disableFreeTrial: config.disableTrial,
+ carryExistingUsages: config.carryUsage,
+ carryOverTrial: config.carryTrial,
+ anchorToUnix: anchorToUnix,
+ scenario: AttachScenario.Upgrade,
+ logger,
+ });
+ }
- if (res) {
- let apiVersion = attachParams.org.api_version || APIVersion.v1;
- if (apiVersion >= APIVersion.v1_1) {
- res.status(200).json(
- AttachResultSchema.parse({
- customer_id: attachParams.customer.id,
- product_ids: attachParams.products.map((p) => p.id),
- // invoice: attachParams.invoiceOnly
- // ? attachToInvoiceResponse({ invoice: invoices?.[0] })
- // : undefined,
- invoice: attachParams.invoiceOnly
- ? attachToInvoiceResponse({ invoice: latestInvoice || undefined })
- : undefined,
- code: "updated_product_successfully",
- message: `Successfully updated product`,
- })
- );
- } else {
- res.status(200).json({
- success: true,
- message: `Successfully updated product`,
- });
- }
- }
+ if (res) {
+ let apiVersion = attachParams.org.api_version || APIVersion.v1;
+ if (apiVersion >= APIVersion.v1_1) {
+ res.status(200).json(
+ AttachResultSchema.parse({
+ customer_id: attachParams.customer.id,
+ product_ids: attachParams.products.map((p) => p.id),
+ // invoice: attachParams.invoiceOnly
+ // ? attachToInvoiceResponse({ invoice: invoices?.[0] })
+ // : undefined,
+ invoice: attachParams.invoiceOnly
+ ? attachToInvoiceResponse({ invoice: latestInvoice || undefined })
+ : undefined,
+ code: "updated_product_successfully",
+ message: `Successfully updated product`,
+ }),
+ );
+ } else {
+ res.status(200).json({
+ success: true,
+ message: `Successfully updated product`,
+ });
+ }
+ }
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts
index 044224a6f..4dacc3ef3 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlowSchedule.ts
@@ -4,117 +4,117 @@ import { AttachConfig, FullCusProduct } from "@autumn/shared";
import Stripe from "stripe";
import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js";
import {
- logPhases,
- getCurrentPhaseIndex,
+ logPhases,
+ getCurrentPhaseIndex,
} from "../../mergeUtils/phaseUtils/phaseUtils.js";
import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- ACTIVE_STATUSES,
- CusProductService,
+ ACTIVE_STATUSES,
+ CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const handleUpgradeFlowSchedule = async ({
- req,
- attachParams,
- config,
- schedule,
- curSub,
- removeCusProducts,
- logger,
- fromAddProduct = false,
+ req,
+ attachParams,
+ config,
+ schedule,
+ curSub,
+ removeCusProducts,
+ logger,
+ fromAddProduct = false,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- config: AttachConfig;
- schedule: Stripe.SubscriptionSchedule;
- curSub: Stripe.Subscription;
- removeCusProducts?: FullCusProduct[];
- logger: any;
- fromAddProduct?: boolean;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ schedule: Stripe.SubscriptionSchedule;
+ curSub: Stripe.Subscription;
+ removeCusProducts?: FullCusProduct[];
+ logger: any;
+ fromAddProduct?: boolean;
}) => {
- if (fromAddProduct) {
- logger.info(`ADD PRODUCT FLOW, updating schedule ${schedule?.id}`);
- } else {
- logger.info(`UPGRADE FLOW, updating schedule ${schedule?.id}`);
- }
+ if (fromAddProduct) {
+ logger.info(`ADD PRODUCT FLOW, updating schedule ${schedule?.id}`);
+ } else {
+ logger.info(`UPGRADE FLOW, updating schedule ${schedule?.id}`);
+ }
- const { stripeCli, customer, prices } = attachParams;
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const { stripeCli, customer, prices } = attachParams;
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const currentPhaseIndex = getCurrentPhaseIndex({
- schedule,
- now: attachParams.now,
- });
+ const currentPhaseIndex = getCurrentPhaseIndex({
+ schedule,
+ now: attachParams.now,
+ });
- const nextPhaseIndex = currentPhaseIndex + 1;
+ const nextPhaseIndex = currentPhaseIndex + 1;
- if (currentPhaseIndex == -1 || nextPhaseIndex >= schedule.phases.length)
- return;
+ if (currentPhaseIndex == -1 || nextPhaseIndex >= schedule.phases.length)
+ return;
- const newItems = await paramsToScheduleItems({
- req,
- schedule,
- attachParams,
- config,
- billingPeriodEnd: schedule?.phases?.[nextPhaseIndex]?.start_date,
- removeCusProducts,
- });
+ const newItems = await paramsToScheduleItems({
+ req,
+ schedule,
+ attachParams,
+ config,
+ billingPeriodEnd: schedule?.phases?.[nextPhaseIndex]?.start_date,
+ removeCusProducts,
+ });
- // Should release schedule...
- const newCurPhaseIndex = getCurrentPhaseIndex({
- schedule: { phases: newItems.phases } as any,
- now: attachParams.now,
- });
+ // Should release schedule...
+ const newCurPhaseIndex = getCurrentPhaseIndex({
+ schedule: { phases: newItems.phases } as any,
+ now: attachParams.now,
+ });
- // If there are no subsequent phases, release schedule...
- // Example: mergedUpgrade4.test.ts, mergedCancel2.test.ts
- // pro, pro -> free, pro -> premium, pro (need to cancel initial schedule)
- if (newCurPhaseIndex == newItems.phases.length - 1) {
- logger.info(
- `UPGRADE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`
- );
- await stripeCli.subscriptionSchedules.release(schedule!.id);
- await CusProductService.updateByStripeScheduledId({
- db: req.db,
- stripeScheduledId: schedule!.id,
- updates: { scheduled_ids: [] },
- });
+ // If there are no subsequent phases, release schedule...
+ // Example: mergedUpgrade4.test.ts, mergedCancel2.test.ts
+ // pro, pro -> free, pro -> premium, pro (need to cancel initial schedule)
+ if (newCurPhaseIndex == newItems.phases.length - 1) {
+ logger.info(
+ `UPGRADE FLOW: no subsequent phases, releasing schedule ${schedule?.id}`,
+ );
+ await stripeCli.subscriptionSchedules.release(schedule!.id);
+ await CusProductService.updateByStripeScheduledId({
+ db: req.db,
+ stripeScheduledId: schedule!.id,
+ updates: { scheduled_ids: [] },
+ });
- // Should we cancel the sub...?
- // If all other products are canceled, and new product is free, cancel the sub...
- const shouldCancelSub =
- customer.customer_products
- .filter(
- (cp) =>
- cp.id !== curCusProduct?.id &&
- cp.subscription_ids?.includes(curSub.id) &&
- ACTIVE_STATUSES.includes(cp.status)
- )
- .every((cp) => cp.canceled) && isFreeProduct(prices);
+ // Should we cancel the sub...?
+ // If all other products are canceled, and new product is free, cancel the sub...
+ const shouldCancelSub =
+ customer.customer_products
+ .filter(
+ (cp) =>
+ cp.id !== curCusProduct?.id &&
+ cp.subscription_ids?.includes(curSub.id) &&
+ ACTIVE_STATUSES.includes(cp.status),
+ )
+ .every((cp) => cp.canceled) && isFreeProduct(prices);
- if (shouldCancelSub) {
- logger.info(`UPGRADE FLOW: canceling sub ${curSub?.id}`);
- await stripeCli.subscriptions.update(curSub.id, {
- cancel_at_period_end: true,
- });
- }
+ if (shouldCancelSub) {
+ logger.info(`UPGRADE FLOW: canceling sub ${curSub?.id}`);
+ await stripeCli.subscriptions.update(curSub.id, {
+ cancel_at_period_end: true,
+ });
+ }
- return;
- }
+ return;
+ }
- // await logPhases({
- // phases: newItems.phases,
- // db: req.db,
- // });
+ // await logPhases({
+ // phases: newItems.phases,
+ // db: req.db,
+ // });
- await updateCurSchedule({
- req,
- attachParams,
- schedule,
- newPhases: newItems.phases,
- sub: curSub!,
- });
+ await updateCurSchedule({
+ req,
+ attachParams,
+ schedule,
+ newPhases: newItems.phases,
+ sub: curSub!,
+ });
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts
index 23097a957..a53b7770e 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/upgradeFlowUtils.ts
@@ -1,16 +1,16 @@
import Stripe from "stripe";
export const shouldCancelSub = ({
- sub,
- newSubItems,
+ sub,
+ newSubItems,
}: {
- sub: Stripe.Subscription;
- newSubItems: Stripe.SubscriptionUpdateParams.Item[];
+ sub: Stripe.Subscription;
+ newSubItems: Stripe.SubscriptionUpdateParams.Item[];
}) => {
- for (const item of sub.items.data) {
- const isDeleted = newSubItems.some((i) => i.id == item.id && i.deleted);
- if (!isDeleted) return false;
- }
+ for (const item of sub.items.data) {
+ const isDeleted = newSubItems.some((i) => i.id == item.id && i.deleted);
+ if (!isDeleted) return false;
+ }
- return sub.items.data.length == newSubItems.length;
+ return sub.items.data.length == newSubItems.length;
};
diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts
index 4ec5b1119..0cca8d14d 100644
--- a/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts
+++ b/server/src/internal/customers/attach/attachFunctions/upgradeSameIntFlow/comparePriceEntIds.ts
@@ -2,45 +2,45 @@ import { EntitlementWithFeature } from "@autumn/shared";
import { Price } from "@autumn/shared";
export const hasPriceIdsChanged = ({
- oldPrices,
- newPrices,
+ oldPrices,
+ newPrices,
}: {
- oldPrices: Price[];
- newPrices: Price[];
+ oldPrices: Price[];
+ newPrices: Price[];
}) => {
- for (const price of oldPrices) {
- if (!newPrices.some((p) => p.id === price.id)) {
- return true;
- }
- }
+ for (const price of oldPrices) {
+ if (!newPrices.some((p) => p.id === price.id)) {
+ return true;
+ }
+ }
- for (const price of newPrices) {
- if (!oldPrices.some((p) => p.id === price.id)) {
- return true;
- }
- }
+ for (const price of newPrices) {
+ if (!oldPrices.some((p) => p.id === price.id)) {
+ return true;
+ }
+ }
- return false;
+ return false;
};
export const hasEntIdsChanged = ({
- oldEntitlements,
- newEntitlements,
+ oldEntitlements,
+ newEntitlements,
}: {
- oldEntitlements: EntitlementWithFeature[];
- newEntitlements: EntitlementWithFeature[];
+ oldEntitlements: EntitlementWithFeature[];
+ newEntitlements: EntitlementWithFeature[];
}) => {
- for (const entitlement of oldEntitlements) {
- if (!newEntitlements.some((e) => e.id === entitlement.id)) {
- return true;
- }
- }
+ for (const entitlement of oldEntitlements) {
+ if (!newEntitlements.some((e) => e.id === entitlement.id)) {
+ return true;
+ }
+ }
- for (const entitlement of newEntitlements) {
- if (!oldEntitlements.some((e) => e.id === entitlement.id)) {
- return true;
- }
- }
+ for (const entitlement of newEntitlements) {
+ if (!oldEntitlements.some((e) => e.id === entitlement.id)) {
+ return true;
+ }
+ }
- return false;
+ return false;
};
diff --git a/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts b/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts
index 12a8f7c43..8871b5afa 100644
--- a/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts
+++ b/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts
@@ -1,131 +1,131 @@
import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js";
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import {
- getPriceEntitlement,
- getPriceForOverage,
+ getPriceEntitlement,
+ getPriceForOverage,
} from "@/internal/products/prices/priceUtils.js";
import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import {
- isFixedPrice,
- isOneOffPrice,
+ isFixedPrice,
+ isOneOffPrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- formatReward,
- getAmountAfterReward,
- getAmountAfterStripeDiscounts,
+ formatReward,
+ getAmountAfterReward,
+ getAmountAfterStripeDiscounts,
} from "@/internal/rewards/rewardUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import {
- EntitlementWithFeature,
- formatAmount,
- FullProduct,
- Organization,
- Price,
- Reward,
+ EntitlementWithFeature,
+ formatAmount,
+ FullProduct,
+ Organization,
+ Price,
+ Reward,
} from "@autumn/shared";
import Stripe from "stripe";
export const priceToNewPreviewItem = ({
- org,
- price,
- entitlements,
- skipOneOff,
- now,
- anchor,
- productQuantity = 1,
- product,
- onTrial,
- rewards,
- subDiscounts,
+ org,
+ price,
+ entitlements,
+ skipOneOff,
+ now,
+ anchor,
+ productQuantity = 1,
+ product,
+ onTrial,
+ rewards,
+ subDiscounts,
}: {
- org: Organization;
- price: Price;
- entitlements: EntitlementWithFeature[];
- skipOneOff?: boolean;
- now?: number;
- anchor?: number;
- productQuantity?: number;
- product: FullProduct;
- onTrial?: boolean;
- rewards?: Reward[];
- subDiscounts?: Stripe.Discount[];
+ org: Organization;
+ price: Price;
+ entitlements: EntitlementWithFeature[];
+ skipOneOff?: boolean;
+ now?: number;
+ anchor?: number;
+ productQuantity?: number;
+ product: FullProduct;
+ onTrial?: boolean;
+ rewards?: Reward[];
+ subDiscounts?: Stripe.Discount[];
}) => {
- if (skipOneOff && isOneOffPrice({ price })) return;
+ if (skipOneOff && isOneOffPrice({ price })) return;
- now = now ?? Date.now();
+ now = now ?? Date.now();
- const ent = getPriceEntitlement(price, entitlements);
+ const ent = getPriceEntitlement(price, entitlements);
- const finalProration = getProration({
- anchor,
- now,
- intervalConfig: {
- interval: price.config.interval!,
- intervalCount: price.config.interval_count || 1,
- },
- });
+ const finalProration = getProration({
+ anchor,
+ now,
+ intervalConfig: {
+ interval: price.config.interval!,
+ intervalCount: price.config.interval_count || 1,
+ },
+ });
- const applyRewards = rewards?.filter(
- (r) =>
- r.discount_config?.price_ids?.includes(price.id) ||
- r.discount_config?.apply_to_all
- );
+ const applyRewards = rewards?.filter(
+ (r) =>
+ r.discount_config?.price_ids?.includes(price.id) ||
+ r.discount_config?.apply_to_all,
+ );
- for (const reward of applyRewards ?? []) {
- console.log("Apply Reward", formatReward({ reward }));
- }
+ for (const reward of applyRewards ?? []) {
+ console.log("Apply Reward", formatReward({ reward }));
+ }
- if (isFixedPrice({ price })) {
- let amount = priceToInvoiceAmount({
- price,
- quantity: 1,
- proration: finalProration,
- productQuantity,
- now,
- });
+ if (isFixedPrice({ price })) {
+ let amount = priceToInvoiceAmount({
+ price,
+ quantity: 1,
+ proration: finalProration,
+ productQuantity,
+ now,
+ });
- if (onTrial) {
- amount = 0;
- }
+ if (onTrial) {
+ amount = 0;
+ }
- for (const reward of applyRewards ?? []) {
- amount = getAmountAfterReward({
- amount,
- reward,
- subDiscounts: subDiscounts ?? [],
- });
- }
+ for (const reward of applyRewards ?? []) {
+ amount = getAmountAfterReward({
+ amount,
+ reward,
+ subDiscounts: subDiscounts ?? [],
+ });
+ }
- amount = getAmountAfterStripeDiscounts({
- price,
- amount,
- product,
- stripeDiscounts: subDiscounts ?? [],
- });
+ amount = getAmountAfterStripeDiscounts({
+ price,
+ amount,
+ product,
+ stripeDiscounts: subDiscounts ?? [],
+ });
- let description = newPriceToInvoiceDescription({
- org,
- price,
- product,
- });
+ let description = newPriceToInvoiceDescription({
+ org,
+ price,
+ product,
+ });
- if (productQuantity > 1) {
- description = `${description} x ${productQuantity}`;
- }
+ if (productQuantity > 1) {
+ description = `${description} x ${productQuantity}`;
+ }
- if (finalProration) {
- description = `${description} (from ${formatUnixToDate(now)})`;
- }
+ if (finalProration) {
+ description = `${description} (from ${formatUnixToDate(now)})`;
+ }
- return {
- price_id: price.id,
- price: formatAmount({ org, amount }),
- description,
- amount,
- usage_model: priceToUsageModel(price),
- feature_id: ent?.feature_id,
- };
- }
+ return {
+ price_id: price.id,
+ price: formatAmount({ org, amount }),
+ description,
+ amount,
+ usage_model: priceToUsageModel(price),
+ feature_id: ent?.feature_id,
+ };
+ }
};
diff --git a/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts b/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts
index d3a424c30..41b28bc4f 100644
--- a/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts
+++ b/server/src/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.ts
@@ -3,13 +3,13 @@ import { priceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtil
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import {
- Price,
- FullCusProduct,
- Organization,
- formatAmount,
- UsagePriceConfig,
- getTotalCusProdQuantity,
- FullCustomer,
+ Price,
+ FullCusProduct,
+ Organization,
+ formatAmount,
+ UsagePriceConfig,
+ getTotalCusProdQuantity,
+ FullCustomer,
} from "@autumn/shared";
import { logger } from "better-auth";
import Stripe from "stripe";
@@ -17,8 +17,8 @@ import { isTrialing } from "@autumn/shared";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js";
import {
- getPriceEntitlement,
- getPriceOptions,
+ getPriceEntitlement,
+ getPriceOptions,
} from "@/internal/products/prices/priceUtils.js";
import { cusProductToEnts } from "@autumn/shared";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
@@ -26,136 +26,136 @@ import { getUnusedAmountAfterDiscount } from "@/internal/rewards/rewardUtils.js"
import { Decimal } from "decimal.js";
const getDiscountsApplied = ({
- invoiceItem,
- subDiscounts,
+ invoiceItem,
+ subDiscounts,
}: {
- invoiceItem?: Stripe.InvoiceLineItem;
- subDiscounts?: Stripe.Discount[];
+ invoiceItem?: Stripe.InvoiceLineItem;
+ subDiscounts?: Stripe.Discount[];
}) => {
- if (!invoiceItem || !subDiscounts) return [];
- const discountsApplied: Stripe.Discount[] = [];
- for (const dAmount of invoiceItem?.discount_amounts || []) {
- const discount = subDiscounts?.find((d) => d.id == dAmount.discount);
- if (discount && dAmount.amount > 0) {
- // console.log("Discount applied: ", discount.id);
- // console.log("Amount off: ", dAmount.amount);
- discountsApplied.push(discount);
- }
- }
- return discountsApplied;
+ if (!invoiceItem || !subDiscounts) return [];
+ const discountsApplied: Stripe.Discount[] = [];
+ for (const dAmount of invoiceItem?.discount_amounts || []) {
+ const discount = subDiscounts?.find((d) => d.id == dAmount.discount);
+ if (discount && dAmount.amount > 0) {
+ // console.log("Discount applied: ", discount.id);
+ // console.log("Amount off: ", dAmount.amount);
+ discountsApplied.push(discount);
+ }
+ }
+ return discountsApplied;
};
export const priceToUnusedPreviewItem = ({
- customer,
- price,
- stripeItems,
- cusProduct,
- now,
- org,
- subDiscounts,
- latestInvoice,
+ customer,
+ price,
+ stripeItems,
+ cusProduct,
+ now,
+ org,
+ subDiscounts,
+ latestInvoice,
}: {
- customer?: FullCustomer;
- price: Price;
- stripeItems: Stripe.SubscriptionItem[];
- cusProduct: FullCusProduct;
- now?: number;
- org?: Organization;
- subDiscounts?: Stripe.Discount[];
- latestInvoice?: Stripe.Invoice;
+ customer?: FullCustomer;
+ price: Price;
+ stripeItems: Stripe.SubscriptionItem[];
+ cusProduct: FullCusProduct;
+ now?: number;
+ org?: Organization;
+ subDiscounts?: Stripe.Discount[];
+ latestInvoice?: Stripe.Invoice;
}) => {
- now = now || Date.now();
- const onTrial = isTrialing({ cusProduct, now });
+ now = now || Date.now();
+ const onTrial = isTrialing({ cusProduct, now });
- const subItem = findStripeItemForPrice({
- price,
- stripeItems,
- stripeProdId: cusProduct?.product.processor?.id,
- }) as Stripe.SubscriptionItem | undefined;
+ const subItem = findStripeItemForPrice({
+ price,
+ stripeItems,
+ stripeProdId: cusProduct?.product.processor?.id,
+ }) as Stripe.SubscriptionItem | undefined;
- const invoiceItem = findStripeItemForPrice({
- price,
- invoiceLineItems: latestInvoice?.lines.data || [],
- stripeProdId: cusProduct?.product.processor?.id,
- }) as Stripe.InvoiceLineItem | undefined;
+ const invoiceItem = findStripeItemForPrice({
+ price,
+ invoiceLineItems: latestInvoice?.lines.data || [],
+ stripeProdId: cusProduct?.product.processor?.id,
+ }) as Stripe.InvoiceLineItem | undefined;
- if (!subItem) return undefined;
+ if (!subItem) return undefined;
- const ents = cusProductToEnts({ cusProduct });
- const ent = getPriceEntitlement(price, ents);
- const options = getPriceOptions(price, cusProduct.options);
- const config = price.config as UsagePriceConfig;
+ const ents = cusProductToEnts({ cusProduct });
+ const ent = getPriceEntitlement(price, ents);
+ const options = getPriceOptions(price, cusProduct.options);
+ const config = price.config as UsagePriceConfig;
- let quantity = notNullish(options?.quantity)
- ? options?.quantity! * config.billing_units!
- : 1;
+ let quantity = notNullish(options?.quantity)
+ ? options?.quantity! * config.billing_units!
+ : 1;
- if (isFixedPrice({ price })) {
- quantity = customer
- ? getTotalCusProdQuantity({
- cusProducts: customer.customer_products,
- productId: cusProduct.product_id,
- })
- : cusProduct.quantity || 1;
- }
+ if (isFixedPrice({ price })) {
+ quantity = customer
+ ? getTotalCusProdQuantity({
+ cusProducts: customer.customer_products,
+ productId: cusProduct.product_id,
+ })
+ : cusProduct.quantity || 1;
+ }
- const finalProration = getProration({
- now,
- intervalConfig: {
- interval: price.config.interval!,
- intervalCount: price.config.interval_count || 1,
- },
+ const finalProration = getProration({
+ now,
+ intervalConfig: {
+ interval: price.config.interval!,
+ intervalCount: price.config.interval_count || 1,
+ },
- anchor: subItem?.current_period_end
- ? subItem.current_period_end * 1000
- : undefined,
- })!;
+ anchor: subItem?.current_period_end
+ ? subItem.current_period_end * 1000
+ : undefined,
+ })!;
- let amount = onTrial
- ? 0
- : -priceToInvoiceAmount({
- price,
- quantity,
- proration: finalProration,
- productQuantity: quantity,
- now,
- });
+ let amount = onTrial
+ ? 0
+ : -priceToInvoiceAmount({
+ price,
+ quantity,
+ proration: finalProration,
+ productQuantity: quantity,
+ now,
+ });
- const ratio = new Decimal(quantity)
- .div(invoiceItem?.quantity || 1)
- .toNumber();
+ const ratio = new Decimal(quantity)
+ .div(invoiceItem?.quantity || 1)
+ .toNumber();
- amount = -getUnusedAmountAfterDiscount({
- amount,
- discountAmounts: invoiceItem?.discount_amounts || [],
- ratio,
- });
+ amount = -getUnusedAmountAfterDiscount({
+ amount,
+ discountAmounts: invoiceItem?.discount_amounts || [],
+ ratio,
+ });
- let description = priceToInvoiceDescription({
- price,
- org,
- cusProduct,
- quantity,
- logger,
- });
+ let description = priceToInvoiceDescription({
+ price,
+ org,
+ cusProduct,
+ quantity,
+ logger,
+ });
- description = `Unused ${description}`;
- if (quantity && quantity > 1) {
- description = `${description} x ${quantity}`;
- }
+ description = `Unused ${description}`;
+ if (quantity && quantity > 1) {
+ description = `${description} x ${quantity}`;
+ }
- if (finalProration) {
- description = `${description} (from ${formatUnixToDate(now)})`;
- }
+ if (finalProration) {
+ description = `${description} (from ${formatUnixToDate(now)})`;
+ }
- return {
- price: formatAmount({
- org: org,
- amount,
- }),
- description,
- amount,
- usage_model: priceToUsageModel(price),
- price_id: price.id!,
- feature_id: ent?.feature.id,
- };
+ return {
+ price: formatAmount({
+ org: org,
+ amount,
+ }),
+ description,
+ amount,
+ usage_model: priceToUsageModel(price),
+ price_id: price.id!,
+ feature_id: ent?.feature.id,
+ };
};
diff --git a/server/src/internal/customers/attach/attachRouter.ts b/server/src/internal/customers/attach/attachRouter.ts
index 00282e821..c98bcc252 100644
--- a/server/src/internal/customers/attach/attachRouter.ts
+++ b/server/src/internal/customers/attach/attachRouter.ts
@@ -1,34 +1,34 @@
import { Router } from "express";
import RecaseError from "@/utils/errorUtils.js";
import {
- APIVersion,
- AttachConfig,
- BillingType,
- FullCusProduct,
+ APIVersion,
+ AttachConfig,
+ BillingType,
+ FullCusProduct,
} from "@autumn/shared";
import { ErrCode } from "@/errors/errCodes.js";
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import {
- getBillingType,
- getEntOptions,
- getPriceEntitlement,
- getProductForPrice,
- priceIsOneOffAndTiered,
+ getBillingType,
+ getEntOptions,
+ getPriceEntitlement,
+ getProductForPrice,
+ priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import {
- checkStripeProductExists,
- isFreeProduct,
+ checkStripeProductExists,
+ isFreeProduct,
} from "@/internal/products/productUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
import {
- notNullish,
- notNullOrUndefined,
- nullish,
- nullOrUndefined,
+ notNullish,
+ notNullOrUndefined,
+ nullish,
+ nullOrUndefined,
} from "@/utils/genUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { orgToVersion } from "@/utils/versionUtils.js";
@@ -46,217 +46,217 @@ import { attachParamsToCurCusProduct } from "./attachUtils/convertAttachParams.j
export const attachRouter: Router = Router();
export const handlePrepaidErrors = async ({
- attachParams,
- config,
- useCheckout = false,
+ attachParams,
+ config,
+ useCheckout = false,
}: {
- attachParams: AttachParams;
- config: AttachConfig;
- useCheckout?: boolean;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ useCheckout?: boolean;
}) => {
- const { prices, entitlements, optionsList } = attachParams;
+ const { prices, entitlements, optionsList } = attachParams;
- // 2. Check if options are valid
- for (const price of prices) {
- const billingType = getBillingType(price.config!);
+ // 2. Check if options are valid
+ for (const price of prices) {
+ const billingType = getBillingType(price.config!);
- if (billingType === BillingType.UsageInAdvance) {
- // Get options for price
- let priceEnt = getPriceEntitlement(price, entitlements);
- let options = getEntOptions(optionsList, priceEnt);
+ if (billingType === BillingType.UsageInAdvance) {
+ // Get options for price
+ let priceEnt = getPriceEntitlement(price, entitlements);
+ let options = getEntOptions(optionsList, priceEnt);
- // 1. If not checkout, quantity should be defined
- const regularCheckout = useCheckout && !config.invoiceCheckout;
- if (!regularCheckout && nullOrUndefined(options?.quantity)) {
- throw new RecaseError({
- message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
+ // 1. If not checkout, quantity should be defined
+ const regularCheckout = useCheckout && !config.invoiceCheckout;
+ if (!regularCheckout && nullOrUndefined(options?.quantity)) {
+ throw new RecaseError({
+ message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
- if (
- nullOrUndefined(options?.quantity) &&
- priceIsOneOffAndTiered(price, priceEnt)
- ) {
- throw new RecaseError({
- code: ErrCode.InvalidRequest,
- message:
- "Quantity is required for start of period price that is one off and tiered",
- statusCode: 400,
- });
- }
+ if (
+ nullOrUndefined(options?.quantity) &&
+ priceIsOneOffAndTiered(price, priceEnt)
+ ) {
+ throw new RecaseError({
+ code: ErrCode.InvalidRequest,
+ message:
+ "Quantity is required for start of period price that is one off and tiered",
+ statusCode: 400,
+ });
+ }
- // 3. Quantity cannot be negative
- if (notNullish(options?.quantity) && options?.quantity! < 0) {
- throw new RecaseError({
- message: `Quantity cannot be negative`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
+ // 3. Quantity cannot be negative
+ if (notNullish(options?.quantity) && options?.quantity! < 0) {
+ throw new RecaseError({
+ message: `Quantity cannot be negative`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
- // 4. If there's only one price, quantity must be greater than 0
- if (options?.quantity === 0 && prices.length === 1) {
- throw new RecaseError({
- message: `When there's only one price, quantity must be greater than 0`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
- }
- }
+ // 4. If there's only one price, quantity must be greater than 0
+ if (options?.quantity === 0 && prices.length === 1) {
+ throw new RecaseError({
+ message: `When there's only one price, quantity must be greater than 0`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
+ }
+ }
};
export const handlePublicAttachErrors = async ({
- curCusProduct,
- isPublic,
+ curCusProduct,
+ isPublic,
}: {
- curCusProduct: FullCusProduct | null;
- isPublic: boolean;
+ curCusProduct: FullCusProduct | null;
+ isPublic: boolean;
}) => {
- if (!isPublic) {
- return;
- }
+ if (!isPublic) {
+ return;
+ }
- if (!curCusProduct) {
- return;
- }
+ if (!curCusProduct) {
+ return;
+ }
- // 1. If on paid plan, not allowed to switch product
- const curProductFree = isFreeProduct(
- curCusProduct?.customer_prices.map((cp: any) => cp.price) || [] // if no current product...
- );
+ // 1. If on paid plan, not allowed to switch product
+ const curProductFree = isFreeProduct(
+ curCusProduct?.customer_prices.map((cp: any) => cp.price) || [], // if no current product...
+ );
- if (!curProductFree) {
- throw new RecaseError({
- message: "Public attach: not allowed to upgrade / downgrade (from paid)",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!curProductFree) {
+ throw new RecaseError({
+ message: "Public attach: not allowed to upgrade / downgrade (from paid)",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
};
export const checkStripeConnections = async ({
- req,
- attachParams,
- createCus = true,
- useCheckout = false,
+ req,
+ attachParams,
+ createCus = true,
+ useCheckout = false,
}: {
- req: any;
- attachParams: AttachParams;
- createCus?: boolean;
- useCheckout?: boolean;
+ req: any;
+ attachParams: AttachParams;
+ createCus?: boolean;
+ useCheckout?: boolean;
}) => {
- const { org, customer, products, stripeCus, stripeCli } = attachParams;
- const logger = req.logtail;
- const env = customer.env;
+ const { org, customer, products, stripeCus, stripeCli } = attachParams;
+ const logger = req.logtail;
+ const env = customer.env;
- // 2. If invoice only and no email, save email
- if (attachParams.invoiceOnly && !customer.email) {
- customer.email = `${customer.id}-${org.id}@invoices.useautumn.com`;
- await Promise.all([
- CusService.update({
- db: req.db,
- internalCusId: customer.internal_id,
- update: {
- email: customer.email,
- },
- }),
- stripeCus &&
- stripeCli.customers.update(stripeCus.id, {
- email: customer.email,
- }),
- ]);
- }
+ // 2. If invoice only and no email, save email
+ if (attachParams.invoiceOnly && !customer.email) {
+ customer.email = `${customer.id}-${org.id}@invoices.useautumn.com`;
+ await Promise.all([
+ CusService.update({
+ db: req.db,
+ internalCusId: customer.internal_id,
+ update: {
+ email: customer.email,
+ },
+ }),
+ stripeCus &&
+ stripeCli.customers.update(stripeCus.id, {
+ email: customer.email,
+ }),
+ ]);
+ }
- const batchProductUpdates = [];
+ const batchProductUpdates = [];
- if (createCus) {
- batchProductUpdates.push(
- createStripeCusIfNotExists({
- db: req.db,
- org,
- env,
- customer,
- logger,
- })
- );
- }
+ if (createCus) {
+ batchProductUpdates.push(
+ createStripeCusIfNotExists({
+ db: req.db,
+ org,
+ env,
+ customer,
+ logger,
+ }),
+ );
+ }
- for (const product of products) {
- batchProductUpdates.push(
- checkStripeProductExists({
- db: req.db,
- org,
- env,
- product,
- logger,
- })
- );
- }
- await Promise.all(batchProductUpdates);
+ for (const product of products) {
+ batchProductUpdates.push(
+ checkStripeProductExists({
+ db: req.db,
+ org,
+ env,
+ product,
+ logger,
+ }),
+ );
+ }
+ await Promise.all(batchProductUpdates);
- await createStripePrices({
- attachParams,
- useCheckout,
- req,
- logger,
- });
+ await createStripePrices({
+ attachParams,
+ useCheckout,
+ req,
+ logger,
+ });
};
export const createStripePrices = async ({
- attachParams,
- useCheckout,
- req,
- logger,
+ attachParams,
+ useCheckout,
+ req,
+ logger,
}: {
- attachParams: AttachParams;
- useCheckout: boolean;
- req: any;
- logger: any;
+ attachParams: AttachParams;
+ useCheckout: boolean;
+ req: any;
+ logger: any;
}) => {
- const { prices, entitlements, products, org, stripeCli } = attachParams;
+ const { prices, entitlements, products, org, stripeCli } = attachParams;
- const batchPriceUpdates = [];
+ const batchPriceUpdates = [];
- // const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ // const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- for (const price of prices) {
- let product = getProductForPrice(price, products);
+ for (const price of prices) {
+ let product = getProductForPrice(price, products);
- batchPriceUpdates.push(
- createStripePriceIFNotExist({
- db: req.db,
- stripeCli,
- price,
- entitlements,
- product: product!,
- org,
- logger,
- internalEntityId: attachParams.internalEntityId,
- useCheckout,
- })
- );
- }
- await Promise.all(batchPriceUpdates);
+ batchPriceUpdates.push(
+ createStripePriceIFNotExist({
+ db: req.db,
+ stripeCli,
+ price,
+ entitlements,
+ product: product!,
+ org,
+ logger,
+ internalEntityId: attachParams.internalEntityId,
+ useCheckout,
+ }),
+ );
+ }
+ await Promise.all(batchPriceUpdates);
};
export const customerHasPm = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- // SCENARIO 3: No payment method, checkout required
- const paymentMethod = await getCusPaymentMethod({
- stripeCli: createStripeCli({
- org: attachParams.org,
- env: attachParams.customer.env,
- }),
- stripeId: attachParams.customer.processor?.id,
- });
+ // SCENARIO 3: No payment method, checkout required
+ const paymentMethod = await getCusPaymentMethod({
+ stripeCli: createStripeCli({
+ org: attachParams.org,
+ env: attachParams.customer.env,
+ }),
+ stripeId: attachParams.customer.processor?.id,
+ });
- return notNullOrUndefined(paymentMethod) ? true : false;
+ return notNullOrUndefined(paymentMethod) ? true : false;
};
attachRouter.post("/attach", handleAttach);
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts
index 8dea49f7a..439c9d228 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getCusAndProducts.ts
@@ -8,83 +8,83 @@ import { ErrCode, CusProductStatus } from "@autumn/shared";
import { AttachBody } from "@autumn/shared";
const getProductsForAttach = async ({
- req,
- attachBody,
+ req,
+ attachBody,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
}) => {
- const {
- product_id,
- product_ids,
- version,
- products: inputProducts,
- } = attachBody;
+ const {
+ product_id,
+ product_ids,
+ version,
+ products: inputProducts,
+ } = attachBody;
- let products = await ProductService.listFull({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- inIds: inputProducts
- ? inputProducts.map((p) => p.product_id)
- : product_ids || [product_id!],
- version,
- });
+ let products = await ProductService.listFull({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ inIds: inputProducts
+ ? inputProducts.map((p) => p.product_id)
+ : product_ids || [product_id!],
+ version,
+ });
- if (notNullish(product_ids)) {
- let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
+ if (notNullish(product_ids)) {
+ let freeTrialProds = products.filter((prod) => notNullish(prod.free_trial));
- if (freeTrialProds.length > 1) {
- throw new RecaseError({
- message:
- "When providing product_ids, can't have multiple free trial products",
- code: ErrCode.InvalidRequest,
- });
- }
+ if (freeTrialProds.length > 1) {
+ throw new RecaseError({
+ message:
+ "When providing product_ids, can't have multiple free trial products",
+ code: ErrCode.InvalidRequest,
+ });
+ }
- for (const prod of products) {
- if (prod.is_add_on) continue;
+ for (const prod of products) {
+ if (prod.is_add_on) continue;
- let otherProd = products.find(
- (p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id
- );
+ let otherProd = products.find(
+ (p) => p.group === prod.group && !p.is_add_on && p.id !== prod.id,
+ );
- if (otherProd && !otherProd.is_add_on && !isOneOff(prod.prices)) {
- throw new RecaseError({
- message:
- "Can't attach multiple products from the same group that are not add-ons",
- code: ErrCode.InvalidRequest,
- });
- }
- }
- }
+ if (otherProd && !otherProd.is_add_on && !isOneOff(prod.prices)) {
+ throw new RecaseError({
+ message:
+ "Can't attach multiple products from the same group that are not add-ons",
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ }
+ }
- return products;
+ return products;
};
export const getCustomerAndProducts = async ({
- req,
- attachBody,
+ req,
+ attachBody,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
}) => {
- const [customer, products] = await Promise.all([
- getOrCreateCustomer({
- req,
- customerId: attachBody.customer_id,
- customerData: attachBody.customer_data,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.Scheduled,
- CusProductStatus.PastDue,
- ],
- withEntities: true,
- entityId: attachBody.entity_id || undefined,
- entityData: attachBody.entity_data,
- }),
- getProductsForAttach({ req, attachBody }),
- ]);
+ const [customer, products] = await Promise.all([
+ getOrCreateCustomer({
+ req,
+ customerId: attachBody.customer_id,
+ customerData: attachBody.customer_data,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.Scheduled,
+ CusProductStatus.PastDue,
+ ],
+ withEntities: true,
+ entityId: attachBody.entity_id || undefined,
+ entityData: attachBody.entity_data,
+ }),
+ getProductsForAttach({ req, attachBody }),
+ ]);
- return { customer, products };
+ return { customer, products };
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts
index 738af9161..ef92361d5 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getPricesAndEnts.ts
@@ -2,133 +2,133 @@ import { cusProductToPrices, cusProductToEnts } from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils.js";
import {
- getFreeTrialAfterFingerprint,
- handleNewFreeTrial,
+ getFreeTrialAfterFingerprint,
+ handleNewFreeTrial,
} from "@/internal/products/free-trials/freeTrialUtils.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { isMainProduct } from "@/internal/products/productUtils/classifyProduct.js";
import { notNullish } from "@/utils/genUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- FullCustomer,
- FullProduct,
- Price,
- Entitlement,
- CreateFreeTrial,
+ FullCustomer,
+ FullProduct,
+ Price,
+ Entitlement,
+ CreateFreeTrial,
} from "@autumn/shared";
import { AttachBody } from "@autumn/shared";
import { mapOptionsList } from "../../mapOptionsList.js";
export const getPricesAndEnts = async ({
- req,
- attachBody,
- customer,
- products,
+ req,
+ attachBody,
+ customer,
+ products,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
- customer: FullCustomer;
- products: FullProduct[];
+ req: ExtendedRequest;
+ attachBody: AttachBody;
+ customer: FullCustomer;
+ products: FullProduct[];
}) => {
- const { options: optionsInput, is_custom, items, free_trial } = attachBody;
- const { features, db, org, logtail: logger } = req;
+ const { options: optionsInput, is_custom, items, free_trial } = attachBody;
+ const { features, db, org, logtail: logger } = req;
- const { curMainProduct, curSameProduct } = getExistingCusProducts({
- product: products[0],
- cusProducts: customer.customer_products,
- internalEntityId: customer.entity?.internal_id,
- });
+ const { curMainProduct, curSameProduct } = getExistingCusProducts({
+ product: products[0],
+ cusProducts: customer.customer_products,
+ internalEntityId: customer.entity?.internal_id,
+ });
- // Not custom
- if (!is_custom) {
- let prices = products.flatMap((p: FullProduct) => p.prices);
- let entitlements = products.flatMap((p: FullProduct) => p.entitlements);
+ // Not custom
+ if (!is_custom) {
+ let prices = products.flatMap((p: FullProduct) => p.prices);
+ let entitlements = products.flatMap((p: FullProduct) => p.entitlements);
- let freeTrial = null;
- let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
+ let freeTrial = null;
+ let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
- if (freeTrialProduct) {
- freeTrial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: freeTrialProduct.free_trial,
- fingerprint: customer.fingerprint,
- internalCustomerId: customer.internal_id,
- multipleAllowed: org.config.multiple_trials,
- productId: freeTrialProduct.id,
- });
- }
+ if (freeTrialProduct) {
+ freeTrial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: freeTrialProduct.free_trial,
+ fingerprint: customer.fingerprint,
+ internalCustomerId: customer.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ productId: freeTrialProduct.id,
+ });
+ }
- return {
- optionsList: mapOptionsList({
- optionsInput: optionsInput || [],
- features,
- prices,
- curCusProduct: curMainProduct,
- }),
- prices,
- entitlements,
- freeTrial,
- cusProducts: customer.customer_products,
- };
- }
+ return {
+ optionsList: mapOptionsList({
+ optionsInput: optionsInput || [],
+ features,
+ prices,
+ curCusProduct: curMainProduct,
+ }),
+ prices,
+ entitlements,
+ freeTrial,
+ cusProducts: customer.customer_products,
+ };
+ }
- const product = products[0];
+ const product = products[0];
- let curPrices: Price[] = product!.prices;
- let curEnts: Entitlement[] = product!.entitlements;
+ let curPrices: Price[] = product!.prices;
+ let curEnts: Entitlement[] = product!.entitlements;
- if (curMainProduct?.product.id === product.id) {
- curPrices = cusProductToPrices({ cusProduct: curMainProduct });
- curEnts = cusProductToEnts({ cusProduct: curMainProduct });
- }
+ if (curMainProduct?.product.id === product.id) {
+ curPrices = cusProductToPrices({ cusProduct: curMainProduct });
+ curEnts = cusProductToEnts({ cusProduct: curMainProduct });
+ }
- let {
- prices,
- entitlements: ents,
- customPrices,
- customEnts,
- } = await handleNewProductItems({
- db,
- curPrices,
- curEnts,
- newItems: attachBody.items || [],
- features,
- product,
- logger,
- isCustom: true,
- });
+ let {
+ prices,
+ entitlements: ents,
+ customPrices,
+ customEnts,
+ } = await handleNewProductItems({
+ db,
+ curPrices,
+ curEnts,
+ newItems: attachBody.items || [],
+ features,
+ product,
+ logger,
+ isCustom: true,
+ });
- const freeTrial = await handleNewFreeTrial({
- db,
- curFreeTrial: product!.free_trial,
- newFreeTrial: (free_trial as CreateFreeTrial) || null,
- internalProductId: product!.internal_id,
- isCustom: true,
- });
+ const freeTrial = await handleNewFreeTrial({
+ db,
+ curFreeTrial: product!.free_trial,
+ newFreeTrial: (free_trial as CreateFreeTrial) || null,
+ internalProductId: product!.internal_id,
+ isCustom: true,
+ });
- const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: freeTrial,
- fingerprint: customer.fingerprint,
- internalCustomerId: customer.internal_id,
- multipleAllowed: org.config.multiple_trials,
- productId: product.id,
- });
+ const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: freeTrial,
+ fingerprint: customer.fingerprint,
+ internalCustomerId: customer.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ productId: product.id,
+ });
- return {
- optionsList: mapOptionsList({
- optionsInput: optionsInput || [],
- features,
- prices,
- curCusProduct: curMainProduct,
- }),
- prices,
- entitlements: getEntsWithFeature({
- ents,
- features,
- }),
- freeTrial: uniqueFreeTrial,
- customPrices,
- customEnts,
- };
+ return {
+ optionsList: mapOptionsList({
+ optionsInput: optionsInput || [],
+ features,
+ prices,
+ curCusProduct: curMainProduct,
+ }),
+ prices,
+ entitlements: getEntsWithFeature({
+ ents,
+ features,
+ }),
+ freeTrial: uniqueFreeTrial,
+ customPrices,
+ customEnts,
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts
index 3f3463f14..4bd236682 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.ts
@@ -1,57 +1,57 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- createStripeCusIfNotExists,
- listCusPaymentMethods,
+ createStripeCusIfNotExists,
+ listCusPaymentMethods,
} from "@/external/stripe/stripeCusUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, Customer, ErrCode, Organization } from "@autumn/shared";
import Stripe from "stripe";
export const getStripeCusData = async ({
- stripeCli,
- db,
- org,
- env,
- customer,
- logger,
- allowNoStripe,
+ stripeCli,
+ db,
+ org,
+ env,
+ customer,
+ logger,
+ allowNoStripe,
}: {
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- customer: Customer;
- logger: any;
- allowNoStripe?: boolean;
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ customer: Customer;
+ logger: any;
+ allowNoStripe?: boolean;
}) => {
- if (allowNoStripe && !customer.processor?.id) {
- return { stripeCus: undefined, paymentMethod: null, now: undefined };
- }
+ if (allowNoStripe && !customer.processor?.id) {
+ return { stripeCus: undefined, paymentMethod: null, now: undefined };
+ }
- let stripeCus = (await createStripeCusIfNotExists({
- db,
- org,
- env,
- customer,
- logger,
- })) as Stripe.Customer;
+ let stripeCus = (await createStripeCusIfNotExists({
+ db,
+ org,
+ env,
+ customer,
+ logger,
+ })) as Stripe.Customer;
- let testClock = stripeCus.test_clock as Stripe.TestHelpers.TestClock | null;
+ let testClock = stripeCus.test_clock as Stripe.TestHelpers.TestClock | null;
- // let now = testClock ? testClock.frozen_time * 1000 : Date.now();
- let now = testClock ? testClock.frozen_time * 1000 : undefined;
+ // let now = testClock ? testClock.frozen_time * 1000 : Date.now();
+ let now = testClock ? testClock.frozen_time * 1000 : undefined;
- let paymentMethod = stripeCus.invoice_settings
- ?.default_payment_method as Stripe.PaymentMethod | null;
+ let paymentMethod = stripeCus.invoice_settings
+ ?.default_payment_method as Stripe.PaymentMethod | null;
- if (!paymentMethod) {
- let paymentMethods = await listCusPaymentMethods({
- stripeCli,
- stripeId: stripeCus.id,
- });
+ if (!paymentMethod) {
+ let paymentMethods = await listCusPaymentMethods({
+ stripeCli,
+ stripeId: stripeCus.id,
+ });
- paymentMethod = paymentMethods.length ? paymentMethods[0] : null;
- }
+ paymentMethod = paymentMethods.length ? paymentMethods[0] : null;
+ }
- return { stripeCus, paymentMethod, now };
+ return { stripeCus, paymentMethod, now };
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts
index ad19d234e..4f64f6afd 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts
@@ -2,82 +2,82 @@ import { createStripeCli } from "@/external/stripe/utils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- APIVersion,
- Feature,
- FullCusProduct,
- FullCustomer,
- FullProduct,
+ APIVersion,
+ Feature,
+ FullCusProduct,
+ FullCustomer,
+ FullProduct,
} from "@autumn/shared";
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
import { getFreeTrialAfterFingerprint } from "@/internal/products/free-trials/freeTrialUtils.js";
import { orgToVersion } from "@/utils/versionUtils.js";
export const checkToAttachParams = async ({
- req,
- customer,
- product,
- logger,
+ req,
+ customer,
+ product,
+ logger,
}: {
- req: ExtendedRequest;
- customer: FullCustomer;
- product: FullProduct;
- logger: any;
+ req: ExtendedRequest;
+ customer: FullCustomer;
+ product: FullProduct;
+ logger: any;
}) => {
- const { org, env, db } = req;
+ const { org, env, db } = req;
- const apiVersion =
- orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- }) || APIVersion.v1;
+ const apiVersion =
+ orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ }) || APIVersion.v1;
- const stripeCli = createStripeCli({ org, env });
- let stripeCusData = await getStripeCusData({
- stripeCli,
- db,
- org,
- env,
- customer,
- logger,
- allowNoStripe: true,
- });
+ const stripeCli = createStripeCli({ org, env });
+ let stripeCusData = await getStripeCusData({
+ stripeCli,
+ db,
+ org,
+ env,
+ customer,
+ logger,
+ allowNoStripe: true,
+ });
- let freeTrial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: product.free_trial,
- fingerprint: customer.fingerprint,
- internalCustomerId: customer.internal_id,
- multipleAllowed: org.config.multiple_trials,
- productId: product.id,
- });
+ let freeTrial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: product.free_trial,
+ fingerprint: customer.fingerprint,
+ internalCustomerId: customer.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ productId: product.id,
+ });
- const { stripeCus, paymentMethod, now } = stripeCusData;
+ const { stripeCus, paymentMethod, now } = stripeCusData;
- const attachParams: AttachParams = {
- stripeCli,
- stripeCus,
- now,
- paymentMethod,
+ const attachParams: AttachParams = {
+ stripeCli,
+ stripeCus,
+ now,
+ paymentMethod,
- customer,
- products: [product],
- optionsList: [],
- prices: product.prices,
- entitlements: product.entitlements,
- freeTrial,
- replaceables: [],
+ customer,
+ products: [product],
+ optionsList: [],
+ prices: product.prices,
+ entitlements: product.entitlements,
+ freeTrial,
+ replaceables: [],
- // Others
- req,
- org: req.org,
- entities: customer.entities,
- features: req.features,
- internalEntityId: customer.entity?.internal_id,
- cusProducts: customer.customer_products,
+ // Others
+ req,
+ org: req.org,
+ entities: customer.entities,
+ features: req.features,
+ internalEntityId: customer.entity?.internal_id,
+ cusProducts: customer.customer_products,
- // Others
- apiVersion,
- };
+ // Others
+ apiVersion,
+ };
- return attachParams;
+ return attachParams;
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts
index 7651abbe2..310db456d 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts
@@ -1,205 +1,205 @@
import {
- type Customer,
- cusProductToEnts,
- cusProductToPrices,
- cusProductToProduct,
- type Entity,
- type FreeTrial,
- type FullCusProduct,
- type FullCustomer,
- type FullProduct,
- type FullRewardProgram,
- type Organization,
+ type Customer,
+ cusProductToEnts,
+ cusProductToPrices,
+ cusProductToProduct,
+ type Entity,
+ type FreeTrial,
+ type FullCusProduct,
+ type FullCustomer,
+ type FullProduct,
+ type FullRewardProgram,
+ type Organization,
} from "@autumn/shared";
import type Stripe from "stripe";
import { createStripeCli } from "@/external/stripe/utils.js";
import type {
- AttachParams,
- InsertCusProductParams,
+ AttachParams,
+ InsertCusProductParams,
} from "@/internal/customers/cusProducts/AttachParams.js";
import { newCusToFullCus } from "@/internal/customers/cusUtils/cusUtils.js";
import {
- isFreeProduct,
- isOneOff,
- itemsAreOneOff,
+ isFreeProduct,
+ isOneOff,
+ itemsAreOneOff,
} from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
export const webhookToAttachParams = ({
- req,
- stripeCli,
- paymentMethod,
- cusProduct,
- fullCus,
- entities,
+ req,
+ stripeCli,
+ paymentMethod,
+ cusProduct,
+ fullCus,
+ entities,
}: {
- req: ExtendedRequest;
- stripeCli: Stripe;
- paymentMethod?: Stripe.PaymentMethod | null;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
- entities?: Entity[];
+ req: ExtendedRequest;
+ stripeCli: Stripe;
+ paymentMethod?: Stripe.PaymentMethod | null;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
+ entities?: Entity[];
}): AttachParams => {
- const fullProduct = cusProductToProduct({ cusProduct });
+ const fullProduct = cusProductToProduct({ cusProduct });
- const params: AttachParams = {
- stripeCli,
- paymentMethod,
- customer: fullCus,
- org: req.org,
- products: [fullProduct],
- prices: cusProductToPrices({ cusProduct }),
- entitlements: cusProductToEnts({ cusProduct }),
- features: req.features,
- freeTrial: cusProduct.free_trial || null,
- optionsList: cusProduct.options,
- cusProducts: [cusProduct],
+ const params: AttachParams = {
+ stripeCli,
+ paymentMethod,
+ customer: fullCus,
+ org: req.org,
+ products: [fullProduct],
+ prices: cusProductToPrices({ cusProduct }),
+ entitlements: cusProductToEnts({ cusProduct }),
+ features: req.features,
+ freeTrial: cusProduct.free_trial || null,
+ optionsList: cusProduct.options,
+ cusProducts: [cusProduct],
- internalEntityId: cusProduct.internal_entity_id || undefined,
- entities: entities || [],
- replaceables: [],
- };
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ entities: entities || [],
+ replaceables: [],
+ };
- return params;
+ return params;
};
export const productToInsertParams = ({
- req,
- fullCus,
- newProduct,
- entities,
+ req,
+ fullCus,
+ newProduct,
+ entities,
}: {
- req: ExtendedRequest;
- fullCus: FullCustomer;
- newProduct: FullProduct;
- entities?: Entity[];
+ req: ExtendedRequest;
+ fullCus: FullCustomer;
+ newProduct: FullProduct;
+ entities?: Entity[];
}): InsertCusProductParams => {
- const params: InsertCusProductParams = {
- customer: fullCus,
- org: req.org,
- product: newProduct,
- prices: newProduct.prices,
- entitlements: newProduct.entitlements,
- features: req.features,
- cusProducts: fullCus.customer_products,
- freeTrial: null,
- optionsList: [],
- internalEntityId: undefined,
- entities: entities || [],
- replaceables: [],
- };
+ const params: InsertCusProductParams = {
+ customer: fullCus,
+ org: req.org,
+ product: newProduct,
+ prices: newProduct.prices,
+ entitlements: newProduct.entitlements,
+ features: req.features,
+ cusProducts: fullCus.customer_products,
+ freeTrial: null,
+ optionsList: [],
+ internalEntityId: undefined,
+ entities: entities || [],
+ replaceables: [],
+ };
- return params;
+ return params;
};
export const newCusToAttachParams = ({
- req,
- newCus,
- products,
- stripeCli,
- freeTrial = null,
+ req,
+ newCus,
+ products,
+ stripeCli,
+ freeTrial = null,
}: {
- req: ExtendedRequest;
- newCus: FullCustomer;
- products: FullProduct[];
- stripeCli: Stripe;
- freeTrial?: FreeTrial | null;
+ req: ExtendedRequest;
+ newCus: FullCustomer;
+ products: FullProduct[];
+ stripeCli: Stripe;
+ freeTrial?: FreeTrial | null;
}) => {
- if (!newCus.customer_products) {
- newCus.customer_products = [];
- }
+ if (!newCus.customer_products) {
+ newCus.customer_products = [];
+ }
- if (!newCus.entities) {
- newCus.entities = [];
- }
+ if (!newCus.entities) {
+ newCus.entities = [];
+ }
- // isDefaultTrial
- const isDefaultTrial = freeTrial && !freeTrial.card_required;
+ // isDefaultTrial
+ const isDefaultTrial = freeTrial && !freeTrial.card_required;
- const attachParams: AttachParams = {
- stripeCli,
- paymentMethod: null,
- req,
- org: req.org,
- customer: newCus,
- products,
- prices: products.flatMap((p) => p.prices),
- entitlements: products.flatMap((p) => p.entitlements),
- freeTrial,
- replaceables: [],
- optionsList: [],
- cusProducts: [],
- entities: [],
- features: [],
- invoiceOnly: isDefaultTrial ? false : true,
- };
- return attachParams;
+ const attachParams: AttachParams = {
+ stripeCli,
+ paymentMethod: null,
+ req,
+ org: req.org,
+ customer: newCus,
+ products,
+ prices: products.flatMap((p) => p.prices),
+ entitlements: products.flatMap((p) => p.entitlements),
+ freeTrial,
+ replaceables: [],
+ optionsList: [],
+ cusProducts: [],
+ entities: [],
+ features: [],
+ invoiceOnly: isDefaultTrial ? false : true,
+ };
+ return attachParams;
};
export const newCusToInsertParams = ({
- req,
- newCus,
- product,
- freeTrial = null,
+ req,
+ newCus,
+ product,
+ freeTrial = null,
}: {
- req: ExtendedRequest;
- newCus: Customer;
- product: FullProduct;
- freeTrial?: FreeTrial | null;
+ req: ExtendedRequest;
+ newCus: Customer;
+ product: FullProduct;
+ freeTrial?: FreeTrial | null;
}) => {
- return {
- req,
- org: req.org,
- customer: newCusToFullCus({ newCus }),
- product,
- prices: product.prices,
- entitlements: product.entitlements,
- replaceables: [],
- freeTrial,
- optionsList: [],
- cusProducts: [],
- entities: [],
- features: [],
- } satisfies InsertCusProductParams;
+ return {
+ req,
+ org: req.org,
+ customer: newCusToFullCus({ newCus }),
+ product,
+ prices: product.prices,
+ entitlements: product.entitlements,
+ replaceables: [],
+ freeTrial,
+ optionsList: [],
+ cusProducts: [],
+ entities: [],
+ features: [],
+ } satisfies InsertCusProductParams;
};
export const rewardProgramToAttachParams = ({
- req,
- rewardProgram,
- customer,
- product,
- org,
+ req,
+ rewardProgram,
+ customer,
+ product,
+ org,
}: {
- req: ExtendedRequest;
- rewardProgram: FullRewardProgram;
- customer: FullCustomer;
- product: FullProduct;
- org?: Organization;
+ req: ExtendedRequest;
+ rewardProgram: FullRewardProgram;
+ customer: FullCustomer;
+ product: FullProduct;
+ org?: Organization;
}): AttachParams => {
- const reward = rewardProgram.reward;
- const isPaid = !isFreeProduct(product.prices);
- const isRecurring =
- !isOneOff(product.prices) && !itemsAreOneOff(product.entitlements);
+ const reward = rewardProgram.reward;
+ const isPaid = !isFreeProduct(product.prices);
+ const isRecurring =
+ !isOneOff(product.prices) && !itemsAreOneOff(product.entitlements);
- return {
- req,
- org: org || req.org,
- customer,
- products: [product],
- prices: product.prices,
- entitlements: product.entitlements,
- freeTrial: null,
- // rewardTrial:
- // (isPaid && isRecurring && reward.free_product_config)
- // ? reward.free_product_config
- // : null,
- rewards: [reward],
- optionsList: [],
- cusProducts: customer.customer_products,
- entities: [],
- features: req.features,
- stripeCli: createStripeCli({ org: org || req.org, env: req.env }),
- paymentMethod: null,
- replaceables: [],
- } satisfies AttachParams;
+ return {
+ req,
+ org: org || req.org,
+ customer,
+ products: [product],
+ prices: product.prices,
+ entitlements: product.entitlements,
+ freeTrial: null,
+ // rewardTrial:
+ // (isPaid && isRecurring && reward.free_product_config)
+ // ? reward.free_product_config
+ // : null,
+ rewards: [reward],
+ optionsList: [],
+ cusProducts: customer.customer_products,
+ entities: [],
+ features: req.features,
+ stripeCli: createStripeCli({ org: org || req.org, env: req.env }),
+ paymentMethod: null,
+ replaceables: [],
+ } satisfies AttachParams;
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts
index a63a7c9ce..480fe6a9d 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts
@@ -7,85 +7,85 @@ import { AttachParams } from "../../../cusProducts/AttachParams.js";
import { nullish } from "@/utils/genUtils.js";
export const getAttachParams = async ({
- req,
- attachBody,
+ req,
+ attachBody,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
}) => {
- const {
- customer,
- products,
- optionsList,
- prices,
- entitlements,
- freeTrial,
- customPrices,
- customEnts,
- stripeVars,
- rewards,
- } = await processAttachBody({
- req,
- attachBody,
- });
+ const {
+ customer,
+ products,
+ optionsList,
+ prices,
+ entitlements,
+ freeTrial,
+ customPrices,
+ customEnts,
+ stripeVars,
+ rewards,
+ } = await processAttachBody({
+ req,
+ attachBody,
+ });
- const { org } = req;
+ const { org } = req;
- const apiVersion =
- orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- }) || APIVersion.v1;
+ const apiVersion =
+ orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ }) || APIVersion.v1;
- const entityId = attachBody.entity_id;
- const internalEntityId = entityId ? customer.entity?.internal_id : undefined;
- const { stripeCli, stripeCus, paymentMethod, now } = stripeVars;
+ const entityId = attachBody.entity_id;
+ const internalEntityId = entityId ? customer.entity?.internal_id : undefined;
+ const { stripeCli, stripeCus, paymentMethod, now } = stripeVars;
- if (nullish(attachBody.finalize_invoice)) {
- attachBody.finalize_invoice = true;
- }
+ if (nullish(attachBody.finalize_invoice)) {
+ attachBody.finalize_invoice = true;
+ }
- const attachParams: AttachParams = {
- stripeCli,
- stripeCus,
- now,
- paymentMethod,
+ const attachParams: AttachParams = {
+ stripeCli,
+ stripeCus,
+ now,
+ paymentMethod,
- customer,
- products,
- optionsList,
- prices,
- entitlements,
- freeTrial,
- replaceables: [],
- rewards,
- // From req
- req,
- org: req.org,
- entities: customer.entities,
- features: req.features,
- internalEntityId,
- entityId: entityId || undefined,
- cusProducts: customer.customer_products,
+ customer,
+ products,
+ optionsList,
+ prices,
+ entitlements,
+ freeTrial,
+ replaceables: [],
+ rewards,
+ // From req
+ req,
+ org: req.org,
+ entities: customer.entities,
+ features: req.features,
+ internalEntityId,
+ entityId: entityId || undefined,
+ cusProducts: customer.customer_products,
- // Others
- apiVersion,
- successUrl: attachBody.success_url,
- invoiceOnly: attachBody.invoice,
- productsList: attachBody.products || undefined,
- // || attachBody.invoice_only
+ // Others
+ apiVersion,
+ successUrl: attachBody.success_url,
+ invoiceOnly: attachBody.invoice,
+ productsList: attachBody.products || undefined,
+ // || attachBody.invoice_only
- billingAnchor: attachBody.billing_cycle_anchor,
- metadata: attachBody.metadata,
- disableFreeTrial: attachBody.free_trial === false || false,
- checkoutSessionParams: attachBody.checkout_session_params,
- isCustom: attachBody.is_custom,
- setupPayment: attachBody.setup_payment,
- };
+ billingAnchor: attachBody.billing_cycle_anchor,
+ metadata: attachBody.metadata,
+ disableFreeTrial: attachBody.free_trial === false || false,
+ checkoutSessionParams: attachBody.checkout_session_params,
+ isCustom: attachBody.is_custom,
+ setupPayment: attachBody.setup_payment,
+ };
- return {
- attachParams,
- customPrices,
- customEnts,
- };
+ return {
+ attachParams,
+ customPrices,
+ customEnts,
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts
index b2c666f8e..5f168541a 100644
--- a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts
@@ -10,124 +10,124 @@ import { ErrCode } from "@autumn/shared";
import Stripe from "stripe";
export const getRewards = async ({
- req,
- attachBody,
- stripeCli,
+ req,
+ attachBody,
+ stripeCli,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
- stripeCli: Stripe;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
+ stripeCli: Stripe;
}) => {
- const { reward: idOrCode } = attachBody;
+ const { reward: idOrCode } = attachBody;
- if (!idOrCode) {
- return undefined;
- }
+ if (!idOrCode) {
+ return undefined;
+ }
- const rewardArray = typeof idOrCode === "string" ? [idOrCode] : idOrCode;
+ const rewardArray = typeof idOrCode === "string" ? [idOrCode] : idOrCode;
- if (rewardArray.length === 0) {
- return undefined;
- }
+ if (rewardArray.length === 0) {
+ return undefined;
+ }
- // 1. Get reward by id or promo code
- const rewards = await RewardService.getByIdOrCode({
- db: req.db,
- codes: rewardArray,
- orgId: req.org.id,
- env: req.env,
- });
+ // 1. Get reward by id or promo code
+ const rewards = await RewardService.getByIdOrCode({
+ db: req.db,
+ codes: rewardArray,
+ orgId: req.org.id,
+ env: req.env,
+ });
- for (const reward of rewardArray) {
- const corresponding = rewards.find(
- (r) => r.id === reward || r.promo_codes.some((c) => c.code === reward)
- );
+ for (const reward of rewardArray) {
+ const corresponding = rewards.find(
+ (r) => r.id === reward || r.promo_codes.some((c) => c.code === reward),
+ );
- if (!corresponding) {
- throw new RecaseError({
- message: `Reward ${reward} not found`,
- code: ErrCode.RewardNotFound,
- statusCode: 404,
- });
- }
- }
+ if (!corresponding) {
+ throw new RecaseError({
+ message: `Reward ${reward} not found`,
+ code: ErrCode.RewardNotFound,
+ statusCode: 404,
+ });
+ }
+ }
- return rewards;
+ return rewards;
- // const stripeCoupon = await stripeCli.coupons.retrieve(reward.id);
+ // const stripeCoupon = await stripeCli.coupons.retrieve(reward.id);
- // return {
- // reward,
- // stripeCoupon,
- // };
+ // return {
+ // reward,
+ // stripeCoupon,
+ // };
};
export const processAttachBody = async ({
- req,
- attachBody,
+ req,
+ attachBody,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
}) => {
- // 1. Get customer and products
- const { org, env } = req;
+ // 1. Get customer and products
+ const { org, env } = req;
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- const { customer, products } = await getCustomerAndProducts({
- req,
- attachBody,
- });
+ const { customer, products } = await getCustomerAndProducts({
+ req,
+ attachBody,
+ });
- const [stripeCusData, rewardData] = await Promise.all([
- getStripeCusData({
- stripeCli,
- db: req.db,
- org,
- env,
- customer,
- logger: req.logtail,
- }),
- getRewards({
- req,
- attachBody,
- stripeCli,
- }),
- ]);
+ const [stripeCusData, rewardData] = await Promise.all([
+ getStripeCusData({
+ stripeCli,
+ db: req.db,
+ org,
+ env,
+ customer,
+ logger: req.logtail,
+ }),
+ getRewards({
+ req,
+ attachBody,
+ stripeCli,
+ }),
+ ]);
- const { stripeCus, paymentMethod, now } = stripeCusData;
+ const { stripeCus, paymentMethod, now } = stripeCusData;
- const {
- optionsList,
- prices,
- entitlements,
- freeTrial,
- customPrices,
- customEnts,
- } = await getPricesAndEnts({
- req,
- attachBody,
- customer,
- products,
- });
+ const {
+ optionsList,
+ prices,
+ entitlements,
+ freeTrial,
+ customPrices,
+ customEnts,
+ } = await getPricesAndEnts({
+ req,
+ attachBody,
+ customer,
+ products,
+ });
- return {
- customer,
- products,
- rewards: rewardData,
- optionsList,
- prices,
- entitlements,
- freeTrial,
- customPrices,
- customEnts,
+ return {
+ customer,
+ products,
+ rewards: rewardData,
+ optionsList,
+ prices,
+ entitlements,
+ freeTrial,
+ customPrices,
+ customEnts,
- // Additional data
- stripeVars: {
- stripeCli,
- stripeCus,
- paymentMethod,
- now,
- },
- };
+ // Additional data
+ stripeVars: {
+ stripeCli,
+ stripeCus,
+ paymentMethod,
+ now,
+ },
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/attachUtils.ts b/server/src/internal/customers/attach/attachUtils/attachUtils.ts
index a8c55326f..93ceea42e 100644
--- a/server/src/internal/customers/attach/attachUtils/attachUtils.ts
+++ b/server/src/internal/customers/attach/attachUtils/attachUtils.ts
@@ -1,13 +1,13 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- getLargestInterval,
- intervalsDifferent,
+ getLargestInterval,
+ intervalsDifferent,
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
import {
- attachParamsToProduct,
- attachParamToCusProducts,
+ attachParamsToProduct,
+ attachParamToCusProducts,
} from "./convertAttachParams.js";
import { FullCusProduct } from "@autumn/shared";
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
@@ -15,78 +15,78 @@ import { isTrialing } from "@autumn/shared";
import { ACTIVE_STATUSES } from "../../cusProducts/CusProductService.js";
export const getCycleWillReset = ({
- attachParams,
- stripeSubs,
+ attachParams,
+ stripeSubs,
}: {
- attachParams: AttachParams;
- stripeSubs: Stripe.Subscription[];
+ attachParams: AttachParams;
+ stripeSubs: Stripe.Subscription[];
}) => {
- const product = attachParamsToProduct({ attachParams });
- const firstInterval = getLargestInterval({ prices: product.prices });
- const prevInterval = subToAutumnInterval(stripeSubs[0]);
- return intervalsDifferent({
- intervalA: firstInterval,
- intervalB: prevInterval,
- });
+ const product = attachParamsToProduct({ attachParams });
+ const firstInterval = getLargestInterval({ prices: product.prices });
+ const prevInterval = subToAutumnInterval(stripeSubs[0]);
+ return intervalsDifferent({
+ intervalA: firstInterval,
+ intervalB: prevInterval,
+ });
};
export const removeCurCusProductItems = async ({
- sub,
- cusProduct,
- subItems,
+ sub,
+ cusProduct,
+ subItems,
}: {
- sub?: Stripe.Subscription | null;
- cusProduct?: FullCusProduct;
- subItems: any[];
+ sub?: Stripe.Subscription | null;
+ cusProduct?: FullCusProduct;
+ subItems: any[];
}) => {
- if (!sub || !cusProduct) {
- return subItems;
- }
+ if (!sub || !cusProduct) {
+ return subItems;
+ }
- const newItems: any[] = structuredClone(subItems);
- for (const item of sub.items.data) {
- let shouldRemove = subItemInCusProduct({
- cusProduct,
- subItem: item,
- });
+ const newItems: any[] = structuredClone(subItems);
+ for (const item of sub.items.data) {
+ let shouldRemove = subItemInCusProduct({
+ cusProduct,
+ subItem: item,
+ });
- if (shouldRemove) {
- newItems.push({
- id: item.id,
- deleted: true,
- });
- }
- }
+ if (shouldRemove) {
+ newItems.push({
+ id: item.id,
+ deleted: true,
+ });
+ }
+ }
- return newItems;
+ return newItems;
};
export const isMainTrialBranch = ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- // 1. get cur main product
- const { curMainProduct } = attachParamToCusProducts({ attachParams });
- if (!isTrialing({ cusProduct: curMainProduct!, now: attachParams.now }))
- return false;
+ // 1. get cur main product
+ const { curMainProduct } = attachParamToCusProducts({ attachParams });
+ if (!isTrialing({ cusProduct: curMainProduct!, now: attachParams.now }))
+ return false;
- const subId = curMainProduct?.subscription_ids?.[0];
+ const subId = curMainProduct?.subscription_ids?.[0];
- if (!subId) return true; // probably free product with trial, can just cancel and replace?
+ if (!subId) return true; // probably free product with trial, can just cancel and replace?
- // 2. Check if sub ID is shared by any other cus products
- const allCusProducts = attachParams.customer.customer_products;
- const otherCusProductsOnSub = allCusProducts.filter(
- (cp) =>
- cp.id !== curMainProduct!.id &&
- ACTIVE_STATUSES.includes(cp.status) &&
- cp.subscription_ids?.includes(subId)
- );
+ // 2. Check if sub ID is shared by any other cus products
+ const allCusProducts = attachParams.customer.customer_products;
+ const otherCusProductsOnSub = allCusProducts.filter(
+ (cp) =>
+ cp.id !== curMainProduct!.id &&
+ ACTIVE_STATUSES.includes(cp.status) &&
+ cp.subscription_ids?.includes(subId),
+ );
- if (otherCusProductsOnSub.length > 1) {
- return false;
- }
+ if (otherCusProductsOnSub.length > 1) {
+ return false;
+ }
- return true;
+ return true;
};
diff --git a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts
index f315d0db6..b145b1a55 100644
--- a/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts
+++ b/server/src/internal/customers/attach/attachUtils/convertAttachParams.ts
@@ -4,268 +4,268 @@ import { CusProductStatus, cusProductToProduct } from "@autumn/shared";
import Stripe from "stripe";
export const attachParamsToCurCusProduct = ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { curMainProduct, curSameProduct, curScheduledProduct } =
- attachParamToCusProducts({ attachParams });
+ const { curMainProduct, curSameProduct, curScheduledProduct } =
+ attachParamToCusProducts({ attachParams });
- return curSameProduct || curMainProduct;
+ return curSameProduct || curMainProduct;
};
export const attachParamToCusProducts = ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- if (attachParams.products.length === 0 && !attachParams.cusProduct) {
- throw new Error(
- "attachParams.products should have at least one product OR attachParams.cusProduct should exist"
- );
- }
+ if (attachParams.products.length === 0 && !attachParams.cusProduct) {
+ throw new Error(
+ "attachParams.products should have at least one product OR attachParams.cusProduct should exist",
+ );
+ }
- const product =
- attachParams.products.length > 0
- ? attachParams.products[0]
- : cusProductToProduct({ cusProduct: attachParams.cusProduct! });
+ const product =
+ attachParams.products.length > 0
+ ? attachParams.products[0]
+ : cusProductToProduct({ cusProduct: attachParams.cusProduct! });
- const { curMainProduct, curSameProduct, curScheduledProduct } =
- getExistingCusProducts({
- product,
- cusProducts: attachParams.cusProducts!,
- internalEntityId: attachParams.internalEntityId,
- });
+ const { curMainProduct, curSameProduct, curScheduledProduct } =
+ getExistingCusProducts({
+ product,
+ cusProducts: attachParams.cusProducts!,
+ internalEntityId: attachParams.internalEntityId,
+ });
- const curCusProduct = curMainProduct || curSameProduct;
+ const curCusProduct = curMainProduct || curSameProduct;
- return { curMainProduct, curSameProduct, curScheduledProduct, curCusProduct };
+ return { curMainProduct, curSameProduct, curScheduledProduct, curCusProduct };
};
export const attachParamsToProduct = ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { org, features, prices, entitlements, freeTrial } = attachParams;
- const product = attachParams.products[0];
+ const { org, features, prices, entitlements, freeTrial } = attachParams;
+ const product = attachParams.products[0];
- return {
- ...product,
- prices,
- entitlements,
- free_trial: freeTrial,
- };
+ return {
+ ...product,
+ prices,
+ entitlements,
+ free_trial: freeTrial,
+ };
};
export const getSubForAttach = async ({
- subId,
- stripeCli,
+ subId,
+ stripeCli,
}: {
- subId: string;
- stripeCli: Stripe;
+ subId: string;
+ stripeCli: Stripe;
}) => {
- const sub = await stripeCli.subscriptions.retrieve(subId, {
- expand: ["items.data.price.tiers"],
- });
+ const sub = await stripeCli.subscriptions.retrieve(subId, {
+ expand: ["items.data.price.tiers"],
+ });
- return sub;
+ return sub;
};
export const getCustomerSub = async ({
- attachParams,
- onlySubId,
+ attachParams,
+ onlySubId,
}: {
- attachParams: AttachParams;
- onlySubId?: boolean;
+ attachParams: AttachParams;
+ onlySubId?: boolean;
}) => {
- const { stripeCli } = attachParams;
- const fullCus = attachParams.customer;
- let cusProducts = fullCus.customer_products;
+ const { stripeCli } = attachParams;
+ const fullCus = attachParams.customer;
+ let cusProducts = fullCus.customer_products;
- const targetGroup = attachParams.products[0].group;
- const targetEntityId = attachParams.internalEntityId || null;
- const targetProductId = attachParams.products[0].id;
+ const targetGroup = attachParams.products[0].group;
+ const targetEntityId = attachParams.internalEntityId || null;
+ const targetProductId = attachParams.products[0].id;
- cusProducts.sort((a, b) => {
- // 1. Check same group
- const aGroupMatches = a.product.group === targetGroup;
- const bGroupMatches = b.product.group === targetGroup;
+ cusProducts.sort((a, b) => {
+ // 1. Check same group
+ const aGroupMatches = a.product.group === targetGroup;
+ const bGroupMatches = b.product.group === targetGroup;
- if (aGroupMatches && !bGroupMatches) return -1;
- if (!aGroupMatches && bGroupMatches) return 1;
+ if (aGroupMatches && !bGroupMatches) return -1;
+ if (!aGroupMatches && bGroupMatches) return 1;
- // 2. Check main product
- const aMain = !a.product.is_add_on;
- const bMain = !b.product.is_add_on;
+ // 2. Check main product
+ const aMain = !a.product.is_add_on;
+ const bMain = !b.product.is_add_on;
- if (aMain && !bMain) return -1;
- if (!aMain && bMain) return 1;
+ if (aMain && !bMain) return -1;
+ if (!aMain && bMain) return 1;
- // 3. Check same product
- const aProductIdMatches = a.product.id === targetProductId;
- const bProductIdMatches = b.product.id === targetProductId;
+ // 3. Check same product
+ const aProductIdMatches = a.product.id === targetProductId;
+ const bProductIdMatches = b.product.id === targetProductId;
- if (aProductIdMatches && !bProductIdMatches) return -1;
- if (!aProductIdMatches && bProductIdMatches) return 1;
+ if (aProductIdMatches && !bProductIdMatches) return -1;
+ if (!aProductIdMatches && bProductIdMatches) return 1;
- // 4. Check same entity
- const aEntityIdMatches = (a.internal_entity_id || null) === targetEntityId;
- const bEntityIdMatches = (b.internal_entity_id || null) === targetEntityId;
+ // 4. Check same entity
+ const aEntityIdMatches = (a.internal_entity_id || null) === targetEntityId;
+ const bEntityIdMatches = (b.internal_entity_id || null) === targetEntityId;
- if (aEntityIdMatches && !bEntityIdMatches) return -1;
- if (!aEntityIdMatches && bEntityIdMatches) return 1;
+ if (aEntityIdMatches && !bEntityIdMatches) return -1;
+ if (!aEntityIdMatches && bEntityIdMatches) return 1;
- return 0;
- });
+ return 0;
+ });
- // const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
- const cusProduct = cusProducts.find(
- (cp) => cp.subscription_ids && cp.subscription_ids.length > 0
- );
+ // const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
+ const cusProduct = cusProducts.find(
+ (cp) => cp.subscription_ids && cp.subscription_ids.length > 0,
+ );
- if (!cusProduct) return { sub: undefined, cusProduct: undefined };
- const subId = cusProduct.subscription_ids![0];
+ if (!cusProduct) return { sub: undefined, cusProduct: undefined };
+ const subId = cusProduct.subscription_ids![0];
- if (onlySubId) {
- return { subId: subId, sub: undefined, cusProduct: undefined };
- }
+ if (onlySubId) {
+ return { subId: subId, sub: undefined, cusProduct: undefined };
+ }
- // If there's only one customer product on sub, and it's still trialing, return undefined, because should just replace sub.
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ // If there's only one customer product on sub, and it's still trialing, return undefined, because should just replace sub.
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const sub = await stripeCli.subscriptions.retrieve(subId, {
- expand: [
- "items.data.price.tiers",
- "discounts.coupon.applies_to",
- "latest_invoice",
- ],
- });
+ const sub = await stripeCli.subscriptions.retrieve(subId, {
+ expand: [
+ "items.data.price.tiers",
+ "discounts.coupon.applies_to",
+ "latest_invoice",
+ ],
+ });
- return { subId, sub, cusProduct };
+ return { subId, sub, cusProduct };
};
export const getCustomerSchedule = async ({
- attachParams,
- subId,
- logger,
+ attachParams,
+ subId,
+ logger,
}: {
- attachParams: AttachParams;
- subId?: string;
- logger: any;
+ attachParams: AttachParams;
+ subId?: string;
+ logger: any;
}) => {
- const { stripeCli } = attachParams;
- const fullCus = attachParams.customer;
- let cusProducts = fullCus.customer_products;
+ const { stripeCli } = attachParams;
+ const fullCus = attachParams.customer;
+ let cusProducts = fullCus.customer_products;
- const targetGroup = attachParams.products[0].group;
- const targetEntityId = attachParams.internalEntityId || null;
- const targetProductId = attachParams.products[0].id;
+ const targetGroup = attachParams.products[0].group;
+ const targetEntityId = attachParams.internalEntityId || null;
+ const targetProductId = attachParams.products[0].id;
- cusProducts.sort((a, b) => {
- // 1. Check same group
- const aGroupMatches = a.product.group === targetGroup;
- const bGroupMatches = b.product.group === targetGroup;
+ cusProducts.sort((a, b) => {
+ // 1. Check same group
+ const aGroupMatches = a.product.group === targetGroup;
+ const bGroupMatches = b.product.group === targetGroup;
- if (aGroupMatches && !bGroupMatches) return -1;
- if (!aGroupMatches && bGroupMatches) return 1;
+ if (aGroupMatches && !bGroupMatches) return -1;
+ if (!aGroupMatches && bGroupMatches) return 1;
- // 2. Check main product
- const aMain = !a.product.is_add_on;
- const bMain = !b.product.is_add_on;
+ // 2. Check main product
+ const aMain = !a.product.is_add_on;
+ const bMain = !b.product.is_add_on;
- if (aMain && !bMain) return -1;
- if (!aMain && bMain) return 1;
+ if (aMain && !bMain) return -1;
+ if (!aMain && bMain) return 1;
- // 3. Check same product
- const aProductIdMatches = a.product.id === targetProductId;
- const bProductIdMatches = b.product.id === targetProductId;
+ // 3. Check same product
+ const aProductIdMatches = a.product.id === targetProductId;
+ const bProductIdMatches = b.product.id === targetProductId;
- if (aProductIdMatches && !bProductIdMatches) return -1;
- if (!aProductIdMatches && bProductIdMatches) return 1;
+ if (aProductIdMatches && !bProductIdMatches) return -1;
+ if (!aProductIdMatches && bProductIdMatches) return 1;
- // 4. Check same entity
- const aEntityIdMatches = (a.internal_entity_id || null) === targetEntityId;
- const bEntityIdMatches = (b.internal_entity_id || null) === targetEntityId;
+ // 4. Check same entity
+ const aEntityIdMatches = (a.internal_entity_id || null) === targetEntityId;
+ const bEntityIdMatches = (b.internal_entity_id || null) === targetEntityId;
- if (aEntityIdMatches && !bEntityIdMatches) return -1;
- if (!aEntityIdMatches && bEntityIdMatches) return 1;
+ if (aEntityIdMatches && !bEntityIdMatches) return -1;
+ if (!aEntityIdMatches && bEntityIdMatches) return 1;
- return 0;
- });
+ return 0;
+ });
- // const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
- const scheduleIds = cusProducts.flatMap((cp) => cp.scheduled_ids || []);
- if (scheduleIds.length === 0) return { schedule: undefined };
+ // const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
+ const scheduleIds = cusProducts.flatMap((cp) => cp.scheduled_ids || []);
+ if (scheduleIds.length === 0) return { schedule: undefined };
- try {
- const schedules = await stripeCli.subscriptionSchedules.list({
- customer: fullCus.processor.id!,
- expand: ["data.phases.items.price"],
- });
+ try {
+ const schedules = await stripeCli.subscriptionSchedules.list({
+ customer: fullCus.processor.id!,
+ expand: ["data.phases.items.price"],
+ });
- const schedule = schedules.data.find(
- (schedule) =>
- scheduleIds.includes(schedule.id) &&
- (subId ? schedule.subscription === subId : true)
- );
+ const schedule = schedules.data.find(
+ (schedule) =>
+ scheduleIds.includes(schedule.id) &&
+ (subId ? schedule.subscription === subId : true),
+ );
- return { schedule };
- } catch (error: any) {
- logger.error(`Error getting schedule, ids: ${scheduleIds}`, {
- message: error.message,
- });
- return { schedule: undefined };
- }
+ return { schedule };
+ } catch (error: any) {
+ logger.error(`Error getting schedule, ids: ${scheduleIds}`, {
+ message: error.message,
+ });
+ return { schedule: undefined };
+ }
};
export const paramsToCurSub = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { stripeCli } = attachParams;
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const { stripeCli } = attachParams;
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const subIds = curCusProduct?.subscription_ids || [];
- if (subIds.length === 0) {
- return undefined;
- }
+ const subIds = curCusProduct?.subscription_ids || [];
+ if (subIds.length === 0) {
+ return undefined;
+ }
- const sub = await stripeCli.subscriptions.retrieve(subIds[0], {
- expand: [
- "items.data.price.tiers",
- "latest_invoice",
- "discounts.coupon.applies_to",
- ],
- });
+ const sub = await stripeCli.subscriptions.retrieve(subIds[0], {
+ expand: [
+ "items.data.price.tiers",
+ "latest_invoice",
+ "discounts.coupon.applies_to",
+ ],
+ });
- return sub;
+ return sub;
};
export const paramsToCurSubSchedule = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { stripeCli } = attachParams;
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const { stripeCli } = attachParams;
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- const subScheduleIds = curCusProduct?.scheduled_ids || [];
- if (subScheduleIds.length === 0) {
- return undefined;
- }
+ const subScheduleIds = curCusProduct?.scheduled_ids || [];
+ if (subScheduleIds.length === 0) {
+ return undefined;
+ }
- const schedule = await stripeCli.subscriptionSchedules.retrieve(
- subScheduleIds[0],
- {
- expand: ["phases.items.price"],
- }
- );
+ const schedule = await stripeCli.subscriptionSchedules.retrieve(
+ subScheduleIds[0],
+ {
+ expand: ["phases.items.price"],
+ },
+ );
- if (schedule.status == "canceled") {
- return undefined;
- }
+ if (schedule.status == "canceled") {
+ return undefined;
+ }
- return schedule as Stripe.SubscriptionSchedule;
+ return schedule as Stripe.SubscriptionSchedule;
};
diff --git a/server/src/internal/customers/attach/attachUtils/deleteCurrentScheduledProduct.ts b/server/src/internal/customers/attach/attachUtils/deleteCurrentScheduledProduct.ts
index ecbd0ed65..3144aaf8e 100644
--- a/server/src/internal/customers/attach/attachUtils/deleteCurrentScheduledProduct.ts
+++ b/server/src/internal/customers/attach/attachUtils/deleteCurrentScheduledProduct.ts
@@ -5,56 +5,56 @@ import { attachParamToCusProducts } from "./convertAttachParams.js";
import { cancelScheduledProduct } from "../../cancel/cancelScheduledProduct.js";
export const deleteCurrentScheduledProduct = async ({
- req,
- org,
- attachParams,
- attachFunc,
- logger,
+ req,
+ org,
+ attachParams,
+ attachFunc,
+ logger,
}: {
- req: any;
- org: Organization;
- attachParams: AttachParams;
- attachFunc: AttachFunction;
- logger: any;
+ req: any;
+ org: Organization;
+ attachParams: AttachParams;
+ attachFunc: AttachFunction;
+ logger: any;
}) => {
- const stripeCli = attachParams.stripeCli;
+ const stripeCli = attachParams.stripeCli;
- const { curScheduledProduct, curMainProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curScheduledProduct, curMainProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- if (curScheduledProduct) {
- logger.info(
- `deleteCurrentScheduledProduct: cancelling scheduled - ${curScheduledProduct.product.name}`
- );
+ if (curScheduledProduct) {
+ logger.info(
+ `deleteCurrentScheduledProduct: cancelling scheduled - ${curScheduledProduct.product.name}`,
+ );
- // 2. Delete scheduled product
- await CusProductService.delete({
- db: req.db,
- cusProductId: curScheduledProduct.id,
- });
- }
+ // 2. Delete scheduled product
+ await CusProductService.delete({
+ db: req.db,
+ cusProductId: curScheduledProduct.id,
+ });
+ }
- if (curScheduledProduct || attachFunc == AttachFunction.Renew) {
- await cancelScheduledProduct({
- req,
- curScheduledProduct,
- fullCus: attachParams.customer,
- curMainProduct,
- });
- }
+ if (curScheduledProduct || attachFunc == AttachFunction.Renew) {
+ await cancelScheduledProduct({
+ req,
+ curScheduledProduct,
+ fullCus: attachParams.customer,
+ curMainProduct,
+ });
+ }
- // if (attachFunc == AttachFunction.Renew || curScheduledProduct) {
- // await cancelFutureProductSchedule({
- // req,
- // db: req.db,
- // org,
- // cusProducts: attachParams.cusProducts!,
- // product: attachParams.products[0],
- // stripeCli,
- // logger,
- // env: attachParams.customer.env,
- // internalEntityId: attachParams.internalEntityId || undefined,
- // });
- // }
+ // if (attachFunc == AttachFunction.Renew || curScheduledProduct) {
+ // await cancelFutureProductSchedule({
+ // req,
+ // db: req.db,
+ // org,
+ // cusProducts: attachParams.cusProducts!,
+ // product: attachParams.products[0],
+ // stripeCli,
+ // logger,
+ // env: attachParams.customer.env,
+ // internalEntityId: attachParams.internalEntityId || undefined,
+ // });
+ // }
};
diff --git a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts
index ef80b235f..0f58d78e8 100644
--- a/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts
+++ b/server/src/internal/customers/attach/attachUtils/getAttachBranch.ts
@@ -9,324 +9,324 @@ import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js";
import { ErrCode } from "@/errors/errCodes.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- isFreeProduct,
- isProductUpgrade,
+ isFreeProduct,
+ isProductUpgrade,
} from "@/internal/products/productUtils.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
import { FeatureOptions, FullCusProduct } from "@autumn/shared";
import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js";
import { hasPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- attachParamToCusProducts,
- getCustomerSub,
+ attachParamToCusProducts,
+ getCustomerSub,
} from "./convertAttachParams.js";
import { findPrepaidPrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { isMainTrialBranch } from "./attachUtils.js";
const handleMultiProductErrors = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- let { products } = attachParams;
+ let { products } = attachParams;
- if (pricesOnlyOneOff(attachParams.prices)) {
- return true;
- }
+ if (pricesOnlyOneOff(attachParams.prices)) {
+ return true;
+ }
- for (const product of products) {
- let { curMainProduct, curSameProduct, curScheduledProduct } =
- getExistingCusProducts({
- product,
- cusProducts: attachParams.cusProducts!,
- internalEntityId: attachParams.internalEntityId,
- });
+ for (const product of products) {
+ let { curMainProduct, curSameProduct, curScheduledProduct } =
+ getExistingCusProducts({
+ product,
+ cusProducts: attachParams.cusProducts!,
+ internalEntityId: attachParams.internalEntityId,
+ });
- // 1. If product is add on, allow attach
- if (product.is_add_on) {
- continue;
- }
+ // 1. If product is add on, allow attach
+ if (product.is_add_on) {
+ continue;
+ }
- // 1. If same product exists, not allowed
- if (curSameProduct) {
- throw new RecaseError({
- message: `Product ${product.name} is already attached, can't attach again`,
- code: ErrCode.InvalidRequest,
- });
- }
+ // 1. If same product exists, not allowed
+ if (curSameProduct) {
+ throw new RecaseError({
+ message: `Product ${product.name} is already attached, can't attach again`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- let curPaidProduct =
- curMainProduct &&
- !isFreeProduct(cusProductToPrices({ cusProduct: curMainProduct }));
+ let curPaidProduct =
+ curMainProduct &&
+ !isFreeProduct(cusProductToPrices({ cusProduct: curMainProduct }));
- // 2. If existing paid product, not allowed
- if (curPaidProduct) {
- throw new RecaseError({
- message: `Upgrade / downgrade to ${product.name} not allowed with multiple products`,
- code: ErrCode.InvalidRequest,
- });
- }
+ // 2. If existing paid product, not allowed
+ if (curPaidProduct) {
+ throw new RecaseError({
+ message: `Upgrade / downgrade to ${product.name} not allowed with multiple products`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- if (curScheduledProduct) {
- throw new RecaseError({
- message: `Can't attach multiple products at once when scheduled product exists`,
- code: ErrCode.InvalidRequest,
- });
- }
- }
+ if (curScheduledProduct) {
+ throw new RecaseError({
+ message: `Can't attach multiple products at once when scheduled product exists`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ }
};
const getOptionsToUpdate = ({
- oldOptionsList,
- newOptionsList,
- curSameProduct,
+ oldOptionsList,
+ newOptionsList,
+ curSameProduct,
}: {
- oldOptionsList: FeatureOptions[];
- newOptionsList: FeatureOptions[];
- curSameProduct: FullCusProduct;
+ oldOptionsList: FeatureOptions[];
+ newOptionsList: FeatureOptions[];
+ curSameProduct: FullCusProduct;
}) => {
- let optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = [];
- const prices = cusProductToPrices({ cusProduct: curSameProduct });
+ let optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[] = [];
+ const prices = cusProductToPrices({ cusProduct: curSameProduct });
- for (const newOptions of newOptionsList) {
- let internalFeatureId = newOptions.internal_feature_id;
- let existingOptions = oldOptionsList.find(
- (o) => o.internal_feature_id === internalFeatureId
- );
+ for (const newOptions of newOptionsList) {
+ let internalFeatureId = newOptions.internal_feature_id;
+ let existingOptions = oldOptionsList.find(
+ (o) => o.internal_feature_id === internalFeatureId,
+ );
- let price = findPrepaidPrice({
- prices,
- internalFeatureId: internalFeatureId!,
- });
+ let price = findPrepaidPrice({
+ prices,
+ internalFeatureId: internalFeatureId!,
+ });
- if (price?.config.interval == BillingInterval.OneOff) continue;
+ if (price?.config.interval == BillingInterval.OneOff) continue;
- if (existingOptions && existingOptions.quantity !== newOptions.quantity) {
- optionsToUpdate.push({
- new: newOptions,
- old: existingOptions,
- });
- }
- }
+ if (existingOptions && existingOptions.quantity !== newOptions.quantity) {
+ optionsToUpdate.push({
+ new: newOptions,
+ old: existingOptions,
+ });
+ }
+ }
- return optionsToUpdate;
+ return optionsToUpdate;
};
export const checkSameCustom = async ({
- attachParams,
- curSameProduct,
- fromPreview,
- optionsToUpdate,
+ attachParams,
+ curSameProduct,
+ fromPreview,
+ optionsToUpdate,
}: {
- attachParams: AttachParams;
- curSameProduct: FullCusProduct;
- fromPreview?: boolean;
- optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[];
+ attachParams: AttachParams;
+ curSameProduct: FullCusProduct;
+ fromPreview?: boolean;
+ optionsToUpdate: { new: FeatureOptions; old: FeatureOptions }[];
}) => {
- let product = attachParams.products[0];
+ let product = attachParams.products[0];
- let { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({
- newProductV1: {
- ...product,
- prices: attachParams.prices,
- entitlements: attachParams.entitlements,
- free_trial: attachParams.freeTrial,
- },
- curProductV1: cusProductToProduct({ cusProduct: curSameProduct }),
+ let { itemsSame, freeTrialsSame, onlyEntsChanged } = productsAreSame({
+ newProductV1: {
+ ...product,
+ prices: attachParams.prices,
+ entitlements: attachParams.entitlements,
+ free_trial: attachParams.freeTrial,
+ },
+ curProductV1: cusProductToProduct({ cusProduct: curSameProduct }),
- features: attachParams.features,
- });
+ features: attachParams.features,
+ });
- if (itemsSame && freeTrialsSame) {
- if (
- fromPreview &&
- hasPrepaidPrice({ prices: attachParams.prices, excludeOneOff: true })
- ) {
- return AttachBranch.UpdatePrepaidQuantity;
- }
+ if (itemsSame && freeTrialsSame) {
+ if (
+ fromPreview &&
+ hasPrepaidPrice({ prices: attachParams.prices, excludeOneOff: true })
+ ) {
+ return AttachBranch.UpdatePrepaidQuantity;
+ }
- // 1. If prepaid quantity changed
- if (optionsToUpdate.length > 0) {
- attachParams.optionsToUpdate = optionsToUpdate;
- return AttachBranch.UpdatePrepaidQuantity;
- }
+ // 1. If prepaid quantity changed
+ if (optionsToUpdate.length > 0) {
+ attachParams.optionsToUpdate = optionsToUpdate;
+ return AttachBranch.UpdatePrepaidQuantity;
+ }
- throw new RecaseError({
- message: `Items specified for ${product.name} are the same as the existing product, can't attach again`,
- code: ErrCode.InvalidRequest,
- });
- }
+ throw new RecaseError({
+ message: `Items specified for ${product.name} are the same as the existing product, can't attach again`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- const curPrices = cusProductToPrices({ cusProduct: curSameProduct });
- if (isFreeProduct(curPrices)) {
- return AttachBranch.MainIsFree;
- }
+ const curPrices = cusProductToPrices({ cusProduct: curSameProduct });
+ if (isFreeProduct(curPrices)) {
+ return AttachBranch.MainIsFree;
+ }
- if (onlyEntsChanged) {
- return AttachBranch.SameCustomEnts;
- }
+ if (onlyEntsChanged) {
+ return AttachBranch.SameCustomEnts;
+ }
- return AttachBranch.SameCustom;
+ return AttachBranch.SameCustom;
};
const getSameProductBranch = async ({
- attachParams,
- fromPreview,
+ attachParams,
+ fromPreview,
}: {
- attachParams: AttachParams;
- fromPreview?: boolean;
+ attachParams: AttachParams;
+ fromPreview?: boolean;
}) => {
- let product = attachParams.products[0];
+ let product = attachParams.products[0];
- let { curSameProduct, curScheduledProduct } = attachParamToCusProducts({
- attachParams,
- });
+ let { curSameProduct, curScheduledProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- curSameProduct = curSameProduct!;
+ curSameProduct = curSameProduct!;
- // 1. If new version?
+ // 1. If new version?
- if (curSameProduct.product.version !== product.version) {
- return AttachBranch.NewVersion;
- }
+ if (curSameProduct.product.version !== product.version) {
+ return AttachBranch.NewVersion;
+ }
- let optionsToUpdate = getOptionsToUpdate({
- oldOptionsList: curSameProduct.options,
- newOptionsList: attachParams.optionsList,
- curSameProduct,
- });
+ let optionsToUpdate = getOptionsToUpdate({
+ oldOptionsList: curSameProduct.options,
+ newOptionsList: attachParams.optionsList,
+ curSameProduct,
+ });
- // 2. Same custom?
- if (attachParams.isCustom && curScheduledProduct?.product.id !== product.id) {
- return await checkSameCustom({
- attachParams,
- curSameProduct,
- fromPreview,
- optionsToUpdate,
- });
- }
+ // 2. Same custom?
+ if (attachParams.isCustom && curScheduledProduct?.product.id !== product.id) {
+ return await checkSameCustom({
+ attachParams,
+ curSameProduct,
+ fromPreview,
+ optionsToUpdate,
+ });
+ }
- // 1. If prepaid quantity changed
- if (optionsToUpdate.length > 0) {
- attachParams.optionsToUpdate = optionsToUpdate;
- return AttachBranch.UpdatePrepaidQuantity;
- }
+ // 1. If prepaid quantity changed
+ if (optionsToUpdate.length > 0) {
+ attachParams.optionsToUpdate = optionsToUpdate;
+ return AttachBranch.UpdatePrepaidQuantity;
+ }
- // 3. If main product
- if (curScheduledProduct && !product.is_add_on) {
- if (curScheduledProduct.product.id == product.id) {
- throw new RecaseError({
- message: `Product ${product.name} is already scheduled, can't attach again`,
- code: ErrCode.InvalidRequest,
- });
- }
+ // 3. If main product
+ if (curScheduledProduct && !product.is_add_on) {
+ if (curScheduledProduct.product.id == product.id) {
+ throw new RecaseError({
+ message: `Product ${product.name} is already scheduled, can't attach again`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- return AttachBranch.Renew;
- }
+ return AttachBranch.Renew;
+ }
- if (curSameProduct.canceled_at || curSameProduct.canceled) {
- return AttachBranch.Renew;
- }
+ if (curSameProduct.canceled_at || curSameProduct.canceled) {
+ return AttachBranch.Renew;
+ }
- if (fromPreview) {
- if (hasPrepaidPrice({ prices: attachParams.prices, excludeOneOff: true })) {
- return AttachBranch.UpdatePrepaidQuantity;
- }
- }
+ if (fromPreview) {
+ if (hasPrepaidPrice({ prices: attachParams.prices, excludeOneOff: true })) {
+ return AttachBranch.UpdatePrepaidQuantity;
+ }
+ }
- // 2. If add on product
- if (product.is_add_on) {
- return AttachBranch.AddOn;
- }
+ // 2. If add on product
+ if (product.is_add_on) {
+ return AttachBranch.AddOn;
+ }
- // Invalid, can't attach same product
- throw new RecaseError({
- message: `Product ${product.name} is already attached, can't attach again`,
- code: AttachErrCode.ProductAlreadyAttached,
- });
+ // Invalid, can't attach same product
+ throw new RecaseError({
+ message: `Product ${product.name} is already attached, can't attach again`,
+ code: AttachErrCode.ProductAlreadyAttached,
+ });
};
const getChangeProductBranch = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- // 1. If main product is free, it's the same as adding a new product
+ // 1. If main product is free, it's the same as adding a new product
- let mainProduct = cusProductToProduct({ cusProduct: curMainProduct! });
- if (isFreeProduct(mainProduct.prices)) {
- return AttachBranch.MainIsFree;
- }
+ let mainProduct = cusProductToProduct({ cusProduct: curMainProduct! });
+ if (isFreeProduct(mainProduct.prices)) {
+ return AttachBranch.MainIsFree;
+ }
- // 2. If main product is paid, check if upgrade or downgrade
- let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
- let newPrices = attachParams.prices;
+ // 2. If main product is paid, check if upgrade or downgrade
+ let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
+ let newPrices = attachParams.prices;
- let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices });
+ let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices });
- if (isUpgrade) {
- if (isMainTrialBranch({ attachParams })) {
- return AttachBranch.MainIsTrial;
- }
+ if (isUpgrade) {
+ if (isMainTrialBranch({ attachParams })) {
+ return AttachBranch.MainIsTrial;
+ }
- return AttachBranch.Upgrade;
- }
+ return AttachBranch.Upgrade;
+ }
- return AttachBranch.Downgrade;
+ return AttachBranch.Downgrade;
};
export const getAttachBranch = async ({
- req,
- attachBody,
- attachParams,
- fromPreview,
+ req,
+ attachBody,
+ attachParams,
+ fromPreview,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
- attachParams: AttachParams;
- fromPreview?: boolean;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
+ attachParams: AttachParams;
+ fromPreview?: boolean;
}) => {
- if (notNullish(attachBody.products)) {
- // 1.
- const { subId } = await getCustomerSub({ attachParams, onlySubId: true });
+ if (notNullish(attachBody.products)) {
+ // 1.
+ const { subId } = await getCustomerSub({ attachParams, onlySubId: true });
- if (subId) {
- return AttachBranch.MultiAttachUpdate;
- }
- return AttachBranch.MultiAttach;
- }
+ if (subId) {
+ return AttachBranch.MultiAttachUpdate;
+ }
+ return AttachBranch.MultiAttach;
+ }
- if (pricesOnlyOneOff(attachParams.prices)) {
- return AttachBranch.OneOff;
- }
+ if (pricesOnlyOneOff(attachParams.prices)) {
+ return AttachBranch.OneOff;
+ }
- if (notNullish(attachBody.product_ids)) {
- await handleMultiProductErrors({ attachParams });
- return AttachBranch.MultiProduct;
- }
+ if (notNullish(attachBody.product_ids)) {
+ await handleMultiProductErrors({ attachParams });
+ return AttachBranch.MultiProduct;
+ }
- let { curSameProduct, curMainProduct } = attachParamToCusProducts({
- attachParams,
- });
+ let { curSameProduct, curMainProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- // 3. Same product
- if (curSameProduct) {
- return await getSameProductBranch({ attachParams, fromPreview });
- }
+ // 3. Same product
+ if (curSameProduct) {
+ return await getSameProductBranch({ attachParams, fromPreview });
+ }
- let product = attachParams.products[0];
- if (product.is_add_on) {
- return AttachBranch.AddOn;
- }
+ let product = attachParams.products[0];
+ if (product.is_add_on) {
+ return AttachBranch.AddOn;
+ }
- // 4. Main product exists
- if (curMainProduct) {
- return getChangeProductBranch({ attachParams });
- }
+ // 4. Main product exists
+ if (curMainProduct) {
+ return getChangeProductBranch({ attachParams });
+ }
- return AttachBranch.New;
+ return AttachBranch.New;
};
diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts
index 7167a3411..a941c1153 100644
--- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts
+++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts
@@ -1,10 +1,10 @@
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachFlags } from "../models/AttachFlags.js";
import {
- AttachConfig,
- AttachBranch,
- intervalsSame,
- intervalToValue,
+ AttachConfig,
+ AttachBranch,
+ intervalsSame,
+ intervalToValue,
} from "@autumn/shared";
import { AttachBody } from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
@@ -16,164 +16,164 @@ import { cusProductToPrices } from "@autumn/shared";
import { willMergeSub } from "../mergeUtils/mergeUtils.js";
export const intervalsAreSame = ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- let { curMainProduct, curSameProduct } = attachParamToCusProducts({
- attachParams,
- });
+ let { curMainProduct, curSameProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- let curCusProduct = curSameProduct || curMainProduct;
+ let curCusProduct = curSameProduct || curMainProduct;
- if (!curCusProduct) {
- return false;
- }
+ if (!curCusProduct) {
+ return false;
+ }
- let newProduct = attachParamsToProduct({ attachParams });
- let curPrices = cusProductToPrices({ cusProduct: curCusProduct! });
+ let newProduct = attachParamsToProduct({ attachParams });
+ let curPrices = cusProductToPrices({ cusProduct: curCusProduct! });
- const curIntervals = new Set(
- curPrices.map((p) =>
- intervalToValue(p.config.interval, p.config.interval_count)
- )
- );
+ const curIntervals = new Set(
+ curPrices.map((p) =>
+ intervalToValue(p.config.interval, p.config.interval_count),
+ ),
+ );
- const newIntervals = new Set(
- newProduct.prices.map((p) =>
- intervalToValue(p.config.interval, p.config.interval_count)
- )
- );
+ const newIntervals = new Set(
+ newProduct.prices.map((p) =>
+ intervalToValue(p.config.interval, p.config.interval_count),
+ ),
+ );
- return (
- curIntervals.size === newIntervals.size &&
- [...curIntervals].every((interval) => newIntervals.has(interval))
- );
+ return (
+ curIntervals.size === newIntervals.size &&
+ [...curIntervals].every((interval) => newIntervals.has(interval))
+ );
};
export const getAttachConfig = async ({
- req,
- attachParams,
- attachBody,
- branch,
+ req,
+ attachParams,
+ attachBody,
+ branch,
}: {
- req: any;
- attachParams: AttachParams;
- attachBody: AttachBody;
- branch: AttachBranch;
+ req: any;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ branch: AttachBranch;
}) => {
- const { org, prices, paymentMethod } = attachParams;
+ const { org, prices, paymentMethod } = attachParams;
- let flags: AttachFlags = {
- isPublic: req.isPublic,
- forceCheckout: attachBody.force_checkout || false,
- invoiceOnly: attachParams.invoiceOnly || false,
- isFree: isFreeProduct(prices),
- noPaymentMethod: nullish(paymentMethod) ? true : false,
- };
+ let flags: AttachFlags = {
+ isPublic: req.isPublic,
+ forceCheckout: attachBody.force_checkout || false,
+ invoiceOnly: attachParams.invoiceOnly || false,
+ isFree: isFreeProduct(prices),
+ noPaymentMethod: nullish(paymentMethod) ? true : false,
+ };
- const { isPublic, forceCheckout, invoiceOnly, isFree, noPaymentMethod } =
- flags;
+ const { isPublic, forceCheckout, invoiceOnly, isFree, noPaymentMethod } =
+ flags;
- let proration =
- branch == AttachBranch.SameCustomEnts || branch == AttachBranch.NewVersion
- ? ProrationBehavior.None
- : org.config.bill_upgrade_immediately
- ? ProrationBehavior.Immediately
- : ProrationBehavior.NextBilling;
+ let proration =
+ branch == AttachBranch.SameCustomEnts || branch == AttachBranch.NewVersion
+ ? ProrationBehavior.None
+ : org.config.bill_upgrade_immediately
+ ? ProrationBehavior.Immediately
+ : ProrationBehavior.NextBilling;
- let carryUsage =
- branch == AttachBranch.SameCustomEnts ||
- branch == AttachBranch.SameCustom ||
- branch == AttachBranch.NewVersion;
+ let carryUsage =
+ branch == AttachBranch.SameCustomEnts ||
+ branch == AttachBranch.SameCustom ||
+ branch == AttachBranch.NewVersion;
- // Disable trial if doing a merge sub or something else...
- // Is merge sub...
- const willMerge = await willMergeSub({ attachParams, branch });
+ // Disable trial if doing a merge sub or something else...
+ // Is merge sub...
+ const willMerge = await willMergeSub({ attachParams, branch });
- let disableTrial =
- branch === AttachBranch.NewVersion ||
- branch == AttachBranch.Downgrade ||
- willMerge ||
- attachBody.free_trial === false;
+ let disableTrial =
+ branch === AttachBranch.NewVersion ||
+ branch == AttachBranch.Downgrade ||
+ willMerge ||
+ attachBody.free_trial === false;
- let freeTrialWithoutCardRequired =
- notNullish(attachParams.freeTrial) &&
- attachParams.freeTrial?.card_required === false;
+ let freeTrialWithoutCardRequired =
+ notNullish(attachParams.freeTrial) &&
+ attachParams.freeTrial?.card_required === false;
- let carryTrial = branch === AttachBranch.NewVersion || willMerge;
+ let carryTrial = branch === AttachBranch.NewVersion || willMerge;
- let sameIntervals = intervalsAreSame({ attachParams });
+ let sameIntervals = intervalsAreSame({ attachParams });
- // let disableMerge =
- // branch == AttachBranch.MainIsTrial ||
- // org.config.merge_billing_cycles === false;
+ // let disableMerge =
+ // branch == AttachBranch.MainIsTrial ||
+ // org.config.merge_billing_cycles === false;
- const invoiceAndEnable =
- attachParams.invoiceOnly && attachBody.enable_product_immediately;
+ const invoiceAndEnable =
+ attachParams.invoiceOnly && attachBody.enable_product_immediately;
- const invoiceCheckout =
- attachParams.invoiceOnly === true && !attachBody.enable_product_immediately;
+ const invoiceCheckout =
+ attachParams.invoiceOnly === true && !attachBody.enable_product_immediately;
- const checkoutFlow =
- isPublic ||
- forceCheckout ||
- invoiceCheckout ||
- (noPaymentMethod &&
- !invoiceAndEnable &&
- branch != AttachBranch.MultiAttachUpdate);
+ const checkoutFlow =
+ isPublic ||
+ forceCheckout ||
+ invoiceCheckout ||
+ (noPaymentMethod &&
+ !invoiceAndEnable &&
+ branch != AttachBranch.MultiAttachUpdate);
- const onlyCheckout = !isFree && checkoutFlow && !freeTrialWithoutCardRequired;
- const disableMerge = branch == AttachBranch.MainIsTrial || onlyCheckout;
+ const onlyCheckout = !isFree && checkoutFlow && !freeTrialWithoutCardRequired;
+ const disableMerge = branch == AttachBranch.MainIsTrial || onlyCheckout;
- // Require payment method...
- let paymentMethodRequired = true;
- if (
- !disableTrial &&
- attachParams.freeTrial &&
- attachParams.freeTrial.card_required === false
- ) {
- paymentMethodRequired = false;
- }
- if (attachParams.invoiceOnly) {
- paymentMethodRequired = false;
- }
+ // Require payment method...
+ let paymentMethodRequired = true;
+ if (
+ !disableTrial &&
+ attachParams.freeTrial &&
+ attachParams.freeTrial.card_required === false
+ ) {
+ paymentMethodRequired = false;
+ }
+ if (attachParams.invoiceOnly) {
+ paymentMethodRequired = false;
+ }
- let config: AttachConfig = {
- branch,
- onlyCheckout,
- carryUsage,
- proration,
- disableTrial,
- invoiceOnly: flags.invoiceOnly,
- invoiceCheckout,
- disableMerge,
- sameIntervals,
- carryTrial,
- finalizeInvoice: notNullish(attachBody.finalize_invoice)
- ? attachBody.finalize_invoice!
- : true,
- requirePaymentMethod: paymentMethodRequired,
- };
+ let config: AttachConfig = {
+ branch,
+ onlyCheckout,
+ carryUsage,
+ proration,
+ disableTrial,
+ invoiceOnly: flags.invoiceOnly,
+ invoiceCheckout,
+ disableMerge,
+ sameIntervals,
+ carryTrial,
+ finalizeInvoice: notNullish(attachBody.finalize_invoice)
+ ? attachBody.finalize_invoice!
+ : true,
+ requirePaymentMethod: paymentMethodRequired,
+ };
- return { flags, config };
+ return { flags, config };
};
export const getDefaultAttachConfig = () => {
- const config: AttachConfig = {
- branch: AttachBranch.New,
- carryUsage: false,
- onlyCheckout: false,
- proration: ProrationBehavior.None,
- disableTrial: false,
- invoiceOnly: false,
- disableMerge: false,
- sameIntervals: false,
- carryTrial: false,
- invoiceCheckout: false,
- finalizeInvoice: true,
- requirePaymentMethod: true,
- };
+ const config: AttachConfig = {
+ branch: AttachBranch.New,
+ carryUsage: false,
+ onlyCheckout: false,
+ proration: ProrationBehavior.None,
+ disableTrial: false,
+ invoiceOnly: false,
+ disableMerge: false,
+ sameIntervals: false,
+ carryTrial: false,
+ invoiceCheckout: false,
+ finalizeInvoice: true,
+ requirePaymentMethod: true,
+ };
- return config;
+ return config;
};
diff --git a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts
index 5729d904c..5c43fdbbe 100644
--- a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts
+++ b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts
@@ -1,9 +1,9 @@
import {
- type AttachBody,
- AttachBranch,
- type AttachConfig,
- AttachFunction,
- CusProductStatus,
+ type AttachBody,
+ AttachBranch,
+ type AttachConfig,
+ AttachFunction,
+ CusProductStatus,
} from "@autumn/shared";
import chalk from "chalk";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
@@ -18,8 +18,8 @@ import { handleScheduleFunction2 } from "../attachFunctions/scheduleFlow/handleS
import { handleUpdateQuantityFunction } from "../attachFunctions/updateQuantityFlow/updateQuantityFlow.js";
import { handleUpgradeFlow } from "../attachFunctions/upgradeFlow/handleUpgradeFlow.js";
import {
- attachParamsToCurCusProduct,
- attachParamToCusProducts,
+ attachParamsToCurCusProduct,
+ attachParamToCusProducts,
} from "./convertAttachParams.js";
/*
diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts
index df2c24037..46faaa628 100644
--- a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts
+++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts
@@ -1,11 +1,11 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- BillingInterval,
- BillingType,
- FullCusProduct,
- FullProduct,
- intervalsDifferent,
- intervalsSame,
+ BillingInterval,
+ BillingType,
+ FullCusProduct,
+ FullProduct,
+ intervalsDifferent,
+ intervalsSame,
} from "@autumn/shared";
import Stripe from "stripe";
import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js";
@@ -17,142 +17,142 @@ import { intervalsAreSame } from "../getAttachConfig.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const filterContUsageProrations = async ({
- sub,
- stripeCli,
- curCusProduct,
- newProduct,
- logger,
+ sub,
+ stripeCli,
+ curCusProduct,
+ newProduct,
+ logger,
}: {
- sub: Stripe.Subscription;
- stripeCli: Stripe;
- curCusProduct: FullCusProduct;
- newProduct: FullProduct;
- logger: any;
+ sub: Stripe.Subscription;
+ stripeCli: Stripe;
+ curCusProduct: FullCusProduct;
+ newProduct: FullProduct;
+ logger: any;
}) => {
- const curPrices = cusProductToPrices({
- cusProduct: curCusProduct,
- });
- let allPrices = [...curPrices, ...newProduct.prices];
+ const curPrices = cusProductToPrices({
+ cusProduct: curCusProduct,
+ });
+ let allPrices = [...curPrices, ...newProduct.prices];
- // const upcomingLines = await stripeCli.invoices.listUpcomingLines({
- // subscription: sub.id,
- // });
+ // const upcomingLines = await stripeCli.invoices.listUpcomingLines({
+ // subscription: sub.id,
+ // });
- const pendingItems = await stripeCli.invoiceItems.list({
- pending: true,
- customer: sub.customer as string,
- });
+ const pendingItems = await stripeCli.invoiceItems.list({
+ pending: true,
+ customer: sub.customer as string,
+ });
- const intervalSet = subToAutumnInterval(sub);
+ const intervalSet = subToAutumnInterval(sub);
- for (const item of pendingItems.data) {
- // console.log("LINE ITEM:", item);
- if (!item.proration) continue;
+ for (const item of pendingItems.data) {
+ // console.log("LINE ITEM:", item);
+ if (!item.proration) continue;
- let price = findPriceInStripeItems({
- prices: allPrices,
- lineItem: item,
- billingType: BillingType.InArrearProrated,
- });
+ let price = findPriceInStripeItems({
+ prices: allPrices,
+ lineItem: item,
+ billingType: BillingType.InArrearProrated,
+ });
- if (!price) continue;
+ if (!price) continue;
- logger.info(
- `Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})`
- );
+ logger.info(
+ `Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})`,
+ );
- await stripeCli.invoiceItems.del(
- item.id
- // @ts-ignore -- Stripe types are not correct
- // item.parent.subscription_item_details.invoice_item
- );
- }
+ await stripeCli.invoiceItems.del(
+ item.id,
+ // @ts-ignore -- Stripe types are not correct
+ // item.parent.subscription_item_details.invoice_item
+ );
+ }
};
export const createAndFilterContUseItems = async ({
- attachParams,
- curMainProduct,
- // stripeSubs,
- sub,
- logger,
- interval,
- intervalCount,
+ attachParams,
+ curMainProduct,
+ // stripeSubs,
+ sub,
+ logger,
+ interval,
+ intervalCount,
}: {
- attachParams: AttachParams;
- curMainProduct: FullCusProduct;
- // stripeSubs: Stripe.Subscription[];
- sub: Stripe.Subscription;
- logger: any;
- interval?: BillingInterval;
- intervalCount?: number;
+ attachParams: AttachParams;
+ curMainProduct: FullCusProduct;
+ // stripeSubs: Stripe.Subscription[];
+ sub: Stripe.Subscription;
+ logger: any;
+ interval?: BillingInterval;
+ intervalCount?: number;
}) => {
- const { stripeCli, customer, org } = attachParams;
- const product = attachParamsToProduct({ attachParams });
- // const sameIntervals = intervalsAreSame({ attachParams });
- const now = attachParams.now || Date.now();
+ const { stripeCli, customer, org } = attachParams;
+ const product = attachParamsToProduct({ attachParams });
+ // const sameIntervals = intervalsAreSame({ attachParams });
+ const now = attachParams.now || Date.now();
- // if (!sameIntervals) {
- // return { newItems: [], oldItems: [], replaceables: [] };
- // }
+ // if (!sameIntervals) {
+ // return { newItems: [], oldItems: [], replaceables: [] };
+ // }
- let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
- attachParams,
- cusProduct: curMainProduct!,
- sub,
- logger,
- });
+ let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
+ attachParams,
+ cusProduct: curMainProduct!,
+ sub,
+ logger,
+ });
- await filterContUsageProrations({
- sub,
- stripeCli,
- curCusProduct: curMainProduct,
- newProduct: product,
- logger,
- });
+ await filterContUsageProrations({
+ sub,
+ stripeCli,
+ curCusProduct: curMainProduct,
+ newProduct: product,
+ logger,
+ });
- const items = [...oldItems, ...newItems];
- const curPrices = cusProductToPrices({
- cusProduct: curMainProduct,
- });
+ const items = [...oldItems, ...newItems];
+ const curPrices = cusProductToPrices({
+ cusProduct: curMainProduct,
+ });
- for (const item of items) {
- if (!item.amount || item.amount === 0) {
- continue;
- }
+ for (const item of items) {
+ if (!item.amount || item.amount === 0) {
+ continue;
+ }
- let price =
- product.prices.find((p) => p.id === item.price_id) ||
- curPrices.find((p) => p.id === item.price_id);
+ let price =
+ product.prices.find((p) => p.id === item.price_id) ||
+ curPrices.find((p) => p.id === item.price_id);
- if (
- interval &&
- price?.config &&
- intervalsDifferent({
- // price?.config.interval !== interval
- intervalA: price?.config,
- intervalB: { interval, intervalCount },
- })
- ) {
- continue;
- }
+ if (
+ interval &&
+ price?.config &&
+ intervalsDifferent({
+ // price?.config.interval !== interval
+ intervalA: price?.config,
+ intervalB: { interval, intervalCount },
+ })
+ ) {
+ continue;
+ }
- logger.info(
- `Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`
- );
+ logger.info(
+ `Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`,
+ );
- const { start, end } = subToPeriodStartEnd({ sub });
- await stripeCli.invoiceItems.create({
- customer: customer.processor?.id!,
- amount: Math.round(item.amount * 100),
- description: item.description,
- currency: org.default_currency || "usd",
- subscription: sub.id,
- period: {
- start: Math.floor(now / 1000),
- end: end,
- },
- });
- }
+ const { start, end } = subToPeriodStartEnd({ sub });
+ await stripeCli.invoiceItems.create({
+ customer: customer.processor?.id!,
+ amount: Math.round(item.amount * 100),
+ description: item.description,
+ currency: org.default_currency || "usd",
+ subscription: sub.id,
+ period: {
+ start: Math.floor(now / 1000),
+ end: end,
+ },
+ });
+ }
- return { newItems, oldItems, replaceables };
+ return { newItems, oldItems, replaceables };
};
diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts
index 1b5909dad..432d4a43c 100644
--- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts
+++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseDowngradeItems.ts
@@ -4,11 +4,11 @@ import { Proration } from "@/internal/invoices/prorationUtils.js";
import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js";
import { generateId } from "@/utils/genUtils.js";
import {
- FullEntitlement,
- FullCustomerEntitlement,
- PreviewLineItem,
- Price,
- usageToFeatureName,
+ FullEntitlement,
+ FullCustomerEntitlement,
+ PreviewLineItem,
+ Price,
+ usageToFeatureName,
} from "@autumn/shared";
import { attachParamsToProduct } from "../convertAttachParams.js";
@@ -16,103 +16,103 @@ import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceT
import { AttachReplaceableSchema } from "@autumn/shared";
export const getContUseDowngradeItems = async ({
- price,
- ent,
- prevCusEnt,
- attachParams,
- curItem,
- curUsage,
- proration,
- logger,
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ curItem,
+ curUsage,
+ proration,
+ logger,
}: {
- price: Price;
- ent: FullEntitlement;
- prevCusEnt: FullCustomerEntitlement;
- attachParams: AttachParams;
- curItem: PreviewLineItem;
- curUsage: number;
- proration?: Proration;
- logger: any;
+ price: Price;
+ ent: FullEntitlement;
+ prevCusEnt: FullCustomerEntitlement;
+ attachParams: AttachParams;
+ curItem: PreviewLineItem;
+ curUsage: number;
+ proration?: Proration;
+ logger: any;
}) => {
- let prevInvoiceItem = curItem;
- let prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
- const product = attachParamsToProduct({ attachParams });
- const feature = prevCusEnt.entitlement.feature;
+ let prevInvoiceItem = curItem;
+ let prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
+ const product = attachParamsToProduct({ attachParams });
+ const feature = prevCusEnt.entitlement.feature;
- let { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
- ent: prevCusEnt.entitlement,
- price,
- balance: prevBalance,
- });
+ let { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
+ ent: prevCusEnt.entitlement,
+ price,
+ balance: prevBalance,
+ });
- let { usage: newUsage, overage: newOverage } = getUsageFromBalance({
- ent,
- price,
- balance: prevBalance,
- });
+ let { usage: newUsage, overage: newOverage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: prevBalance,
+ });
- if (prevOverage == 0) {
- let { usage: newUsage } = getUsageFromBalance({
- ent,
- price,
- balance: ent.allowance! - curUsage,
- });
+ if (prevOverage == 0) {
+ let { usage: newUsage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: ent.allowance! - curUsage,
+ });
- let newItem = priceToInvoiceItem({
- price,
- ent,
- org: attachParams.org,
- usage: newUsage,
- prodName: product.name,
- proration,
- now: attachParams.now,
- allowNegative: false,
- });
+ let newItem = priceToInvoiceItem({
+ price,
+ ent,
+ org: attachParams.org,
+ usage: newUsage,
+ prodName: product.name,
+ proration,
+ now: attachParams.now,
+ allowNegative: false,
+ });
- return {
- oldItem: prevInvoiceItem,
- newItem,
- newUsageItem: null,
- replaceables: [],
- };
- }
+ return {
+ oldItem: prevInvoiceItem,
+ newItem,
+ newUsageItem: null,
+ replaceables: [],
+ };
+ }
- const newItem = priceToInvoiceItem({
- price,
- ent,
- org: attachParams.org,
- usage: newUsage,
- prodName: product.name,
- proration,
- now: attachParams.now,
- });
+ const newItem = priceToInvoiceItem({
+ price,
+ ent,
+ org: attachParams.org,
+ usage: newUsage,
+ prodName: product.name,
+ proration,
+ now: attachParams.now,
+ });
- let numReplaceables = newUsage - prevUsage;
+ let numReplaceables = newUsage - prevUsage;
- let replaceables = Array.from({ length: numReplaceables }, (_, i) =>
- AttachReplaceableSchema.parse({
- ent: ent,
- id: generateId("rep"),
- created_at: Date.now(),
- delete_next_cycle: false,
- }),
- );
+ let replaceables = Array.from({ length: numReplaceables }, (_, i) =>
+ AttachReplaceableSchema.parse({
+ ent: ent,
+ id: generateId("rep"),
+ created_at: Date.now(),
+ delete_next_cycle: false,
+ }),
+ );
- const featureName = usageToFeatureName({
- usage: numReplaceables,
- feature,
- });
+ const featureName = usageToFeatureName({
+ usage: numReplaceables,
+ feature,
+ });
- let replaceableItem = constructPreviewItem({
- priceStr: `${numReplaceables} free ${featureName}`,
- price,
- description: `${product.name} - ${featureName}`,
- });
+ let replaceableItem = constructPreviewItem({
+ priceStr: `${numReplaceables} free ${featureName}`,
+ price,
+ description: `${product.name} - ${featureName}`,
+ });
- return {
- oldItem: prevInvoiceItem,
- newItem,
- newUsageItem: replaceableItem,
- replaceables,
- };
+ return {
+ oldItem: prevInvoiceItem,
+ newItem,
+ newUsageItem: replaceableItem,
+ replaceables,
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts
index 4fd691e04..42e323a00 100644
--- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts
+++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts
@@ -1,27 +1,27 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- AttachReplaceable,
- BillingType,
- FullCusProduct,
- FullCustomerEntitlement,
- FullEntitlement,
- getFeatureInvoiceDescription,
- intervalsSame,
- PreviewLineItem,
- Price,
+ AttachReplaceable,
+ BillingType,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ FullEntitlement,
+ getFeatureInvoiceDescription,
+ intervalsSame,
+ PreviewLineItem,
+ Price,
} from "@autumn/shared";
import Stripe from "stripe";
import { attachParamsToProduct } from "../convertAttachParams.js";
import {
- getBillingType,
- getPriceEntitlement,
- getPriceForOverage,
+ getBillingType,
+ getPriceEntitlement,
+ getPriceForOverage,
} from "@/internal/products/prices/priceUtils.js";
import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import {
- getExistingUsageFromCusProducts,
- getRelatedCusPrice,
+ getExistingUsageFromCusProducts,
+ getRelatedCusPrice,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { Decimal } from "decimal.js";
@@ -34,175 +34,175 @@ import { priceToContUseItem } from "./priceToContUseItem.js";
import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
export const getContUseNewItems = async ({
- price,
- ent,
- attachParams,
- prevCusEnt,
+ price,
+ ent,
+ attachParams,
+ prevCusEnt,
}: {
- price: Price;
- ent: FullEntitlement;
- attachParams: AttachParams;
- prevCusEnt?: FullCustomerEntitlement;
+ price: Price;
+ ent: FullEntitlement;
+ attachParams: AttachParams;
+ prevCusEnt?: FullCustomerEntitlement;
}) => {
- const { org, features } = attachParams;
- const newProduct = attachParamsToProduct({ attachParams });
- const intervalsSame = intervalsAreSame({ attachParams });
+ const { org, features } = attachParams;
+ const newProduct = attachParamsToProduct({ attachParams });
+ const intervalsSame = intervalsAreSame({ attachParams });
- let usage = getExistingUsageFromCusProducts({
- entitlement: ent,
- cusProducts: attachParams.cusProducts,
- entities: attachParams.entities,
- carryExistingUsages: undefined,
- internalEntityId: attachParams.internalEntityId,
- });
+ let usage = getExistingUsageFromCusProducts({
+ entitlement: ent,
+ cusProducts: attachParams.cusProducts,
+ entities: attachParams.entities,
+ carryExistingUsages: undefined,
+ internalEntityId: attachParams.internalEntityId,
+ });
- let description = newPriceToInvoiceDescription({
- org,
- price,
- product: newProduct,
- quantity: usage,
- });
+ let description = newPriceToInvoiceDescription({
+ org,
+ price,
+ product: newProduct,
+ quantity: usage,
+ });
- if (usage == 0) {
- return {
- price_id: price.id,
- price: getDefaultPriceStr({ org, price, ent, features }),
- amount: undefined,
- description,
- usage_model: priceToUsageModel(price),
- feature_id: ent.feature_id,
- } as PreviewLineItem;
- } else {
- let overage = new Decimal(usage).sub(ent.allowance!).toNumber();
+ if (usage == 0) {
+ return {
+ price_id: price.id,
+ price: getDefaultPriceStr({ org, price, ent, features }),
+ amount: undefined,
+ description,
+ usage_model: priceToUsageModel(price),
+ feature_id: ent.feature_id,
+ } as PreviewLineItem;
+ } else {
+ let overage = new Decimal(usage).sub(ent.allowance!).toNumber();
- if (
- intervalsSame &&
- prevCusEnt &&
- !shouldProrate(price.proration_config?.on_decrease)
- ) {
- let isDowngrade = ent.allowance! > prevCusEnt.entitlement.allowance!;
- let prevBalance = prevCusEnt.balance!;
+ if (
+ intervalsSame &&
+ prevCusEnt &&
+ !shouldProrate(price.proration_config?.on_decrease)
+ ) {
+ let isDowngrade = ent.allowance! > prevCusEnt.entitlement.allowance!;
+ let prevBalance = prevCusEnt.balance!;
- if (isDowngrade && prevBalance < 0) {
- overage = new Decimal(prevBalance).abs().toNumber();
- usage = ent.allowance! - prevBalance;
- }
- }
+ if (isDowngrade && prevBalance < 0) {
+ overage = new Decimal(prevBalance).abs().toNumber();
+ usage = ent.allowance! - prevBalance;
+ }
+ }
- let amount = getPriceForOverage(price, overage);
- let description = getFeatureInvoiceDescription({
- feature: ent.feature,
- usage: usage,
- prodName: newProduct.name,
- });
+ let amount = getPriceForOverage(price, overage);
+ let description = getFeatureInvoiceDescription({
+ feature: ent.feature,
+ usage: usage,
+ prodName: newProduct.name,
+ });
- return {
- price_id: price.id,
- price: "",
- description,
- amount,
- usage_model: priceToUsageModel(price),
- feature_id: ent.feature_id,
- } as PreviewLineItem;
- }
+ return {
+ price_id: price.id,
+ price: "",
+ description,
+ amount,
+ usage_model: priceToUsageModel(price),
+ feature_id: ent.feature_id,
+ } as PreviewLineItem;
+ }
};
export const getContUseInvoiceItems = async ({
- cusProduct,
- sub,
- attachParams,
- logger,
+ cusProduct,
+ sub,
+ attachParams,
+ logger,
}: {
- cusProduct?: FullCusProduct;
- sub?: Stripe.Subscription;
- attachParams: AttachParams;
- logger: any;
+ cusProduct?: FullCusProduct;
+ sub?: Stripe.Subscription;
+ attachParams: AttachParams;
+ logger: any;
}) => {
- const cusPrices = cusProduct ? cusProduct.customer_prices : [];
- const cusEnts = cusProduct ? cusProduct.customer_entitlements : [];
+ const cusPrices = cusProduct ? cusProduct.customer_prices : [];
+ const cusEnts = cusProduct ? cusProduct.customer_entitlements : [];
- const product = attachParamsToProduct({ attachParams });
- // const allIntervalsSame = intervalsAreSame({ attachParams });
- const curItems = sub
- ? await getCurContUseItems({
- sub,
- attachParams,
- })
- : [];
+ const product = attachParamsToProduct({ attachParams });
+ // const allIntervalsSame = intervalsAreSame({ attachParams });
+ const curItems = sub
+ ? await getCurContUseItems({
+ sub,
+ attachParams,
+ })
+ : [];
- let newEnts = product.entitlements;
- let oldItems: PreviewLineItem[] = [];
- let newItems: PreviewLineItem[] = [];
- let replaceables: AttachReplaceable[] = [];
+ let newEnts = product.entitlements;
+ let oldItems: PreviewLineItem[] = [];
+ let newItems: PreviewLineItem[] = [];
+ let replaceables: AttachReplaceable[] = [];
- for (const price of product.prices) {
- let billingType = getBillingType(price.config);
- if (billingType !== BillingType.InArrearProrated) {
- continue;
- }
+ for (const price of product.prices) {
+ let billingType = getBillingType(price.config);
+ if (billingType !== BillingType.InArrearProrated) {
+ continue;
+ }
- let ent = getPriceEntitlement(price, newEnts);
- let prevCusEnt = findCusEnt({
- cusEnts,
- feature: ent.feature,
- });
+ let ent = getPriceEntitlement(price, newEnts);
+ let prevCusEnt = findCusEnt({
+ cusEnts,
+ feature: ent.feature,
+ });
- let prevCusPrice = prevCusEnt
- ? getRelatedCusPrice(prevCusEnt, cusPrices)!
- : undefined;
+ let prevCusPrice = prevCusEnt
+ ? getRelatedCusPrice(prevCusEnt, cusPrices)!
+ : undefined;
- if (!prevCusEnt || !sub) {
- const newItem = await getContUseNewItems({
- price,
- ent,
- attachParams,
- prevCusEnt,
- });
+ if (!prevCusEnt || !sub) {
+ const newItem = await getContUseNewItems({
+ price,
+ ent,
+ attachParams,
+ prevCusEnt,
+ });
- const prevItem = curItems.find(
- (item) => item.price_id === prevCusPrice?.price.id
- );
+ const prevItem = curItems.find(
+ (item) => item.price_id === prevCusPrice?.price.id,
+ );
- newItems.push(newItem);
+ newItems.push(newItem);
- if (prevItem) {
- oldItems.push(prevItem);
- }
+ if (prevItem) {
+ oldItems.push(prevItem);
+ }
- continue;
- }
+ continue;
+ }
- const curItem = curItems.find(
- (item) => item.price_id === prevCusPrice?.price.id
- );
+ const curItem = curItems.find(
+ (item) => item.price_id === prevCusPrice?.price.id,
+ );
- let {
- oldItem,
- newItems: newItems_,
- replaceables: replaceables_,
- } = await priceToContUseItem({
- price,
- ent,
- prevCusEnt,
- attachParams,
- sub,
- logger,
- curItem: curItem!,
- });
+ let {
+ oldItem,
+ newItems: newItems_,
+ replaceables: replaceables_,
+ } = await priceToContUseItem({
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ sub,
+ logger,
+ curItem: curItem!,
+ });
- if (oldItem) {
- oldItems.push(oldItem);
- }
+ if (oldItem) {
+ oldItems.push(oldItem);
+ }
- newItems.push(...newItems_.filter((item) => item.amount !== 0));
- replaceables.push(...replaceables_);
- }
+ newItems.push(...newItems_.filter((item) => item.amount !== 0));
+ replaceables.push(...replaceables_);
+ }
- // console.log("Replaceables:", replaceables);
+ // console.log("Replaceables:", replaceables);
- return {
- oldItems,
- newItems,
- replaceables,
- };
+ return {
+ oldItems,
+ newItems,
+ replaceables,
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseUpgradeItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseUpgradeItems.ts
index 7431c16c8..475f660fc 100644
--- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseUpgradeItems.ts
+++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseUpgradeItems.ts
@@ -4,11 +4,11 @@ import { Proration } from "@/internal/invoices/prorationUtils.js";
import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js";
import {
- FullEntitlement,
- FullCustomerEntitlement,
- PreviewLineItem,
- Price,
- usageToFeatureName,
+ FullEntitlement,
+ FullCustomerEntitlement,
+ PreviewLineItem,
+ Price,
+ usageToFeatureName,
} from "@autumn/shared";
import { attachParamsToProduct } from "../convertAttachParams.js";
@@ -18,85 +18,85 @@ import { Decimal } from "decimal.js";
import { getPrevAndNewPriceForUpgrade } from "@/trigger/arrearProratedUsage/handleProratedUpgrade.js";
export const getContUseUpgradeItems = async ({
- price,
- ent,
- prevCusEnt,
- attachParams,
- curItem,
- curUsage,
- proration,
- logger,
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ curItem,
+ curUsage,
+ proration,
+ logger,
}: {
- price: Price;
- ent: FullEntitlement;
- prevCusEnt: FullCustomerEntitlement;
- attachParams: AttachParams;
- curItem: PreviewLineItem;
- curUsage: number;
- proration?: Proration;
- logger: any;
+ price: Price;
+ ent: FullEntitlement;
+ prevCusEnt: FullCustomerEntitlement;
+ attachParams: AttachParams;
+ curItem: PreviewLineItem;
+ curUsage: number;
+ proration?: Proration;
+ logger: any;
}) => {
- let prevInvoiceItem = curItem;
- let prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
- let newBalance = ent.allowance! - curUsage;
- let usageDiff = prevBalance - newBalance;
+ let prevInvoiceItem = curItem;
+ let prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
+ let newBalance = ent.allowance! - curUsage;
+ let usageDiff = prevBalance - newBalance;
- const product = attachParamsToProduct({ attachParams });
- const feature = prevCusEnt.entitlement.feature;
+ const product = attachParamsToProduct({ attachParams });
+ const feature = prevCusEnt.entitlement.feature;
- let { usage: prevUsage } = getUsageFromBalance({
- ent: prevCusEnt.entitlement,
- price,
- balance: prevBalance,
- });
+ let { usage: prevUsage } = getUsageFromBalance({
+ ent: prevCusEnt.entitlement,
+ price,
+ balance: prevBalance,
+ });
- let { usage: newUsage } = getUsageFromBalance({
- ent,
- price,
- balance: prevBalance,
- });
+ let { usage: newUsage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: prevBalance,
+ });
- let { usage: totalUsage } = getUsageFromBalance({
- ent,
- price,
- balance: newBalance,
- });
+ let { usage: totalUsage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: newBalance,
+ });
- const newItem = priceToInvoiceItem({
- price,
- ent,
- org: attachParams.org,
- usage: newUsage,
- prodName: product.name,
- proration,
- now: attachParams.now,
- });
+ const newItem = priceToInvoiceItem({
+ price,
+ ent,
+ org: attachParams.org,
+ usage: newUsage,
+ prodName: product.name,
+ proration,
+ now: attachParams.now,
+ });
- const featureName = usageToFeatureName({
- usage: usageDiff,
- feature,
- });
+ const featureName = usageToFeatureName({
+ usage: usageDiff,
+ feature,
+ });
- const { prevPrice, newPrice } = getPrevAndNewPriceForUpgrade({
- price,
- ent,
- prevBalance,
- newBalance,
- logger,
- });
+ const { prevPrice, newPrice } = getPrevAndNewPriceForUpgrade({
+ price,
+ ent,
+ prevBalance,
+ newBalance,
+ logger,
+ });
- const newUsageAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
+ const newUsageAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
- const newUsageItem = constructPreviewItem({
- price,
- amount: newUsageAmount,
- description: `${product.name} - ${usageDiff} additional ${featureName}`,
- });
+ const newUsageItem = constructPreviewItem({
+ price,
+ amount: newUsageAmount,
+ description: `${product.name} - ${usageDiff} additional ${featureName}`,
+ });
- return {
- oldItem: prevInvoiceItem,
- newItem,
- newUsageItem,
- replaceables: [],
- };
+ return {
+ oldItem: prevInvoiceItem,
+ newItem,
+ newUsageItem,
+ replaceables: [],
+ };
};
diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts
index 5d626b92b..42515df36 100644
--- a/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts
+++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts
@@ -1,15 +1,15 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- FullCustomerEntitlement,
- FullEntitlement,
- PreviewLineItem,
- Price,
+ FullCustomerEntitlement,
+ FullEntitlement,
+ PreviewLineItem,
+ Price,
} from "@autumn/shared";
import Stripe from "stripe";
import { attachParamsToProduct } from "../convertAttachParams.js";
import {
- getExistingUsageFromCusProducts,
- getRelatedCusPrice,
+ getExistingUsageFromCusProducts,
+ getRelatedCusPrice,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getContUseDowngradeItems } from "./getContUseDowngradeItems.js";
import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
@@ -22,134 +22,134 @@ import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeS
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const priceToContUseItem = async ({
- price,
- ent,
- prevCusEnt,
- attachParams,
- sub,
- logger,
- curItem,
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ sub,
+ logger,
+ curItem,
}: {
- price: Price;
- ent: FullEntitlement;
- prevCusEnt: FullCustomerEntitlement;
- attachParams: AttachParams;
- sub: Stripe.Subscription | undefined;
- logger: any;
- curItem: PreviewLineItem;
+ price: Price;
+ ent: FullEntitlement;
+ prevCusEnt: FullCustomerEntitlement;
+ attachParams: AttachParams;
+ sub: Stripe.Subscription | undefined;
+ logger: any;
+ curItem: PreviewLineItem;
}) => {
- const { cusProducts, entities, internalEntityId, now } = attachParams;
- const product = attachParamsToProduct({ attachParams });
- const prevEnt = prevCusEnt?.entitlement;
+ const { cusProducts, entities, internalEntityId, now } = attachParams;
+ const product = attachParamsToProduct({ attachParams });
+ const prevEnt = prevCusEnt?.entitlement;
- const prevCusPrice = getRelatedCusPrice(
- prevCusEnt,
- cusProductsToCusPrices({ cusProducts })
- )!;
+ const prevCusPrice = getRelatedCusPrice(
+ prevCusEnt,
+ cusProductsToCusPrices({ cusProducts }),
+ )!;
- let { start, end } = subToPeriodStartEnd({ sub });
+ let { start, end } = subToPeriodStartEnd({ sub });
- if (prevCusPrice) {
- const subItem = findStripeItemForPrice({
- price: prevCusPrice.price,
- stripeItems: sub?.items.data || [],
- });
+ if (prevCusPrice) {
+ const subItem = findStripeItemForPrice({
+ price: prevCusPrice.price,
+ stripeItems: sub?.items.data || [],
+ });
- if (subItem) {
- start = (subItem as Stripe.SubscriptionItem).current_period_start;
- end = (subItem as Stripe.SubscriptionItem).current_period_end;
- }
- }
+ if (subItem) {
+ start = (subItem as Stripe.SubscriptionItem).current_period_start;
+ end = (subItem as Stripe.SubscriptionItem).current_period_end;
+ }
+ }
- const proration = sub
- ? {
- start: start * 1000,
- end: end * 1000,
- }
- : undefined;
+ const proration = sub
+ ? {
+ start: start * 1000,
+ end: end * 1000,
+ }
+ : undefined;
- const isDowngrade = ent.allowance! > prevEnt?.allowance!;
- const willProrate = isDowngrade
- ? shouldProrate(price.proration_config?.on_decrease)
- : shouldProrate(price.proration_config?.on_increase);
+ const isDowngrade = ent.allowance! > prevEnt?.allowance!;
+ const willProrate = isDowngrade
+ ? shouldProrate(price.proration_config?.on_decrease)
+ : shouldProrate(price.proration_config?.on_increase);
- // 1. Get current usage
- let curUsage = getExistingUsageFromCusProducts({
- entitlement: ent,
- cusProducts,
- entities,
- carryExistingUsages: true,
- internalEntityId,
- });
+ // 1. Get current usage
+ let curUsage = getExistingUsageFromCusProducts({
+ entitlement: ent,
+ cusProducts,
+ entities,
+ carryExistingUsages: true,
+ internalEntityId,
+ });
- // Case 1: Downgrade and no proration
- let res;
- if (isDowngrade && !willProrate) {
- res = await getContUseDowngradeItems({
- price,
- ent,
- prevCusEnt,
- attachParams,
- curItem: curItem!,
- curUsage: curUsage,
- proration,
- logger,
- });
- }
+ // Case 1: Downgrade and no proration
+ let res;
+ if (isDowngrade && !willProrate) {
+ res = await getContUseDowngradeItems({
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ curItem: curItem!,
+ curUsage: curUsage,
+ proration,
+ logger,
+ });
+ }
- // Case 2: Upgrade and no proration
- else if (!isDowngrade && !willProrate) {
- res = await getContUseUpgradeItems({
- price,
- ent,
- prevCusEnt,
- attachParams,
- curItem: curItem!,
- curUsage: curUsage,
- proration,
- logger,
- });
- }
+ // Case 2: Upgrade and no proration
+ else if (!isDowngrade && !willProrate) {
+ res = await getContUseUpgradeItems({
+ price,
+ ent,
+ prevCusEnt,
+ attachParams,
+ curItem: curItem!,
+ curUsage: curUsage,
+ proration,
+ logger,
+ });
+ }
- // Case 3: Regular...
- else {
- const newItem = priceToInvoiceItem({
- price,
- ent,
- org: attachParams.org,
- usage: curUsage,
- prodName: product.name,
- proration,
- now,
- });
+ // Case 3: Regular...
+ else {
+ const newItem = priceToInvoiceItem({
+ price,
+ ent,
+ org: attachParams.org,
+ usage: curUsage,
+ prodName: product.name,
+ proration,
+ now,
+ });
- res = {
- oldItem: curItem,
- newItem,
- replaceables: [],
- };
- }
+ res = {
+ oldItem: curItem,
+ newItem,
+ replaceables: [],
+ };
+ }
- // Clean up items
- // 1. If old item and new item same, remove both
- let oldAmount = res.oldItem?.amount!;
- let newAmount = res.newItem?.amount!;
+ // Clean up items
+ // 1. If old item and new item same, remove both
+ let oldAmount = res.oldItem?.amount!;
+ let newAmount = res.newItem?.amount!;
- if (new Decimal(oldAmount).add(newAmount).eq(0)) {
- return {
- oldItem: null,
- newItems: [res.newUsageItem].filter((item) =>
- notNullish(item)
- ) as PreviewLineItem[],
- replaceables: res.replaceables,
- };
- } else {
- return {
- oldItem: res.oldItem,
- newItems: [res.newItem, res.newUsageItem].filter((item) =>
- notNullish(item)
- ) as PreviewLineItem[],
- replaceables: res.replaceables,
- };
- }
+ if (new Decimal(oldAmount).add(newAmount).eq(0)) {
+ return {
+ oldItem: null,
+ newItems: [res.newUsageItem].filter((item) =>
+ notNullish(item),
+ ) as PreviewLineItem[],
+ replaceables: res.replaceables,
+ };
+ } else {
+ return {
+ oldItem: res.oldItem,
+ newItems: [res.newItem, res.newUsageItem].filter((item) =>
+ notNullish(item),
+ ) as PreviewLineItem[],
+ replaceables: res.replaceables,
+ };
+ }
};
diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts
index 03d963be7..174be35b8 100644
--- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts
+++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors.ts
@@ -3,17 +3,17 @@ import { ErrCode } from "@/errors/errCodes.js";
import { StatusCodes } from "http-status-codes";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- AttachBranch,
- AttachConfig,
- AttachErrCode,
- UsagePriceConfig,
+ AttachBranch,
+ AttachConfig,
+ AttachErrCode,
+ UsagePriceConfig,
} from "@autumn/shared";
import { AttachBody } from "@autumn/shared";
import { AttachFlags } from "../models/AttachFlags.js";
import {
- getEntOptions,
- priceIsOneOffAndTiered,
+ getEntOptions,
+ priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
@@ -27,250 +27,250 @@ import { Decimal } from "decimal.js";
import { handleMultiAttachErrors } from "./handleAttachErrors/handleMultiAttachErrors.js";
const handleNonCheckoutErrors = ({
- flags,
- action,
- config,
+ flags,
+ action,
+ config,
}: {
- flags: AttachFlags;
- config: AttachConfig;
- action: string;
+ flags: AttachFlags;
+ config: AttachConfig;
+ action: string;
}) => {
- const { isPublic, forceCheckout, noPaymentMethod } = flags;
+ const { isPublic, forceCheckout, noPaymentMethod } = flags;
- if (isPublic) {
- throw new RecaseError({
- message: `Not allowed to ${action} when using publishable key`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- } else if (forceCheckout) {
- throw new RecaseError({
- message: `Not allowed to ${action} when using force_checkout`,
- code: ErrCode.InvalidRequest,
- });
- } else if (noPaymentMethod) {
- throw new RecaseError({
- message: `Not allowed to ${action} because customer has no payment method on file`,
- code: ErrCode.InvalidRequest,
- });
- } else if (config.invoiceCheckout) {
- throw new RecaseError({
- message: `Not allowed to ${action} when using 'invoice': true`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isPublic) {
+ throw new RecaseError({
+ message: `Not allowed to ${action} when using publishable key`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ } else if (forceCheckout) {
+ throw new RecaseError({
+ message: `Not allowed to ${action} when using force_checkout`,
+ code: ErrCode.InvalidRequest,
+ });
+ } else if (noPaymentMethod) {
+ throw new RecaseError({
+ message: `Not allowed to ${action} because customer has no payment method on file`,
+ code: ErrCode.InvalidRequest,
+ });
+ } else if (config.invoiceCheckout) {
+ throw new RecaseError({
+ message: `Not allowed to ${action} when using 'invoice': true`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
};
const handlePrepaidErrors = async ({
- attachParams,
- config,
- useCheckout = false,
+ attachParams,
+ config,
+ useCheckout = false,
}: {
- attachParams: AttachParams;
- config: AttachConfig;
- useCheckout?: boolean;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ useCheckout?: boolean;
}) => {
- const { prices, entitlements, optionsList } = attachParams;
+ const { prices, entitlements, optionsList } = attachParams;
- // 2. Check if options are valid
- for (const price of prices) {
- const billingType = getBillingType(price.config!);
+ // 2. Check if options are valid
+ for (const price of prices) {
+ const billingType = getBillingType(price.config!);
- if (billingType === BillingType.UsageInAdvance) {
- // Get options for price
- let priceEnt = getPriceEntitlement(price, entitlements);
- let options = getEntOptions(optionsList, priceEnt);
+ if (billingType === BillingType.UsageInAdvance) {
+ // Get options for price
+ let priceEnt = getPriceEntitlement(price, entitlements);
+ let options = getEntOptions(optionsList, priceEnt);
- // 1. If not checkout, quantity should be defined
+ // 1. If not checkout, quantity should be defined
- const regularCheckout = useCheckout && !config.invoiceCheckout;
+ const regularCheckout = useCheckout && !config.invoiceCheckout;
- if (!regularCheckout && nullOrUndefined(options?.quantity)) {
- throw new RecaseError({
- message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
+ if (!regularCheckout && nullOrUndefined(options?.quantity)) {
+ throw new RecaseError({
+ message: `Pass in 'quantity' for feature ${priceEnt.feature_id} in options`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
- if (
- nullOrUndefined(options?.quantity) &&
- priceIsOneOffAndTiered(price, priceEnt)
- ) {
- throw new RecaseError({
- code: ErrCode.InvalidRequest,
- message:
- "Quantity is required for start of period price that is one off and tiered",
- statusCode: 400,
- });
- }
+ if (
+ nullOrUndefined(options?.quantity) &&
+ priceIsOneOffAndTiered(price, priceEnt)
+ ) {
+ throw new RecaseError({
+ code: ErrCode.InvalidRequest,
+ message:
+ "Quantity is required for start of period price that is one off and tiered",
+ statusCode: 400,
+ });
+ }
- // 3. Quantity cannot be negative
- if (notNullish(options?.quantity) && options?.quantity! < 0) {
- throw new RecaseError({
- message: `Quantity cannot be negative`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
+ // 3. Quantity cannot be negative
+ if (notNullish(options?.quantity) && options?.quantity! < 0) {
+ throw new RecaseError({
+ message: `Quantity cannot be negative`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
- // 4. If there's only one price, quantity must be greater than 0
- if (options?.quantity === 0 && prices.length === 1) {
- throw new RecaseError({
- message: `When there's only one price, quantity must be greater than 0`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
+ // 4. If there's only one price, quantity must be greater than 0
+ if (options?.quantity === 0 && prices.length === 1) {
+ throw new RecaseError({
+ message: `When there's only one price, quantity must be greater than 0`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
- let usageLimit = priceEnt.usage_limit;
- let totalQuantity =
- options?.quantity! * (price.config as UsagePriceConfig).billing_units!;
+ let usageLimit = priceEnt.usage_limit;
+ let totalQuantity =
+ options?.quantity! * (price.config as UsagePriceConfig).billing_units!;
- if (usageLimit && totalQuantity + priceEnt.allowance! > usageLimit) {
- throw new RecaseError({
- message: `Quantity + included usage exceeds usage limit of ${usageLimit} for feature ${priceEnt.feature_id}`,
- code: ErrCode.InvalidOptions,
- statusCode: 400,
- });
- }
- }
- }
+ if (usageLimit && totalQuantity + priceEnt.allowance! > usageLimit) {
+ throw new RecaseError({
+ message: `Quantity + included usage exceeds usage limit of ${usageLimit} for feature ${priceEnt.feature_id}`,
+ code: ErrCode.InvalidOptions,
+ statusCode: 400,
+ });
+ }
+ }
+ }
};
const handleUpdateQuantityErrors = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- const { curMainProduct, curSameProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curMainProduct, curSameProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- if (!curSameProduct && !curMainProduct) {
- return;
- }
+ if (!curSameProduct && !curMainProduct) {
+ return;
+ }
- const cusProduct = curSameProduct || curMainProduct!;
- const cusEnts = cusProductsToCusEnts({ cusProducts: [cusProduct] });
- const prices = cusProductToPrices({ cusProduct });
+ const cusProduct = curSameProduct || curMainProduct!;
+ const cusEnts = cusProductsToCusEnts({ cusProducts: [cusProduct] });
+ const prices = cusProductToPrices({ cusProduct });
- for (const option of attachParams.optionsList) {
- const price = findPriceForFeature({
- prices,
- internalFeatureId: option.internal_feature_id!,
- });
+ for (const option of attachParams.optionsList) {
+ const price = findPriceForFeature({
+ prices,
+ internalFeatureId: option.internal_feature_id!,
+ });
- if (!price) continue;
+ if (!price) continue;
- const totalQuantity =
- option.quantity! * (price?.config as UsagePriceConfig).billing_units!;
+ const totalQuantity =
+ option.quantity! * (price?.config as UsagePriceConfig).billing_units!;
- const totalUsage = cusEnts
- .reduce((acc, curr) => {
- if (
- curr.entitlement.internal_feature_id == option.internal_feature_id
- ) {
- const allowance = getResetBalance({
- entitlement: curr.entitlement,
- options: cusProduct.options.find(
- (o) => o.internal_feature_id == option.internal_feature_id
- ),
- relatedPrice: price,
- });
+ const totalUsage = cusEnts
+ .reduce((acc, curr) => {
+ if (
+ curr.entitlement.internal_feature_id == option.internal_feature_id
+ ) {
+ const allowance = getResetBalance({
+ entitlement: curr.entitlement,
+ options: cusProduct.options.find(
+ (o) => o.internal_feature_id == option.internal_feature_id,
+ ),
+ relatedPrice: price,
+ });
- const usage = new Decimal(allowance!).minus(curr.balance!);
+ const usage = new Decimal(allowance!).minus(curr.balance!);
- return acc.plus(usage);
- }
+ return acc.plus(usage);
+ }
- return acc;
- }, new Decimal(0))
- .toNumber();
+ return acc;
+ }, new Decimal(0))
+ .toNumber();
- if (totalUsage > totalQuantity) {
- throw new RecaseError({
- message: `Current usage for ${option.feature_id} is ${totalUsage}, can't update to ${totalQuantity}`,
- code: AttachErrCode.InvalidOptions,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ if (totalUsage > totalQuantity) {
+ throw new RecaseError({
+ message: `Current usage for ${option.feature_id} is ${totalUsage}, can't update to ${totalQuantity}`,
+ code: AttachErrCode.InvalidOptions,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
};
export const handleAttachErrors = async ({
- attachParams,
- attachBody,
- branch,
- flags,
- config,
+ attachParams,
+ attachBody,
+ branch,
+ flags,
+ config,
}: {
- attachParams: AttachParams;
- attachBody: AttachBody;
- branch: AttachBranch;
- flags: AttachFlags;
- config: AttachConfig;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ branch: AttachBranch;
+ flags: AttachFlags;
+ config: AttachConfig;
}) => {
- const { onlyCheckout } = config;
+ const { onlyCheckout } = config;
- if (branch === AttachBranch.MultiAttach) {
- await handleMultiAttachErrors({
- attachParams,
- attachBody,
- branch,
- });
- return;
- }
+ if (branch === AttachBranch.MultiAttach) {
+ await handleMultiAttachErrors({
+ attachParams,
+ attachBody,
+ branch,
+ });
+ return;
+ }
- // Invoice no payment enabled: onlyCheckout
+ // Invoice no payment enabled: onlyCheckout
- if (onlyCheckout || flags.isPublic) {
- let upgradeDowngradeFlows = [
- AttachBranch.Upgrade,
- AttachBranch.Downgrade,
- AttachBranch.MainIsTrial,
- ];
- if (upgradeDowngradeFlows.includes(branch)) {
- handleNonCheckoutErrors({
- flags,
- config,
- action: "perform upgrade or downgrade",
- });
- }
- let updateProductFlows = [
- AttachBranch.NewVersion,
- AttachBranch.SameCustom,
- AttachBranch.UpdatePrepaidQuantity,
- ];
- if (updateProductFlows.includes(branch)) {
- handleNonCheckoutErrors({
- flags,
- action: "update current product",
- config,
- });
- }
- }
+ if (onlyCheckout || flags.isPublic) {
+ let upgradeDowngradeFlows = [
+ AttachBranch.Upgrade,
+ AttachBranch.Downgrade,
+ AttachBranch.MainIsTrial,
+ ];
+ if (upgradeDowngradeFlows.includes(branch)) {
+ handleNonCheckoutErrors({
+ flags,
+ config,
+ action: "perform upgrade or downgrade",
+ });
+ }
+ let updateProductFlows = [
+ AttachBranch.NewVersion,
+ AttachBranch.SameCustom,
+ AttachBranch.UpdatePrepaidQuantity,
+ ];
+ if (updateProductFlows.includes(branch)) {
+ handleNonCheckoutErrors({
+ flags,
+ action: "update current product",
+ config,
+ });
+ }
+ }
- // 2. If same custom ents, not allowed if is public flow...
- if (branch == AttachBranch.SameCustomEnts) {
- if (flags.isPublic) {
- throw new RecaseError({
- message:
- "Not allowed to update current product when using publishable key",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ // 2. If same custom ents, not allowed if is public flow...
+ if (branch == AttachBranch.SameCustomEnts) {
+ if (flags.isPublic) {
+ throw new RecaseError({
+ message:
+ "Not allowed to update current product when using publishable key",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- await handlePrepaidErrors({
- attachParams,
- config,
- useCheckout: onlyCheckout,
- });
+ await handlePrepaidErrors({
+ attachParams,
+ config,
+ useCheckout: onlyCheckout,
+ });
- // await handleUpdateQuantityErrors({
- // attachParams,
- // });
+ // await handleUpdateQuantityErrors({
+ // attachParams,
+ // });
};
diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts
index 4f2cbffdd..dab76f218 100644
--- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts
+++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts
@@ -2,13 +2,13 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { AttachBranch } from "@autumn/shared";
export const handleCheckoutErrors = ({
- attachParams,
- branch,
+ attachParams,
+ branch,
}: {
- attachParams: AttachParams;
- branch: AttachBranch;
+ attachParams: AttachParams;
+ branch: AttachBranch;
}) => {
- if (attachParams.setupPayment) {
- // Make sure only usage prices are added?
- }
+ if (attachParams.setupPayment) {
+ // Make sure only usage prices are added?
+ }
};
diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleMultiAttachErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleMultiAttachErrors.ts
index 1aabd58cb..6c9883005 100644
--- a/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleMultiAttachErrors.ts
+++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleMultiAttachErrors.ts
@@ -1,65 +1,65 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- isArrearPrice,
- isContUsePrice,
- isUsagePrice,
+ isArrearPrice,
+ isContUsePrice,
+ isUsagePrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- AttachBody,
- AttachBranch,
- notNullish,
- nullish,
- Price,
+ AttachBody,
+ AttachBranch,
+ notNullish,
+ nullish,
+ Price,
} from "@autumn/shared";
export const handleMultiAttachErrors = async ({
- attachParams,
- attachBody,
- branch,
+ attachParams,
+ attachBody,
+ branch,
}: {
- attachParams: AttachParams;
- attachBody: AttachBody;
- branch: AttachBranch;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ branch: AttachBranch;
}) => {
- const { products, prices, productsList } = attachParams;
+ const { products, prices, productsList } = attachParams;
- const usagePrice = prices.find((p: Price) => isUsagePrice({ price: p }));
+ const usagePrice = prices.find((p: Price) => isUsagePrice({ price: p }));
- // 1. Don't support usage prices just yet...
- if (usagePrice) {
- const product = products.find(
- (p) => p.internal_id === usagePrice.internal_product_id
- );
- throw new RecaseError({
- code: "invalid_inputs",
- message: `The 'products' parameter doesn't support prices that are variable (usage-based) at the moment. The product ${product?.name} contains this.`,
- });
- }
+ // 1. Don't support usage prices just yet...
+ if (usagePrice) {
+ const product = products.find(
+ (p) => p.internal_id === usagePrice.internal_product_id,
+ );
+ throw new RecaseError({
+ code: "invalid_inputs",
+ message: `The 'products' parameter doesn't support prices that are variable (usage-based) at the moment. The product ${product?.name} contains this.`,
+ });
+ }
- // If there are multiple products...
- const cusProducts = attachParams.customer.customer_products;
- for (const prodOptions of productsList!) {
- const newQuantity = prodOptions.quantity || 1;
- const curCusQuantity =
- cusProducts.find(
- (cp) =>
- cp.product_id === prodOptions.product_id &&
- nullish(cp.internal_entity_id)
- )?.quantity || 0;
+ // If there are multiple products...
+ const cusProducts = attachParams.customer.customer_products;
+ for (const prodOptions of productsList!) {
+ const newQuantity = prodOptions.quantity || 1;
+ const curCusQuantity =
+ cusProducts.find(
+ (cp) =>
+ cp.product_id === prodOptions.product_id &&
+ nullish(cp.internal_entity_id),
+ )?.quantity || 0;
- const curEntityQuantity =
- cusProducts.filter(
- (cp) =>
- cp.product_id === prodOptions.product_id &&
- notNullish(cp.internal_entity_id)
- )?.length || 0;
+ const curEntityQuantity =
+ cusProducts.filter(
+ (cp) =>
+ cp.product_id === prodOptions.product_id &&
+ notNullish(cp.internal_entity_id),
+ )?.length || 0;
- if (newQuantity < curEntityQuantity) {
- throw new RecaseError({
- code: "invalid_inputs",
- message: `Product ${prodOptions.product_id} is assigned to ${curEntityQuantity} entities and therefore can't be decreased to ${newQuantity}.`,
- });
- }
- }
+ if (newQuantity < curEntityQuantity) {
+ throw new RecaseError({
+ code: "invalid_inputs",
+ message: `Product ${prodOptions.product_id} is assigned to ${curEntityQuantity} entities and therefore can't be decreased to ${newQuantity}.`,
+ });
+ }
+ }
};
diff --git a/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts b/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts
index 17db86b41..16ba2567f 100644
--- a/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts
+++ b/server/src/internal/customers/attach/attachUtils/insertCustomItems.ts
@@ -4,21 +4,21 @@ import { PriceService } from "@/internal/products/prices/PriceService.js";
import { Entitlement, Price } from "@autumn/shared";
export const insertCustomItems = async ({
- db,
- customPrices,
- customEnts,
+ db,
+ customPrices,
+ customEnts,
}: {
- db: DrizzleCli;
- customPrices: Price[];
- customEnts: Entitlement[];
+ db: DrizzleCli;
+ customPrices: Price[];
+ customEnts: Entitlement[];
}) => {
- await EntitlementService.insert({
- db,
- data: customEnts,
- });
+ await EntitlementService.insert({
+ db,
+ data: customEnts,
+ });
- await PriceService.insert({
- db,
- data: customPrices,
- });
+ await PriceService.insert({
+ db,
+ data: customPrices,
+ });
};
diff --git a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts
index 88bbdf18d..9b30e4344 100644
--- a/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts
+++ b/server/src/internal/customers/attach/attachUtils/mapOptionsList.ts
@@ -9,77 +9,77 @@ import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
export const mapOptionsList = ({
- optionsInput,
- features,
- prices,
- curCusProduct,
+ optionsInput,
+ features,
+ prices,
+ curCusProduct,
}: {
- optionsInput?: FeatureOptions[]; // options input
- features: Feature[];
- prices: Price[];
- curCusProduct?: FullCusProduct;
+ optionsInput?: FeatureOptions[]; // options input
+ features: Feature[];
+ prices: Price[];
+ curCusProduct?: FullCusProduct;
}) => {
- let newOptionsList: FeatureOptions[] = [];
+ let newOptionsList: FeatureOptions[] = [];
- for (const options of optionsInput || []) {
- const feature = features.find(
- (feature) => feature.id === options.feature_id
- );
+ for (const options of optionsInput || []) {
+ const feature = features.find(
+ (feature) => feature.id === options.feature_id,
+ );
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${options.feature_id} passed into options but not found`,
- code: ErrCode.FeatureNotFound,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${options.feature_id} passed into options but not found`,
+ code: ErrCode.FeatureNotFound,
+ });
+ }
- const prepaidPrice = findPrepaidPrice({
- prices,
- internalFeatureId: feature.internal_id,
- });
+ const prepaidPrice = findPrepaidPrice({
+ prices,
+ internalFeatureId: feature.internal_id,
+ });
- if (!prepaidPrice) {
- throw new RecaseError({
- message: `No prepaid price found for feature ${feature.id}`,
- code: ErrCode.PriceNotFound,
- });
- }
+ if (!prepaidPrice) {
+ throw new RecaseError({
+ message: `No prepaid price found for feature ${feature.id}`,
+ code: ErrCode.PriceNotFound,
+ });
+ }
- // const ent = getPriceEntitlement(prepaidPrice, entitlements)
+ // const ent = getPriceEntitlement(prepaidPrice, entitlements)
- let config = prepaidPrice.config as UsagePriceConfig;
+ let config = prepaidPrice.config as UsagePriceConfig;
- let dividedQuantity = new Decimal(options.quantity!)
- .div(config.billing_units || 1)
- .ceil()
- .toNumber();
+ let dividedQuantity = new Decimal(options.quantity!)
+ .div(config.billing_units || 1)
+ .ceil()
+ .toNumber();
- newOptionsList.push({
- ...options,
- internal_feature_id: feature.internal_id,
- quantity: dividedQuantity,
- });
- }
+ newOptionsList.push({
+ ...options,
+ internal_feature_id: feature.internal_id,
+ quantity: dividedQuantity,
+ });
+ }
- // If product is one off, return
- if (isOneOff(prices) || isFreeProduct(prices)) return newOptionsList;
+ // If product is one off, return
+ if (isOneOff(prices) || isFreeProduct(prices)) return newOptionsList;
- const curOptionsList = curCusProduct?.options || [];
- for (const option of curOptionsList) {
- const inNewOptions = newOptionsList.find(
- (newOption) =>
- newOption.internal_feature_id === option.internal_feature_id
- );
+ const curOptionsList = curCusProduct?.options || [];
+ for (const option of curOptionsList) {
+ const inNewOptions = newOptionsList.find(
+ (newOption) =>
+ newOption.internal_feature_id === option.internal_feature_id,
+ );
- const prepaidPriceExists = findPrepaidPrice({
- prices,
- internalFeatureId: option.internal_feature_id!,
- });
+ const prepaidPriceExists = findPrepaidPrice({
+ prices,
+ internalFeatureId: option.internal_feature_id!,
+ });
- if (!inNewOptions && prepaidPriceExists) {
- newOptionsList.push(option);
- }
- }
+ if (!inNewOptions && prepaidPriceExists) {
+ newOptionsList.push(option);
+ }
+ }
- return newOptionsList;
+ return newOptionsList;
};
diff --git a/server/src/internal/customers/attach/checkout/getHasProrations.ts b/server/src/internal/customers/attach/checkout/getHasProrations.ts
index 1f45a96c4..2f96643d8 100644
--- a/server/src/internal/customers/attach/checkout/getHasProrations.ts
+++ b/server/src/internal/customers/attach/checkout/getHasProrations.ts
@@ -2,35 +2,35 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachBranch } from "@autumn/shared";
import {
- attachParamsToCurCusProduct,
- attachParamToCusProducts,
+ attachParamsToCurCusProduct,
+ attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const getHasProrations = async ({
- req,
- branch,
- attachParams,
+ req,
+ branch,
+ attachParams,
}: {
- req: ExtendedRequest;
- branch: AttachBranch;
- attachParams: AttachParams;
+ req: ExtendedRequest;
+ branch: AttachBranch;
+ attachParams: AttachParams;
}) => {
- let hasProrations = false;
+ let hasProrations = false;
- let { curMainProduct } = attachParamToCusProducts({ attachParams });
- if (branch == AttachBranch.Upgrade) {
- let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
+ let { curMainProduct } = attachParamToCusProducts({ attachParams });
+ if (branch == AttachBranch.Upgrade) {
+ let curPrices = cusProductToPrices({ cusProduct: curMainProduct! });
- if (!isFreeProduct(curPrices)) {
- return true;
- }
- }
+ if (!isFreeProduct(curPrices)) {
+ return true;
+ }
+ }
- if (branch == AttachBranch.UpdatePrepaidQuantity) {
- return true;
- }
+ if (branch == AttachBranch.UpdatePrepaidQuantity) {
+ return true;
+ }
- return false;
+ return false;
};
diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts
index 410fd1764..1080a6ceb 100644
--- a/server/src/internal/customers/attach/checkout/handleCheckout.ts
+++ b/server/src/internal/customers/attach/checkout/handleCheckout.ts
@@ -10,8 +10,8 @@ import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
import {
- checkStripeConnections,
- handlePrepaidErrors,
+ checkStripeConnections,
+ handlePrepaidErrors,
} from "../attachRouter.js";
import { attachParamsToPreview } from "../handleAttachPreview/attachParamsToPreview.js";
import { previewToCheckoutRes } from "./previewToCheckoutRes.js";
@@ -26,176 +26,176 @@ import { z } from "zod";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
const getAttachVars = async ({
- req,
- attachBody,
+ req,
+ attachBody,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
}) => {
- const { attachParams } = await getAttachParams({
- req,
- attachBody,
- });
+ const { attachParams } = await getAttachParams({
+ req,
+ attachBody,
+ });
- const branch = await getAttachBranch({
- req,
- attachBody,
- attachParams,
- fromPreview: true,
- });
+ const branch = await getAttachBranch({
+ req,
+ attachBody,
+ attachParams,
+ fromPreview: true,
+ });
- const { flags, config } = await getAttachConfig({
- req,
- attachParams,
- attachBody,
- branch,
- });
+ const { flags, config } = await getAttachConfig({
+ req,
+ attachParams,
+ attachBody,
+ branch,
+ });
- const func = await getAttachFunction({
- branch,
- attachParams,
- attachBody,
- config,
- });
+ const func = await getAttachFunction({
+ branch,
+ attachParams,
+ attachBody,
+ config,
+ });
- return {
- attachParams,
- flags,
- branch,
- config,
- func,
- };
+ return {
+ attachParams,
+ flags,
+ branch,
+ config,
+ func,
+ };
};
const getCheckoutOptions = async ({
- req,
- attachParams,
+ req,
+ attachParams,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
}) => {
- const product = attachParamsToProduct({ attachParams });
- const prepaidPrices = product.prices.filter((p) =>
- isPrepaidPrice({ price: p })
- );
+ const product = attachParamsToProduct({ attachParams });
+ const prepaidPrices = product.prices.filter((p) =>
+ isPrepaidPrice({ price: p }),
+ );
- let newOptions: FeatureOptions[] = structuredClone(attachParams.optionsList);
- for (const prepaidPrice of prepaidPrices) {
- const feature = priceToFeature({
- price: prepaidPrice,
- features: req.features,
- });
- let option = getPriceOptions(prepaidPrice, attachParams.optionsList);
- if (!option) {
- newOptions.push({
- feature_id: feature?.id!,
- internal_feature_id: feature?.internal_id,
- quantity: 1,
- });
- }
- }
+ let newOptions: FeatureOptions[] = structuredClone(attachParams.optionsList);
+ for (const prepaidPrice of prepaidPrices) {
+ const feature = priceToFeature({
+ price: prepaidPrice,
+ features: req.features,
+ });
+ let option = getPriceOptions(prepaidPrice, attachParams.optionsList);
+ if (!option) {
+ newOptions.push({
+ feature_id: feature?.id!,
+ internal_feature_id: feature?.internal_id,
+ quantity: 1,
+ });
+ }
+ }
- attachParams.optionsList = newOptions;
- return newOptions;
+ attachParams.optionsList = newOptions;
+ return newOptions;
};
export const handleCheckout = (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "attach-preview",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { logger, features } = req;
+ routeHandler({
+ req,
+ res,
+ action: "attach-preview",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { logger, features } = req;
- const attachBody = AttachBodySchema.parse(req.body);
+ const attachBody = AttachBodySchema.parse(req.body);
- const { attachParams, branch, func, config } = await getAttachVars({
- req,
- attachBody,
- });
+ const { attachParams, branch, func, config } = await getAttachVars({
+ req,
+ attachBody,
+ });
- let checkoutUrl = null;
+ let checkoutUrl = null;
- if (func == AttachFunction.CreateCheckout) {
- await checkStripeConnections({
- req,
- attachParams,
- createCus: true,
- useCheckout: true,
- });
+ if (func == AttachFunction.CreateCheckout) {
+ await checkStripeConnections({
+ req,
+ attachParams,
+ createCus: true,
+ useCheckout: true,
+ });
- await handlePrepaidErrors({
- attachParams,
- config,
- useCheckout: config.onlyCheckout,
- });
+ await handlePrepaidErrors({
+ attachParams,
+ config,
+ useCheckout: config.onlyCheckout,
+ });
- if (config.invoiceCheckout) {
- const result = await handleCreateInvoiceCheckout({
- req,
- attachParams,
- attachBody,
- branch,
- config,
- });
+ if (config.invoiceCheckout) {
+ const result = await handleCreateInvoiceCheckout({
+ req,
+ attachParams,
+ attachBody,
+ branch,
+ config,
+ });
- checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
- } else {
- const checkout = await handleCreateCheckout({
- req,
- res,
- attachParams,
- config,
- returnCheckout: true,
- });
+ checkoutUrl = result?.invoices?.[0]?.hosted_invoice_url;
+ } else {
+ const checkout = await handleCreateCheckout({
+ req,
+ res,
+ attachParams,
+ config,
+ returnCheckout: true,
+ });
- checkoutUrl = checkout?.url;
- }
- }
+ checkoutUrl = checkout?.url;
+ }
+ }
- console.log(`Branch: ${branch}, Func: ${func}`);
+ console.log(`Branch: ${branch}, Func: ${func}`);
- await getCheckoutOptions({
- req,
- attachParams,
- });
+ await getCheckoutOptions({
+ req,
+ attachParams,
+ });
- const preview = await attachParamsToPreview({
- req,
- attachParams,
- logger,
- attachBody,
- withPrepaid: true,
- });
+ const preview = await attachParamsToPreview({
+ req,
+ attachParams,
+ logger,
+ attachBody,
+ withPrepaid: true,
+ });
- const checkoutRes = await previewToCheckoutRes({
- req,
- attachParams,
- preview,
- branch,
- });
+ const checkoutRes = await previewToCheckoutRes({
+ req,
+ attachParams,
+ preview,
+ branch,
+ });
- // Get has prorations
- const hasProrations = await getHasProrations({
- req,
- branch,
- attachParams,
- });
+ // Get has prorations
+ const hasProrations = await getHasProrations({
+ req,
+ branch,
+ attachParams,
+ });
- if (checkoutRes.next_cycle) {
- const nextCycle = checkoutRes.next_cycle;
- }
+ if (checkoutRes.next_cycle) {
+ const nextCycle = checkoutRes.next_cycle;
+ }
- res.status(200).json({
- ...checkoutRes,
- url: checkoutUrl,
- options: attachParams.optionsList.map((o) => ({
- quantity: o.quantity,
- feature_id: o.feature_id,
- })),
- has_prorations: hasProrations,
- });
+ res.status(200).json({
+ ...checkoutRes,
+ url: checkoutUrl,
+ options: attachParams.optionsList.map((o) => ({
+ quantity: o.quantity,
+ feature_id: o.feature_id,
+ })),
+ has_prorations: hasProrations,
+ });
- return;
- },
- });
+ return;
+ },
+ });
diff --git a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts b/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts
index df1d8367d..02a063db6 100644
--- a/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts
+++ b/server/src/internal/customers/attach/checkout/previewToCheckoutRes.ts
@@ -1,25 +1,25 @@
import {
- AttachBranch,
- AttachPreview,
- CheckoutLine,
- CheckoutResponseSchema,
- UsageModel,
+ AttachBranch,
+ AttachPreview,
+ CheckoutLine,
+ CheckoutResponseSchema,
+ UsageModel,
} from "@autumn/shared";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- attachParamsToProduct,
- attachParamToCusProducts,
+ attachParamsToProduct,
+ attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import {
- cusProductToEnts,
- cusProductToPrices,
- cusProductToProduct,
+ cusProductToEnts,
+ cusProductToPrices,
+ cusProductToProduct,
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- getProductItemResponse,
- getProductResponse,
+ getProductItemResponse,
+ getProductResponse,
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
import { toProductItem } from "@autumn/shared";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
@@ -28,136 +28,136 @@ import { isPriceItem } from "@/internal/products/product-items/productItemUtils/
import { Decimal } from "decimal.js";
export const previewToCheckoutRes = async ({
- req,
- attachParams,
- preview,
- branch,
+ req,
+ attachParams,
+ preview,
+ branch,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- branch: AttachBranch;
- preview: AttachPreview;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ branch: AttachBranch;
+ preview: AttachPreview;
}) => {
- const { logger, features, org } = req;
- const product = attachParamsToProduct({ attachParams });
+ const { logger, features, org } = req;
+ const product = attachParamsToProduct({ attachParams });
- const { curCusProduct } = attachParamToCusProducts({ attachParams });
- let curPrices = curCusProduct
- ? cusProductToPrices({ cusProduct: curCusProduct })
- : [];
- let curEnts = curCusProduct
- ? cusProductToEnts({ cusProduct: curCusProduct })
- : [];
+ const { curCusProduct } = attachParamToCusProducts({ attachParams });
+ let curPrices = curCusProduct
+ ? cusProductToPrices({ cusProduct: curCusProduct })
+ : [];
+ let curEnts = curCusProduct
+ ? cusProductToEnts({ cusProduct: curCusProduct })
+ : [];
- let newPrices = attachParams.prices;
- let newEnts = attachParams.entitlements;
- let allPrices = [...curPrices, ...newPrices];
- let allEnts = [...curEnts, ...newEnts];
- let lines: CheckoutLine[] = [];
+ let newPrices = attachParams.prices;
+ let newEnts = attachParams.entitlements;
+ let allPrices = [...curPrices, ...newPrices];
+ let allEnts = [...curEnts, ...newEnts];
+ let lines: CheckoutLine[] = [];
- if (preview.due_today && preview.due_today.line_items.length > 0) {
- lines = preview.due_today.line_items
- .map((li: any) => {
- let price = allPrices.find((p) => p.id == li.price_id);
+ if (preview.due_today && preview.due_today.line_items.length > 0) {
+ lines = preview.due_today.line_items
+ .map((li: any) => {
+ let price = allPrices.find((p) => p.id == li.price_id);
- if (!price) {
- return null;
- }
+ if (!price) {
+ return null;
+ }
- let ent = getPriceEntitlement(price, allEnts);
+ let ent = getPriceEntitlement(price, allEnts);
- return {
- description: li.description || "",
- amount: li.amount || 0,
- item: getProductItemResponse({
- item: toProductItem({ ent, price }),
- features,
- currency: org.default_currency,
- withDisplay: true,
- options: attachParams.optionsList,
- }),
- };
- })
- .filter(notNullish) as CheckoutLine[];
- }
+ return {
+ description: li.description || "",
+ amount: li.amount || 0,
+ item: getProductItemResponse({
+ item: toProductItem({ ent, price }),
+ features,
+ currency: org.default_currency,
+ withDisplay: true,
+ options: attachParams.optionsList,
+ }),
+ };
+ })
+ .filter(notNullish) as CheckoutLine[];
+ }
- const newProduct = await getProductResponse({
- product,
- features,
- currency: org.default_currency,
- options: attachParams.optionsList,
- fullCus: attachParams.customer,
- });
+ const newProduct = await getProductResponse({
+ product,
+ features,
+ currency: org.default_currency,
+ options: attachParams.optionsList,
+ fullCus: attachParams.customer,
+ });
- let curProduct = curCusProduct
- ? await getProductResponse({
- product: cusProductToProduct({ cusProduct: curCusProduct }),
- features,
- currency: org.default_currency,
- options: curCusProduct?.options,
- })
- : null;
+ let curProduct = curCusProduct
+ ? await getProductResponse({
+ product: cusProductToProduct({ cusProduct: curCusProduct }),
+ features,
+ currency: org.default_currency,
+ options: curCusProduct?.options,
+ })
+ : null;
- const total = lines.reduce((acc, line) => acc + line.amount, 0);
+ const total = lines.reduce((acc, line) => acc + line.amount, 0);
- let nextCycle = undefined;
+ let nextCycle = undefined;
- if (
- notNullish(preview.due_next_cycle) &&
- notNullish(preview.due_next_cycle.due_at)
- ) {
- let total = newProduct.items
- .reduce((acc, item) => {
- if (item.usage_model == UsageModel.PayPerUse) {
- return acc;
- }
+ if (
+ notNullish(preview.due_next_cycle) &&
+ notNullish(preview.due_next_cycle.due_at)
+ ) {
+ let total = newProduct.items
+ .reduce((acc, item) => {
+ if (item.usage_model == UsageModel.PayPerUse) {
+ return acc;
+ }
- if (isPriceItem(item)) {
- return acc.plus(item.price || 0);
- }
+ if (isPriceItem(item)) {
+ return acc.plus(item.price || 0);
+ }
- let prepaidQuantity =
- attachParams.optionsList.find((o) => o.feature_id == item.feature_id)
- ?.quantity || 0;
+ let prepaidQuantity =
+ attachParams.optionsList.find((o) => o.feature_id == item.feature_id)
+ ?.quantity || 0;
- return acc.plus(prepaidQuantity * (item.price || 0));
- }, new Decimal(0))
- .toNumber();
+ return acc.plus(prepaidQuantity * (item.price || 0));
+ }, new Decimal(0))
+ .toNumber();
- try {
- if (
- preview.due_next_cycle &&
- preview.due_next_cycle.line_items &&
- preview.due_next_cycle.line_items.length > 0
- ) {
- total = preview.due_next_cycle.line_items
- .reduce((acc, item) => {
- return acc.plus(item.amount || 0);
- }, new Decimal(0))
- .toNumber();
- }
- } catch (error) {
- logger.error("Error calculating total for due next cycle", {
- error,
- });
- }
+ try {
+ if (
+ preview.due_next_cycle &&
+ preview.due_next_cycle.line_items &&
+ preview.due_next_cycle.line_items.length > 0
+ ) {
+ total = preview.due_next_cycle.line_items
+ .reduce((acc, item) => {
+ return acc.plus(item.amount || 0);
+ }, new Decimal(0))
+ .toNumber();
+ }
+ } catch (error) {
+ logger.error("Error calculating total for due next cycle", {
+ error,
+ });
+ }
- nextCycle = {
- starts_at: preview.due_next_cycle.due_at,
- total: total,
- };
- }
+ nextCycle = {
+ starts_at: preview.due_next_cycle.due_at,
+ total: total,
+ };
+ }
- return CheckoutResponseSchema.parse({
- customer_id: attachParams.customer.id,
- lines,
- product: newProduct,
- current_product: curProduct,
- total: new Decimal(total).toDecimalPlaces(2).toNumber(),
- currency: org.default_currency || "usd",
- next_cycle_at: notNullish(preview.due_next_cycle)
- ? preview.due_next_cycle.due_at
- : null,
- next_cycle: nextCycle,
- });
+ return CheckoutResponseSchema.parse({
+ customer_id: attachParams.customer.id,
+ lines,
+ product: newProduct,
+ current_product: curProduct,
+ total: new Decimal(total).toDecimalPlaces(2).toNumber(),
+ currency: org.default_currency || "usd",
+ next_cycle_at: notNullish(preview.due_next_cycle)
+ ? preview.due_next_cycle.due_at
+ : null,
+ next_cycle: nextCycle,
+ });
};
diff --git a/server/src/internal/customers/attach/handleAttach.ts b/server/src/internal/customers/attach/handleAttach.ts
index 99e14fd98..83242dc1a 100644
--- a/server/src/internal/customers/attach/handleAttach.ts
+++ b/server/src/internal/customers/attach/handleAttach.ts
@@ -11,83 +11,83 @@ import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
export const handleAttach = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "attach",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- await handleAttachRaceCondition({ req, res });
+ routeHandler({
+ req,
+ res,
+ action: "attach",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ await handleAttachRaceCondition({ req, res });
- const attachBody = AttachBodySchema.parse(req.body);
+ const attachBody = AttachBodySchema.parse(req.body);
- const { attachParams, customPrices, customEnts } = await getAttachParams({
- req,
- attachBody,
- });
+ const { attachParams, customPrices, customEnts } = await getAttachParams({
+ req,
+ attachBody,
+ });
- // Handle existing product
- const branch = await getAttachBranch({
- req,
- attachBody,
- attachParams,
- });
+ // Handle existing product
+ const branch = await getAttachBranch({
+ req,
+ attachBody,
+ attachParams,
+ });
- const { flags, config } = await getAttachConfig({
- req,
- attachParams,
- attachBody,
- branch,
- });
+ const { flags, config } = await getAttachConfig({
+ req,
+ attachParams,
+ attachBody,
+ branch,
+ });
- await handleAttachErrors({
- attachParams,
- attachBody,
- branch,
- flags,
- config,
- });
+ await handleAttachErrors({
+ attachParams,
+ attachBody,
+ branch,
+ flags,
+ config,
+ });
- await checkStripeConnections({
- req,
- attachParams,
- useCheckout: config.onlyCheckout,
- });
+ await checkStripeConnections({
+ req,
+ attachParams,
+ useCheckout: config.onlyCheckout,
+ });
- await insertCustomItems({
- db: req.db,
- customPrices: customPrices || [],
- customEnts: customEnts || [],
- });
+ await insertCustomItems({
+ db: req.db,
+ customPrices: customPrices || [],
+ customEnts: customEnts || [],
+ });
- try {
- req.logger.info(`Attach params: `, {
- data: {
- products: attachParams.products.map((p) => ({
- id: p.id,
- name: p.name,
- processor: p.processor,
- version: p.version,
- })),
- prices: attachParams.prices.map((p) => ({
- id: p.id,
- config: p.config,
- })),
- entitlements: attachParams.entitlements.map((e) => ({
- internal_feature_id: e.internal_feature_id,
- feature_id: e.feature_id,
- })),
- freeTrial: attachParams.freeTrial,
- },
- });
- } catch (error) {}
+ try {
+ req.logger.info(`Attach params: `, {
+ data: {
+ products: attachParams.products.map((p) => ({
+ id: p.id,
+ name: p.name,
+ processor: p.processor,
+ version: p.version,
+ })),
+ prices: attachParams.prices.map((p) => ({
+ id: p.id,
+ config: p.config,
+ })),
+ entitlements: attachParams.entitlements.map((e) => ({
+ internal_feature_id: e.internal_feature_id,
+ feature_id: e.feature_id,
+ })),
+ freeTrial: attachParams.freeTrial,
+ },
+ });
+ } catch (error) {}
- await runAttachFunction({
- req,
- res,
- attachParams,
- branch,
- attachBody,
- config,
- });
- },
- });
+ await runAttachFunction({
+ req,
+ res,
+ attachParams,
+ branch,
+ attachBody,
+ config,
+ });
+ },
+ });
diff --git a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts
index 79d4e5684..03ddf41f5 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/attachParamsToPreview.ts
@@ -14,113 +14,113 @@ import { getMultiAttachPreview } from "./getMultiAttachPreview.js";
import { notNullish } from "@/utils/genUtils.js";
export const attachParamsToPreview = async ({
- req,
- attachParams,
- attachBody,
- logger,
- withPrepaid = false,
+ req,
+ attachParams,
+ attachBody,
+ logger,
+ withPrepaid = false,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- attachBody: AttachBody;
- logger: any;
- withPrepaid?: boolean;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ attachBody: AttachBody;
+ logger: any;
+ withPrepaid?: boolean;
}) => {
- // Handle existing product
+ // Handle existing product
- const branch = await getAttachBranch({
- req,
- attachBody,
- attachParams,
- fromPreview: true,
- });
+ const branch = await getAttachBranch({
+ req,
+ attachBody,
+ attachParams,
+ fromPreview: true,
+ });
- const { flags, config } = await getAttachConfig({
- req,
- attachParams,
- attachBody,
- branch,
- });
+ const { flags, config } = await getAttachConfig({
+ req,
+ attachParams,
+ attachBody,
+ branch,
+ });
- const func = await getAttachFunction({
- branch,
- attachParams,
- attachBody,
- config,
- });
+ const func = await getAttachFunction({
+ branch,
+ attachParams,
+ attachBody,
+ config,
+ });
- logger.info("--------------------------------");
- logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
- logger.info(`Branch: ${branch}, Function: ${func}`);
+ logger.info("--------------------------------");
+ logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
+ logger.info(`Branch: ${branch}, Function: ${func}`);
- let now = attachParams.now || Date.now();
+ let now = attachParams.now || Date.now();
- let preview: any = null;
+ let preview: any = null;
- if (
- branch == AttachBranch.MultiAttach ||
- notNullish(attachParams.productsList)
- ) {
- preview = await getMultiAttachPreview({
- req,
- attachBody,
- attachParams,
- logger,
- config,
- branch,
- });
- } else if (
- func == AttachFunction.AddProduct ||
- func == AttachFunction.CreateCheckout ||
- func == AttachFunction.OneOff
- ) {
- preview = await getNewProductPreview({
- branch,
- attachParams,
- logger,
- config,
- withPrepaid,
- });
- }
+ if (
+ branch == AttachBranch.MultiAttach ||
+ notNullish(attachParams.productsList)
+ ) {
+ preview = await getMultiAttachPreview({
+ req,
+ attachBody,
+ attachParams,
+ logger,
+ config,
+ branch,
+ });
+ } else if (
+ func == AttachFunction.AddProduct ||
+ func == AttachFunction.CreateCheckout ||
+ func == AttachFunction.OneOff
+ ) {
+ preview = await getNewProductPreview({
+ branch,
+ attachParams,
+ logger,
+ config,
+ withPrepaid,
+ });
+ }
- if (func == AttachFunction.ScheduleProduct) {
- preview = await getDowngradeProductPreview({
- attachParams,
- now,
- logger,
- branch,
- config,
- });
- }
+ if (func == AttachFunction.ScheduleProduct) {
+ preview = await getDowngradeProductPreview({
+ attachParams,
+ now,
+ logger,
+ branch,
+ config,
+ });
+ }
- if (
- func == AttachFunction.UpgradeDiffInterval ||
- func == AttachFunction.UpgradeSameInterval ||
- func == AttachFunction.UpdatePrepaidQuantity
- ) {
- preview = await getUpgradeProductPreview({
- req,
- attachParams,
- branch,
- now,
- withPrepaid,
- config,
- });
- }
+ if (
+ func == AttachFunction.UpgradeDiffInterval ||
+ func == AttachFunction.UpgradeSameInterval ||
+ func == AttachFunction.UpdatePrepaidQuantity
+ ) {
+ preview = await getUpgradeProductPreview({
+ req,
+ attachParams,
+ branch,
+ now,
+ withPrepaid,
+ config,
+ });
+ }
- const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- return {
- branch,
- func,
- ...preview,
- current_product: curMainProduct
- ? cusProductToProduct({
- cusProduct: curMainProduct,
- })
- : null,
- scheduled_product: curScheduledProduct,
- };
+ return {
+ branch,
+ func,
+ ...preview,
+ current_product: curMainProduct
+ ? cusProductToProduct({
+ cusProduct: curMainProduct,
+ })
+ : null,
+ scheduled_product: curScheduledProduct,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/attachPreviewUtils.ts b/server/src/internal/customers/attach/handleAttachPreview/attachPreviewUtils.ts
index 8b1378917..e69de29bb 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/attachPreviewUtils.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/attachPreviewUtils.ts
@@ -1 +0,0 @@
-
diff --git a/server/src/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.ts
index 44ce0d1c3..dfb03132a 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/getDowngradeProductPreview.ts
@@ -2,62 +2,62 @@ import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- attachParamsToProduct,
- attachParamToCusProducts,
- paramsToCurSub,
+ attachParamsToProduct,
+ attachParamToCusProducts,
+ paramsToCurSub,
} from "../attachUtils/convertAttachParams.js";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { AttachBranch, AttachConfig, UsageModel } from "@autumn/shared";
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const getDowngradeProductPreview = async ({
- attachParams,
- now,
- logger,
- branch,
- config,
+ attachParams,
+ now,
+ logger,
+ branch,
+ config,
}: {
- attachParams: AttachParams;
- now: number;
- logger: any;
- branch: AttachBranch;
- config: AttachConfig;
+ attachParams: AttachParams;
+ now: number;
+ logger: any;
+ branch: AttachBranch;
+ config: AttachConfig;
}) => {
- const newProduct = attachParamsToProduct({ attachParams });
+ const newProduct = attachParamsToProduct({ attachParams });
- const { curCusProduct } = attachParamToCusProducts({ attachParams });
- const sub = await paramsToCurSub({ attachParams });
+ const { curCusProduct } = attachParamToCusProducts({ attachParams });
+ const sub = await paramsToCurSub({ attachParams });
- let items = await getItemsForNewProduct({
- newProduct,
- attachParams,
- now,
- logger,
- });
+ let items = await getItemsForNewProduct({
+ newProduct,
+ attachParams,
+ now,
+ logger,
+ });
- items = items.filter((item) => item.usage_model !== UsageModel.Prepaid);
+ items = items.filter((item) => item.usage_model !== UsageModel.Prepaid);
- let options = getOptions({
- prodItems: mapToProductItems({
- prices: newProduct.prices,
- entitlements: newProduct.entitlements,
- features: attachParams.features,
- }),
- features: attachParams.features,
- // anchorToUnix,
- });
+ let options = getOptions({
+ prodItems: mapToProductItems({
+ prices: newProduct.prices,
+ entitlements: newProduct.entitlements,
+ features: attachParams.features,
+ }),
+ features: attachParams.features,
+ // anchorToUnix,
+ });
- const latestPeriodEnd = sub ? getLatestPeriodEnd({ sub }) * 1000 : undefined;
- let nextCycleAt = curCusProduct?.trial_ends_at
- ? curCusProduct.trial_ends_at
- : latestPeriodEnd;
+ const latestPeriodEnd = sub ? getLatestPeriodEnd({ sub }) * 1000 : undefined;
+ let nextCycleAt = curCusProduct?.trial_ends_at
+ ? curCusProduct.trial_ends_at
+ : latestPeriodEnd;
- return {
- currency: attachParams.org.default_currency,
- due_next_cycle: {
- line_items: items,
- due_at: nextCycleAt,
- },
- options,
- };
+ return {
+ currency: attachParams.org.default_currency,
+ due_next_cycle: {
+ line_items: items,
+ due_at: nextCycleAt,
+ },
+ options,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts
index 150078bce..a105cbf77 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/getMultiAttachPreview.ts
@@ -13,8 +13,8 @@ import { getAddAndRemoveProducts } from "../attachFunctions/multiAttach/getAddAn
import { priceToNewPreviewItem } from "../attachPreviewUtils/priceToNewPreviewItem.js";
import {
- getEarliestPeriodEnd,
- getLatestPeriodStart,
+ getEarliestPeriodEnd,
+ getLatestPeriodStart,
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { addIntervalForProration } from "@/internal/products/prices/billingIntervalUtils.js";
@@ -26,142 +26,142 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
export const getMultiAttachPreview = async ({
- req,
- attachBody,
- attachParams,
- logger,
- config,
- branch,
+ req,
+ attachBody,
+ attachParams,
+ logger,
+ config,
+ branch,
}: {
- req: ExtendedRequest;
- attachBody: AttachBody;
- attachParams: AttachParams;
- logger: any;
- config: any;
- branch: AttachBranch;
+ req: ExtendedRequest;
+ attachBody: AttachBody;
+ attachParams: AttachParams;
+ logger: any;
+ config: any;
+ branch: AttachBranch;
}) => {
- await handleMultiAttachErrors({ attachParams, attachBody, branch });
+ await handleMultiAttachErrors({ attachParams, attachBody, branch });
- const { customer } = attachParams;
- const cusProducts = customer.customer_products;
- const { sub } = await getCustomerSub({ attachParams });
+ const { customer } = attachParams;
+ const cusProducts = customer.customer_products;
+ const { sub } = await getCustomerSub({ attachParams });
- let items: PreviewLineItem[] = [];
- const subItems = sub?.items.data || [];
+ let items: PreviewLineItem[] = [];
+ const subItems = sub?.items.data || [];
- // 1. Get remove cus products...
- const { expireCusProducts } = await getAddAndRemoveProducts({
- attachParams,
- config,
- });
+ // 1. Get remove cus products...
+ const { expireCusProducts } = await getAddAndRemoveProducts({
+ attachParams,
+ config,
+ });
- const prices = cusProductsToPrices({ cusProducts: expireCusProducts });
+ const prices = cusProductsToPrices({ cusProducts: expireCusProducts });
- for (const price of prices) {
- const cusProduct = cusProducts.find(
- (cp) => cp.internal_product_id == price.internal_product_id
- )!;
+ for (const price of prices) {
+ const cusProduct = cusProducts.find(
+ (cp) => cp.internal_product_id == price.internal_product_id,
+ )!;
- const previewLineItem = priceToUnusedPreviewItem({
- customer,
- price,
- stripeItems: subItems,
- cusProduct,
- now: attachParams.now!,
- org: attachParams.org,
- latestInvoice: sub?.latest_invoice as Stripe.Invoice,
- subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
- });
+ const previewLineItem = priceToUnusedPreviewItem({
+ customer,
+ price,
+ stripeItems: subItems,
+ cusProduct,
+ now: attachParams.now!,
+ org: attachParams.org,
+ latestInvoice: sub?.latest_invoice as Stripe.Invoice,
+ subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
+ });
- if (!previewLineItem) continue;
+ if (!previewLineItem) continue;
- items.push(previewLineItem);
- }
+ items.push(previewLineItem);
+ }
- const productList = attachParams.productsList!;
- const newItems: PreviewLineItem[] = [];
- const itemsWithoutTrial: PreviewLineItem[] = [];
+ const productList = attachParams.productsList!;
+ const newItems: PreviewLineItem[] = [];
+ const itemsWithoutTrial: PreviewLineItem[] = [];
- for (const productOptions of productList) {
- const product = attachParams.products.find(
- (p) => p.id === productOptions.product_id
- )!;
+ for (const productOptions of productList) {
+ const product = attachParams.products.find(
+ (p) => p.id === productOptions.product_id,
+ )!;
- // Anchor to unix...
- let anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
- if (config.disableTrial) {
- attachParams.freeTrial = null;
- }
+ // Anchor to unix...
+ let anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
+ if (config.disableTrial) {
+ attachParams.freeTrial = null;
+ }
- const onTrial =
- notNullish(attachParams?.freeTrial) || sub?.status == "trialing";
+ const onTrial =
+ notNullish(attachParams?.freeTrial) || sub?.status == "trialing";
- for (const price of product.prices) {
- const newItem = priceToNewPreviewItem({
- org: attachParams.org,
- price,
- entitlements: product.entitlements,
- skipOneOff: false,
- now: attachParams.now!,
- anchor,
- productQuantity: productOptions.quantity ?? 1,
- product,
- onTrial,
- rewards: attachParams.rewards,
- subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
- });
+ for (const price of product.prices) {
+ const newItem = priceToNewPreviewItem({
+ org: attachParams.org,
+ price,
+ entitlements: product.entitlements,
+ skipOneOff: false,
+ now: attachParams.now!,
+ anchor,
+ productQuantity: productOptions.quantity ?? 1,
+ product,
+ onTrial,
+ rewards: attachParams.rewards,
+ subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
+ });
- const noTrialItem = priceToNewPreviewItem({
- org: attachParams.org,
- price,
- entitlements: product.entitlements,
- skipOneOff: false,
- now: attachParams.now!,
- // anchorToUnix,
- productQuantity: productOptions.quantity ?? 1,
- product,
- onTrial: false,
- rewards: attachParams.rewards,
- subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
- });
+ const noTrialItem = priceToNewPreviewItem({
+ org: attachParams.org,
+ price,
+ entitlements: product.entitlements,
+ skipOneOff: false,
+ now: attachParams.now!,
+ // anchorToUnix,
+ productQuantity: productOptions.quantity ?? 1,
+ product,
+ onTrial: false,
+ rewards: attachParams.rewards,
+ subDiscounts: (sub?.discounts ?? []) as Stripe.Discount[],
+ });
- if (newItem) {
- newItems.push(newItem);
- }
- if (noTrialItem) {
- itemsWithoutTrial.push(noTrialItem);
- }
- }
- }
+ if (newItem) {
+ newItems.push(newItem);
+ }
+ if (noTrialItem) {
+ itemsWithoutTrial.push(noTrialItem);
+ }
+ }
+ }
- const totalDueToday = newItems.reduce(
- (acc, item) => acc + (item.amount ?? 0),
- 0
- );
+ const totalDueToday = newItems.reduce(
+ (acc, item) => acc + (item.amount ?? 0),
+ 0,
+ );
- const freeTrial = attachParams.freeTrial;
- let dueNextCycle = undefined;
- if (freeTrial || sub?.status == "trialing") {
- const nextCycleAt = freeTrial
- ? freeTrialToStripeTimestamp({ freeTrial, now: attachParams.now })! * 1000
- : sub
- ? getEarliestPeriodEnd({ sub }) * 1000
- : undefined;
+ const freeTrial = attachParams.freeTrial;
+ let dueNextCycle = undefined;
+ if (freeTrial || sub?.status == "trialing") {
+ const nextCycleAt = freeTrial
+ ? freeTrialToStripeTimestamp({ freeTrial, now: attachParams.now })! * 1000
+ : sub
+ ? getEarliestPeriodEnd({ sub }) * 1000
+ : undefined;
- if (nextCycleAt) {
- dueNextCycle = {
- line_items: itemsWithoutTrial,
- due_at: nextCycleAt,
- };
- }
- }
+ if (nextCycleAt) {
+ dueNextCycle = {
+ line_items: itemsWithoutTrial,
+ due_at: nextCycleAt,
+ };
+ }
+ }
- return {
- // items,
- due_today: {
- line_items: [...items, ...newItems],
- total: new Decimal(totalDueToday).toNumber(),
- },
- due_next_cycle: dueNextCycle,
- };
+ return {
+ // items,
+ due_today: {
+ line_items: [...items, ...newItems],
+ total: new Decimal(totalDueToday).toNumber(),
+ },
+ due_next_cycle: dueNextCycle,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts
index 2469cffbe..9a3039951 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/getNewProductPreview.ts
@@ -1,16 +1,16 @@
import {
- AttachBranch,
- AttachConfig,
- BillingInterval,
- FullProduct,
- FreeTrial,
+ AttachBranch,
+ AttachConfig,
+ BillingInterval,
+ FullProduct,
+ FreeTrial,
} from "@autumn/shared";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- attachParamsToProduct,
- getCustomerSub,
+ attachParamsToProduct,
+ getCustomerSub,
} from "../attachUtils/convertAttachParams.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
@@ -21,150 +21,150 @@ import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2
import { formatUnixToDate } from "@/utils/genUtils.js";
const getNextCycleItems = async ({
- newProduct,
- attachParams,
- anchor,
- branch,
- withPrepaid,
- logger,
- config,
- trialEnds,
+ newProduct,
+ attachParams,
+ anchor,
+ branch,
+ withPrepaid,
+ logger,
+ config,
+ trialEnds,
}: {
- newProduct: FullProduct;
- attachParams: AttachParams;
- anchor?: number;
- branch: AttachBranch;
- withPrepaid?: boolean;
- logger: any;
- config: AttachConfig;
- trialEnds?: number | null;
+ newProduct: FullProduct;
+ attachParams: AttachParams;
+ anchor?: number;
+ branch: AttachBranch;
+ withPrepaid?: boolean;
+ logger: any;
+ config: AttachConfig;
+ trialEnds?: number | null;
}) => {
- // 2. If free trial
- let nextCycleAt = undefined;
- if (attachParams.freeTrial) {
- if (trialEnds) {
- nextCycleAt = trialEnds;
- } else {
- nextCycleAt =
- freeTrialToStripeTimestamp({
- freeTrial: attachParams.freeTrial,
- now: attachParams.now,
- })! * 1000;
- }
- } else if (branch != AttachBranch.OneOff && anchor) {
- // Yearly one
- const largestInterval = getLargestInterval({ prices: newProduct.prices });
- if (largestInterval) {
- nextCycleAt = getAlignedUnix({
- anchor,
- intervalConfig: largestInterval,
- now: attachParams.now,
- });
- }
- }
+ // 2. If free trial
+ let nextCycleAt = undefined;
+ if (attachParams.freeTrial) {
+ if (trialEnds) {
+ nextCycleAt = trialEnds;
+ } else {
+ nextCycleAt =
+ freeTrialToStripeTimestamp({
+ freeTrial: attachParams.freeTrial,
+ now: attachParams.now,
+ })! * 1000;
+ }
+ } else if (branch != AttachBranch.OneOff && anchor) {
+ // Yearly one
+ const largestInterval = getLargestInterval({ prices: newProduct.prices });
+ if (largestInterval) {
+ nextCycleAt = getAlignedUnix({
+ anchor,
+ intervalConfig: largestInterval,
+ now: attachParams.now,
+ });
+ }
+ }
- const items = await getItemsForNewProduct({
- newProduct,
- attachParams,
- now: attachParams.now,
- logger,
- withPrepaid,
- anchor,
- });
+ const items = await getItemsForNewProduct({
+ newProduct,
+ attachParams,
+ now: attachParams.now,
+ logger,
+ withPrepaid,
+ anchor,
+ });
- return {
- line_items: items,
- due_at: nextCycleAt,
- };
+ return {
+ line_items: items,
+ due_at: nextCycleAt,
+ };
};
export const getNewProductPreview = async ({
- branch,
- attachParams,
- logger,
- config,
- withPrepaid = false,
+ branch,
+ attachParams,
+ logger,
+ config,
+ withPrepaid = false,
}: {
- branch: AttachBranch;
- attachParams: AttachParams;
- logger: any;
- config: AttachConfig;
- withPrepaid?: boolean;
+ branch: AttachBranch;
+ attachParams: AttachParams;
+ logger: any;
+ config: AttachConfig;
+ withPrepaid?: boolean;
}) => {
- const { org } = attachParams;
- const newProduct = attachParamsToProduct({ attachParams });
+ const { org } = attachParams;
+ const newProduct = attachParamsToProduct({ attachParams });
- const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
- attachParams,
- });
+ const { sub: mergeSub, cusProduct: mergeCusProduct } = await getCustomerSub({
+ attachParams,
+ });
- let trialEnds = undefined;
+ let trialEnds = undefined;
- // Scenario where we update a current sub with new product (so no create sub)
- let anchor = undefined;
- if (mergeSub && !config.disableMerge) {
- if (mergeCusProduct?.free_trial) {
- if (isTrialing({ cusProduct: mergeCusProduct, now: attachParams.now })) {
- trialEnds = mergeCusProduct.trial_ends_at;
- attachParams.freeTrial = mergeCusProduct.free_trial;
- } else {
- attachParams.freeTrial = null;
- }
- }
+ // Scenario where we update a current sub with new product (so no create sub)
+ let anchor = undefined;
+ if (mergeSub && !config.disableMerge) {
+ if (mergeCusProduct?.free_trial) {
+ if (isTrialing({ cusProduct: mergeCusProduct, now: attachParams.now })) {
+ trialEnds = mergeCusProduct.trial_ends_at;
+ attachParams.freeTrial = mergeCusProduct.free_trial;
+ } else {
+ attachParams.freeTrial = null;
+ }
+ }
- anchor = mergeSub.billing_cycle_anchor * 1000;
- } else if (org.config.anchor_start_of_month) {
- anchor = getNextStartOfMonthUnix({
- interval: BillingInterval.Month,
- intervalCount: 1,
- });
- }
+ anchor = mergeSub.billing_cycle_anchor * 1000;
+ } else if (org.config.anchor_start_of_month) {
+ anchor = getNextStartOfMonthUnix({
+ interval: BillingInterval.Month,
+ intervalCount: 1,
+ });
+ }
- const items = await getItemsForNewProduct({
- newProduct,
- attachParams,
- now: attachParams.now,
- freeTrial: attachParams.freeTrial,
- anchor,
- logger,
- withPrepaid,
- });
+ const items = await getItemsForNewProduct({
+ newProduct,
+ attachParams,
+ now: attachParams.now,
+ freeTrial: attachParams.freeTrial,
+ anchor,
+ logger,
+ withPrepaid,
+ });
- const dueNextCycle = await getNextCycleItems({
- newProduct,
- attachParams,
- anchor,
- branch,
- withPrepaid,
- logger,
- config,
- trialEnds,
- });
+ const dueNextCycle = await getNextCycleItems({
+ newProduct,
+ attachParams,
+ anchor,
+ branch,
+ withPrepaid,
+ logger,
+ config,
+ trialEnds,
+ });
- let options = getOptions({
- prodItems: mapToProductItems({
- prices: newProduct.prices,
- entitlements: newProduct.entitlements,
- features: attachParams.features,
- }),
- features: attachParams.features,
- anchor,
- now: attachParams.now || Date.now(),
- freeTrial: attachParams.freeTrial,
- });
+ let options = getOptions({
+ prodItems: mapToProductItems({
+ prices: newProduct.prices,
+ entitlements: newProduct.entitlements,
+ features: attachParams.features,
+ }),
+ features: attachParams.features,
+ anchor,
+ now: attachParams.now || Date.now(),
+ freeTrial: attachParams.freeTrial,
+ });
- const dueTodayAmt = items.reduce((acc, item) => {
- return acc + (item.amount ?? 0);
- }, 0);
+ const dueTodayAmt = items.reduce((acc, item) => {
+ return acc + (item.amount ?? 0);
+ }, 0);
- return {
- currency: attachParams.org.default_currency,
- due_today: {
- line_items: items,
- total: dueTodayAmt,
- },
- due_next_cycle: dueNextCycle,
- free_trial: attachParams.freeTrial,
- options,
- };
+ return {
+ currency: attachParams.org.default_currency,
+ due_today: {
+ line_items: items,
+ total: dueTodayAmt,
+ },
+ due_next_cycle: dueNextCycle,
+ free_trial: attachParams.freeTrial,
+ options,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts
index 5af51ed3f..6ff274065 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/getUpdateEntsPreview.ts
@@ -1,31 +1,31 @@
import { productsAreSame } from "@/internal/products/productUtils/compareProductUtils.js";
import {
- attachParamsToProduct,
- attachParamToCusProducts,
+ attachParamsToProduct,
+ attachParamToCusProducts,
} from "../attachUtils/convertAttachParams.js";
import { cusProductToProduct } from "@autumn/shared";
export const getUpdateEntsPreview = async ({
- req,
- attachParams,
- now,
+ req,
+ attachParams,
+ now,
}: {
- req: any;
- attachParams: any;
- now: number;
+ req: any;
+ attachParams: any;
+ now: number;
}) => {
- const { curMainProduct } = attachParamToCusProducts({ attachParams });
- const curProduct = cusProductToProduct({ cusProduct: curMainProduct! });
- const newProduct = attachParamsToProduct({ attachParams });
- const features = attachParams.features;
+ const { curMainProduct } = attachParamToCusProducts({ attachParams });
+ const curProduct = cusProductToProduct({ cusProduct: curMainProduct! });
+ const newProduct = attachParamsToProduct({ attachParams });
+ const features = attachParams.features;
- const res = productsAreSame({
- newProductV1: newProduct,
- curProductV1: curProduct,
- features,
- });
+ const res = productsAreSame({
+ newProductV1: newProduct,
+ curProductV1: curProduct,
+ features,
+ });
- return {
- new_items: res.newItems,
- };
+ return {
+ new_items: res.newItems,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts
index fa90b48cc..9e1f2fb7d 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/getUpgradeProductPreview.ts
@@ -1,8 +1,8 @@
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- attachParamsToProduct,
- attachParamToCusProducts,
- paramsToCurSub,
+ attachParamsToProduct,
+ attachParamToCusProducts,
+ paramsToCurSub,
} from "../attachUtils/convertAttachParams.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
@@ -13,16 +13,16 @@ import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import Stripe from "stripe";
import {
- AttachBranch,
- FreeTrial,
- FullCusProduct,
- PreviewLineItem,
- Price,
- UsageModel,
- AttachConfig,
- UsagePriceConfig,
- OnDecrease,
- OnIncrease,
+ AttachBranch,
+ FreeTrial,
+ FullCusProduct,
+ PreviewLineItem,
+ Price,
+ UsageModel,
+ AttachConfig,
+ UsagePriceConfig,
+ OnDecrease,
+ OnIncrease,
} from "@autumn/shared";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
@@ -33,265 +33,265 @@ import { isTrialing } from "@autumn/shared";
import { cusProductToPrices } from "@autumn/shared";
import { isPrepaidPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- addIntervalToAnchor,
- getAlignedUnix,
+ addIntervalToAnchor,
+ getAlignedUnix,
} from "@/internal/products/prices/billingIntervalUtils2.js";
const getNextCycleAt = ({
- prices,
- sub,
- now,
- freeTrial,
- branch,
- curCusProduct,
+ prices,
+ sub,
+ now,
+ freeTrial,
+ branch,
+ curCusProduct,
}: {
- prices: Price[];
- sub: Stripe.Subscription;
- now?: number;
- freeTrial?: FreeTrial | null;
- branch: AttachBranch;
- curCusProduct?: FullCusProduct;
+ prices: Price[];
+ sub: Stripe.Subscription;
+ now?: number;
+ freeTrial?: FreeTrial | null;
+ branch: AttachBranch;
+ curCusProduct?: FullCusProduct;
}) => {
- now = now || Date.now();
+ now = now || Date.now();
- if (
- branch == AttachBranch.NewVersion &&
- curCusProduct &&
- isTrialing({ cusProduct: curCusProduct, now })
- ) {
- return curCusProduct.trial_ends_at;
- }
+ if (
+ branch == AttachBranch.NewVersion &&
+ curCusProduct &&
+ isTrialing({ cusProduct: curCusProduct, now })
+ ) {
+ return curCusProduct.trial_ends_at;
+ }
- if (freeTrial) {
- return (
- freeTrialToStripeTimestamp({
- freeTrial,
- now,
- })! * 1000
- );
- }
+ if (freeTrial) {
+ return (
+ freeTrialToStripeTimestamp({
+ freeTrial,
+ now,
+ })! * 1000
+ );
+ }
- const largestInterval = getLargestInterval({ prices });
- if (nullish(largestInterval) || !sub.billing_cycle_anchor) return now;
+ const largestInterval = getLargestInterval({ prices });
+ if (nullish(largestInterval) || !sub.billing_cycle_anchor) return now;
- const nextCycleAt = getAlignedUnix({
- anchor: sub.billing_cycle_anchor * 1000,
- intervalConfig: largestInterval!,
- now,
- });
+ const nextCycleAt = getAlignedUnix({
+ anchor: sub.billing_cycle_anchor * 1000,
+ intervalConfig: largestInterval!,
+ now,
+ });
- return nextCycleAt;
+ return nextCycleAt;
};
const filterNoProratePrepaidItems = ({
- items,
- attachParams,
- curSameProduct,
+ items,
+ attachParams,
+ curSameProduct,
}: {
- items: PreviewLineItem[];
- attachParams: AttachParams;
- curSameProduct?: FullCusProduct;
+ items: PreviewLineItem[];
+ attachParams: AttachParams;
+ curSameProduct?: FullCusProduct;
}) => {
- if (!curSameProduct) {
- return items;
- }
+ if (!curSameProduct) {
+ return items;
+ }
- let filteredItems = items;
- const curPrices = cusProductToPrices({ cusProduct: curSameProduct! });
- for (const option of attachParams.optionsList) {
- const { feature_id, internal_feature_id, quantity } = option;
- const prevQuantity = curSameProduct?.options.find(
- (o) => o.feature_id == feature_id
- )?.quantity;
+ let filteredItems = items;
+ const curPrices = cusProductToPrices({ cusProduct: curSameProduct! });
+ for (const option of attachParams.optionsList) {
+ const { feature_id, internal_feature_id, quantity } = option;
+ const prevQuantity = curSameProduct?.options.find(
+ (o) => o.feature_id == feature_id,
+ )?.quantity;
- const curPrice = curPrices.find(
- (p) =>
- (p.config as UsagePriceConfig)?.internal_feature_id ==
- internal_feature_id && isPrepaidPrice({ price: p })
- );
+ const curPrice = curPrices.find(
+ (p) =>
+ (p.config as UsagePriceConfig)?.internal_feature_id ==
+ internal_feature_id && isPrepaidPrice({ price: p }),
+ );
- const onDecrease = curPrice?.proration_config?.on_decrease;
- const decreaseIsNone = onDecrease == OnDecrease.None;
+ const onDecrease = curPrice?.proration_config?.on_decrease;
+ const decreaseIsNone = onDecrease == OnDecrease.None;
- if (decreaseIsNone && prevQuantity && quantity < prevQuantity) {
- console.log(
- `Quantity for ${feature_id} decreased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}`
- );
- filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
- }
+ if (decreaseIsNone && prevQuantity && quantity < prevQuantity) {
+ console.log(
+ `Quantity for ${feature_id} decreased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}`,
+ );
+ filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
+ }
- const onIncrease = curPrice?.proration_config?.on_increase;
- if (
- onIncrease == OnIncrease.ProrateNextCycle &&
- prevQuantity &&
- quantity > prevQuantity
- ) {
- console.log(
- `Quantity for ${feature_id} increased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}`
- );
- filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
- }
- }
- return filteredItems;
+ const onIncrease = curPrice?.proration_config?.on_increase;
+ if (
+ onIncrease == OnIncrease.ProrateNextCycle &&
+ prevQuantity &&
+ quantity > prevQuantity
+ ) {
+ console.log(
+ `Quantity for ${feature_id} increased from ${prevQuantity} to ${quantity}, Removing price: ${curPrice?.id}`,
+ );
+ filteredItems = items.filter((item) => item.price_id !== curPrice?.id);
+ }
+ }
+ return filteredItems;
};
export const getUpgradeProductPreview = async ({
- req,
- attachParams,
- branch,
- now,
- withPrepaid = false,
- config,
+ req,
+ attachParams,
+ branch,
+ now,
+ withPrepaid = false,
+ config,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- branch: AttachBranch;
- now: number;
- withPrepaid?: boolean;
- config: AttachConfig;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ branch: AttachBranch;
+ now: number;
+ withPrepaid?: boolean;
+ config: AttachConfig;
}) => {
- const { logtail: logger } = req;
+ const { logtail: logger } = req;
- const { curMainProduct, curSameProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curMainProduct, curSameProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- const curCusProduct = curSameProduct || curMainProduct!;
- const sub = await paramsToCurSub({ attachParams });
+ const curCusProduct = curSameProduct || curMainProduct!;
+ const sub = await paramsToCurSub({ attachParams });
- const curPreviewItems = await getItemsForCurProduct({
- sub: sub!,
- attachParams,
- branch,
- config,
- now,
- logger,
- });
+ const curPreviewItems = await getItemsForCurProduct({
+ sub: sub!,
+ attachParams,
+ branch,
+ config,
+ now,
+ logger,
+ });
- // Get prorated amounts for new product
- const newProduct = attachParamsToProduct({ attachParams });
+ // Get prorated amounts for new product
+ const newProduct = attachParamsToProduct({ attachParams });
- if (config?.disableTrial) attachParams.freeTrial = null;
- let freeTrial = attachParams.freeTrial;
- let anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
+ if (config?.disableTrial) attachParams.freeTrial = null;
+ let freeTrial = attachParams.freeTrial;
+ let anchor = sub ? sub.billing_cycle_anchor * 1000 : undefined;
- if (
- config?.carryTrial &&
- curCusProduct?.free_trial &&
- isTrialing({ cusProduct: curCusProduct, now })
- ) {
- freeTrial = curCusProduct.free_trial;
- }
+ if (
+ config?.carryTrial &&
+ curCusProduct?.free_trial &&
+ isTrialing({ cusProduct: curCusProduct, now })
+ ) {
+ freeTrial = curCusProduct.free_trial;
+ }
- const newPreviewItems = await getItemsForNewProduct({
- newProduct,
- attachParams,
- now,
- freeTrial,
- sub: sub!,
- logger,
- withPrepaid,
- anchor,
- });
+ const newPreviewItems = await getItemsForNewProduct({
+ newProduct,
+ attachParams,
+ now,
+ freeTrial,
+ sub: sub!,
+ logger,
+ withPrepaid,
+ anchor,
+ });
- let dueNextCycle = undefined;
- if (!isFreeProduct(newProduct.prices)) {
- const nextCycleAt = getNextCycleAt({
- prices: newProduct.prices,
- sub: sub!,
- now,
- freeTrial: attachParams.freeTrial,
- branch,
- curCusProduct,
- });
+ let dueNextCycle = undefined;
+ if (!isFreeProduct(newProduct.prices)) {
+ const nextCycleAt = getNextCycleAt({
+ prices: newProduct.prices,
+ sub: sub!,
+ now,
+ freeTrial: attachParams.freeTrial,
+ branch,
+ curCusProduct,
+ });
- let nextCycleItems = await getItemsForNewProduct({
- newProduct,
- attachParams,
- logger,
- withPrepaid,
- });
+ let nextCycleItems = await getItemsForNewProduct({
+ newProduct,
+ attachParams,
+ logger,
+ withPrepaid,
+ });
- dueNextCycle = {
- line_items: nextCycleItems,
- due_at: nextCycleAt,
- };
- }
+ dueNextCycle = {
+ line_items: nextCycleItems,
+ due_at: nextCycleAt,
+ };
+ }
- let items = [...curPreviewItems, ...newPreviewItems];
+ let items = [...curPreviewItems, ...newPreviewItems];
- for (const item of structuredClone(curPreviewItems)) {
- let priceId = item.price_id;
- let newItem = newPreviewItems.find((i) => i.price_id == priceId);
+ for (const item of structuredClone(curPreviewItems)) {
+ let priceId = item.price_id;
+ let newItem = newPreviewItems.find((i) => i.price_id == priceId);
- if (!newItem) {
- continue;
- }
+ if (!newItem) {
+ continue;
+ }
- let newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2);
- let curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2);
+ let newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2);
+ let curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2);
- if (newItemAmount.add(curItemAmount).eq(0)) {
- items = items.filter((i) => i.price_id !== priceId);
- }
- }
+ if (newItemAmount.add(curItemAmount).eq(0)) {
+ items = items.filter((i) => i.price_id !== priceId);
+ }
+ }
- const dueTodayAmt = items
- .reduce((acc, item) => acc.plus(item.amount ?? 0), new Decimal(0))
- .toDecimalPlaces(2)
- .toNumber();
+ const dueTodayAmt = items
+ .reduce((acc, item) => acc.plus(item.amount ?? 0), new Decimal(0))
+ .toDecimalPlaces(2)
+ .toNumber();
- let options = getOptions({
- prodItems: mapToProductItems({
- prices: newProduct.prices,
- entitlements: newProduct.entitlements,
- features: attachParams.features,
- }),
- features: attachParams.features,
- anchor,
- now,
- freeTrial: attachParams.freeTrial,
- cusProduct: curCusProduct,
- });
+ let options = getOptions({
+ prodItems: mapToProductItems({
+ prices: newProduct.prices,
+ entitlements: newProduct.entitlements,
+ features: attachParams.features,
+ }),
+ features: attachParams.features,
+ anchor,
+ now,
+ freeTrial: attachParams.freeTrial,
+ cusProduct: curCusProduct,
+ });
- items = items.filter((item) => item.amount !== 0);
+ items = items.filter((item) => item.amount !== 0);
- if (branch == AttachBranch.UpdatePrepaidQuantity) {
- items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
- dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
- (item) => item.usage_model == UsageModel.Prepaid
- );
+ if (branch == AttachBranch.UpdatePrepaidQuantity) {
+ items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
+ dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
+ (item) => item.usage_model == UsageModel.Prepaid,
+ );
- items = filterNoProratePrepaidItems({
- items,
- attachParams,
- curSameProduct: curSameProduct!,
- });
- }
+ items = filterNoProratePrepaidItems({
+ items,
+ attachParams,
+ curSameProduct: curSameProduct!,
+ });
+ }
- let dueToday:
- | {
- line_items: PreviewLineItem[];
- total: number;
- }
- | undefined = {
- line_items: items,
- total: dueTodayAmt,
- };
+ let dueToday:
+ | {
+ line_items: PreviewLineItem[];
+ total: number;
+ }
+ | undefined = {
+ line_items: items,
+ total: dueTodayAmt,
+ };
- if (branch == AttachBranch.SameCustomEnts) {
- dueToday = undefined;
- }
+ if (branch == AttachBranch.SameCustomEnts) {
+ dueToday = undefined;
+ }
- if (branch == AttachBranch.NewVersion && dueToday) {
- dueToday.line_items = [];
- dueToday.total = 0;
- }
+ if (branch == AttachBranch.NewVersion && dueToday) {
+ dueToday.line_items = [];
+ dueToday.total = 0;
+ }
- return {
- currency: attachParams.org.default_currency,
- due_today: dueToday,
- due_next_cycle: dueNextCycle,
- options,
- };
+ return {
+ currency: attachParams.org.default_currency,
+ due_today: dueToday,
+ due_next_cycle: dueNextCycle,
+ options,
+ };
};
diff --git a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts
index 01285787b..1a9efbda0 100644
--- a/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts
+++ b/server/src/internal/customers/attach/handleAttachPreview/handleAttachPreview.ts
@@ -9,29 +9,29 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { attachParamsToPreview } from "./attachParamsToPreview.js";
export const handleAttachPreview = (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "attach-preview",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { logtail: logger } = req;
- const attachBody = AttachBodySchema.parse(req.body);
+ routeHandler({
+ req,
+ res,
+ action: "attach-preview",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { logtail: logger } = req;
+ const attachBody = AttachBodySchema.parse(req.body);
- // console.log("attachBody", attachBody);
- const { attachParams } = await getAttachParams({
- req,
- attachBody,
- });
+ // console.log("attachBody", attachBody);
+ const { attachParams } = await getAttachParams({
+ req,
+ attachBody,
+ });
- const attachPreview = await attachParamsToPreview({
- req,
- attachParams,
- attachBody,
- logger,
- });
+ const attachPreview = await attachParamsToPreview({
+ req,
+ attachParams,
+ attachBody,
+ logger,
+ });
- res.status(200).json(attachPreview);
+ res.status(200).json(attachPreview);
- return;
- },
- });
+ return;
+ },
+ });
diff --git a/server/src/internal/customers/attach/handleSetupPayment.ts b/server/src/internal/customers/attach/handleSetupPayment.ts
index b506014fa..bdc093266 100644
--- a/server/src/internal/customers/attach/handleSetupPayment.ts
+++ b/server/src/internal/customers/attach/handleSetupPayment.ts
@@ -8,104 +8,104 @@ import { ErrCode } from "@/errors/errCodes.js";
import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js";
export const handleSetupPayment = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "setup_payment",
- handler: async (req: ExtendedRequest, res: any) => {
- const { db, env, org } = req;
- const logger = req.logger;
+ routeHandler({
+ req,
+ res,
+ action: "setup_payment",
+ handler: async (req: ExtendedRequest, res: any) => {
+ const { db, env, org } = req;
+ const logger = req.logger;
- let { customer_id, customer_data, success_url, checkout_session_params } =
- req.body;
+ let { customer_id, customer_data, success_url, checkout_session_params } =
+ req.body;
- let customer = await getOrCreateCustomer({
- req,
- customerId: customer_id,
- customerData: customer_data as any,
- });
+ let customer = await getOrCreateCustomer({
+ req,
+ customerId: customer_id,
+ customerData: customer_data as any,
+ });
- await createStripeCusIfNotExists({
- db,
- org,
- env,
- customer,
- logger: req.logger,
- });
+ await createStripeCusIfNotExists({
+ db,
+ org,
+ env,
+ customer,
+ logger: req.logger,
+ });
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- // check if user already specified payment methods in their request
- const hasUserSpecifiedPaymentMethods =
- checkout_session_params && checkout_session_params.payment_method_types;
+ // check if user already specified payment methods in their request
+ const hasUserSpecifiedPaymentMethods =
+ checkout_session_params && checkout_session_params.payment_method_types;
- const sessionParams = {
- customer: customer.processor?.id,
- mode: "setup",
- success_url: success_url || toSuccessUrl({ org, env }),
- currency: org.default_currency || "usd",
- ...(checkout_session_params as any),
- };
+ const sessionParams = {
+ customer: customer.processor?.id,
+ mode: "setup",
+ success_url: success_url || toSuccessUrl({ org, env }),
+ currency: org.default_currency || "usd",
+ ...(checkout_session_params as any),
+ };
- try {
- // let stripe automatically determine payment methods
- const session = await stripeCli.checkout.sessions.create(sessionParams);
- return res.json({
- customer_id: customer.id,
- url: session.url,
- });
- } catch (error: any) {
- // payment method errors
- if (
- error.message &&
- (error.message.includes("payment method") ||
- error.message.includes("No valid payment"))
- ) {
- logger.warn("Stripe checkout session creation failed", {
- customerId: customer.id,
- error: error.message,
- });
+ try {
+ // let stripe automatically determine payment methods
+ const session = await stripeCli.checkout.sessions.create(sessionParams);
+ return res.json({
+ customer_id: customer.id,
+ url: session.url,
+ });
+ } catch (error: any) {
+ // payment method errors
+ if (
+ error.message &&
+ (error.message.includes("payment method") ||
+ error.message.includes("No valid payment"))
+ ) {
+ logger.warn("Stripe checkout session creation failed", {
+ customerId: customer.id,
+ error: error.message,
+ });
- if (hasUserSpecifiedPaymentMethods) {
- throw error;
- }
+ if (hasUserSpecifiedPaymentMethods) {
+ throw error;
+ }
- try {
- // card payment method fallback
- const fallbackSession = await stripeCli.checkout.sessions.create({
- ...sessionParams,
- payment_method_types: ["card"],
- });
+ try {
+ // card payment method fallback
+ const fallbackSession = await stripeCli.checkout.sessions.create({
+ ...sessionParams,
+ payment_method_types: ["card"],
+ });
- logger.info("Created checkout session with card payment method", {
- customerId: customer.id,
- });
+ logger.info("Created checkout session with card payment method", {
+ customerId: customer.id,
+ });
- return res.json({
- customer_id: customer.id,
- url: fallbackSession.url,
- });
- } catch (fallbackError: any) {
- // if fallback failed
- logger.error(
- "Failed to create checkout session even with card payment method",
- {
- customerId: customer.id,
- error: fallbackError.message,
- }
- );
+ return res.json({
+ customer_id: customer.id,
+ url: fallbackSession.url,
+ });
+ } catch (fallbackError: any) {
+ // if fallback failed
+ logger.error(
+ "Failed to create checkout session even with card payment method",
+ {
+ customerId: customer.id,
+ error: fallbackError.message,
+ },
+ );
- throw new RecaseError({
- code: ErrCode.InvalidRequest,
- message:
- "Unable to create checkout session. Please ensure you have activated card payment method in your Stripe dashboard.",
- statusCode: 400,
- });
- }
- }
+ throw new RecaseError({
+ code: ErrCode.InvalidRequest,
+ message:
+ "Unable to create checkout session. Please ensure you have activated card payment method in your Stripe dashboard.",
+ statusCode: 400,
+ });
+ }
+ }
- // Re-throw errors
- throw error;
- }
- },
- });
+ // Re-throw errors
+ throw error;
+ }
+ },
+ });
diff --git a/server/src/internal/customers/attach/mergeUtils/mergeNewSubItems.ts b/server/src/internal/customers/attach/mergeUtils/mergeNewSubItems.ts
index 37d8d803c..1be9277e3 100644
--- a/server/src/internal/customers/attach/mergeUtils/mergeNewSubItems.ts
+++ b/server/src/internal/customers/attach/mergeUtils/mergeNewSubItems.ts
@@ -3,80 +3,80 @@ import { ItemSet } from "@/utils/models/ItemSet.js";
import Stripe from "stripe";
export const mergeNewSubItems = ({
- itemSet,
- curSubItems,
+ itemSet,
+ curSubItems,
}: {
- itemSet: ItemSet;
- curSubItems: Stripe.SubscriptionItem[];
+ itemSet: ItemSet;
+ curSubItems: Stripe.SubscriptionItem[];
}) => {
- // 1. Don't need to add arrear prices if they already exist...
- let newSubItems = structuredClone(itemSet.subItems);
- const newArrearSubItems: any[] = [];
+ // 1. Don't need to add arrear prices if they already exist...
+ let newSubItems = structuredClone(itemSet.subItems);
+ const newArrearSubItems: any[] = [];
- newSubItems = newSubItems.filter((newSi) => {
- const existingItem = curSubItems.find((si) => si.price?.id === newSi.price);
- if (isArrearPrice({ price: newSi.autumnPrice }) && existingItem) {
- newArrearSubItems.push(newSi);
- return false;
- }
- return true;
- });
+ newSubItems = newSubItems.filter((newSi) => {
+ const existingItem = curSubItems.find((si) => si.price?.id === newSi.price);
+ if (isArrearPrice({ price: newSi.autumnPrice }) && existingItem) {
+ newArrearSubItems.push(newSi);
+ return false;
+ }
+ return true;
+ });
- // 2. Add new subItems
- for (let i = 0; i < newSubItems.length; i++) {
- const newItem = newSubItems[i];
- const existingItem = curSubItems.find(
- (si) => si.price?.id === newItem.price
- );
+ // 2. Add new subItems
+ for (let i = 0; i < newSubItems.length; i++) {
+ const newItem = newSubItems[i];
+ const existingItem = curSubItems.find(
+ (si) => si.price?.id === newItem.price,
+ );
- if (!existingItem) continue;
+ if (!existingItem) continue;
- newSubItems[i] = {
- id: existingItem.id,
- quantity: (existingItem.quantity || 0) + (newItem.quantity || 0),
- // price: newItem.price,
- // autumnPrice: newItem.autumnPrice,
- };
- }
+ newSubItems[i] = {
+ id: existingItem.id,
+ quantity: (existingItem.quantity || 0) + (newItem.quantity || 0),
+ // price: newItem.price,
+ // autumnPrice: newItem.autumnPrice,
+ };
+ }
- return newSubItems;
+ return newSubItems;
};
export const mergeNewScheduleItems = ({
- itemSet,
- curScheduleItems,
+ itemSet,
+ curScheduleItems,
}: {
- itemSet: ItemSet;
- curScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
+ itemSet: ItemSet;
+ curScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
}) => {
- const originalScheduleItems = structuredClone(curScheduleItems);
- let newScheduleItems: any[] = structuredClone(curScheduleItems).map((si) => ({
- price: (si.price as Stripe.Price).id,
- quantity: si.quantity,
- }));
+ const originalScheduleItems = structuredClone(curScheduleItems);
+ let newScheduleItems: any[] = structuredClone(curScheduleItems).map((si) => ({
+ price: (si.price as Stripe.Price).id,
+ quantity: si.quantity,
+ }));
- for (const newItem of itemSet.subItems) {
- const existingIndex = newScheduleItems.findIndex(
- (si) => si.price === newItem.price
- );
+ for (const newItem of itemSet.subItems) {
+ const existingIndex = newScheduleItems.findIndex(
+ (si) => si.price === newItem.price,
+ );
- if (existingIndex !== -1) {
- newScheduleItems[existingIndex].quantity =
- (newScheduleItems[existingIndex].quantity || 0) +
- (newItem.quantity || 0);
- } else {
- newScheduleItems.push({
- price: newItem.price,
- quantity: newItem.quantity,
- });
- }
- }
+ if (existingIndex !== -1) {
+ newScheduleItems[existingIndex].quantity =
+ (newScheduleItems[existingIndex].quantity || 0) +
+ (newItem.quantity || 0);
+ } else {
+ newScheduleItems.push({
+ price: newItem.price,
+ quantity: newItem.quantity,
+ });
+ }
+ }
- return newScheduleItems.map((si) => ({
- price: si.price,
- quantity: si.quantity,
- // scheduleItem: originalScheduleItems.find(
- // (osi) => (osi.price as Stripe.Price)?.id === si.price
- // ),
- }));
+ return newScheduleItems.map((si) => ({
+ price: si.price,
+ quantity: si.quantity,
+ // scheduleItem: originalScheduleItems.find(
+ // (osi) => (osi.price as Stripe.Price)?.id === si.price
+ // ),
+ }));
};
diff --git a/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts b/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts
index 28cd8092b..79cf181f8 100644
--- a/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts
+++ b/server/src/internal/customers/attach/mergeUtils/mergeUtils.ts
@@ -1,11 +1,11 @@
import {
- getPriceEntitlement,
- getPriceOptions,
+ getPriceEntitlement,
+ getPriceOptions,
} from "@/internal/products/prices/priceUtils.js";
import {
- isContUsePrice,
- isFixedPrice,
- isPrepaidPrice,
+ isContUsePrice,
+ isFixedPrice,
+ isPrepaidPrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { AttachBranch, Entity, FullCusProduct, Price } from "@autumn/shared";
import Stripe from "stripe";
@@ -13,99 +13,100 @@ import { getExistingUsageFromCusProducts } from "../../cusProducts/cusEnts/cusEn
import { cusProductToEnts } from "@autumn/shared";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import {
- attachParamsToCurCusProduct,
- getCustomerSub,
+ attachParamsToCurCusProduct,
+ getCustomerSub,
} from "../attachUtils/convertAttachParams.js";
export const isMultiProductSub = ({
- sub,
- cusProducts,
+ sub,
+ cusProducts,
}: {
- sub: Stripe.Subscription;
- cusProducts: FullCusProduct[];
+ sub: Stripe.Subscription;
+ cusProducts: FullCusProduct[];
}) => {
- const cusProductsOnSub = cusProducts.filter((cp) =>
- cp.subscription_ids?.some((id) => id === sub.id)
- );
+ const cusProductsOnSub = cusProducts.filter((cp) =>
+ cp.subscription_ids?.some((id) => id === sub.id),
+ );
- return cusProductsOnSub.length > 1;
+ return cusProductsOnSub.length > 1;
};
export const getQuantityToRemove = ({
- cusProduct,
- price,
- entities,
+ cusProduct,
+ price,
+ entities,
}: {
- cusProduct: FullCusProduct;
- price: Price;
- entities: Entity[];
+ cusProduct: FullCusProduct;
+ price: Price;
+ entities: Entity[];
}) => {
- let finalQuantity = 1;
- const fixedPriceMultiplier = cusProduct.quantity || 1;
+ let finalQuantity = 1;
+ const fixedPriceMultiplier = cusProduct.quantity || 1;
- if (isPrepaidPrice({ price })) {
- const options = getPriceOptions(price, cusProduct.options);
+ if (isPrepaidPrice({ price })) {
+ const options = getPriceOptions(price, cusProduct.options);
- if (!options) return finalQuantity;
+ if (!options) return finalQuantity;
- // Remove quantity
- finalQuantity = options.upcoming_quantity || options.quantity || 1;
- }
+ // Remove quantity
+ finalQuantity = options.upcoming_quantity || options.quantity || 1;
+ }
- if (isContUsePrice({ price })) {
- const ents = cusProductToEnts({ cusProduct });
- const relatedEnt = getPriceEntitlement(price, ents);
- let existingUsage = getExistingUsageFromCusProducts({
- entitlement: relatedEnt,
- cusProducts: [cusProduct],
- entities,
- carryExistingUsages: true,
- internalEntityId: cusProduct.internal_entity_id || undefined,
- });
+ if (isContUsePrice({ price })) {
+ const ents = cusProductToEnts({ cusProduct });
+ const relatedEnt = getPriceEntitlement(price, ents);
+ let existingUsage = getExistingUsageFromCusProducts({
+ entitlement: relatedEnt,
+ cusProducts: [cusProduct],
+ entities,
+ carryExistingUsages: true,
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ });
- finalQuantity = existingUsage || 0;
- }
+ finalQuantity = existingUsage || 0;
+ }
- if (isFixedPrice({ price })) {
- finalQuantity = fixedPriceMultiplier * (finalQuantity || 1);
- }
+ if (isFixedPrice({ price })) {
+ finalQuantity = fixedPriceMultiplier * (finalQuantity || 1);
+ }
- return finalQuantity;
+ return finalQuantity;
};
export const willMergeSub = async ({
- attachParams,
- branch,
+ attachParams,
+ branch,
}: {
- attachParams: AttachParams;
- branch: AttachBranch;
+ attachParams: AttachParams;
+ branch: AttachBranch;
}) => {
- const { subId } = await getCustomerSub({ attachParams, onlySubId: true });
+ const { subId } = await getCustomerSub({ attachParams, onlySubId: true });
- if (branch == AttachBranch.MainIsTrial) {
- return false;
- }
+ if (branch == AttachBranch.MainIsTrial) {
+ return false;
+ }
- const cusProducts = attachParams.customer.customer_products;
- const curCusProduct = attachParamsToCurCusProduct({ attachParams });
+ const cusProducts = attachParams.customer.customer_products;
+ const curCusProduct = attachParamsToCurCusProduct({ attachParams });
- if (branch == AttachBranch.MultiAttachUpdate) {
- return true;
- }
+ if (branch == AttachBranch.MultiAttachUpdate) {
+ return true;
+ }
- // Case where upgrading to free trial...
- if (
- subId &&
- curCusProduct?.subscription_ids?.includes(subId!) &&
- !cusProducts.some(
- (cp) => cp.subscription_ids?.includes(subId!) && cp.id != curCusProduct.id
- ) &&
- attachParams.freeTrial
- ) {
- return false;
- }
+ // Case where upgrading to free trial...
+ if (
+ subId &&
+ curCusProduct?.subscription_ids?.includes(subId!) &&
+ !cusProducts.some(
+ (cp) =>
+ cp.subscription_ids?.includes(subId!) && cp.id != curCusProduct.id,
+ ) &&
+ attachParams.freeTrial
+ ) {
+ return false;
+ }
- if (subId) return true;
+ if (subId) return true;
- return false;
+ return false;
};
diff --git a/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts b/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts
index bfbd9b5f5..803929388 100644
--- a/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts
+++ b/server/src/internal/customers/attach/mergeUtils/paramsToScheduleItems.ts
@@ -9,8 +9,8 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToPrices } from "@autumn/shared";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- priceToScheduleItem,
- scheduleItemInCusProduct,
+ priceToScheduleItem,
+ scheduleItemInCusProduct,
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
import { differenceInDays } from "date-fns";
@@ -20,337 +20,337 @@ import { preparePhasesForBillingPeriod } from "./phaseUtils/upsertNewPhase.js";
import { getQuantityToRemove } from "./mergeUtils.js";
export const removeCusProductFromScheduleItems = async ({
- curScheduleItems,
- updateScheduleItems,
- allCusProducts,
- cusProduct,
- itemSet,
- phaseStart,
- attachParams,
+ curScheduleItems,
+ updateScheduleItems,
+ allCusProducts,
+ cusProduct,
+ itemSet,
+ phaseStart,
+ attachParams,
}: {
- curScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
- updateScheduleItems: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
- allCusProducts: FullCusProduct[];
- cusProduct: FullCusProduct;
- itemSet?: ItemSet;
- phaseStart?: number;
- attachParams: AttachParams;
+ curScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
+ updateScheduleItems: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
+ allCusProducts: FullCusProduct[];
+ cusProduct: FullCusProduct;
+ itemSet?: ItemSet;
+ phaseStart?: number;
+ attachParams: AttachParams;
}) => {
- const prices = cusProductToPrices({ cusProduct });
- let newScheduleItems = structuredClone(updateScheduleItems);
+ const prices = cusProductToPrices({ cusProduct });
+ let newScheduleItems = structuredClone(updateScheduleItems);
- const removePriceIds: string[] = [];
+ const removePriceIds: string[] = [];
- const printRemoveLogs = false;
- for (const price of prices) {
- const existingScheduleItem = priceToScheduleItem({
- price,
- scheduleItems: curScheduleItems,
- stripeProdId: cusProduct.product.processor?.id,
- });
+ const printRemoveLogs = false;
+ for (const price of prices) {
+ const existingScheduleItem = priceToScheduleItem({
+ price,
+ scheduleItems: curScheduleItems,
+ stripeProdId: cusProduct.product.processor?.id,
+ });
- if (printRemoveLogs) {
- console.log(
- "Removing price: ",
- `${cusProduct.product.name} - ${formatPrice({ price })},`
- );
- console.log(
- "Existing schedule item:",
- (existingScheduleItem?.price as Stripe.Price)?.id
- );
- console.log("---");
- }
+ if (printRemoveLogs) {
+ console.log(
+ "Removing price: ",
+ `${cusProduct.product.name} - ${formatPrice({ price })},`,
+ );
+ console.log(
+ "Existing schedule item:",
+ (existingScheduleItem?.price as Stripe.Price)?.id,
+ );
+ console.log("---");
+ }
- if (!existingScheduleItem) continue;
+ if (!existingScheduleItem) continue;
- // 1. If arrear price
- if (isArrearPrice({ price })) {
- if (
- allCusProducts.some((cp) => {
- if (cp.id === cusProduct.id) return false;
- if (cp.canceled) return false;
+ // 1. If arrear price
+ if (isArrearPrice({ price })) {
+ if (
+ allCusProducts.some((cp) => {
+ if (cp.id === cusProduct.id) return false;
+ if (cp.canceled) return false;
- if (
- phaseStart &&
- differenceInDays(cusProduct.starts_at, phaseStart * 1000) > 1
- )
- return false;
+ if (
+ phaseStart &&
+ differenceInDays(cusProduct.starts_at, phaseStart * 1000) > 1
+ )
+ return false;
- if (
- scheduleItemInCusProduct({
- cusProduct: cp,
- scheduleItem: existingScheduleItem as any,
- })
- ) {
- return true;
- }
- return false;
- })
- ) {
- continue;
- }
+ if (
+ scheduleItemInCusProduct({
+ cusProduct: cp,
+ scheduleItem: existingScheduleItem as any,
+ })
+ ) {
+ return true;
+ }
+ return false;
+ })
+ ) {
+ continue;
+ }
- if (
- itemSet?.subItems.some(
- (si) => si.price == (existingScheduleItem.price as Stripe.Price)?.id
- )
- ) {
- continue;
- }
+ if (
+ itemSet?.subItems.some(
+ (si) => si.price == (existingScheduleItem.price as Stripe.Price)?.id,
+ )
+ ) {
+ continue;
+ }
- removePriceIds.push((existingScheduleItem.price as Stripe.Price).id);
+ removePriceIds.push((existingScheduleItem.price as Stripe.Price).id);
- continue;
- }
+ continue;
+ }
- // 1. Get quantity to remove
- // Get quantity from cus product...
- const quantityToRemove = getQuantityToRemove({
- cusProduct,
- price,
- entities: attachParams.customer.entities,
- });
+ // 1. Get quantity to remove
+ // Get quantity from cus product...
+ const quantityToRemove = getQuantityToRemove({
+ cusProduct,
+ price,
+ entities: attachParams.customer.entities,
+ });
- // 2. Check if item already exists in newSubItems
- const existingItemIndex = newScheduleItems.findIndex(
- (si) => si.price === (existingScheduleItem.price as Stripe.Price)?.id
- );
+ // 2. Check if item already exists in newSubItems
+ const existingItemIndex = newScheduleItems.findIndex(
+ (si) => si.price === (existingScheduleItem.price as Stripe.Price)?.id,
+ );
- if (existingItemIndex !== -1) {
- // Update existing item in newSubItems
- const currentQuantity = newScheduleItems[existingItemIndex].quantity || 0;
- const newQuantity = currentQuantity - quantityToRemove;
+ if (existingItemIndex !== -1) {
+ // Update existing item in newSubItems
+ const currentQuantity = newScheduleItems[existingItemIndex].quantity || 0;
+ const newQuantity = currentQuantity - quantityToRemove;
- if (newQuantity <= 0) {
- removePriceIds.push(
- newScheduleItems[existingItemIndex].price as string
- );
- }
+ if (newQuantity <= 0) {
+ removePriceIds.push(
+ newScheduleItems[existingItemIndex].price as string,
+ );
+ }
- newScheduleItems[existingItemIndex].quantity = newQuantity;
- }
- }
+ newScheduleItems[existingItemIndex].quantity = newQuantity;
+ }
+ }
- newScheduleItems = newScheduleItems.filter(
- (si) => !removePriceIds.includes(si.price as string)
- );
+ newScheduleItems = newScheduleItems.filter(
+ (si) => !removePriceIds.includes(si.price as string),
+ );
- return newScheduleItems;
+ return newScheduleItems;
};
const logScheduleItems = ({
- items,
- cusProducts,
+ items,
+ cusProducts,
}: {
- items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
- cusProducts: FullCusProduct[];
+ items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
+ cusProducts: FullCusProduct[];
}) => {
- for (const item of items) {
- let matchedPrice = null;
- let matchedCusProduct = null;
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const price = prices.find((p) => {
- return p.config.stripe_price_id == item.price;
- });
+ for (const item of items) {
+ let matchedPrice = null;
+ let matchedCusProduct = null;
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const price = prices.find((p) => {
+ return p.config.stripe_price_id == item.price;
+ });
- if (price) {
- matchedPrice = price;
- matchedCusProduct = cusProduct;
- }
- }
- console.log({
- priceStr: matchedPrice
- ? `${matchedCusProduct?.product.name} - ${formatPrice({
- price: matchedPrice,
- })}`
- : "N/A",
- price: item.price,
- quantity: item.quantity,
- });
- }
+ if (price) {
+ matchedPrice = price;
+ matchedCusProduct = cusProduct;
+ }
+ }
+ console.log({
+ priceStr: matchedPrice
+ ? `${matchedCusProduct?.product.name} - ${formatPrice({
+ price: matchedPrice,
+ })}`
+ : "N/A",
+ price: item.price,
+ quantity: item.quantity,
+ });
+ }
- console.log("--------------------------------");
+ console.log("--------------------------------");
};
// Helper: merge new items and then remove items from selected customer products
const computeUpdatedScheduleItems = async ({
- itemSet,
- baseCurScheduleItems,
- attachParams,
- removeCusProducts,
- phaseStart,
+ itemSet,
+ baseCurScheduleItems,
+ attachParams,
+ removeCusProducts,
+ phaseStart,
}: {
- itemSet: ItemSet;
- baseCurScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
- attachParams: AttachParams;
- removeCusProducts?: FullCusProduct[];
- phaseStart?: number;
+ itemSet: ItemSet;
+ baseCurScheduleItems: Stripe.SubscriptionSchedule.Phase.Item[];
+ attachParams: AttachParams;
+ removeCusProducts?: FullCusProduct[];
+ phaseStart?: number;
}): Promise => {
- let newScheduleItems: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[] =
- mergeNewScheduleItems({
- itemSet,
- curScheduleItems: baseCurScheduleItems,
- });
+ let newScheduleItems: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[] =
+ mergeNewScheduleItems({
+ itemSet,
+ curScheduleItems: baseCurScheduleItems,
+ });
- const cusProductsToRemove =
- removeCusProducts ||
- getCusProductsToRemove({ attachParams, includeScheduled: true });
+ const cusProductsToRemove =
+ removeCusProducts ||
+ getCusProductsToRemove({ attachParams, includeScheduled: true });
- // console.log(
- // "REMOVING CUS PRODUCTS:",
- // cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`)
- // );
+ // console.log(
+ // "REMOVING CUS PRODUCTS:",
+ // cusProductsToRemove?.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`)
+ // );
- const allCusProducts = attachParams.customer.customer_products;
+ const allCusProducts = attachParams.customer.customer_products;
- // console.log("New schedule items:");
- // logScheduleItems({ items: newScheduleItems, cusProducts: allCusProducts });
- for (const cusProduct of cusProductsToRemove) {
- newScheduleItems = await removeCusProductFromScheduleItems({
- curScheduleItems: baseCurScheduleItems,
- updateScheduleItems: newScheduleItems,
- allCusProducts,
- cusProduct,
- itemSet,
- phaseStart,
- attachParams,
- });
- }
+ // console.log("New schedule items:");
+ // logScheduleItems({ items: newScheduleItems, cusProducts: allCusProducts });
+ for (const cusProduct of cusProductsToRemove) {
+ newScheduleItems = await removeCusProductFromScheduleItems({
+ curScheduleItems: baseCurScheduleItems,
+ updateScheduleItems: newScheduleItems,
+ allCusProducts,
+ cusProduct,
+ itemSet,
+ phaseStart,
+ attachParams,
+ });
+ }
- // console.log("New schedule items after removing cus products:");
- // logScheduleItems({ items: newScheduleItems, cusProducts: allCusProducts });
+ // console.log("New schedule items after removing cus products:");
+ // logScheduleItems({ items: newScheduleItems, cusProducts: allCusProducts });
- return newScheduleItems;
+ return newScheduleItems;
};
export const paramsToScheduleItems = async ({
- req,
- sub,
- schedule,
- attachParams,
- config,
- removeCusProducts,
- billingPeriodEnd,
+ req,
+ sub,
+ schedule,
+ attachParams,
+ config,
+ removeCusProducts,
+ billingPeriodEnd,
}: {
- req: ExtendedRequest;
- sub?: Stripe.Subscription;
- schedule?: Stripe.SubscriptionSchedule;
- attachParams: AttachParams;
- config: AttachConfig;
- removeCusProducts?: FullCusProduct[];
- billingPeriodEnd?: number;
+ req: ExtendedRequest;
+ sub?: Stripe.Subscription;
+ schedule?: Stripe.SubscriptionSchedule;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ removeCusProducts?: FullCusProduct[];
+ billingPeriodEnd?: number;
}) => {
- const { logger } = req;
+ const { logger } = req;
- const itemSet = await getStripeSubItems2({
- attachParams,
- config,
- });
+ const itemSet = await getStripeSubItems2({
+ attachParams,
+ config,
+ });
- let curScheduleItems: any[] = [];
- let phaseIndex = -1;
+ let curScheduleItems: any[] = [];
+ let phaseIndex = -1;
- if (billingPeriodEnd && schedule && schedule.phases.length > 1) {
- phaseIndex = schedule.phases.findIndex(
- (phase) => billingPeriodEnd <= phase.start_date
- );
- }
+ if (billingPeriodEnd && schedule && schedule.phases.length > 1) {
+ phaseIndex = schedule.phases.findIndex(
+ (phase) => billingPeriodEnd <= phase.start_date,
+ );
+ }
- const printPhaseLogs = false;
- if (printPhaseLogs) {
- console.log("Phase index:", phaseIndex);
- console.log(
- "Billing period end:",
- formatUnixToDateTime(billingPeriodEnd! * 1000)
- );
- console.log(
- "Phases:",
- schedule?.phases.map((p) => ({
- start_date: formatUnixToDateTime(p.start_date * 1000),
- end_date: formatUnixToDateTime(p.end_date * 1000),
- }))
- );
- }
+ const printPhaseLogs = false;
+ if (printPhaseLogs) {
+ console.log("Phase index:", phaseIndex);
+ console.log(
+ "Billing period end:",
+ formatUnixToDateTime(billingPeriodEnd! * 1000),
+ );
+ console.log(
+ "Phases:",
+ schedule?.phases.map((p) => ({
+ start_date: formatUnixToDateTime(p.start_date * 1000),
+ end_date: formatUnixToDateTime(p.end_date * 1000),
+ })),
+ );
+ }
- if (phaseIndex === -1) {
- let curScheduleItems = [];
+ if (phaseIndex === -1) {
+ let curScheduleItems = [];
- if (sub) {
- curScheduleItems = structuredClone(sub?.items.data || []);
- } else {
- curScheduleItems = schedule!.phases[schedule!.phases.length - 1].items;
- }
+ if (sub) {
+ curScheduleItems = structuredClone(sub?.items.data || []);
+ } else {
+ curScheduleItems = schedule!.phases[schedule!.phases.length - 1].items;
+ }
- const newScheduleItems = await computeUpdatedScheduleItems({
- itemSet,
- baseCurScheduleItems: curScheduleItems as any,
- attachParams,
- removeCusProducts,
- });
+ const newScheduleItems = await computeUpdatedScheduleItems({
+ itemSet,
+ baseCurScheduleItems: curScheduleItems as any,
+ attachParams,
+ removeCusProducts,
+ });
- const curPhases = schedule?.phases || [];
- const mappedExistingPhases = curPhases.map((phase, index) => {
- const items = phase.items.map((item) => ({
- price: (item.price as Stripe.Price).id,
- quantity: item.quantity,
- trial_end: phase.trial_end || undefined,
- }));
- const isLast = index === schedule!.phases.length - 1;
- const end_date = isLast ? billingPeriodEnd : phase.end_date;
- return { items, start_date: phase.start_date, end_date };
- });
+ const curPhases = schedule?.phases || [];
+ const mappedExistingPhases = curPhases.map((phase, index) => {
+ const items = phase.items.map((item) => ({
+ price: (item.price as Stripe.Price).id,
+ quantity: item.quantity,
+ trial_end: phase.trial_end || undefined,
+ }));
+ const isLast = index === schedule!.phases.length - 1;
+ const end_date = isLast ? billingPeriodEnd : phase.end_date;
+ return { items, start_date: phase.start_date, end_date };
+ });
- const appendedPhase = {
- items: newScheduleItems,
- start_date: billingPeriodEnd,
- };
+ const appendedPhase = {
+ items: newScheduleItems,
+ start_date: billingPeriodEnd,
+ };
- const finalPhases = mergeAdjacentPhasesWithSameItems([
- ...mappedExistingPhases,
- appendedPhase,
- ] as any);
+ const finalPhases = mergeAdjacentPhasesWithSameItems([
+ ...mappedExistingPhases,
+ appendedPhase,
+ ] as any);
- return {
- items: newScheduleItems,
- phases: finalPhases,
- invoiceItems: itemSet.invoiceItems,
- usageFeatures: itemSet.usageFeatures,
- };
- } else {
- const {
- phases: newPhases,
- insertIndex,
- shouldInsert,
- originalIndexFor,
- } = preparePhasesForBillingPeriod({
- schedule: schedule!,
- phaseIndex,
- billingPeriodEnd: billingPeriodEnd!,
- });
+ return {
+ items: newScheduleItems,
+ phases: finalPhases,
+ invoiceItems: itemSet.invoiceItems,
+ usageFeatures: itemSet.usageFeatures,
+ };
+ } else {
+ const {
+ phases: newPhases,
+ insertIndex,
+ shouldInsert,
+ originalIndexFor,
+ } = preparePhasesForBillingPeriod({
+ schedule: schedule!,
+ phaseIndex,
+ billingPeriodEnd: billingPeriodEnd!,
+ });
- // Propagate new items from the target phase to the right
- for (let i = insertIndex; i < newPhases.length; i++) {
- const baseCurScheduleItems = schedule!.phases[originalIndexFor(i)]
- .items as Stripe.SubscriptionSchedule.Phase.Item[];
+ // Propagate new items from the target phase to the right
+ for (let i = insertIndex; i < newPhases.length; i++) {
+ const baseCurScheduleItems = schedule!.phases[originalIndexFor(i)]
+ .items as Stripe.SubscriptionSchedule.Phase.Item[];
- const updatedItems = await computeUpdatedScheduleItems({
- itemSet,
- baseCurScheduleItems,
- attachParams,
- removeCusProducts,
- phaseStart: schedule!.phases[phaseIndex].start_date,
- });
+ const updatedItems = await computeUpdatedScheduleItems({
+ itemSet,
+ baseCurScheduleItems,
+ attachParams,
+ removeCusProducts,
+ phaseStart: schedule!.phases[phaseIndex].start_date,
+ });
- newPhases[i].items = updatedItems as any;
- }
+ newPhases[i].items = updatedItems as any;
+ }
- const mergedPhases = mergeAdjacentPhasesWithSameItems(newPhases as any);
+ const mergedPhases = mergeAdjacentPhasesWithSameItems(newPhases as any);
- return {
- phases: mergedPhases,
- invoiceItems: itemSet.invoiceItems,
- usageFeatures: itemSet.usageFeatures,
- };
- }
+ return {
+ phases: mergedPhases,
+ invoiceItems: itemSet.invoiceItems,
+ usageFeatures: itemSet.usageFeatures,
+ };
+ }
};
diff --git a/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts b/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts
index 69c0fc7e2..411bc7f7c 100644
--- a/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts
+++ b/server/src/internal/customers/attach/mergeUtils/paramsToSubItems.ts
@@ -7,8 +7,8 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- findStripeItemForPrice,
- subItemInCusProduct,
+ findStripeItemForPrice,
+ subItemInCusProduct,
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { mergeNewSubItems } from "./mergeNewSubItems.js";
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
@@ -17,223 +17,223 @@ import { notNullish } from "@/utils/genUtils.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
export const getCusProductsToRemove = ({
- attachParams,
- includeCanceled = false,
- includeScheduled = false,
+ attachParams,
+ includeCanceled = false,
+ includeScheduled = false,
}: {
- attachParams: AttachParams;
- includeCanceled?: boolean;
- includeScheduled?: boolean;
+ attachParams: AttachParams;
+ includeCanceled?: boolean;
+ includeScheduled?: boolean;
}) => {
- const products = attachParams.products;
- const cusProducts = attachParams.cusProducts;
+ const products = attachParams.products;
+ const cusProducts = attachParams.cusProducts;
- const cusProductsToRemove: FullCusProduct[] = [];
- const prods =
- products.length > 0
- ? products
- : [cusProductToProduct({ cusProduct: attachParams.cusProduct! })];
+ const cusProductsToRemove: FullCusProduct[] = [];
+ const prods =
+ products.length > 0
+ ? products
+ : [cusProductToProduct({ cusProduct: attachParams.cusProduct! })];
- for (const product of prods) {
- // Get cur main and cur same
- const { curMainProduct, curSameProduct, curScheduledProduct } =
- getExistingCusProducts({
- product,
- cusProducts: attachParams.cusProducts,
- internalEntityId: attachParams.internalEntityId,
- });
+ for (const product of prods) {
+ // Get cur main and cur same
+ const { curMainProduct, curSameProduct, curScheduledProduct } =
+ getExistingCusProducts({
+ product,
+ cusProducts: attachParams.cusProducts,
+ internalEntityId: attachParams.internalEntityId,
+ });
- // 1. If curScheduledProduct and curMainProduct, want to remove scheduled, main should already be removed...
- if (
- includeScheduled &&
- !product.is_add_on &&
- curScheduledProduct &&
- curMainProduct
- ) {
- cusProductsToRemove.push(curScheduledProduct);
- }
+ // 1. If curScheduledProduct and curMainProduct, want to remove scheduled, main should already be removed...
+ if (
+ includeScheduled &&
+ !product.is_add_on &&
+ curScheduledProduct &&
+ curMainProduct
+ ) {
+ cusProductsToRemove.push(curScheduledProduct);
+ }
- // 1. If product is an add on, and there's current same, add it
- else if (curSameProduct) {
- cusProductsToRemove.push(curSameProduct);
- }
+ // 1. If product is an add on, and there's current same, add it
+ else if (curSameProduct) {
+ cusProductsToRemove.push(curSameProduct);
+ }
- // 2. If product is a main product, add curMain
- else if (!product.is_add_on && curMainProduct) {
- cusProductsToRemove.push(curMainProduct);
- }
- }
+ // 2. If product is a main product, add curMain
+ else if (!product.is_add_on && curMainProduct) {
+ cusProductsToRemove.push(curMainProduct);
+ }
+ }
- if (includeCanceled) {
- const subId = cusProductsToRemove.find(
- (cp) => cp.subscription_ids && cp.subscription_ids.length > 0
- )?.subscription_ids?.[0];
+ if (includeCanceled) {
+ const subId = cusProductsToRemove.find(
+ (cp) => cp.subscription_ids && cp.subscription_ids.length > 0,
+ )?.subscription_ids?.[0];
- const canceledCusProducts = cusProducts.filter(
- (cp) =>
- cp.canceled && (subId ? cp.subscription_ids?.includes(subId!) : true)
- );
- cusProductsToRemove.push(...canceledCusProducts);
- }
+ const canceledCusProducts = cusProducts.filter(
+ (cp) =>
+ cp.canceled && (subId ? cp.subscription_ids?.includes(subId!) : true),
+ );
+ cusProductsToRemove.push(...canceledCusProducts);
+ }
- // Get unique cus products, by cusProduct.id
- const uniqueCusProductsToRemove = cusProductsToRemove.filter(
- (cusProduct, index, array) =>
- array.findIndex((cp) => cp.id === cusProduct.id) === index
- );
+ // Get unique cus products, by cusProduct.id
+ const uniqueCusProductsToRemove = cusProductsToRemove.filter(
+ (cusProduct, index, array) =>
+ array.findIndex((cp) => cp.id === cusProduct.id) === index,
+ );
- return uniqueCusProductsToRemove;
+ return uniqueCusProductsToRemove;
};
export const paramsToSubItems = async ({
- req,
- sub,
- attachParams,
- config,
- removeCusProducts,
- addItemSet,
+ req,
+ sub,
+ attachParams,
+ config,
+ removeCusProducts,
+ addItemSet,
}: {
- req: ExtendedRequest;
- sub?: Stripe.Subscription;
- attachParams: AttachParams;
- config: AttachConfig;
- removeCusProducts?: FullCusProduct[];
- addItemSet?: ItemSet;
+ req: ExtendedRequest;
+ sub?: Stripe.Subscription;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ removeCusProducts?: FullCusProduct[];
+ addItemSet?: ItemSet;
}) => {
- const { logger } = req;
- let curSubItems = sub?.items.data || [];
+ const { logger } = req;
+ let curSubItems = sub?.items.data || [];
- const itemSet = notNullish(addItemSet)
- ? addItemSet!
- : await getStripeSubItems2({
- attachParams,
- config,
- });
+ const itemSet = notNullish(addItemSet)
+ ? addItemSet!
+ : await getStripeSubItems2({
+ attachParams,
+ config,
+ });
- // 1. Remove items related to cur cus product...
- const cusProductsToRemove = notNullish(removeCusProducts)
- ? removeCusProducts!
- : getCusProductsToRemove({ attachParams });
+ // 1. Remove items related to cur cus product...
+ const cusProductsToRemove = notNullish(removeCusProducts)
+ ? removeCusProducts!
+ : getCusProductsToRemove({ attachParams });
- console.log(
- "Cus products to remove:",
- cusProductsToRemove.map((cp) => cp.product.name)
- );
+ console.log(
+ "Cus products to remove:",
+ cusProductsToRemove.map((cp) => cp.product.name),
+ );
- let newSubItems = mergeNewSubItems({
- itemSet,
- curSubItems,
- });
+ let newSubItems = mergeNewSubItems({
+ itemSet,
+ curSubItems,
+ });
- const allCusProducts = attachParams.customer.customer_products;
+ const allCusProducts = attachParams.customer.customer_products;
- // 3. Remove items related to cus products to remove
- const printRemoveLogs = false;
- for (const cusProduct of cusProductsToRemove) {
- const prices = cusProductToPrices({ cusProduct });
+ // 3. Remove items related to cus products to remove
+ const printRemoveLogs = false;
+ for (const cusProduct of cusProductsToRemove) {
+ const prices = cusProductToPrices({ cusProduct });
- if (printRemoveLogs) {
- console.log("Removing cus product:", cusProduct.product.name);
- }
+ if (printRemoveLogs) {
+ console.log("Removing cus product:", cusProduct.product.name);
+ }
- for (const price of prices) {
- const existingSubItem = findStripeItemForPrice({
- price,
- stripeItems: curSubItems,
- stripeProdId: cusProduct.product.processor?.id,
- }) as Stripe.SubscriptionItem | undefined;
+ for (const price of prices) {
+ const existingSubItem = findStripeItemForPrice({
+ price,
+ stripeItems: curSubItems,
+ stripeProdId: cusProduct.product.processor?.id,
+ }) as Stripe.SubscriptionItem | undefined;
- if (printRemoveLogs) {
- console.log("Price:", formatPrice({ price }));
- console.log(
- "Existing sub item:",
- existingSubItem
- ? {
- id: existingSubItem?.id,
- price: existingSubItem?.price?.id,
- quantity: existingSubItem?.quantity,
- }
- : "N/A"
- );
- }
+ if (printRemoveLogs) {
+ console.log("Price:", formatPrice({ price }));
+ console.log(
+ "Existing sub item:",
+ existingSubItem
+ ? {
+ id: existingSubItem?.id,
+ price: existingSubItem?.price?.id,
+ quantity: existingSubItem?.quantity,
+ }
+ : "N/A",
+ );
+ }
- if (!existingSubItem) continue;
+ if (!existingSubItem) continue;
- // 1. If arrear price
- if (isArrearPrice({ price })) {
- if (
- allCusProducts.some((cp) => {
- if (cp.id === cusProduct.id) return false;
+ // 1. If arrear price
+ if (isArrearPrice({ price })) {
+ if (
+ allCusProducts.some((cp) => {
+ if (cp.id === cusProduct.id) return false;
- if (cp.status == CusProductStatus.Scheduled) return false;
+ if (cp.status == CusProductStatus.Scheduled) return false;
- return subItemInCusProduct({
- cusProduct: cp,
- subItem: existingSubItem as Stripe.SubscriptionItem,
- });
- })
- ) {
- continue;
- }
+ return subItemInCusProduct({
+ cusProduct: cp,
+ subItem: existingSubItem as Stripe.SubscriptionItem,
+ });
+ })
+ ) {
+ continue;
+ }
- if (
- itemSet.subItems.some((si) => si.price == existingSubItem.price?.id)
- ) {
- continue;
- }
+ if (
+ itemSet.subItems.some((si) => si.price == existingSubItem.price?.id)
+ ) {
+ continue;
+ }
- newSubItems.push({
- id: existingSubItem.id,
- deleted: true,
- });
+ newSubItems.push({
+ id: existingSubItem.id,
+ deleted: true,
+ });
- continue;
- }
+ continue;
+ }
- // Helper function to handle quantity updates and deletion
- const updateItemQuantity = (item: any, newQuantity: number) => {
- if (newQuantity <= 0) {
- item.deleted = true;
- item.quantity = undefined;
- } else {
- item.quantity = newQuantity;
- }
- };
+ // Helper function to handle quantity updates and deletion
+ const updateItemQuantity = (item: any, newQuantity: number) => {
+ if (newQuantity <= 0) {
+ item.deleted = true;
+ item.quantity = undefined;
+ } else {
+ item.quantity = newQuantity;
+ }
+ };
- // 1. Get quantity to remove
- const quantityToRemove = getQuantityToRemove({
- cusProduct,
- price,
- entities: attachParams.customer.entities,
- });
+ // 1. Get quantity to remove
+ const quantityToRemove = getQuantityToRemove({
+ cusProduct,
+ price,
+ entities: attachParams.customer.entities,
+ });
- // 2. Check if item already exists in newSubItems
- const existingItemIndex = newSubItems.findIndex(
- (si) => si.id === existingSubItem.id
- );
+ // 2. Check if item already exists in newSubItems
+ const existingItemIndex = newSubItems.findIndex(
+ (si) => si.id === existingSubItem.id,
+ );
- if (existingItemIndex !== -1) {
- const currentQuantity = newSubItems[existingItemIndex].quantity || 0;
- const newQuantity = currentQuantity - quantityToRemove;
+ if (existingItemIndex !== -1) {
+ const currentQuantity = newSubItems[existingItemIndex].quantity || 0;
+ const newQuantity = currentQuantity - quantityToRemove;
- updateItemQuantity(newSubItems[existingItemIndex], newQuantity);
- } else {
- const currentQuantity = existingSubItem.quantity || 0;
- const newQuantity = currentQuantity - quantityToRemove;
+ updateItemQuantity(newSubItems[existingItemIndex], newQuantity);
+ } else {
+ const currentQuantity = existingSubItem.quantity || 0;
+ const newQuantity = currentQuantity - quantityToRemove;
- newSubItems.push({
- id: existingSubItem.id,
- quantity: newQuantity,
- });
+ newSubItems.push({
+ id: existingSubItem.id,
+ quantity: newQuantity,
+ });
- updateItemQuantity(newSubItems[newSubItems.length - 1], newQuantity);
- }
- }
- }
+ updateItemQuantity(newSubItems[newSubItems.length - 1], newQuantity);
+ }
+ }
+ }
- return {
- subItems: newSubItems,
- invoiceItems: itemSet.invoiceItems,
- usageFeatures: itemSet.usageFeatures,
- };
+ return {
+ subItems: newSubItems,
+ invoiceItems: itemSet.invoiceItems,
+ usageFeatures: itemSet.usageFeatures,
+ };
};
diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts
index f21346b3f..6d53bddd7 100644
--- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts
+++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/mergeSimilarPhases.ts
@@ -2,139 +2,140 @@ import Stripe from "stripe";
// Normalize items for comparison (order-agnostic, price id as string)
const normalizePhaseItems = (
- items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]
+ items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[],
) => {
- return items
- .map((item) => ({
- price: item.price as string,
- quantity: item.quantity || 0,
- }))
- .sort((a, b) => {
- if (a.price === b.price) return (a.quantity || 0) - (b.quantity || 0);
- return a.price.localeCompare(b.price);
- });
+ return items
+ .map((item) => ({
+ price: item.price as string,
+ quantity: item.quantity || 0,
+ }))
+ .sort((a, b) => {
+ if (a.price === b.price) return (a.quantity || 0) - (b.quantity || 0);
+ return a.price.localeCompare(b.price);
+ });
};
// Compare two item lists for equality (same length, same price+quantity pairs)
const haveSameItems = (
- a: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[],
- b: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]
+ a: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[],
+ b: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[],
) => {
- if (a.length !== b.length) return false;
- const na = normalizePhaseItems(a);
- const nb = normalizePhaseItems(b);
- for (let i = 0; i < na.length; i++) {
- if (na[i].price !== nb[i].price) return false;
- if ((na[i].quantity || 0) !== (nb[i].quantity || 0)) return false;
- }
- return true;
+ if (a.length !== b.length) return false;
+ const na = normalizePhaseItems(a);
+ const nb = normalizePhaseItems(b);
+ for (let i = 0; i < na.length; i++) {
+ if (na[i].price !== nb[i].price) return false;
+ if ((na[i].quantity || 0) !== (nb[i].quantity || 0)) return false;
+ }
+ return true;
};
// Check if a phase has no items
const hasNoItems = (phase: Stripe.SubscriptionScheduleUpdateParams.Phase) => {
- const items =
- (phase.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) || [];
- return items.length === 0;
+ const items =
+ (phase.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) || [];
+ return items.length === 0;
};
// Remove empty phases and adjust dates to maintain timeline continuity
const removeEmptyPhasesAndAdjustDates = (
- phases: Stripe.SubscriptionScheduleUpdateParams.Phase[]
+ phases: Stripe.SubscriptionScheduleUpdateParams.Phase[],
): Stripe.SubscriptionScheduleUpdateParams.Phase[] => {
- if (!phases || phases.length <= 1) return phases;
+ if (!phases || phases.length <= 1) return phases;
- const result: Stripe.SubscriptionScheduleUpdateParams.Phase[] = [];
+ const result: Stripe.SubscriptionScheduleUpdateParams.Phase[] = [];
- for (let i = 0; i < phases.length; i++) {
- const phase = phases[i];
+ for (let i = 0; i < phases.length; i++) {
+ const phase = phases[i];
- if (!hasNoItems(phase)) {
- const clonedPhase = structuredClone(phase);
+ if (!hasNoItems(phase)) {
+ const clonedPhase = structuredClone(phase);
- // If this is not the first non-empty phase, we need to check if there were
- // empty phases before it and extend the previous non-empty phase to cover the gap
- if (result.length > 0) {
- const prevPhase = result[result.length - 1];
+ // If this is not the first non-empty phase, we need to check if there were
+ // empty phases before it and extend the previous non-empty phase to cover the gap
+ if (result.length > 0) {
+ const prevPhase = result[result.length - 1];
- // Find if there are empty phases between the previous non-empty phase and current
- let hasEmptyPhasesBetween = false;
- for (let j = i - 1; j >= 0; j--) {
- const checkPhase = phases[j];
- if (!hasNoItems(checkPhase)) {
- // Found the previous non-empty phase
- break;
- }
- hasEmptyPhasesBetween = true;
- }
+ // Find if there are empty phases between the previous non-empty phase and current
+ let hasEmptyPhasesBetween = false;
+ for (let j = i - 1; j >= 0; j--) {
+ const checkPhase = phases[j];
+ if (!hasNoItems(checkPhase)) {
+ // Found the previous non-empty phase
+ break;
+ }
+ hasEmptyPhasesBetween = true;
+ }
- // If there were empty phases, extend the previous phase's end_date to current phase's start_date
- if (hasEmptyPhasesBetween && clonedPhase.start_date) {
- prevPhase.end_date = clonedPhase.start_date;
- }
- }
+ // If there were empty phases, extend the previous phase's end_date to current phase's start_date
+ if (hasEmptyPhasesBetween && clonedPhase.start_date) {
+ prevPhase.end_date = clonedPhase.start_date;
+ }
+ }
- result.push(clonedPhase);
- }
- }
+ result.push(clonedPhase);
+ }
+ }
- return result;
+ return result;
};
// Merge adjacent phases that have identical items
export const mergeAdjacentPhasesWithSameItems = (
- phases: Stripe.SubscriptionScheduleUpdateParams.Phase[]
+ phases: Stripe.SubscriptionScheduleUpdateParams.Phase[],
): Stripe.SubscriptionScheduleUpdateParams.Phase[] => {
- if (!phases || phases.length <= 1) return phases;
+ if (!phases || phases.length <= 1) return phases;
- // First, remove empty phases and adjust dates to maintain continuity
- const nonEmptyPhases = removeEmptyPhasesAndAdjustDates(phases);
+ // First, remove empty phases and adjust dates to maintain continuity
+ const nonEmptyPhases = removeEmptyPhasesAndAdjustDates(phases);
- if (nonEmptyPhases.length <= 1) return nonEmptyPhases;
+ if (nonEmptyPhases.length <= 1) return nonEmptyPhases;
- // Then, merge adjacent phases with same items
- const merged: Stripe.SubscriptionScheduleUpdateParams.Phase[] = [];
- let current = structuredClone(nonEmptyPhases[0]);
+ // Then, merge adjacent phases with same items
+ const merged: Stripe.SubscriptionScheduleUpdateParams.Phase[] = [];
+ let current = structuredClone(nonEmptyPhases[0]);
- for (let i = 1; i < nonEmptyPhases.length; i++) {
- const next = nonEmptyPhases[i];
- const same = haveSameItems(
- (current.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) ||
- [],
- (next.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) || []
- );
+ for (let i = 1; i < nonEmptyPhases.length; i++) {
+ const next = nonEmptyPhases[i];
+ const same = haveSameItems(
+ (current.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) ||
+ [],
+ (next.items as Stripe.SubscriptionScheduleUpdateParams.Phase.Item[]) ||
+ [],
+ );
- if (same) {
- // Keep earliest start_date
- const curStart = current.start_date as number | undefined;
- const nextStart = next.start_date as number | undefined;
- if (
- typeof curStart === "number" &&
- typeof nextStart === "number" &&
- nextStart < curStart
- ) {
- current.start_date = nextStart;
- }
+ if (same) {
+ // Keep earliest start_date
+ const curStart = current.start_date as number | undefined;
+ const nextStart = next.start_date as number | undefined;
+ if (
+ typeof curStart === "number" &&
+ typeof nextStart === "number" &&
+ nextStart < curStart
+ ) {
+ current.start_date = nextStart;
+ }
- // Extend end_date to the later one
- const curEnd = current.end_date as number | undefined;
- const nextEnd = next.end_date as number | undefined;
- if (
- typeof curEnd === "number" &&
- typeof nextEnd === "number" &&
- nextEnd > curEnd
- ) {
- current.end_date = nextEnd;
- } else if (typeof curEnd !== "number") {
- // If current has no end_date but next does, adopt it
- current.end_date = next.end_date;
- }
- // Do not push next; it is merged into current
- } else {
- merged.push(current);
- current = structuredClone(next);
- }
- }
+ // Extend end_date to the later one
+ const curEnd = current.end_date as number | undefined;
+ const nextEnd = next.end_date as number | undefined;
+ if (
+ typeof curEnd === "number" &&
+ typeof nextEnd === "number" &&
+ nextEnd > curEnd
+ ) {
+ current.end_date = nextEnd;
+ } else if (typeof curEnd !== "number") {
+ // If current has no end_date but next does, adopt it
+ current.end_date = next.end_date;
+ }
+ // Do not push next; it is merged into current
+ } else {
+ merged.push(current);
+ current = structuredClone(next);
+ }
+ }
- merged.push(current);
- return merged;
+ merged.push(current);
+ return merged;
};
diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts
index 9291c6e97..c99d3349f 100644
--- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts
+++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.ts
@@ -7,107 +7,107 @@ import { differenceInDays, subDays } from "date-fns";
import Stripe from "stripe";
export const cusProductInPhase = ({
- phaseStart,
- phaseStartMillis,
- cusProduct,
+ phaseStart,
+ phaseStartMillis,
+ cusProduct,
}: {
- phaseStart?: number;
- phaseStartMillis?: number;
- cusProduct: FullCusProduct;
+ phaseStart?: number;
+ phaseStartMillis?: number;
+ cusProduct: FullCusProduct;
}) => {
- // Require customer product to start at most one day before the phase start
- const oneDayBeforeCusProductStartsAt = subDays(
- cusProduct.starts_at,
- 1
- ).getTime();
- const finalPhaseStart = phaseStartMillis ?? phaseStart! * 1000;
+ // Require customer product to start at most one day before the phase start
+ const oneDayBeforeCusProductStartsAt = subDays(
+ cusProduct.starts_at,
+ 1,
+ ).getTime();
+ const finalPhaseStart = phaseStartMillis ?? phaseStart! * 1000;
- // Phase start should happen at after cus product starts at
- return finalPhaseStart >= oneDayBeforeCusProductStartsAt;
+ // Phase start should happen at after cus product starts at
+ return finalPhaseStart >= oneDayBeforeCusProductStartsAt;
};
export const similarUnix = ({
- unix1,
- unix2,
+ unix1,
+ unix2,
}: {
- unix1: number;
- unix2: number;
+ unix1: number;
+ unix2: number;
}) => {
- return Math.abs(differenceInDays(unix1, unix2)) <= 1;
+ return Math.abs(differenceInDays(unix1, unix2)) <= 1;
};
// Price quantity pair
export const logPhaseItems = async ({
- items,
- db,
+ items,
+ db,
}: {
- items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
- db: DrizzleCli;
+ items: Stripe.SubscriptionScheduleUpdateParams.Phase.Item[];
+ db: DrizzleCli;
}) => {
- const priceIds = items
- .map((item) => {
- if (typeof item.price === "string") return item.price;
+ const priceIds = items
+ .map((item) => {
+ if (typeof item.price === "string") return item.price;
- return (item.price as unknown as Stripe.Price)?.id;
- })
- .filter(notNullish);
+ return (item.price as unknown as Stripe.Price)?.id;
+ })
+ .filter(notNullish);
- const autumnPrices = await PriceService.getByStripeIds({
- db,
- stripePriceIds: priceIds,
- });
- for (const item of items) {
- const priceId =
- typeof item.price === "string" ? item.price : (item.price as any)?.id;
- console.log({
- price: priceId,
- quantity: item.quantity,
- autumnPrice: autumnPrices[priceId]
- ? `${autumnPrices[priceId]?.product.name} - ${formatPrice({ price: autumnPrices[priceId] })}`
- : "N/A",
- });
- }
+ const autumnPrices = await PriceService.getByStripeIds({
+ db,
+ stripePriceIds: priceIds,
+ });
+ for (const item of items) {
+ const priceId =
+ typeof item.price === "string" ? item.price : (item.price as any)?.id;
+ console.log({
+ price: priceId,
+ quantity: item.quantity,
+ autumnPrice: autumnPrices[priceId]
+ ? `${autumnPrices[priceId]?.product.name} - ${formatPrice({ price: autumnPrices[priceId] })}`
+ : "N/A",
+ });
+ }
};
export const getCurrentPhaseIndex = ({
- schedule,
- now,
+ schedule,
+ now,
}: {
- schedule: Stripe.SubscriptionSchedule;
- now?: number;
+ schedule: Stripe.SubscriptionSchedule;
+ now?: number;
}) => {
- return schedule.phases.findIndex(
- (phase) =>
- (now || Date.now()) / 1000 >= phase.start_date &&
- (now || Date.now()) / 1000 < phase.end_date
- );
+ return schedule.phases.findIndex(
+ (phase) =>
+ (now || Date.now()) / 1000 >= phase.start_date &&
+ (now || Date.now()) / 1000 < phase.end_date,
+ );
};
// Helper function to convert timestamp to milliseconds if needed
const ensureMilliseconds = (timestamp: number | undefined): number => {
- if (!timestamp) return 0;
+ if (!timestamp) return 0;
- // If timestamp is less than year 2001 in milliseconds (978307200000),
- // it's likely in seconds and needs conversion
- // This threshold works because modern timestamps in seconds are much larger
- if (timestamp < 978307200000) {
- return timestamp * 1000;
- }
+ // If timestamp is less than year 2001 in milliseconds (978307200000),
+ // it's likely in seconds and needs conversion
+ // This threshold works because modern timestamps in seconds are much larger
+ if (timestamp < 978307200000) {
+ return timestamp * 1000;
+ }
- return timestamp;
+ return timestamp;
};
export const logPhases = async ({
- phases,
- db,
+ phases,
+ db,
}: {
- phases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
- db: DrizzleCli;
+ phases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
+ db: DrizzleCli;
}) => {
- for (const phase of phases) {
- // @ts-ignore
- const timestampInMillis = ensureMilliseconds(phase.start_date);
- console.log(`Phase ${formatUnixToDate(timestampInMillis)}:`);
- await logPhaseItems({ items: phase.items, db });
- }
+ for (const phase of phases) {
+ // @ts-ignore
+ const timestampInMillis = ensureMilliseconds(phase.start_date);
+ console.log(`Phase ${formatUnixToDate(timestampInMillis)}:`);
+ await logPhaseItems({ items: phase.items, db });
+ }
};
diff --git a/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts b/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts
index d84784aed..680c55522 100644
--- a/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts
+++ b/server/src/internal/customers/attach/mergeUtils/phaseUtils/upsertNewPhase.ts
@@ -2,73 +2,73 @@ import { differenceInDays } from "date-fns";
import Stripe from "stripe";
export const phaseAndUnixMatch = ({
- phase,
- unix,
+ phase,
+ unix,
}: {
- phase: Stripe.SubscriptionSchedule.Phase;
- unix: number; // in seconds
+ phase: Stripe.SubscriptionSchedule.Phase;
+ unix: number; // in seconds
}) => {
- const startDateSec = phase.start_date; // seconds
- // Convert seconds to milliseconds for date-fns and compare absolute difference within 1 day
- return (
- Math.abs(differenceInDays(unix * 1000, (startDateSec || 0) * 1000)) <= 1
- );
+ const startDateSec = phase.start_date; // seconds
+ // Convert seconds to milliseconds for date-fns and compare absolute difference within 1 day
+ return (
+ Math.abs(differenceInDays(unix * 1000, (startDateSec || 0) * 1000)) <= 1
+ );
};
// Helper: build phases by inserting or replacing at the target index
export const preparePhasesForBillingPeriod = ({
- schedule,
- phaseIndex,
- billingPeriodEnd,
+ schedule,
+ phaseIndex,
+ billingPeriodEnd,
}: {
- schedule: Stripe.SubscriptionSchedule;
- phaseIndex: number;
- billingPeriodEnd: number;
+ schedule: Stripe.SubscriptionSchedule;
+ phaseIndex: number;
+ billingPeriodEnd: number;
}): {
- phases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
- insertIndex: number;
- shouldInsert: boolean;
- originalIndexFor: (i: number) => number;
+ phases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
+ insertIndex: number;
+ shouldInsert: boolean;
+ originalIndexFor: (i: number) => number;
} => {
- const phases: Stripe.SubscriptionScheduleUpdateParams.Phase[] =
- schedule.phases.map((phase) => ({
- items: phase.items.map((item) => ({
- price: (item.price as Stripe.Price).id,
- quantity: item.quantity,
- })),
- start_date: phase.start_date,
- end_date: phase.end_date,
- trial_end: phase.trial_end || undefined,
- }));
+ const phases: Stripe.SubscriptionScheduleUpdateParams.Phase[] =
+ schedule.phases.map((phase) => ({
+ items: phase.items.map((item) => ({
+ price: (item.price as Stripe.Price).id,
+ quantity: item.quantity,
+ })),
+ start_date: phase.start_date,
+ end_date: phase.end_date,
+ trial_end: phase.trial_end || undefined,
+ }));
- const shouldInsert = !phaseAndUnixMatch({
- phase: schedule.phases[phaseIndex],
- unix: billingPeriodEnd,
- });
+ const shouldInsert = !phaseAndUnixMatch({
+ phase: schedule.phases[phaseIndex],
+ unix: billingPeriodEnd,
+ });
- const insertIndex = phaseIndex;
- const phaseToCopyIndex = Math.max(0, insertIndex - 1);
+ const insertIndex = phaseIndex;
+ const phaseToCopyIndex = Math.max(0, insertIndex - 1);
- if (shouldInsert) {
- const copiedPhase = structuredClone(phases[phaseToCopyIndex]);
- copiedPhase.start_date = billingPeriodEnd;
- const nextPhaseOriginalStart = phases[insertIndex]?.start_date;
- if (typeof nextPhaseOriginalStart === "number") {
- copiedPhase.end_date = nextPhaseOriginalStart;
- }
- phases.splice(insertIndex, 0, copiedPhase);
- if (insertIndex - 1 >= 0) {
- phases[insertIndex - 1].end_date = billingPeriodEnd;
- }
- }
+ if (shouldInsert) {
+ const copiedPhase = structuredClone(phases[phaseToCopyIndex]);
+ copiedPhase.start_date = billingPeriodEnd;
+ const nextPhaseOriginalStart = phases[insertIndex]?.start_date;
+ if (typeof nextPhaseOriginalStart === "number") {
+ copiedPhase.end_date = nextPhaseOriginalStart;
+ }
+ phases.splice(insertIndex, 0, copiedPhase);
+ if (insertIndex - 1 >= 0) {
+ phases[insertIndex - 1].end_date = billingPeriodEnd;
+ }
+ }
- const originalIndexFor = (i: number) => {
- if (shouldInsert) {
- if (i === insertIndex) return phaseToCopyIndex;
- return Math.min(i - 1, schedule.phases.length - 1);
- }
- return Math.min(i, schedule.phases.length - 1);
- };
+ const originalIndexFor = (i: number) => {
+ if (shouldInsert) {
+ if (i === insertIndex) return phaseToCopyIndex;
+ return Math.min(i - 1, schedule.phases.length - 1);
+ }
+ return Math.min(i, schedule.phases.length - 1);
+ };
- return { phases, insertIndex, shouldInsert, originalIndexFor };
+ return { phases, insertIndex, shouldInsert, originalIndexFor };
};
diff --git a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts b/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts
index 5eed7e262..c57b42584 100644
--- a/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts
+++ b/server/src/internal/customers/attach/mergeUtils/subToNewSchedule.ts
@@ -10,103 +10,103 @@ import { logPhases } from "./phaseUtils/phaseUtils.js";
import { getCusProductsToRemove } from "./paramsToSubItems.js";
export const subToNewSchedule = async ({
- req,
- sub,
- attachParams,
- config,
- endOfBillingPeriod,
- removeCusProducts,
+ req,
+ sub,
+ attachParams,
+ config,
+ endOfBillingPeriod,
+ removeCusProducts,
}: {
- req: ExtendedRequest;
- sub: Stripe.Subscription;
- attachParams: AttachParams;
- config: AttachConfig;
- endOfBillingPeriod: number;
- removeCusProducts?: FullCusProduct[];
+ req: ExtendedRequest;
+ sub: Stripe.Subscription;
+ attachParams: AttachParams;
+ config: AttachConfig;
+ endOfBillingPeriod: number;
+ removeCusProducts?: FullCusProduct[];
}) => {
- const { logger } = req;
- const itemSet = await getStripeSubItems2({
- attachParams,
- config,
- });
+ const { logger } = req;
+ 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(", ")}`
- ); // See mergedDowngrade5.test.ts -- if there's no schedule and creating one, might need to include canceled cus products
+ let cusProductsToRemove: FullCusProduct[] = [];
+ cusProductsToRemove = getCusProductsToRemove({
+ attachParams,
+ includeCanceled: true,
+ });
+ console.log(
+ `REMOVING CUS PRODUCTS: ${cusProductsToRemove.map((cp) => `${cp.product.id} (E: ${cp.entity_id})`).join(", ")}`,
+ ); // See mergedDowngrade5.test.ts -- if there's no schedule and creating one, might need to include canceled cus products
- const res = await paramsToScheduleItems({
- req,
- sub,
- attachParams,
- config,
- removeCusProducts: removeCusProducts || cusProductsToRemove,
- billingPeriodEnd: endOfBillingPeriod,
- });
+ const res = await paramsToScheduleItems({
+ req,
+ sub,
+ attachParams,
+ config,
+ removeCusProducts: removeCusProducts || cusProductsToRemove,
+ billingPeriodEnd: endOfBillingPeriod,
+ });
- const { stripeCli } = attachParams;
- let newSchedule: Stripe.SubscriptionSchedule | undefined = undefined;
+ const { stripeCli } = attachParams;
+ let newSchedule: Stripe.SubscriptionSchedule | undefined = undefined;
- // if (sub.cancel_at) {
- // logger.info(`UNCANCELING SUB ${sub.id}`);
- // await stripeCli.subscriptions.update(sub.id, {
- // cancel_at: null,
- // });
- // }
+ // 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");
+ // 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;
- const curSubItems = sub.items.data;
+ if (res.phases[0].items.length > 0) {
+ itemSet.subItems = res.phases[0].items;
+ const curSubItems = sub.items.data;
- // Create schedule from existing subscription
- newSchedule = await stripeCli.subscriptionSchedules.create({
- from_subscription: sub.id,
- });
+ // Create schedule from existing subscription
+ newSchedule = await stripeCli.subscriptionSchedules.create({
+ from_subscription: sub.id,
+ });
- // console.log("SChedule ID: ", newSchedule.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) => ({
- price: item.price as string,
- quantity: item.quantity,
- })),
- 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",
- });
+ // const newScheduleId = "sub_sched_1RxyM89mx3u0jkgOgbbsAbDS";
+ const newScheduleId = newSchedule.id;
+ await stripeCli.subscriptionSchedules.update(newScheduleId, {
+ phases: [
+ {
+ items: newSchedule.phases[0].items.map((item) => ({
+ price: item.price as string,
+ quantity: item.quantity,
+ })),
+ 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: req.db,
- stripeSubId: sub.id!,
- updates: {
- scheduled_ids: [newSchedule!.id],
- },
- });
- }
+ await CusProductService.updateByStripeSubId({
+ db: req.db,
+ stripeSubId: sub.id!,
+ updates: {
+ scheduled_ids: [newSchedule!.id],
+ },
+ });
+ }
- return newSchedule as Stripe.SubscriptionSchedule;
+ return newSchedule as Stripe.SubscriptionSchedule;
};
// phases: ([
diff --git a/server/src/internal/customers/attach/mergeUtils/updateCurSchedule.ts b/server/src/internal/customers/attach/mergeUtils/updateCurSchedule.ts
index c05d691d2..dbe0da6ac 100644
--- a/server/src/internal/customers/attach/mergeUtils/updateCurSchedule.ts
+++ b/server/src/internal/customers/attach/mergeUtils/updateCurSchedule.ts
@@ -5,29 +5,29 @@ import { CusProductService } from "../../cusProducts/CusProductService.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
export const updateCurSchedule = async ({
- req,
- attachParams,
- schedule,
- sub,
- newPhases,
+ req,
+ attachParams,
+ schedule,
+ sub,
+ newPhases,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- schedule: Stripe.SubscriptionSchedule;
- sub: Stripe.Subscription;
- newPhases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ schedule: Stripe.SubscriptionSchedule;
+ sub: Stripe.Subscription;
+ newPhases: Stripe.SubscriptionScheduleUpdateParams.Phase[];
}) => {
- const { stripeCli } = attachParams;
+ const { stripeCli } = attachParams;
- if (sub.cancel_at) {
- await stripeCli.subscriptions.update(sub.id, {
- cancel_at: null,
- });
- }
+ if (sub.cancel_at) {
+ await stripeCli.subscriptions.update(sub.id, {
+ cancel_at: null,
+ });
+ }
- await stripeCli.subscriptionSchedules.update(schedule.id, {
- phases: newPhases,
- });
+ await stripeCli.subscriptionSchedules.update(schedule.id, {
+ phases: newPhases,
+ });
- return schedule;
+ return schedule;
};
diff --git a/server/src/internal/customers/attach/models/AttachBranch.ts b/server/src/internal/customers/attach/models/AttachBranch.ts
index 60c616f81..33110c097 100644
--- a/server/src/internal/customers/attach/models/AttachBranch.ts
+++ b/server/src/internal/customers/attach/models/AttachBranch.ts
@@ -21,13 +21,13 @@
// }
export enum AttachFunction {
- CreateCheckout = "create_checkout",
- AddProduct = "add_product",
- UpdateEnts = "update_ents", // only update entitlements
- UpdateProduct = "update_product", // update product
- ScheduleProduct = "schedule_product",
- UpdatePrepaidQuantity = "update_prepaid_quantity",
- Renew = "renew",
+ CreateCheckout = "create_checkout",
+ AddProduct = "add_product",
+ UpdateEnts = "update_ents", // only update entitlements
+ UpdateProduct = "update_product", // update product
+ ScheduleProduct = "schedule_product",
+ UpdatePrepaidQuantity = "update_prepaid_quantity",
+ Renew = "renew",
}
/* Handle checkout / public error:
diff --git a/server/src/internal/customers/attach/models/AttachFlags.ts b/server/src/internal/customers/attach/models/AttachFlags.ts
index 3cee9de1a..be175d18d 100644
--- a/server/src/internal/customers/attach/models/AttachFlags.ts
+++ b/server/src/internal/customers/attach/models/AttachFlags.ts
@@ -1,23 +1,23 @@
import { AttachBranch } from "@autumn/shared";
export enum ProrationBehavior {
- Immediately = "immediately",
- NextBilling = "next_billing",
- None = "none",
+ Immediately = "immediately",
+ NextBilling = "next_billing",
+ None = "none",
}
export interface AttachFlags {
- isPublic: boolean;
- forceCheckout: boolean;
- noPaymentMethod: boolean;
- invoiceOnly: boolean;
- isFree: boolean;
+ isPublic: boolean;
+ forceCheckout: boolean;
+ noPaymentMethod: boolean;
+ invoiceOnly: boolean;
+ isFree: boolean;
}
export interface AttachConfig {
- onlyCheckout: boolean;
- carryUsage: boolean; // Whether to carry over existing usages
- branch: AttachBranch;
- proration: ProrationBehavior;
- disableTrial: boolean;
+ onlyCheckout: boolean;
+ carryUsage: boolean; // Whether to carry over existing usages
+ branch: AttachBranch;
+ proration: ProrationBehavior;
+ disableTrial: boolean;
}
diff --git a/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts b/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts
index 0c4b6d67f..f04202d95 100644
--- a/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts
+++ b/server/src/internal/customers/attach/unifiedAttach/handleUnifiedAttach.ts
@@ -2,27 +2,27 @@ import { AttachConfig, CusProductStatus } from "@autumn/shared";
import { AttachParams } from "../../cusProducts/AttachParams.js";
export const handleUnifiedAttach = async ({
- req,
- res,
- attachParams,
- config,
+ req,
+ res,
+ attachParams,
+ config,
}: {
- req: any;
- res: any;
- attachParams: AttachParams;
- config: AttachConfig;
+ req: any;
+ res: any;
+ attachParams: AttachParams;
+ config: AttachConfig;
}) => {
- // 1.
- const cusProducts = attachParams.customer.customer_products;
+ // 1.
+ const cusProducts = attachParams.customer.customer_products;
- const scheduledCusProducts = cusProducts.filter(
- (cp) => cp.status === CusProductStatus.Scheduled
- );
+ const scheduledCusProducts = cusProducts.filter(
+ (cp) => cp.status === CusProductStatus.Scheduled,
+ );
- res.status(200).json({
- success: true,
- message: "Unified attach",
- });
+ res.status(200).json({
+ success: true,
+ message: "Unified attach",
+ });
- return;
+ return;
};
diff --git a/server/src/internal/customers/cancel/cancelEndOfCycle.ts b/server/src/internal/customers/cancel/cancelEndOfCycle.ts
index a54b26c99..65bdef876 100644
--- a/server/src/internal/customers/cancel/cancelEndOfCycle.ts
+++ b/server/src/internal/customers/cancel/cancelEndOfCycle.ts
@@ -6,37 +6,37 @@ import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubU
import { CusProductService } from "../cusProducts/CusProductService.js";
export const cancelEndOfCycle = async ({
- req,
- cusProduct,
- fullCus,
+ req,
+ cusProduct,
+ fullCus,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
}) => {
- const { db, org, env, logger } = req;
- const stripeCli = createStripeCli({ org, env });
+ const { db, org, env, logger } = req;
+ const stripeCli = createStripeCli({ org, env });
- const sub = await cusProductToSub({ cusProduct, stripeCli });
- if (sub) {
- const latestPeriodEnd = getLatestPeriodEnd({ sub });
- await stripeCli.subscriptions.update(sub.id, {
- cancel_at: latestPeriodEnd,
- });
+ const sub = await cusProductToSub({ cusProduct, stripeCli });
+ if (sub) {
+ const latestPeriodEnd = getLatestPeriodEnd({ sub });
+ await stripeCli.subscriptions.update(sub.id, {
+ cancel_at: latestPeriodEnd,
+ });
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: { canceled_at: Date.now() },
- });
- } else {
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- ended_at: Date.now(),
- },
- });
- }
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: { canceled_at: Date.now() },
+ });
+ } else {
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ ended_at: Date.now(),
+ },
+ });
+ }
};
diff --git a/server/src/internal/customers/cancel/cancelImmediately.ts b/server/src/internal/customers/cancel/cancelImmediately.ts
index 3ec076e44..69b9550c0 100644
--- a/server/src/internal/customers/cancel/cancelImmediately.ts
+++ b/server/src/internal/customers/cancel/cancelImmediately.ts
@@ -1,10 +1,10 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- AttachScenario,
- CusProductStatus,
- FullCusProduct,
- FullCustomer,
+ AttachScenario,
+ CusProductStatus,
+ FullCusProduct,
+ FullCustomer,
} from "@autumn/shared";
import { cusProductToProduct } from "@autumn/shared";
@@ -17,73 +17,73 @@ import { isOneOff } from "@/internal/products/productUtils.js";
import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const cancelImmediately = async ({
- req,
- cusProduct,
- fullCus,
- prorate,
+ req,
+ cusProduct,
+ fullCus,
+ prorate,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
- prorate: boolean;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
+ prorate: boolean;
}) => {
- const { db, org, env, logger } = req;
- const stripeCli = createStripeCli({ org, env });
+ const { db, org, env, logger } = req;
+ const stripeCli = createStripeCli({ org, env });
- const { curScheduledProduct } = getExistingCusProducts({
- product: cusProduct.product,
- cusProducts: fullCus.customer_products,
- internalEntityId: cusProduct.internal_entity_id,
- });
+ const { curScheduledProduct } = getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts: fullCus.customer_products,
+ internalEntityId: cusProduct.internal_entity_id,
+ });
- const sub = await cusProductToSub({ cusProduct, stripeCli });
+ const sub = await cusProductToSub({ cusProduct, stripeCli });
- if (sub) {
- await stripeCli.subscriptions.cancel(sub.id, {
- prorate: prorate,
- cancellation_details: {
- comment: "autumn_cancel",
- },
- });
- }
+ if (sub) {
+ await stripeCli.subscriptions.cancel(sub.id, {
+ prorate: prorate,
+ cancellation_details: {
+ comment: "autumn_cancel",
+ },
+ });
+ }
- const isMain = !cusProduct.product.is_add_on;
- const product = cusProductToProduct({ cusProduct });
+ const isMain = !cusProduct.product.is_add_on;
+ const product = cusProductToProduct({ cusProduct });
- if (isMain && !isOneOff(product.prices)) {
- // So it doesn't duplicate
- if (curScheduledProduct) {
- await CusProductService.delete({
- db,
- cusProductId: curScheduledProduct.id,
- });
- }
+ if (isMain && !isOneOff(product.prices)) {
+ // So it doesn't duplicate
+ if (curScheduledProduct) {
+ await CusProductService.delete({
+ db,
+ cusProductId: curScheduledProduct.id,
+ });
+ }
- await activateDefaultProduct({
- req,
- productGroup: cusProduct.product.group,
- fullCus,
- });
- }
+ await activateDefaultProduct({
+ req,
+ productGroup: cusProduct.product.group,
+ fullCus,
+ });
+ }
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- ended_at: Date.now(),
- },
- });
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ ended_at: Date.now(),
+ },
+ });
- console.log("Sending webhook for expired product");
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: fullCus.internal_id,
- org,
- env,
- customerId: fullCus.id || null,
- cusProduct,
- scenario: AttachScenario.Expired,
- logger,
- });
+ console.log("Sending webhook for expired product");
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: fullCus.internal_id,
+ org,
+ env,
+ customerId: fullCus.id || null,
+ cusProduct,
+ scenario: AttachScenario.Expired,
+ logger,
+ });
};
diff --git a/server/src/internal/customers/cancel/cancelRouter.ts b/server/src/internal/customers/cancel/cancelRouter.ts
index 59e47d6a5..4cd140a3e 100644
--- a/server/src/internal/customers/cancel/cancelRouter.ts
+++ b/server/src/internal/customers/cancel/cancelRouter.ts
@@ -11,82 +11,82 @@ import { handleCancelProduct } from "./handleCancelProduct.js";
const cancelRouter: Router = Router();
cancelRouter.post("", async (req, res) =>
- routeHandler({
- req,
- res,
- action: "expire",
- handler: async (req, res) => {
- let { db, orgId, env, logtail: logger } = req;
- let {
- customer_id,
- product_id,
- entity_id,
- cancel_immediately,
- prorate: bodyProrate,
- } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "expire",
+ handler: async (req, res) => {
+ let { db, orgId, env, logtail: logger } = req;
+ let {
+ customer_id,
+ product_id,
+ entity_id,
+ cancel_immediately,
+ prorate: bodyProrate,
+ } = req.body;
- let expireImmediately = cancel_immediately || false;
- let prorate = notNullish(bodyProrate) ? bodyProrate : true;
+ let expireImmediately = cancel_immediately || false;
+ let prorate = notNullish(bodyProrate) ? bodyProrate : true;
- let fullCus = await CusService.getFull({
- db,
- orgId,
- idOrInternalId: customer_id,
- env,
- withEntities: true,
- entityId: entity_id,
- inStatuses: RELEVANT_STATUSES,
- allowNotFound: false,
- });
+ let fullCus = await CusService.getFull({
+ db,
+ orgId,
+ idOrInternalId: customer_id,
+ env,
+ withEntities: true,
+ entityId: entity_id,
+ inStatuses: RELEVANT_STATUSES,
+ allowNotFound: false,
+ });
- if (entity_id && !fullCus.entity) {
- throw new RecaseError({
- code: ErrCode.EntityNotFound,
- message: `Entity ${entity_id} not found for customer ${customer_id}`,
- });
- }
+ if (entity_id && !fullCus.entity) {
+ throw new RecaseError({
+ code: ErrCode.EntityNotFound,
+ message: `Entity ${entity_id} not found for customer ${customer_id}`,
+ });
+ }
- let cusProducts = fullCus.customer_products;
- let entity = fullCus.entity;
+ let cusProducts = fullCus.customer_products;
+ let entity = fullCus.entity;
- let cusProduct = cusProducts.find(
- (cusProduct: FullCusProduct) =>
- cusProduct.product.id == product_id &&
- (entity
- ? cusProduct.internal_entity_id == entity.internal_id
- : nullish(cusProduct.internal_entity_id))
- );
+ let cusProduct = cusProducts.find(
+ (cusProduct: FullCusProduct) =>
+ cusProduct.product.id == product_id &&
+ (entity
+ ? cusProduct.internal_entity_id == entity.internal_id
+ : nullish(cusProduct.internal_entity_id)),
+ );
- if (!cusProduct) {
- throw new RecaseError({
- code: ErrCode.ProductNotFound,
- message: `Product ${product_id} not found for customer ${customer_id}`,
- });
- }
+ if (!cusProduct) {
+ throw new RecaseError({
+ code: ErrCode.ProductNotFound,
+ message: `Product ${product_id} not found for customer ${customer_id}`,
+ });
+ }
- // await expireCusProduct({
- // req,
- // cusProduct,
- // fullCus,
- // expireImmediately,
- // prorate,
- // });
+ // await expireCusProduct({
+ // req,
+ // cusProduct,
+ // fullCus,
+ // expireImmediately,
+ // prorate,
+ // });
- await handleCancelProduct({
- req,
- cusProduct,
- fullCus,
- expireImmediately,
- prorate,
- });
+ await handleCancelProduct({
+ req,
+ cusProduct,
+ fullCus,
+ expireImmediately,
+ prorate,
+ });
- res.status(200).json({
- success: true,
- customer_id: customer_id,
- product_id: product_id,
- });
- },
- })
+ res.status(200).json({
+ success: true,
+ customer_id: customer_id,
+ product_id: product_id,
+ });
+ },
+ }),
);
export default cancelRouter;
diff --git a/server/src/internal/customers/cancel/cancelScheduledProduct.ts b/server/src/internal/customers/cancel/cancelScheduledProduct.ts
index 0e265b10f..b1f3368e7 100644
--- a/server/src/internal/customers/cancel/cancelScheduledProduct.ts
+++ b/server/src/internal/customers/cancel/cancelScheduledProduct.ts
@@ -5,53 +5,53 @@ import { CusProductService } from "../cusProducts/CusProductService.js";
import { cusProductToSchedule } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const cancelScheduledProduct = async ({
- req,
- curScheduledProduct,
- fullCus,
- curMainProduct,
+ req,
+ curScheduledProduct,
+ fullCus,
+ curMainProduct,
}: {
- req: ExtendedRequest;
- curScheduledProduct?: FullCusProduct;
- fullCus: FullCustomer;
- curMainProduct?: FullCusProduct;
+ req: ExtendedRequest;
+ curScheduledProduct?: FullCusProduct;
+ fullCus: FullCustomer;
+ curMainProduct?: FullCusProduct;
}) => {
- const { org, env, db, logger } = req;
- const stripeCli = createStripeCli({ org, env });
+ const { org, env, db, logger } = req;
+ const stripeCli = createStripeCli({ org, env });
- // 1. Delete subscription schedule if exists
- if (curScheduledProduct) {
- const schedule = await cusProductToSchedule({
- cusProduct: curScheduledProduct,
- stripeCli,
- });
+ // 1. Delete subscription schedule if exists
+ if (curScheduledProduct) {
+ const schedule = await cusProductToSchedule({
+ cusProduct: curScheduledProduct,
+ stripeCli,
+ });
- if (schedule) {
- await stripeCli.subscriptionSchedules.cancel(schedule.id);
- }
+ if (schedule) {
+ await stripeCli.subscriptionSchedules.cancel(schedule.id);
+ }
- logger.info(`Deleting scheduled prod (${curScheduledProduct.product.id})`);
- await CusProductService.delete({
- db,
- cusProductId: curScheduledProduct.id,
- });
- }
+ logger.info(`Deleting scheduled prod (${curScheduledProduct.product.id})`);
+ await CusProductService.delete({
+ db,
+ cusProductId: curScheduledProduct.id,
+ });
+ }
- // 2. Uncancel current main product
- const subId = curMainProduct?.subscription_ids?.[0];
- if (subId) {
- await stripeCli.subscriptions.update(subId, { cancel_at: null });
- }
+ // 2. Uncancel current main product
+ const subId = curMainProduct?.subscription_ids?.[0];
+ if (subId) {
+ await stripeCli.subscriptions.update(subId, { cancel_at: null });
+ }
- if (curMainProduct) {
- logger.info(`Updating main prod (${curMainProduct!.product.id}) to active`);
- logger.info(`Cus product ID: ${curMainProduct!.id}`);
- await CusProductService.update({
- db,
- cusProductId: curMainProduct!.id,
- updates: {
- status: CusProductStatus.Active,
- canceled_at: null,
- },
- });
- }
+ if (curMainProduct) {
+ logger.info(`Updating main prod (${curMainProduct!.product.id}) to active`);
+ logger.info(`Cus product ID: ${curMainProduct!.id}`);
+ await CusProductService.update({
+ db,
+ cusProductId: curMainProduct!.id,
+ updates: {
+ status: CusProductStatus.Active,
+ canceled_at: null,
+ },
+ });
+ }
};
diff --git a/server/src/internal/customers/cancel/handleCancelProduct.ts b/server/src/internal/customers/cancel/handleCancelProduct.ts
index 39d9673b5..3485b5da6 100644
--- a/server/src/internal/customers/cancel/handleCancelProduct.ts
+++ b/server/src/internal/customers/cancel/handleCancelProduct.ts
@@ -1,13 +1,13 @@
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- CusProductStatus,
- ErrCode,
- FullCusProduct,
- FullCustomer,
- ProrationBehavior,
- AttachBranch,
- Price,
- EntitlementWithFeature,
+ CusProductStatus,
+ ErrCode,
+ FullCusProduct,
+ FullCustomer,
+ ProrationBehavior,
+ AttachBranch,
+ Price,
+ EntitlementWithFeature,
} from "@autumn/shared";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { cusProductToPrices, cusProductToProduct } from "@autumn/shared";
@@ -23,211 +23,211 @@ import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js
import { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js";
import { handleUpgradeFlow } from "../attach/attachFunctions/upgradeFlow/handleUpgradeFlow.js";
import {
- activateDefaultProduct,
- getDefaultProduct,
+ activateDefaultProduct,
+ getDefaultProduct,
} from "../cusProducts/cusProductUtils.js";
export const handleCancelProduct = async ({
- req,
- cusProduct, // cus product to expire
- fullCus,
- expireImmediately = true,
- prorate,
+ req,
+ cusProduct, // cus product to expire
+ fullCus,
+ expireImmediately = true,
+ prorate,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
- expireImmediately: boolean;
- prorate: boolean;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
+ expireImmediately: boolean;
+ prorate: boolean;
}) => {
- const { db, org, env, logger } = req;
- logger.info("--------------------------------");
- logger.info(
- `🔔 Expiring cutomer product (${
- expireImmediately ? "immediately" : "end of cycle"
- })`
- );
- logger.info(
- `Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`
- );
- logger.info(
- `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`
- );
+ const { db, org, env, logger } = req;
+ logger.info("--------------------------------");
+ logger.info(
+ `🔔 Expiring cutomer product (${
+ expireImmediately ? "immediately" : "end of cycle"
+ })`,
+ );
+ logger.info(
+ `Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`,
+ );
+ logger.info(
+ `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`,
+ );
- const { curMainProduct, curSameProduct, curScheduledProduct } =
- getExistingCusProducts({
- product: cusProductToProduct({ cusProduct }),
- cusProducts: fullCus.customer_products,
- internalEntityId: cusProduct.internal_entity_id,
- });
+ const { curMainProduct, curSameProduct, curScheduledProduct } =
+ getExistingCusProducts({
+ product: cusProductToProduct({ cusProduct }),
+ cusProducts: fullCus.customer_products,
+ internalEntityId: cusProduct.internal_entity_id,
+ });
- const stripeCli = createStripeCli({ org, env });
+ const stripeCli = createStripeCli({ org, env });
- // 1. Build attach params
- if (cusProduct.status == CusProductStatus.Scheduled) {
- // Equivalent to renewing product
- // await cancelScheduledProduct({
- // req,
- // curScheduledProduct,
- // fullCus,
- // curMainProduct,
- // });
- const { curMainProduct } = getExistingCusProducts({
- product: cusProduct.product,
- cusProducts: fullCus.customer_products,
- internalEntityId: cusProduct.internal_entity_id,
- });
- const product = cusProductToProduct({ cusProduct: curMainProduct! });
+ // 1. Build attach params
+ if (cusProduct.status == CusProductStatus.Scheduled) {
+ // Equivalent to renewing product
+ // await cancelScheduledProduct({
+ // req,
+ // curScheduledProduct,
+ // fullCus,
+ // curMainProduct,
+ // });
+ const { curMainProduct } = getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts: fullCus.customer_products,
+ internalEntityId: cusProduct.internal_entity_id,
+ });
+ const product = cusProductToProduct({ cusProduct: curMainProduct! });
- await handleRenewProduct({
- req,
- res: null,
- attachParams: {
- stripeCli,
- customer: fullCus,
- org,
- cusProducts: fullCus.customer_products,
- products: [product],
- internalEntityId: cusProduct.internal_entity_id || undefined,
- paymentMethod: null,
- prices: product.prices,
- entitlements: product.entitlements,
- freeTrial: product.free_trial || null,
- optionsList: curMainProduct?.options || [],
- replaceables: [],
- entities: fullCus.entities,
- features: req.features,
- },
- config: getDefaultAttachConfig(),
- });
- return;
- }
+ await handleRenewProduct({
+ req,
+ res: null,
+ attachParams: {
+ stripeCli,
+ customer: fullCus,
+ org,
+ cusProducts: fullCus.customer_products,
+ products: [product],
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ paymentMethod: null,
+ prices: product.prices,
+ entitlements: product.entitlements,
+ freeTrial: product.free_trial || null,
+ optionsList: curMainProduct?.options || [],
+ replaceables: [],
+ entities: fullCus.entities,
+ features: req.features,
+ },
+ config: getDefaultAttachConfig(),
+ });
+ return;
+ }
- // 2. If there's a scheduled product, throw error?
- const isMain = !cusProduct.product.is_add_on;
+ // 2. If there's a scheduled product, throw error?
+ const isMain = !cusProduct.product.is_add_on;
- if (isMain) {
- if (cusProduct.canceled && !expireImmediately) {
- throw new RecaseError({
- message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isMain) {
+ if (cusProduct.canceled && !expireImmediately) {
+ throw new RecaseError({
+ message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (
- curScheduledProduct &&
- !isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
- ) {
- throw new RecaseError({
- message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ if (
+ curScheduledProduct &&
+ !isFreeProduct(cusProductToPrices({ cusProduct: curScheduledProduct }))
+ ) {
+ throw new RecaseError({
+ message: `Please delete scheduled product ${curScheduledProduct.product.name} first`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- // 2. If expire at cycle end, just cancel subscriptions
- if (!expireImmediately) {
- const product = cusProductToProduct({ cusProduct });
- const defaultProduct = await getDefaultProduct({
- req,
- productGroup: product.group,
- });
+ // 2. If expire at cycle end, just cancel subscriptions
+ if (!expireImmediately) {
+ const product = cusProductToProduct({ cusProduct });
+ const defaultProduct = await getDefaultProduct({
+ req,
+ productGroup: product.group,
+ });
- let products = [product];
- let prices: Price[] = [];
- let entitlements: EntitlementWithFeature[] = [];
- let skipInsertCusProduct = true;
- if (
- !isFreeProduct(product.prices) &&
- !product.is_add_on &&
- defaultProduct
- ) {
- products = [defaultProduct];
- prices = defaultProduct.prices;
- entitlements = defaultProduct.entitlements;
- skipInsertCusProduct = false;
- }
+ let products = [product];
+ let prices: Price[] = [];
+ let entitlements: EntitlementWithFeature[] = [];
+ let skipInsertCusProduct = true;
+ if (
+ !isFreeProduct(product.prices) &&
+ !product.is_add_on &&
+ defaultProduct
+ ) {
+ products = [defaultProduct];
+ prices = defaultProduct.prices;
+ entitlements = defaultProduct.entitlements;
+ skipInsertCusProduct = false;
+ }
- await handleScheduleFunction2({
- req,
- res: null,
- attachParams: {
- stripeCli,
- customer: fullCus,
- org,
- cusProducts: fullCus.customer_products,
- products,
- internalEntityId: cusProduct.internal_entity_id || undefined,
- paymentMethod: null,
- prices,
- entitlements,
- freeTrial: null,
- optionsList: [],
- replaceables: [],
- entities: fullCus.entities,
- features: req.features,
- },
- config: getDefaultAttachConfig(),
- skipInsertCusProduct,
- });
+ await handleScheduleFunction2({
+ req,
+ res: null,
+ attachParams: {
+ stripeCli,
+ customer: fullCus,
+ org,
+ cusProducts: fullCus.customer_products,
+ products,
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ paymentMethod: null,
+ prices,
+ entitlements,
+ freeTrial: null,
+ optionsList: [],
+ replaceables: [],
+ entities: fullCus.entities,
+ features: req.features,
+ },
+ config: getDefaultAttachConfig(),
+ skipInsertCusProduct,
+ });
- // Schedule default product...
+ // Schedule default product...
- return;
- }
+ return;
+ }
- // Cancel product immediately
- const product = cusProductToProduct({ cusProduct });
- await handleUpgradeFlow({
- req,
- res: null,
- attachParams: {
- stripeCli,
- customer: fullCus,
- org,
- cusProduct,
- cusProducts: fullCus.customer_products,
- products: [],
- internalEntityId: cusProduct.internal_entity_id || undefined,
- paymentMethod: null,
- prices: [],
- entitlements: [],
- freeTrial: null,
- optionsList: [],
- replaceables: [],
- entities: fullCus.entities,
- features: req.features,
- fromCancel: true,
- },
- config: {
- ...getDefaultAttachConfig(),
- proration: prorate
- ? ProrationBehavior.Immediately
- : ProrationBehavior.None,
- },
- branch: AttachBranch.Cancel,
- });
+ // Cancel product immediately
+ const product = cusProductToProduct({ cusProduct });
+ await handleUpgradeFlow({
+ req,
+ res: null,
+ attachParams: {
+ stripeCli,
+ customer: fullCus,
+ org,
+ cusProduct,
+ cusProducts: fullCus.customer_products,
+ products: [],
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ paymentMethod: null,
+ prices: [],
+ entitlements: [],
+ freeTrial: null,
+ optionsList: [],
+ replaceables: [],
+ entities: fullCus.entities,
+ features: req.features,
+ fromCancel: true,
+ },
+ config: {
+ ...getDefaultAttachConfig(),
+ proration: prorate
+ ? ProrationBehavior.Immediately
+ : ProrationBehavior.None,
+ },
+ branch: AttachBranch.Cancel,
+ });
- // Activate default product
- if (!product.is_add_on && !isOneOff(product.prices)) {
- await activateDefaultProduct({
- req,
- productGroup: cusProduct.product.group,
- fullCus,
- curCusProduct: cusProduct,
- });
- }
- return;
+ // Activate default product
+ if (!product.is_add_on && !isOneOff(product.prices)) {
+ await activateDefaultProduct({
+ req,
+ productGroup: cusProduct.product.group,
+ fullCus,
+ curCusProduct: cusProduct,
+ });
+ }
+ return;
- // Expire product immediately
- await cancelImmediately({
- req,
- cusProduct,
- fullCus,
- prorate,
- });
+ // Expire product immediately
+ await cancelImmediately({
+ req,
+ cusProduct,
+ fullCus,
+ prorate,
+ });
};
// if (isOneOff(cusProduct.customer_prices.map((p) => p.price))) {
diff --git a/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts b/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts
index a91210a2f..08a1f3bb2 100644
--- a/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts
+++ b/server/src/internal/customers/change-product/handleDowngrade/cancelCurSubs.ts
@@ -1,8 +1,8 @@
import { getSubItemsForCusProduct } from "@/external/stripe/stripeSubUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import {
- priceToIntervalKey,
- toIntervalKey,
+ priceToIntervalKey,
+ toIntervalKey,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { notNullish } from "@/utils/genUtils.js";
import { FullCusProduct } from "@autumn/shared";
@@ -11,54 +11,54 @@ import { differenceInDays } from "date-fns";
import Stripe from "stripe";
export const cancelCurSubs = async ({
- curCusProduct,
- curSubs,
- stripeCli,
+ curCusProduct,
+ curSubs,
+ stripeCli,
}: {
- curCusProduct: FullCusProduct;
- curSubs: Stripe.Subscription[];
- stripeCli: Stripe;
+ curCusProduct: FullCusProduct;
+ curSubs: Stripe.Subscription[];
+ stripeCli: Stripe;
}) => {
- // let latestPeriodEnd = curSubs[0].current_period_end;
- // let intervalToOtherSubs: Record<
- // string,
- // {
- // otherSubItems: Stripe.SubscriptionItem[];
- // otherSub: Stripe.Subscription;
- // }
- // > = {};
- // for (const sub of curSubs) {
- // let latestEndDate = new Date(latestPeriodEnd * 1000);
- // let curEndDate = new Date(sub.current_period_end * 1000);
- // const { otherSubItems } = await getSubItemsForCusProduct({
- // stripeSub: sub,
- // cusProduct: curCusProduct,
- // });
- // // let interval = sub.items.data[0].price.recurring!.interval;
- // let subInterval = subToAutumnInterval(sub);
- // let intervalKey = toIntervalKey(subInterval);
- // intervalToOtherSubs[intervalKey] = {
- // otherSubItems,
- // otherSub: sub,
- // };
- // if (notNullish(sub.schedule)) {
- // await stripeCli.subscriptionSchedules.release(sub.schedule as string);
- // }
- // if (differenceInDays(latestEndDate, curEndDate) > 10) {
- // await stripeCli.subscriptions.update(sub.id, {
- // cancel_at: latestPeriodEnd,
- // cancellation_details: {
- // comment: "autumn_downgrade",
- // },
- // });
- // } else {
- // await stripeCli.subscriptions.update(sub.id, {
- // cancel_at_period_end: true,
- // cancellation_details: {
- // comment: "autumn_downgrade",
- // },
- // });
- // }
- // }
- // return intervalToOtherSubs;
+ // let latestPeriodEnd = curSubs[0].current_period_end;
+ // let intervalToOtherSubs: Record<
+ // string,
+ // {
+ // otherSubItems: Stripe.SubscriptionItem[];
+ // otherSub: Stripe.Subscription;
+ // }
+ // > = {};
+ // for (const sub of curSubs) {
+ // let latestEndDate = new Date(latestPeriodEnd * 1000);
+ // let curEndDate = new Date(sub.current_period_end * 1000);
+ // const { otherSubItems } = await getSubItemsForCusProduct({
+ // stripeSub: sub,
+ // cusProduct: curCusProduct,
+ // });
+ // // let interval = sub.items.data[0].price.recurring!.interval;
+ // let subInterval = subToAutumnInterval(sub);
+ // let intervalKey = toIntervalKey(subInterval);
+ // intervalToOtherSubs[intervalKey] = {
+ // otherSubItems,
+ // otherSub: sub,
+ // };
+ // if (notNullish(sub.schedule)) {
+ // await stripeCli.subscriptionSchedules.release(sub.schedule as string);
+ // }
+ // if (differenceInDays(latestEndDate, curEndDate) > 10) {
+ // await stripeCli.subscriptions.update(sub.id, {
+ // cancel_at: latestPeriodEnd,
+ // cancellation_details: {
+ // comment: "autumn_downgrade",
+ // },
+ // });
+ // } else {
+ // await stripeCli.subscriptions.update(sub.id, {
+ // cancel_at_period_end: true,
+ // cancellation_details: {
+ // comment: "autumn_downgrade",
+ // },
+ // });
+ // }
+ // }
+ // return intervalToOtherSubs;
};
diff --git a/server/src/internal/customers/change-product/scheduleUtils.ts b/server/src/internal/customers/change-product/scheduleUtils.ts
index 630bc8cfb..91083d574 100644
--- a/server/src/internal/customers/change-product/scheduleUtils.ts
+++ b/server/src/internal/customers/change-product/scheduleUtils.ts
@@ -1,20 +1,20 @@
import {
- getStripeSchedules,
- getStripeSubs,
+ getStripeSchedules,
+ getStripeSubs,
} from "@/external/stripe/stripeSubUtils.js";
import {
- AppEnv,
- AttachScenario,
- FullCusProduct,
- intervalsSame,
- Organization,
- Product,
+ AppEnv,
+ AttachScenario,
+ FullCusProduct,
+ intervalsSame,
+ Organization,
+ Product,
} from "@autumn/shared";
import Stripe from "stripe";
import { fullCusProductToProduct } from "../cusProducts/cusProductUtils.js";
import {
- ACTIVE_STATUSES,
- CusProductService,
+ ACTIVE_STATUSES,
+ CusProductService,
} from "../cusProducts/CusProductService.js";
import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js";
@@ -23,8 +23,8 @@ import { isFreeProduct } from "@/internal/products/productUtils.js";
import { getFilteredScheduleItems } from "./scheduleUtils/getFilteredScheduleItems.js";
import { updateScheduledSubWithNewItems } from "./scheduleUtils/updateScheduleWithNewItems.js";
import {
- addCurMainProductToSchedule,
- getOtherCusProductsOnSub,
+ addCurMainProductToSchedule,
+ getOtherCusProductsOnSub,
} from "./scheduleUtils/cancelScheduledFreeProduct.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -34,26 +34,26 @@ import { notNullish } from "@/utils/genUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
export const getPricesForCusProduct = ({
- cusProduct,
+ cusProduct,
}: {
- cusProduct: FullCusProduct;
+ cusProduct: FullCusProduct;
}) => {
- if (!cusProduct) {
- return [];
- }
- return cusProduct.customer_prices.map((price) => price.price);
+ if (!cusProduct) {
+ return [];
+ }
+ return cusProduct.customer_prices.map((price) => price.price);
};
export const getScheduleIdsFromCusProducts = ({
- cusProducts,
+ cusProducts,
}: {
- cusProducts: (FullCusProduct | null | undefined)[];
+ cusProducts: (FullCusProduct | null | undefined)[];
}) => {
- let scheduleIds: string[] = [];
- for (const cusProduct of cusProducts) {
- if (cusProduct) {
- scheduleIds = scheduleIds.concat(cusProduct.scheduled_ids || []);
- }
- }
- return scheduleIds;
+ let scheduleIds: string[] = [];
+ for (const cusProduct of cusProducts) {
+ if (cusProduct) {
+ scheduleIds = scheduleIds.concat(cusProduct.scheduled_ids || []);
+ }
+ }
+ return scheduleIds;
};
diff --git a/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts b/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts
index 0dc863237..8f99ac016 100644
--- a/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts
+++ b/server/src/internal/customers/change-product/scheduleUtils/ScheduleObj.ts
@@ -2,8 +2,8 @@ import { BillingInterval } from "@autumn/shared";
import Stripe from "stripe";
export interface ScheduleObj {
- schedule: Stripe.SubscriptionSchedule;
- interval: BillingInterval;
- intervalCount: number;
- prices: Stripe.Price[];
+ schedule: Stripe.SubscriptionSchedule;
+ interval: BillingInterval;
+ intervalCount: number;
+ prices: Stripe.Price[];
}
diff --git a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts
index 97796c8f4..d5a9bbb1a 100644
--- a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts
+++ b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts
@@ -1,8 +1,8 @@
import {
- AppEnv,
- FullCusProduct,
- intervalsSame,
- Organization,
+ AppEnv,
+ FullCusProduct,
+ intervalsSame,
+ Organization,
} from "@autumn/shared";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
@@ -14,100 +14,100 @@ import { CusProductService } from "../../cusProducts/CusProductService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const getOtherCusProductsOnSub = async ({
- cusProducts,
- curMainProduct,
- curMainSubIds = [],
+ cusProducts,
+ curMainProduct,
+ curMainSubIds = [],
}: {
- cusProducts: FullCusProduct[];
- curMainProduct: FullCusProduct;
- curMainSubIds?: string[] | null;
+ cusProducts: FullCusProduct[];
+ curMainProduct: FullCusProduct;
+ curMainSubIds?: string[] | null;
}) => {
- let otherCusProductsWithSameSub: FullCusProduct[] = [];
- for (const cusProduct of cusProducts) {
- // 1. Check if contains one of the curMainSubIds
- if (
- cusProduct.id === curMainProduct.id ||
- !curMainSubIds?.some((subId) =>
- cusProduct?.subscription_ids?.includes(subId)
- )
- ) {
- continue;
- }
+ let otherCusProductsWithSameSub: FullCusProduct[] = [];
+ for (const cusProduct of cusProducts) {
+ // 1. Check if contains one of the curMainSubIds
+ if (
+ cusProduct.id === curMainProduct.id ||
+ !curMainSubIds?.some((subId) =>
+ cusProduct?.subscription_ids?.includes(subId),
+ )
+ ) {
+ continue;
+ }
- // 2. Check if there's a scheduled product
- let { curScheduledProduct: otherScheduledProduct } =
- await getExistingCusProducts({
- product: cusProduct.product,
- cusProducts: cusProducts,
- internalEntityId: cusProduct.internal_entity_id || undefined,
- });
+ // 2. Check if there's a scheduled product
+ let { curScheduledProduct: otherScheduledProduct } =
+ await getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts: cusProducts,
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ });
- if (otherScheduledProduct) {
- otherCusProductsWithSameSub.push(otherScheduledProduct);
- }
- }
- return otherCusProductsWithSameSub;
+ if (otherScheduledProduct) {
+ otherCusProductsWithSameSub.push(otherScheduledProduct);
+ }
+ }
+ return otherCusProductsWithSameSub;
};
// If other cus products on schedule, add cur main product regular items to schedule...
export const addCurMainProductToSchedule = async ({
- db,
- org,
- env,
- stripeCli,
- otherCusProductsOnSub,
- oldItemSets,
- curMainProduct,
- logger,
+ db,
+ org,
+ env,
+ stripeCli,
+ otherCusProductsOnSub,
+ oldItemSets,
+ curMainProduct,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- stripeCli: Stripe;
- otherCusProductsOnSub: FullCusProduct[];
- oldItemSets: ItemSet[];
- curMainProduct: FullCusProduct;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ stripeCli: Stripe;
+ otherCusProductsOnSub: FullCusProduct[];
+ oldItemSets: ItemSet[];
+ curMainProduct: FullCusProduct;
+ logger: any;
}) => {
- let schedules = await getStripeSchedules({
- stripeCli: stripeCli,
- scheduleIds: getScheduleIdsFromCusProducts({
- cusProducts: otherCusProductsOnSub,
- }),
- });
+ let schedules = await getStripeSchedules({
+ stripeCli: stripeCli,
+ scheduleIds: getScheduleIdsFromCusProducts({
+ cusProducts: otherCusProductsOnSub,
+ }),
+ });
- for (const scheduleObj of schedules) {
- const { schedule, interval, intervalCount } = scheduleObj;
+ for (const scheduleObj of schedules) {
+ const { schedule, interval, intervalCount } = scheduleObj;
- // let oldItemSet = oldItemSets.find((itemSet) =>
- // intervalsSame({
- // intervalA: { interval, intervalCount },
- // intervalB: itemSet,
- // })
- // );
+ // let oldItemSet = oldItemSets.find((itemSet) =>
+ // intervalsSame({
+ // intervalA: { interval, intervalCount },
+ // intervalB: itemSet,
+ // })
+ // );
- // await updateScheduledSubWithNewItems({
- // scheduleObj: scheduleObj,
- // newItems: oldItemSet?.items || [],
- // cusProductsForGroup: [],
- // stripeCli: stripeCli,
- // itemSet: null,
- // db,
- // org: org,
- // env: env,
- // });
+ // await updateScheduledSubWithNewItems({
+ // scheduleObj: scheduleObj,
+ // newItems: oldItemSet?.items || [],
+ // cusProductsForGroup: [],
+ // stripeCli: stripeCli,
+ // itemSet: null,
+ // db,
+ // org: org,
+ // env: env,
+ // });
- // Put back schedule id into curMainProduct
- await CusProductService.update({
- db,
- cusProductId: curMainProduct!.id,
- updates: {
- scheduled_ids: [...(curMainProduct!.scheduled_ids || []), schedule.id],
- },
- });
+ // Put back schedule id into curMainProduct
+ await CusProductService.update({
+ db,
+ cusProductId: curMainProduct!.id,
+ updates: {
+ scheduled_ids: [...(curMainProduct!.scheduled_ids || []), schedule.id],
+ },
+ });
- logger.info(
- `✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}`
- );
- }
+ logger.info(
+ `✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}`,
+ );
+ }
};
diff --git a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts
index 3bf93aacd..9b49fe06f 100644
--- a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts
+++ b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts
@@ -4,39 +4,39 @@ import { FullCusProduct } from "@autumn/shared";
import { ScheduleObj } from "./ScheduleObj.js";
import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js";
export const getFilteredScheduleItems = ({
- scheduleObj,
- cusProducts,
+ scheduleObj,
+ cusProducts,
}: {
- scheduleObj: ScheduleObj;
- cusProducts: (FullCusProduct | undefined)[];
+ scheduleObj: ScheduleObj;
+ cusProducts: (FullCusProduct | undefined)[];
}) => {
- const { schedule, prices } = scheduleObj;
- let scheduleItems = schedule.phases[0].items;
+ const { schedule, prices } = scheduleObj;
+ let scheduleItems = schedule.phases[0].items;
- let curPrices: any[] = [];
- for (const cusProduct of cusProducts) {
- if (cusProduct) {
- curPrices = curPrices.concat(getPricesForCusProduct({ cusProduct }));
- }
- }
+ let curPrices: any[] = [];
+ for (const cusProduct of cusProducts) {
+ if (cusProduct) {
+ curPrices = curPrices.concat(getPricesForCusProduct({ cusProduct }));
+ }
+ }
- let products = cusProducts
- .filter((cp): cp is FullCusProduct => !!cp)
- .map((cp: FullCusProduct) => fullCusProductToProduct(cp));
+ let products = cusProducts
+ .filter((cp): cp is FullCusProduct => !!cp)
+ .map((cp: FullCusProduct) => fullCusProductToProduct(cp));
- return scheduleItems.filter((scheduleItem: any) => {
- let stripePrice = prices.find((price) => price.id === scheduleItem.price);
+ return scheduleItems.filter((scheduleItem: any) => {
+ let stripePrice = prices.find((price) => price.id === scheduleItem.price);
- let inCurProduct =
- curPrices.some(
- (price) =>
- price.config?.stripe_price_id === scheduleItem.price ||
- price.config?.stripe_product_id === stripePrice?.product
- ) ||
- products.some(
- (product) => product.processor?.id === stripePrice?.product
- );
+ let inCurProduct =
+ curPrices.some(
+ (price) =>
+ price.config?.stripe_price_id === scheduleItem.price ||
+ price.config?.stripe_product_id === stripePrice?.product,
+ ) ||
+ products.some(
+ (product) => product.processor?.id === stripePrice?.product,
+ );
- return !inCurProduct;
- });
+ return !inCurProduct;
+ });
};
diff --git a/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts b/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts
index 0f4ab6fed..dc3cabb85 100644
--- a/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts
+++ b/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts
@@ -7,64 +7,64 @@ import { getFilteredScheduleItems } from "./getFilteredScheduleItems.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const updateScheduledSubWithNewItems = async ({
- db,
- scheduleObj,
- newItems,
- cusProductsForGroup,
- stripeCli,
- itemSet,
- org,
- env,
+ db,
+ scheduleObj,
+ newItems,
+ cusProductsForGroup,
+ stripeCli,
+ itemSet,
+ org,
+ env,
}: {
- db: DrizzleCli;
- scheduleObj: any;
- newItems: any[];
- cusProductsForGroup: (FullCusProduct | undefined)[];
- stripeCli: Stripe;
- itemSet: ItemSet | null;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ scheduleObj: any;
+ newItems: any[];
+ cusProductsForGroup: (FullCusProduct | undefined)[];
+ stripeCli: Stripe;
+ itemSet: ItemSet | null;
+ org: Organization;
+ env: AppEnv;
}) => {
- const { schedule } = scheduleObj;
+ const { schedule } = scheduleObj;
- let filteredScheduleItems = getFilteredScheduleItems({
- scheduleObj,
- cusProducts: cusProductsForGroup,
- });
+ let filteredScheduleItems = getFilteredScheduleItems({
+ scheduleObj,
+ cusProducts: cusProductsForGroup,
+ });
- // 2. Add new schedule items
- let newScheduleItems = filteredScheduleItems
- .map((item: any) => ({
- price: item.price,
- }))
- .concat(
- ...newItems.map((item: any) => ({
- price: item.price,
- })),
- );
+ // 2. Add new schedule items
+ let newScheduleItems = filteredScheduleItems
+ .map((item: any) => ({
+ price: item.price,
+ }))
+ .concat(
+ ...newItems.map((item: any) => ({
+ price: item.price,
+ })),
+ );
- const stripeSchedule = await stripeCli.subscriptionSchedules.update(
- schedule.id,
- {
- phases: [
- {
- items: newScheduleItems,
- start_date: schedule.phases[0].start_date,
- },
- ],
- },
- );
+ const stripeSchedule = await stripeCli.subscriptionSchedules.update(
+ schedule.id,
+ {
+ phases: [
+ {
+ items: newScheduleItems,
+ start_date: schedule.phases[0].start_date,
+ },
+ ],
+ },
+ );
- // Update sub schedule ID
- if (itemSet) {
- await SubService.addUsageFeatures({
- db,
- scheduleId: scheduleObj.schedule.id,
- usageFeatures: itemSet.usageFeatures,
- orgId: org.id,
- env: env,
- });
- }
+ // Update sub schedule ID
+ if (itemSet) {
+ await SubService.addUsageFeatures({
+ db,
+ scheduleId: scheduleObj.schedule.id,
+ usageFeatures: itemSet.usageFeatures,
+ orgId: org.id,
+ env: env,
+ });
+ }
- return stripeSchedule;
+ return stripeSchedule;
};
diff --git a/server/src/internal/customers/cusCache/cusCacheUtils.ts b/server/src/internal/customers/cusCache/cusCacheUtils.ts
index 6ed1137ec..29268086f 100644
--- a/server/src/internal/customers/cusCache/cusCacheUtils.ts
+++ b/server/src/internal/customers/cusCache/cusCacheUtils.ts
@@ -1,17 +1,17 @@
export const buildBaseCusCacheKey = ({
- idOrInternalId,
- entityId,
- orgId,
- env,
+ idOrInternalId,
+ entityId,
+ orgId,
+ env,
}: {
- idOrInternalId: string;
- entityId?: string;
- orgId: string;
- env: string;
+ idOrInternalId: string;
+ entityId?: string;
+ orgId: string;
+ env: string;
}) => {
- if (entityId) {
- return `customer:${idOrInternalId}_${orgId}_${env}:entity_${entityId}`;
- } else {
- return `customer:${idOrInternalId}_${orgId}_${env}`;
- }
+ if (entityId) {
+ return `customer:${idOrInternalId}_${orgId}_${env}:entity_${entityId}`;
+ } else {
+ return `customer:${idOrInternalId}_${orgId}_${env}`;
+ }
};
diff --git a/server/src/internal/customers/cusCache/getCusWithCache.ts b/server/src/internal/customers/cusCache/getCusWithCache.ts
index 54f442a62..c2f901de8 100644
--- a/server/src/internal/customers/cusCache/getCusWithCache.ts
+++ b/server/src/internal/customers/cusCache/getCusWithCache.ts
@@ -1,9 +1,9 @@
import {
- AppEnv,
- CusExpand,
- EntityExpand,
- FullCustomer,
- Organization,
+ AppEnv,
+ CusExpand,
+ EntityExpand,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import { RELEVANT_STATUSES } from "../cusProducts/CusProductService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -13,86 +13,86 @@ import { initUpstash } from "./upstashUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
export const getCusWithCache = async ({
- db,
- idOrInternalId,
- org,
- env,
- entityId,
- expand = [],
- allowNotFound = true,
- skipCache = false,
- skipGet = false,
- logger,
+ db,
+ idOrInternalId,
+ org,
+ env,
+ entityId,
+ expand = [],
+ allowNotFound = true,
+ skipCache = false,
+ skipGet = false,
+ logger,
}: {
- db: DrizzleCli;
- idOrInternalId: string;
- org: Organization;
- env: AppEnv;
- entityId?: string;
+ db: DrizzleCli;
+ idOrInternalId: string;
+ org: Organization;
+ env: AppEnv;
+ entityId?: string;
- // Optional
- expand?: (CusExpand | EntityExpand)[];
- allowNotFound?: boolean;
- skipCache?: boolean;
- skipGet?: boolean;
- logger: any;
+ // Optional
+ expand?: (CusExpand | EntityExpand)[];
+ allowNotFound?: boolean;
+ skipCache?: boolean;
+ skipGet?: boolean;
+ logger: any;
}): Promise => {
- const statuses = RELEVANT_STATUSES;
- const withEntities = true;
- const withSubs = true;
+ const statuses = RELEVANT_STATUSES;
+ const withEntities = true;
+ const withSubs = true;
- const upstash = await initUpstash();
- // || !org.config.cache_customer
- if (!upstash) skipCache = true;
+ const upstash = await initUpstash();
+ // || !org.config.cache_customer
+ if (!upstash) skipCache = true;
- let cacheKey = buildBaseCusCacheKey({
- idOrInternalId,
- orgId: org.id,
- env,
- entityId,
- });
+ let cacheKey = buildBaseCusCacheKey({
+ idOrInternalId,
+ orgId: org.id,
+ env,
+ entityId,
+ });
- if (expand.length > 0) {
- cacheKey = `${cacheKey}:expand_${expand.join(",")}`;
- }
+ if (expand.length > 0) {
+ cacheKey = `${cacheKey}:expand_${expand.join(",")}`;
+ }
- if (!skipCache && !skipGet) {
- try {
- const cached = await upstash!.get(cacheKey);
- if (cached) {
- return cached as FullCustomer;
- } else {
- // logger.info(`Cache miss: ${cacheKey}`);
- }
- } catch (error) {
- logger.error(`Failed to get cache: ${cacheKey}`, { error });
- }
- }
+ if (!skipCache && !skipGet) {
+ try {
+ const cached = await upstash!.get(cacheKey);
+ if (cached) {
+ return cached as FullCustomer;
+ } else {
+ // logger.info(`Cache miss: ${cacheKey}`);
+ }
+ } catch (error) {
+ logger.error(`Failed to get cache: ${cacheKey}`, { error });
+ }
+ }
- const customer = await CusService.getFull({
- db,
- idOrInternalId,
- orgId: org.id,
- env,
- inStatuses: statuses,
- withEntities,
- withSubs,
- allowNotFound,
- expand,
- entityId,
- });
+ const customer = await CusService.getFull({
+ db,
+ idOrInternalId,
+ orgId: org.id,
+ env,
+ inStatuses: statuses,
+ withEntities,
+ withSubs,
+ allowNotFound,
+ expand,
+ entityId,
+ });
- if (entityId && nullish(customer?.entity)) skipCache = true;
+ if (entityId && nullish(customer?.entity)) skipCache = true;
- if (!skipCache && notNullish(customer)) {
- try {
- await upstash!.set(cacheKey, customer, {
- ex: 300,
- });
- } catch (error) {
- logger.error(`Failed to set cache: ${cacheKey}`, { error });
- }
- }
+ if (!skipCache && notNullish(customer)) {
+ try {
+ await upstash!.set(cacheKey, customer, {
+ ex: 300,
+ });
+ } catch (error) {
+ logger.error(`Failed to set cache: ${cacheKey}`, { error });
+ }
+ }
- return customer;
+ return customer;
};
diff --git a/server/src/internal/customers/cusCache/subCacheUtils.ts b/server/src/internal/customers/cusCache/subCacheUtils.ts
index 918c4fcda..6b0d468e5 100644
--- a/server/src/internal/customers/cusCache/subCacheUtils.ts
+++ b/server/src/internal/customers/cusCache/subCacheUtils.ts
@@ -1,21 +1,21 @@
import { initUpstash } from "./upstashUtils.js";
export const addSubIdToCache = async ({
- subId,
- scenario,
+ subId,
+ scenario,
}: {
- subId: string;
- scenario: string;
+ subId: string;
+ scenario: string;
}) => {
- const upstash = await initUpstash();
- if (!upstash) return;
+ const upstash = await initUpstash();
+ if (!upstash) return;
- await upstash.set(`sub:${subId}`, scenario, {
- ex: 180, // 3 minutes
- });
+ await upstash.set(`sub:${subId}`, scenario, {
+ ex: 180, // 3 minutes
+ });
};
export const getSubScenarioFromCache = async ({ subId }: { subId: string }) => {
- const upstash = await initUpstash();
- if (!upstash) return null;
- return (await upstash.get(`sub:${subId}`)) as string | null;
+ const upstash = await initUpstash();
+ if (!upstash) return null;
+ return (await upstash.get(`sub:${subId}`)) as string | null;
};
diff --git a/server/src/internal/customers/cusCache/updateCachedCus.ts b/server/src/internal/customers/cusCache/updateCachedCus.ts
index 87dba4e45..c392ba9e5 100644
--- a/server/src/internal/customers/cusCache/updateCachedCus.ts
+++ b/server/src/internal/customers/cusCache/updateCachedCus.ts
@@ -1,7 +1,7 @@
import {
- CusExpand,
- FullCusEntWithFullCusProduct,
- Organization,
+ CusExpand,
+ FullCusEntWithFullCusProduct,
+ Organization,
} from "@autumn/shared";
import { AppEnv } from "autumn-js";
import { buildBaseCusCacheKey } from "./cusCacheUtils.js";
@@ -11,97 +11,97 @@ import { logger } from "@/external/logtail/logtailUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const refreshCusCache = async ({
- db,
- customerId,
- entityId,
- // orgId,
- org,
- env,
+ db,
+ customerId,
+ entityId,
+ // orgId,
+ org,
+ env,
}: {
- db: DrizzleCli;
- customerId: string;
- entityId?: string;
- // orgId: string;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ customerId: string;
+ entityId?: string;
+ // orgId: string;
+ org: Organization;
+ env: AppEnv;
}) => {
- try {
- const upstash = await initUpstash();
- if (!upstash) return;
+ try {
+ const upstash = await initUpstash();
+ if (!upstash) return;
- // if (!org.config.cache_customer) return;
+ // if (!org.config.cache_customer) return;
- const baseKey = buildBaseCusCacheKey({
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ const baseKey = buildBaseCusCacheKey({
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- const list = await upstash.keys(`${baseKey}*`);
+ const list = await upstash.keys(`${baseKey}*`);
- const promises = [];
- for (const key of list) {
- const refresh = async () => {
- const keyName = key;
- let params = keyName.split(":");
- let expandParam = params.find((p) => p.startsWith("expand_"));
- let expand = expandParam
- ? expandParam.replace("expand_", "").split(",")
- : [];
+ const promises = [];
+ for (const key of list) {
+ const refresh = async () => {
+ const keyName = key;
+ let params = keyName.split(":");
+ let expandParam = params.find((p) => p.startsWith("expand_"));
+ let expand = expandParam
+ ? expandParam.replace("expand_", "").split(",")
+ : [];
- let entityIdParam = params.find((p) => p.startsWith("entity_"));
- let entityId = entityIdParam
- ? entityIdParam.replace("entity_", "")
- : undefined;
+ let entityIdParam = params.find((p) => p.startsWith("entity_"));
+ let entityId = entityIdParam
+ ? entityIdParam.replace("entity_", "")
+ : undefined;
- await getCusWithCache({
- db,
- idOrInternalId: customerId,
- org,
- env,
- expand: expand as CusExpand[],
- entityId,
- skipGet: true,
- logger: console,
- });
- };
- promises.push(refresh());
- }
- await Promise.all(promises);
- } catch (error) {
- logger.error("Failed to update cache:", { error });
- }
+ await getCusWithCache({
+ db,
+ idOrInternalId: customerId,
+ org,
+ env,
+ expand: expand as CusExpand[],
+ entityId,
+ skipGet: true,
+ logger: console,
+ });
+ };
+ promises.push(refresh());
+ }
+ await Promise.all(promises);
+ } catch (error) {
+ logger.error("Failed to update cache:", { error });
+ }
};
export const deleteCusCache = async ({
- db,
- customerId,
- org,
- env,
+ db,
+ customerId,
+ org,
+ env,
}: {
- db: DrizzleCli;
- customerId: string;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
}) => {
- try {
- const upstash = await initUpstash();
- if (!upstash) return;
+ try {
+ const upstash = await initUpstash();
+ if (!upstash) return;
- // if (!org.config.cache_customer) return;
+ // if (!org.config.cache_customer) return;
- const baseKey = buildBaseCusCacheKey({
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ const baseKey = buildBaseCusCacheKey({
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- const list = await upstash.keys(`${baseKey}*`);
+ const list = await upstash.keys(`${baseKey}*`);
- for (const key of list) {
- await upstash.del(key);
- }
- } catch (error) {
- logger.error("Failed to delete cache:", { error });
- }
+ for (const key of list) {
+ await upstash.del(key);
+ }
+ } catch (error) {
+ logger.error("Failed to delete cache:", { error });
+ }
};
diff --git a/server/src/internal/customers/cusCache/upstashUtils.ts b/server/src/internal/customers/cusCache/upstashUtils.ts
index 072118900..9435d69ae 100644
--- a/server/src/internal/customers/cusCache/upstashUtils.ts
+++ b/server/src/internal/customers/cusCache/upstashUtils.ts
@@ -2,12 +2,12 @@ import "dotenv/config";
import { Redis } from "@upstash/redis";
export const initUpstash = async () => {
- if (!process.env.UPSTASH_TOKEN) {
- return null;
- }
+ if (!process.env.UPSTASH_TOKEN) {
+ return null;
+ }
- return new Redis({
- url: process.env.UPSTASH_URL,
- token: process.env.UPSTASH_TOKEN,
- });
+ return new Redis({
+ url: process.env.UPSTASH_URL,
+ token: process.env.UPSTASH_TOKEN,
+ });
};
diff --git a/server/src/internal/customers/cusProducts/AttachParams.ts b/server/src/internal/customers/cusProducts/AttachParams.ts
index 375c06533..93fae60ea 100644
--- a/server/src/internal/customers/cusProducts/AttachParams.ts
+++ b/server/src/internal/customers/cusProducts/AttachParams.ts
@@ -1,21 +1,21 @@
import type {
- APIVersion,
- AttachReplaceable,
- AttachScenario,
- Customer,
- EntitlementWithFeature,
- Entity,
- Feature,
- FeatureOptions,
- FreeProductConfig,
- FreeTrial,
- FullCusProduct,
- FullCustomer,
- FullProduct,
- Organization,
- Price,
- ProductOptions,
- Reward,
+ APIVersion,
+ AttachReplaceable,
+ AttachScenario,
+ Customer,
+ EntitlementWithFeature,
+ Entity,
+ Feature,
+ FeatureOptions,
+ FreeProductConfig,
+ FreeTrial,
+ FullCusProduct,
+ FullCustomer,
+ FullProduct,
+ Organization,
+ Price,
+ ProductOptions,
+ Reward,
} from "@autumn/shared";
import type Stripe from "stripe";
@@ -24,105 +24,105 @@ import { z } from "zod";
// Get misc
export type AttachParams = {
- stripeCli: Stripe;
- stripeCus?: Stripe.Customer;
- now?: number;
- paymentMethod: Stripe.PaymentMethod | null | undefined;
- rewards?: Reward[];
+ stripeCli: Stripe;
+ stripeCus?: Stripe.Customer;
+ now?: number;
+ paymentMethod: Stripe.PaymentMethod | null | undefined;
+ rewards?: Reward[];
- org: Organization;
- // customer: Customer;
- customer: FullCustomer;
- cusProduct?: FullCusProduct; // cus product to expire or cancel...
- products: FullProduct[];
+ org: Organization;
+ // customer: Customer;
+ customer: FullCustomer;
+ cusProduct?: FullCusProduct; // cus product to expire or cancel...
+ products: FullProduct[];
- prices: Price[];
- entitlements: EntitlementWithFeature[];
+ prices: Price[];
+ entitlements: EntitlementWithFeature[];
- freeTrial: FreeTrial | null;
- rewardTrial?: FreeProductConfig | null;
- optionsList: FeatureOptions[];
- productsList?: ProductOptions[];
+ freeTrial: FreeTrial | null;
+ rewardTrial?: FreeProductConfig | null;
+ optionsList: FeatureOptions[];
+ productsList?: ProductOptions[];
- successUrl?: string | undefined;
- itemSets?: any[];
- cusProducts: FullCusProduct[];
+ successUrl?: string | undefined;
+ itemSets?: any[];
+ cusProducts: FullCusProduct[];
- // Options to update
- optionsToUpdate?: {
- old: FeatureOptions;
- new: FeatureOptions;
- }[];
+ // Options to update
+ optionsToUpdate?: {
+ old: FeatureOptions;
+ new: FeatureOptions;
+ }[];
- replaceables: AttachReplaceable[];
+ replaceables: AttachReplaceable[];
- // CONFIGS
- invoiceOnly?: boolean | undefined;
- billingAnchor?: number | undefined;
- metadata?: Record | undefined;
+ // CONFIGS
+ invoiceOnly?: boolean | undefined;
+ billingAnchor?: number | undefined;
+ metadata?: Record | undefined;
- entities: Entity[];
+ entities: Entity[];
- isCustom?: boolean;
- disableFreeTrial?: boolean;
- features: Feature[];
+ isCustom?: boolean;
+ disableFreeTrial?: boolean;
+ features: Feature[];
- entityId?: string;
- internalEntityId?: string;
+ entityId?: string;
+ internalEntityId?: string;
- checkoutSessionParams?: any;
- apiVersion?: number;
- scenario?: AttachScenario;
+ checkoutSessionParams?: any;
+ apiVersion?: number;
+ scenario?: AttachScenario;
- fromMigration?: boolean;
- finalizeInvoice?: boolean;
- req?: any;
- fromCancel?: boolean;
- setupPayment?: boolean;
+ fromMigration?: boolean;
+ finalizeInvoice?: boolean;
+ req?: any;
+ fromCancel?: boolean;
+ setupPayment?: boolean;
};
export type InsertCusProductParams = {
- req?: any;
- now?: number;
+ req?: any;
+ now?: number;
- customer: Customer;
- org: Organization;
- product: FullProduct;
- prices: Price[];
- entitlements: EntitlementWithFeature[];
+ customer: Customer;
+ org: Organization;
+ product: FullProduct;
+ prices: Price[];
+ entitlements: EntitlementWithFeature[];
- freeTrial: FreeTrial | null;
- optionsList: FeatureOptions[];
+ freeTrial: FreeTrial | null;
+ optionsList: FeatureOptions[];
- successUrl?: string | undefined;
- itemSets?: any[];
+ successUrl?: string | undefined;
+ itemSets?: any[];
- curCusProduct?: FullCusProduct | undefined;
- cusProducts?: FullCusProduct[];
- replaceables: AttachReplaceable[];
+ curCusProduct?: FullCusProduct | undefined;
+ cusProducts?: FullCusProduct[];
+ replaceables: AttachReplaceable[];
- // CONFIGS
- invoiceOnly?: boolean | undefined;
- entities: Entity[];
- isCustom?: boolean;
- disableFreeTrial?: boolean;
- features: Feature[];
+ // CONFIGS
+ invoiceOnly?: boolean | undefined;
+ entities: Entity[];
+ isCustom?: boolean;
+ disableFreeTrial?: boolean;
+ features: Feature[];
- entityId?: string;
- internalEntityId?: string;
- fromMigration?: boolean;
- apiVersion?: APIVersion;
- finalizeInvoice?: boolean;
+ entityId?: string;
+ internalEntityId?: string;
+ fromMigration?: boolean;
+ apiVersion?: APIVersion;
+ finalizeInvoice?: boolean;
};
export const AttachResultSchema = z.object({
- customer_id: z.string(),
- product_ids: z.array(z.string()),
- code: z.string(),
- message: z.string(),
+ customer_id: z.string(),
+ product_ids: z.array(z.string()),
+ code: z.string(),
+ message: z.string(),
- checkout_url: z.string().nullish(),
- invoice: z.any().nullish(),
+ checkout_url: z.string().nullish(),
+ invoice: z.any().nullish(),
});
export type AttachResult = z.infer;
diff --git a/server/src/internal/customers/cusProducts/CusProdReadService.ts b/server/src/internal/customers/cusProducts/CusProdReadService.ts
index d393de7bf..16ff6eed3 100644
--- a/server/src/internal/customers/cusProducts/CusProdReadService.ts
+++ b/server/src/internal/customers/cusProducts/CusProdReadService.ts
@@ -2,122 +2,122 @@ import { AppEnv, CusProductStatus, products } from "@autumn/shared";
import { db, DrizzleCli } from "@/db/initDrizzle.js";
import { customerProducts } from "@autumn/shared";
import {
- eq,
- and,
- isNotNull,
- sql,
- countDistinct,
- count,
- inArray,
+ eq,
+ and,
+ isNotNull,
+ sql,
+ countDistinct,
+ count,
+ inArray,
} from "drizzle-orm";
const activeStatuses = [CusProductStatus.Active, CusProductStatus.PastDue];
export class CusProdReadService {
- static async existsForProduct({
- db,
- internalProductId,
- productId,
- }: {
- db: DrizzleCli;
- internalProductId?: string;
- productId?: string;
- }) {
- let result = await db
- .select({
- id: customerProducts.id,
- })
- .from(customerProducts)
- .where(
- and(
- productId ? eq(customerProducts.product_id, productId) : undefined,
- internalProductId
- ? eq(customerProducts.internal_product_id, internalProductId)
- : undefined
- )
- )
- .limit(1);
+ static async existsForProduct({
+ db,
+ internalProductId,
+ productId,
+ }: {
+ db: DrizzleCli;
+ internalProductId?: string;
+ productId?: string;
+ }) {
+ let result = await db
+ .select({
+ id: customerProducts.id,
+ })
+ .from(customerProducts)
+ .where(
+ and(
+ productId ? eq(customerProducts.product_id, productId) : undefined,
+ internalProductId
+ ? eq(customerProducts.internal_product_id, internalProductId)
+ : undefined,
+ ),
+ )
+ .limit(1);
- return result.length > 0;
- }
+ return result.length > 0;
+ }
- static getCounts = async ({
- db,
- internalProductId,
- }: {
- db: DrizzleCli;
- internalProductId: string;
- }) => {
- let result = await db
- .select({
- active: countDistinct(
- sql`CASE WHEN ${inArray(customerProducts.status, activeStatuses)} THEN ${customerProducts.internal_customer_id} END`
- ).as("active"),
- canceled: count(
- sql`CASE WHEN ${isNotNull(customerProducts.canceled_at)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("canceled"),
- custom: count(
- sql`CASE WHEN ${eq(customerProducts.is_custom, true)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("custom"),
- trialing: count(
- sql`CASE WHEN ${isNotNull(customerProducts.trial_ends_at)} AND ${sql`${customerProducts.trial_ends_at} > (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("trialing"),
- all: countDistinct(customerProducts.internal_customer_id).as("all"),
- })
- .from(customerProducts)
- .where(eq(customerProducts.internal_product_id, internalProductId));
+ static getCounts = async ({
+ db,
+ internalProductId,
+ }: {
+ db: DrizzleCli;
+ internalProductId: string;
+ }) => {
+ let result = await db
+ .select({
+ active: countDistinct(
+ sql`CASE WHEN ${inArray(customerProducts.status, activeStatuses)} THEN ${customerProducts.internal_customer_id} END`,
+ ).as("active"),
+ canceled: count(
+ sql`CASE WHEN ${isNotNull(customerProducts.canceled_at)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("canceled"),
+ custom: count(
+ sql`CASE WHEN ${eq(customerProducts.is_custom, true)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("custom"),
+ trialing: count(
+ sql`CASE WHEN ${isNotNull(customerProducts.trial_ends_at)} AND ${sql`${customerProducts.trial_ends_at} > (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("trialing"),
+ all: countDistinct(customerProducts.internal_customer_id).as("all"),
+ })
+ .from(customerProducts)
+ .where(eq(customerProducts.internal_product_id, internalProductId));
- return result[0];
- };
+ return result[0];
+ };
- static async getCountsForAllVersions({
- db,
- productId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productId: string;
- orgId: string;
- env: AppEnv;
- }) {
- let internalProductIds = await db
- .select({
- internal_id: products.internal_id,
- })
- .from(products)
- .where(
- and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- );
+ static async getCountsForAllVersions({
+ db,
+ productId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ let internalProductIds = await db
+ .select({
+ internal_id: products.internal_id,
+ })
+ .from(products)
+ .where(
+ and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ );
- let internalProductIdsArray = internalProductIds.map(
- (item) => item.internal_id
- );
+ let internalProductIdsArray = internalProductIds.map(
+ (item) => item.internal_id,
+ );
- let result = await db
- .select({
- active: countDistinct(
- sql`CASE WHEN ${inArray(customerProducts.status, activeStatuses)} THEN ${customerProducts.internal_customer_id} END`
- ).as("active"),
- canceled: count(
- sql`CASE WHEN ${isNotNull(customerProducts.canceled_at)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("canceled"),
- custom: count(
- sql`CASE WHEN ${eq(customerProducts.is_custom, true)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("custom"),
- trialing: count(
- sql`CASE WHEN ${isNotNull(customerProducts.trial_ends_at)} AND ${sql`${customerProducts.trial_ends_at} > (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`
- ).as("trialing"),
- all: countDistinct(customerProducts.internal_customer_id).as("all"),
- })
- .from(customerProducts)
- .where(
- inArray(customerProducts.internal_product_id, internalProductIdsArray)
- );
+ let result = await db
+ .select({
+ active: countDistinct(
+ sql`CASE WHEN ${inArray(customerProducts.status, activeStatuses)} THEN ${customerProducts.internal_customer_id} END`,
+ ).as("active"),
+ canceled: count(
+ sql`CASE WHEN ${isNotNull(customerProducts.canceled_at)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("canceled"),
+ custom: count(
+ sql`CASE WHEN ${eq(customerProducts.is_custom, true)} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("custom"),
+ trialing: count(
+ sql`CASE WHEN ${isNotNull(customerProducts.trial_ends_at)} AND ${sql`${customerProducts.trial_ends_at} > (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`} AND ${inArray(customerProducts.status, activeStatuses)} THEN 1 END`,
+ ).as("trialing"),
+ all: countDistinct(customerProducts.internal_customer_id).as("all"),
+ })
+ .from(customerProducts)
+ .where(
+ inArray(customerProducts.internal_product_id, internalProductIdsArray),
+ );
- return result[0];
- }
+ return result[0];
+ }
}
diff --git a/server/src/internal/customers/cusProducts/CusProductService.ts b/server/src/internal/customers/cusProducts/CusProductService.ts
index 388885f7b..aea457636 100644
--- a/server/src/internal/customers/cusProducts/CusProductService.ts
+++ b/server/src/internal/customers/cusProducts/CusProductService.ts
@@ -2,650 +2,650 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- AppEnv,
- CusProduct,
- CusProductStatus,
- customers,
- ErrCode,
- FullCusProduct,
- products,
+ AppEnv,
+ CusProduct,
+ CusProductStatus,
+ customers,
+ ErrCode,
+ FullCusProduct,
+ products,
} from "@autumn/shared";
import { customerProducts } from "@autumn/shared";
import {
- and,
- arrayContains,
- eq,
- inArray,
- isNotNull,
- ne,
- or,
- sql,
+ and,
+ arrayContains,
+ eq,
+ inArray,
+ isNotNull,
+ ne,
+ or,
+ sql,
} from "drizzle-orm";
export const ACTIVE_STATUSES = [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
];
export const RELEVANT_STATUSES = [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
];
export const orgOwnsCusProduct = async ({
- cusProduct,
- orgId,
- env,
+ cusProduct,
+ orgId,
+ env,
}: {
- cusProduct: FullCusProduct;
- orgId: string;
- env: AppEnv;
+ cusProduct: FullCusProduct;
+ orgId: string;
+ env: AppEnv;
}) => {
- if (!cusProduct.product) return false;
- let product = cusProduct.product;
+ if (!cusProduct.product) return false;
+ let product = cusProduct.product;
- if (product.org_id !== orgId || product.env !== env) {
- return false;
- }
+ if (product.org_id !== orgId || product.env !== env) {
+ return false;
+ }
- return true;
+ return true;
};
export const filterByOrgAndEnv = ({
- cusProducts,
- orgId,
- env,
+ cusProducts,
+ orgId,
+ env,
}: {
- cusProducts: FullCusProduct[];
- orgId: string;
- env: AppEnv;
+ cusProducts: FullCusProduct[];
+ orgId: string;
+ env: AppEnv;
}) => {
- return cusProducts.filter((cusProduct) => {
- if (!cusProduct.product) return false;
- let product = cusProduct.product;
+ return cusProducts.filter((cusProduct) => {
+ if (!cusProduct.product) return false;
+ let product = cusProduct.product;
- if (product.org_id !== orgId || product.env !== env) {
- return false;
- }
+ if (product.org_id !== orgId || product.env !== env) {
+ return false;
+ }
- return true;
- });
+ return true;
+ });
};
const getFullCusProdRelations = () => {
- return {
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true as const,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true as const,
- },
- },
- free_trial: true as const,
- } as const;
+ return {
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true as const,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true as const,
+ },
+ },
+ free_trial: true as const,
+ } as const;
};
export class CusProductService {
- static async getByIdForReset({ db, id }: { db: DrizzleCli; id: string }) {
- let cusProduct = await db.query.customerProducts.findFirst({
- where: eq(customerProducts.id, id),
- with: {
- customer: true,
- product: {
- with: {
- org: true,
- },
- },
- },
- });
+ static async getByIdForReset({ db, id }: { db: DrizzleCli; id: string }) {
+ let cusProduct = await db.query.customerProducts.findFirst({
+ where: eq(customerProducts.id, id),
+ with: {
+ customer: true,
+ product: {
+ with: {
+ org: true,
+ },
+ },
+ },
+ });
- if (!cusProduct) {
- throw new RecaseError({
- message: `Cus product not found: ${id}`,
- code: ErrCode.CusProductNotFound,
- statusCode: 404,
- });
- }
+ if (!cusProduct) {
+ throw new RecaseError({
+ message: `Cus product not found: ${id}`,
+ code: ErrCode.CusProductNotFound,
+ statusCode: 404,
+ });
+ }
- return cusProduct;
- }
+ return cusProduct;
+ }
- static async get({
- db,
- id,
- orgId,
- env,
- withCustomer = false,
- }: {
- db: DrizzleCli;
- id: string;
- orgId: string;
- env: AppEnv;
- withCustomer?: boolean;
- }) {
- let cusProduct = (await db.query.customerProducts.findFirst({
- where: eq(customerProducts.id, id),
- with: {
- customer: withCustomer ? true : undefined,
- product: true,
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true,
- },
- },
- free_trial: true,
- },
- })) as FullCusProduct;
+ static async get({
+ db,
+ id,
+ orgId,
+ env,
+ withCustomer = false,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ orgId: string;
+ env: AppEnv;
+ withCustomer?: boolean;
+ }) {
+ let cusProduct = (await db.query.customerProducts.findFirst({
+ where: eq(customerProducts.id, id),
+ with: {
+ customer: withCustomer ? true : undefined,
+ product: true,
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true,
+ },
+ },
+ free_trial: true,
+ },
+ })) as FullCusProduct;
- if (!cusProduct || !orgOwnsCusProduct({ cusProduct, orgId, env })) {
- return null;
- }
+ if (!cusProduct || !orgOwnsCusProduct({ cusProduct, orgId, env })) {
+ return null;
+ }
- return cusProduct;
- }
+ return cusProduct;
+ }
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: CusProduct[] | CusProduct;
- }) {
- if (Array.isArray(data) && data.length == 0) {
- return;
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: CusProduct[] | CusProduct;
+ }) {
+ if (Array.isArray(data) && data.length == 0) {
+ return;
+ }
- await db.insert(customerProducts).values(data as any);
- }
+ await db.insert(customerProducts).values(data as any);
+ }
- static async list({
- db,
- internalCustomerId,
- withCustomer = false,
- inStatuses = [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- withCustomer?: boolean;
- inStatuses?: string[];
- }) {
- let cusProducts = await db.query.customerProducts.findMany({
- where: and(
- eq(customerProducts.internal_customer_id, internalCustomerId),
- inStatuses ? inArray(customerProducts.status, inStatuses) : undefined
- ),
- with: {
- customer: withCustomer ? true : undefined,
- product: true,
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true,
- },
- },
- free_trial: true,
- },
- });
+ static async list({
+ db,
+ internalCustomerId,
+ withCustomer = false,
+ inStatuses = [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ withCustomer?: boolean;
+ inStatuses?: string[];
+ }) {
+ let cusProducts = await db.query.customerProducts.findMany({
+ where: and(
+ eq(customerProducts.internal_customer_id, internalCustomerId),
+ inStatuses ? inArray(customerProducts.status, inStatuses) : undefined,
+ ),
+ with: {
+ customer: withCustomer ? true : undefined,
+ product: true,
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true,
+ },
+ },
+ free_trial: true,
+ },
+ });
- return cusProducts as FullCusProduct[];
- }
+ return cusProducts as FullCusProduct[];
+ }
- static async getByInternalProductId({
- db,
- internalProductId,
- limit = 1,
- }: {
- db: DrizzleCli;
- internalProductId: string;
- limit?: number;
- }) {
- let data = await db.query.customerProducts.findMany({
- where: eq(customerProducts.internal_product_id, internalProductId),
- limit,
- });
+ static async getByInternalProductId({
+ db,
+ internalProductId,
+ limit = 1,
+ }: {
+ db: DrizzleCli;
+ internalProductId: string;
+ limit?: number;
+ }) {
+ let data = await db.query.customerProducts.findMany({
+ where: eq(customerProducts.internal_product_id, internalProductId),
+ limit,
+ });
- return data as CusProduct[];
- }
+ return data as CusProduct[];
+ }
- static async getByProductId({
- db,
- productId,
- orgId,
- env,
- limit = 1,
- }: {
- db: DrizzleCli;
- productId: string;
- orgId: string;
- env: AppEnv;
- limit?: number;
- }) {
- let data = await db
- .select()
- .from(customerProducts)
- .innerJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- )
- .where(
- and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- )
- .limit(1);
+ static async getByProductId({
+ db,
+ productId,
+ orgId,
+ env,
+ limit = 1,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ limit?: number;
+ }) {
+ let data = await db
+ .select()
+ .from(customerProducts)
+ .innerJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ )
+ .where(
+ and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ )
+ .limit(1);
- return data.map((d) => ({
- ...d.customer_products,
- product: d.products,
- }));
- }
+ return data.map((d) => ({
+ ...d.customer_products,
+ product: d.products,
+ }));
+ }
- static async getByStripeSubId({
- db,
- stripeSubId,
- orgId,
- env,
- inStatuses,
- }: {
- db: DrizzleCli;
- stripeSubId: string;
- orgId: string;
- env: AppEnv;
- inStatuses?: string[];
- }) {
- // sql`${customerProducts.subscription_ids} @> ${sql`ARRAY[${stripeSubId}]`}`,
- let data = await db.query.customerProducts.findMany({
- where: (table, { and, or, inArray }) =>
- and(
- or(arrayContains(customerProducts.subscription_ids, [stripeSubId])),
- inStatuses ? inArray(customerProducts.status, inStatuses) : undefined
- ),
+ static async getByStripeSubId({
+ db,
+ stripeSubId,
+ orgId,
+ env,
+ inStatuses,
+ }: {
+ db: DrizzleCli;
+ stripeSubId: string;
+ orgId: string;
+ env: AppEnv;
+ inStatuses?: string[];
+ }) {
+ // sql`${customerProducts.subscription_ids} @> ${sql`ARRAY[${stripeSubId}]`}`,
+ let data = await db.query.customerProducts.findMany({
+ where: (table, { and, or, inArray }) =>
+ and(
+ or(arrayContains(customerProducts.subscription_ids, [stripeSubId])),
+ inStatuses ? inArray(customerProducts.status, inStatuses) : undefined,
+ ),
- with: {
- product: true,
- customer: true,
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true,
- },
- },
- free_trial: true,
- },
- });
+ with: {
+ product: true,
+ customer: true,
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true,
+ },
+ },
+ free_trial: true,
+ },
+ });
- let cusProducts = data as FullCusProduct[];
+ let cusProducts = data as FullCusProduct[];
- return filterByOrgAndEnv({
- cusProducts,
- orgId,
- env,
- });
- }
+ return filterByOrgAndEnv({
+ cusProducts,
+ orgId,
+ env,
+ });
+ }
- static async getByStripeScheduledId({
- db,
- stripeScheduledId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- stripeScheduledId: string;
- orgId: string;
- env: AppEnv;
- }) {
- let data = await db.query.customerProducts.findMany({
- where: (customerProducts, { and, or, eq, sql }) =>
- and(
- or(
- eq(
- sql`${customerProducts.processor}->>'subscription_schedule_id'`,
- stripeScheduledId
- ),
- sql`${customerProducts.scheduled_ids} @> ${sql`ARRAY[${stripeScheduledId}]`}`
- )
- ),
+ static async getByStripeScheduledId({
+ db,
+ stripeScheduledId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ stripeScheduledId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ let data = await db.query.customerProducts.findMany({
+ where: (customerProducts, { and, or, eq, sql }) =>
+ and(
+ or(
+ eq(
+ sql`${customerProducts.processor}->>'subscription_schedule_id'`,
+ stripeScheduledId,
+ ),
+ sql`${customerProducts.scheduled_ids} @> ${sql`ARRAY[${stripeScheduledId}]`}`,
+ ),
+ ),
- with: {
- product: true,
- customer: true,
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true,
- },
- },
- free_trial: true,
- },
- });
+ with: {
+ product: true,
+ customer: true,
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true,
+ },
+ },
+ free_trial: true,
+ },
+ });
- let cusProducts = data as FullCusProduct[];
+ let cusProducts = data as FullCusProduct[];
- return filterByOrgAndEnv({
- cusProducts,
- orgId,
- env,
- });
- }
+ return filterByOrgAndEnv({
+ cusProducts,
+ orgId,
+ env,
+ });
+ }
- static async getByScheduleId({
- db,
- scheduleId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- scheduleId: string;
- orgId: string;
- env: AppEnv;
- }) {
- let fullCusProdRelations = {
- customer_entitlements: {
- with: {
- entitlement: {
- with: {
- feature: true as const,
- },
- },
- replaceables: true,
- rollovers: true,
- },
- },
- customer_prices: {
- with: {
- price: true as const,
- },
- },
- free_trial: true as const,
- } as const;
+ static async getByScheduleId({
+ db,
+ scheduleId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ scheduleId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ let fullCusProdRelations = {
+ customer_entitlements: {
+ with: {
+ entitlement: {
+ with: {
+ feature: true as const,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ },
+ },
+ customer_prices: {
+ with: {
+ price: true as const,
+ },
+ },
+ free_trial: true as const,
+ } as const;
- let data = (await db.query.customerProducts.findMany({
- where: arrayContains(customerProducts.scheduled_ids, [scheduleId]),
- with: {
- product: true,
- customer: true,
- ...fullCusProdRelations,
- },
- })) as FullCusProduct[];
+ let data = (await db.query.customerProducts.findMany({
+ where: arrayContains(customerProducts.scheduled_ids, [scheduleId]),
+ with: {
+ product: true,
+ customer: true,
+ ...fullCusProdRelations,
+ },
+ })) as FullCusProduct[];
- return filterByOrgAndEnv({
- cusProducts: data,
- orgId,
- env,
- });
- }
+ return filterByOrgAndEnv({
+ cusProducts: data,
+ orgId,
+ env,
+ });
+ }
- static async update({
- db,
- cusProductId,
- updates,
- }: {
- db: DrizzleCli;
- cusProductId: string;
- updates: Partial;
- }) {
- return await db
- .update(customerProducts)
- .set(updates as any)
- .where(eq(customerProducts.id, cusProductId))
- .returning();
- }
+ static async update({
+ db,
+ cusProductId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ cusProductId: string;
+ updates: Partial;
+ }) {
+ return await db
+ .update(customerProducts)
+ .set(updates as any)
+ .where(eq(customerProducts.id, cusProductId))
+ .returning();
+ }
- static async updateByStripeSubId({
- db,
- stripeSubId,
- updates,
- inStatuses = RELEVANT_STATUSES,
- }: {
- db: DrizzleCli;
- stripeSubId: string;
- updates: Partial;
- inStatuses?: string[];
- }) {
- let updated = await db
- .update(customerProducts)
- .set(updates as any)
- .where(
- and(
- arrayContains(customerProducts.subscription_ids, [stripeSubId]),
- inStatuses ? inArray(customerProducts.status, inStatuses) : undefined
- )
- )
- .returning({
- id: customerProducts.id,
- });
+ static async updateByStripeSubId({
+ db,
+ stripeSubId,
+ updates,
+ inStatuses = RELEVANT_STATUSES,
+ }: {
+ db: DrizzleCli;
+ stripeSubId: string;
+ updates: Partial;
+ inStatuses?: string[];
+ }) {
+ let updated = await db
+ .update(customerProducts)
+ .set(updates as any)
+ .where(
+ and(
+ arrayContains(customerProducts.subscription_ids, [stripeSubId]),
+ inStatuses ? inArray(customerProducts.status, inStatuses) : undefined,
+ ),
+ )
+ .returning({
+ id: customerProducts.id,
+ });
- let fullUpdated = (await db.query.customerProducts.findMany({
- where: inArray(
- customerProducts.id,
- updated.map((u) => u.id)
- ),
- with: {
- product: true,
- customer: true,
- ...getFullCusProdRelations(),
- },
- })) as FullCusProduct[];
+ let fullUpdated = (await db.query.customerProducts.findMany({
+ where: inArray(
+ customerProducts.id,
+ updated.map((u) => u.id),
+ ),
+ with: {
+ product: true,
+ customer: true,
+ ...getFullCusProdRelations(),
+ },
+ })) as FullCusProduct[];
- return fullUpdated as FullCusProduct[];
- }
- static async updateByStripeScheduledId({
- db,
- stripeScheduledId,
- updates,
- }: {
- db: DrizzleCli;
- stripeScheduledId: string;
- updates: Partial;
- }) {
- let updated = await db
- .update(customerProducts)
- .set(updates as any)
- .where(
- and(
- arrayContains(customerProducts.scheduled_ids, [stripeScheduledId]),
- or(
- eq(customerProducts.status, CusProductStatus.Active),
- eq(customerProducts.status, CusProductStatus.PastDue),
- eq(customerProducts.status, CusProductStatus.Scheduled)
- )
- )
- )
- .returning({
- id: customerProducts.id,
- });
+ return fullUpdated as FullCusProduct[];
+ }
+ static async updateByStripeScheduledId({
+ db,
+ stripeScheduledId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ stripeScheduledId: string;
+ updates: Partial;
+ }) {
+ let updated = await db
+ .update(customerProducts)
+ .set(updates as any)
+ .where(
+ and(
+ arrayContains(customerProducts.scheduled_ids, [stripeScheduledId]),
+ or(
+ eq(customerProducts.status, CusProductStatus.Active),
+ eq(customerProducts.status, CusProductStatus.PastDue),
+ eq(customerProducts.status, CusProductStatus.Scheduled),
+ ),
+ ),
+ )
+ .returning({
+ id: customerProducts.id,
+ });
- let fullUpdated = (await db.query.customerProducts.findMany({
- where: inArray(
- customerProducts.id,
- updated.map((u) => u.id)
- ),
- with: {
- product: true,
- customer: true,
- ...getFullCusProdRelations(),
- },
- })) as FullCusProduct[];
+ let fullUpdated = (await db.query.customerProducts.findMany({
+ where: inArray(
+ customerProducts.id,
+ updated.map((u) => u.id),
+ ),
+ with: {
+ product: true,
+ customer: true,
+ ...getFullCusProdRelations(),
+ },
+ })) as FullCusProduct[];
- return fullUpdated as FullCusProduct[];
- }
+ return fullUpdated as FullCusProduct[];
+ }
- static async delete({
- db,
- cusProductId,
- }: {
- db: DrizzleCli;
- cusProductId: string;
- }) {
- return await db
- .delete(customerProducts)
- .where(eq(customerProducts.id, cusProductId))
- .returning();
- }
+ static async delete({
+ db,
+ cusProductId,
+ }: {
+ db: DrizzleCli;
+ cusProductId: string;
+ }) {
+ return await db
+ .delete(customerProducts)
+ .where(eq(customerProducts.id, cusProductId))
+ .returning();
+ }
- static async getByFingerprint({
- db,
- productId,
- internalCustomerId,
- fingerprint,
- }: {
- db: DrizzleCli;
- productId: string;
- internalCustomerId: string;
- fingerprint?: string;
- }) {
- let data = await db
- .select()
- .from(customerProducts)
- .innerJoin(
- customers,
- eq(customerProducts.internal_customer_id, customers.internal_id)
- )
- .innerJoin(
- products,
- eq(customerProducts.internal_product_id, products.internal_id)
- )
- .where(
- and(
- or(
- fingerprint ? eq(customers.fingerprint, fingerprint) : undefined,
- eq(customers.internal_id, internalCustomerId)
- ),
- eq(products.id, productId),
- isNotNull(customerProducts.free_trial_id)
- )
- );
+ static async getByFingerprint({
+ db,
+ productId,
+ internalCustomerId,
+ fingerprint,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ internalCustomerId: string;
+ fingerprint?: string;
+ }) {
+ let data = await db
+ .select()
+ .from(customerProducts)
+ .innerJoin(
+ customers,
+ eq(customerProducts.internal_customer_id, customers.internal_id),
+ )
+ .innerJoin(
+ products,
+ eq(customerProducts.internal_product_id, products.internal_id),
+ )
+ .where(
+ and(
+ or(
+ fingerprint ? eq(customers.fingerprint, fingerprint) : undefined,
+ eq(customers.internal_id, internalCustomerId),
+ ),
+ eq(products.id, productId),
+ isNotNull(customerProducts.free_trial_id),
+ ),
+ );
- return data;
- }
+ return data;
+ }
- static async getByTrialAndCustomer({
- db,
- freeTrialId,
- internalCustomerId,
- }: {
- db: DrizzleCli;
- freeTrialId: string;
- internalCustomerId: string;
- }) {
- let data = await db.query.customerProducts.findMany({
- where: and(
- eq(customerProducts.free_trial_id, freeTrialId),
- eq(customerProducts.internal_customer_id, internalCustomerId)
- ),
- with: {
- customer: true,
- },
- });
+ static async getByTrialAndCustomer({
+ db,
+ freeTrialId,
+ internalCustomerId,
+ }: {
+ db: DrizzleCli;
+ freeTrialId: string;
+ internalCustomerId: string;
+ }) {
+ let data = await db.query.customerProducts.findMany({
+ where: and(
+ eq(customerProducts.free_trial_id, freeTrialId),
+ eq(customerProducts.internal_customer_id, internalCustomerId),
+ ),
+ with: {
+ customer: true,
+ },
+ });
- return data;
- }
+ return data;
+ }
- static async deleteByProduct({
- db,
- productId,
- internalProductId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productId?: string;
- internalProductId?: string;
- orgId: string;
- env: AppEnv;
- }) {
- if (productId) {
- let res = await db
- .select({
- internal_id: products.internal_id,
- })
- .from(products)
- .where(
- and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- );
+ static async deleteByProduct({
+ db,
+ productId,
+ internalProductId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productId?: string;
+ internalProductId?: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ if (productId) {
+ let res = await db
+ .select({
+ internal_id: products.internal_id,
+ })
+ .from(products)
+ .where(
+ and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ );
- let internalProductIds = res.map((r) => r.internal_id);
+ let internalProductIds = res.map((r) => r.internal_id);
- if (internalProductIds.length > 100) {
- throw new RecaseError({
- message: "Something went wrong... please try again later.",
- code: ErrCode.ProductHasCustomers,
- statusCode: 400,
- });
- }
+ if (internalProductIds.length > 100) {
+ throw new RecaseError({
+ message: "Something went wrong... please try again later.",
+ code: ErrCode.ProductHasCustomers,
+ statusCode: 400,
+ });
+ }
- await db
- .delete(customerProducts)
- .where(
- inArray(customerProducts.internal_product_id, internalProductIds)
- );
- } else {
- await db
- .delete(customerProducts)
- .where(eq(customerProducts.internal_product_id, internalProductId!));
- }
- }
+ await db
+ .delete(customerProducts)
+ .where(
+ inArray(customerProducts.internal_product_id, internalProductIds),
+ );
+ } else {
+ await db
+ .delete(customerProducts)
+ .where(eq(customerProducts.internal_product_id, internalProductId!));
+ }
+ }
}
diff --git a/server/src/internal/customers/cusProducts/attachUtils.ts b/server/src/internal/customers/cusProducts/attachUtils.ts
index 59f401e62..ce4caf3ca 100644
--- a/server/src/internal/customers/cusProducts/attachUtils.ts
+++ b/server/src/internal/customers/cusProducts/attachUtils.ts
@@ -1,21 +1,21 @@
import {
- AppEnv,
- BillingType,
- CusProductStatus,
- Customer,
- CustomerData,
- Entitlement,
- EntitlementWithFeature,
- Entity,
- EntityData,
- Feature,
- FeatureOptions,
- FreeTrial,
- FullCusProduct,
- Organization,
- Price,
- ProductItem,
- UsagePriceConfig,
+ AppEnv,
+ BillingType,
+ CusProductStatus,
+ Customer,
+ CustomerData,
+ Entitlement,
+ EntitlementWithFeature,
+ Entity,
+ EntityData,
+ Feature,
+ FeatureOptions,
+ FreeTrial,
+ FullCusProduct,
+ Organization,
+ Price,
+ ProductItem,
+ UsagePriceConfig,
} from "@autumn/shared";
import { ErrCode } from "@/errors/errCodes.js";
@@ -24,8 +24,8 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- getFreeTrialAfterFingerprint,
- handleNewFreeTrial,
+ getFreeTrialAfterFingerprint,
+ handleNewFreeTrial,
} from "@/internal/products/free-trials/freeTrialUtils.js";
import { StatusCodes } from "http-status-codes";
@@ -39,422 +39,422 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
const getProducts = async ({
- db,
- productId,
- productIds,
- orgId,
- env,
- version,
+ db,
+ productId,
+ productIds,
+ orgId,
+ env,
+ version,
}: {
- db: DrizzleCli;
- productId?: string;
- productIds?: string[];
- orgId: string;
- env: AppEnv;
- version?: number;
+ db: DrizzleCli;
+ productId?: string;
+ productIds?: string[];
+ orgId: string;
+ env: AppEnv;
+ version?: number;
}) => {
- if (productId && productIds) {
- throw new RecaseError({
- message: `Only one of product_id or product_ids can be provided`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (productId && productIds) {
+ throw new RecaseError({
+ message: `Only one of product_id or product_ids can be provided`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (productId) {
- const product = await ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- version,
- });
+ if (productId) {
+ const product = await ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ version,
+ });
- return [product];
- }
+ return [product];
+ }
- if (productIds) {
- if (notNullish(version)) {
- throw new RecaseError({
- message: "Cannot provide version when providing product ids",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (productIds) {
+ if (notNullish(version)) {
+ throw new RecaseError({
+ message: "Cannot provide version when providing product ids",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Check for duplicates in productIds
- const uniqueProductIds = new Set(productIds);
- if (uniqueProductIds.size !== productIds.length) {
- throw new RecaseError({
- message: "Not allowed duplicate product ids",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // Check for duplicates in productIds
+ const uniqueProductIds = new Set(productIds);
+ if (uniqueProductIds.size !== productIds.length) {
+ throw new RecaseError({
+ message: "Not allowed duplicate product ids",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const products = await ProductService.listFull({
- db,
- orgId,
- env,
- inIds: productIds,
- });
+ const products = await ProductService.listFull({
+ db,
+ orgId,
+ env,
+ inIds: productIds,
+ });
- if (products.length === 0) {
- throw new RecaseError({
- message: "No products found",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (products.length === 0) {
+ throw new RecaseError({
+ message: "No products found",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (products.length != productIds.length) {
- // Get product ids that were not found
- throw new RecaseError({
- message:
- "Number of products found does not match number of product ids",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (products.length != productIds.length) {
+ // Get product ids that were not found
+ throw new RecaseError({
+ message:
+ "Number of products found does not match number of product ids",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Check if more than one product has a free trial
- const productsWithFreeTrial = products.filter((p) => p.free_trial !== null);
- if (productsWithFreeTrial.length > 1) {
- throw new RecaseError({
- message: "Cannot attach multiple products with free trials",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // Check if more than one product has a free trial
+ const productsWithFreeTrial = products.filter((p) => p.free_trial !== null);
+ if (productsWithFreeTrial.length > 1) {
+ throw new RecaseError({
+ message: "Cannot attach multiple products with free trials",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Check that there aren't two products in the same group that are both not add-ons
- for (const product of products) {
- if (product.group && !product.is_add_on) {
- // Find another product in the same group that is not an add-on
- const otherProduct = products.find(
- (p) =>
- p.group === product.group && !p.is_add_on && p.id !== product.id,
- );
- if (otherProduct) {
- throw new RecaseError({
- message: `Cannot attach two main products from the same group ${product.group}`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
- }
+ // Check that there aren't two products in the same group that are both not add-ons
+ for (const product of products) {
+ if (product.group && !product.is_add_on) {
+ // Find another product in the same group that is not an add-on
+ const otherProduct = products.find(
+ (p) =>
+ p.group === product.group && !p.is_add_on && p.id !== product.id,
+ );
+ if (otherProduct) {
+ throw new RecaseError({
+ message: `Cannot attach two main products from the same group ${product.group}`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
+ }
- return products;
- }
+ return products;
+ }
- return [];
+ return [];
};
const getCustomerAndProducts = async ({
- req,
- db,
- org,
- features,
- customerId,
- customerData,
- productId,
- productIds,
+ req,
+ db,
+ org,
+ features,
+ customerId,
+ customerData,
+ productId,
+ productIds,
- env,
- logger,
- version,
- entityId,
- entityData,
+ env,
+ logger,
+ version,
+ entityId,
+ entityData,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- org: Organization;
- features: Feature[];
- customerData?: CustomerData;
- customerId: string;
- productId?: string;
- productIds?: string[];
- env: AppEnv;
- logger: any;
- version?: number;
- entityId?: string;
- entityData?: EntityData;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ org: Organization;
+ features: Feature[];
+ customerData?: CustomerData;
+ customerId: string;
+ productId?: string;
+ productIds?: string[];
+ env: AppEnv;
+ logger: any;
+ version?: number;
+ entityId?: string;
+ entityData?: EntityData;
}) => {
- const [customer, products] = await Promise.all([
- getOrCreateCustomer({
- req,
- customerId,
- customerData,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.Scheduled,
- CusProductStatus.PastDue,
- ],
- withEntities: true,
- entityId,
- entityData,
- }),
- getProducts({
- db,
- productId,
- productIds,
- orgId: org.id,
- env,
- version,
- }),
- ]);
+ const [customer, products] = await Promise.all([
+ getOrCreateCustomer({
+ req,
+ customerId,
+ customerData,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.Scheduled,
+ CusProductStatus.PastDue,
+ ],
+ withEntities: true,
+ entityId,
+ entityData,
+ }),
+ getProducts({
+ db,
+ productId,
+ productIds,
+ orgId: org.id,
+ env,
+ version,
+ }),
+ ]);
- let cusProducts = customer.customer_products;
+ let cusProducts = customer.customer_products;
- return { customer, cusProducts, products };
+ return { customer, cusProducts, products };
};
const getEntsWithFeature = (ents: Entitlement[], features: Feature[]) => {
- return ents.map((ent) => ({
- ...ent,
- feature: features.find(
- (f) => f.internal_id === ent.internal_feature_id,
- ) as Feature,
- }));
+ return ents.map((ent) => ({
+ ...ent,
+ feature: features.find(
+ (f) => f.internal_id === ent.internal_feature_id,
+ ) as Feature,
+ }));
};
const mapOptionsList = ({
- optionsListInput,
- features,
- prices,
+ optionsListInput,
+ features,
+ prices,
}: {
- optionsListInput: FeatureOptions[];
- features: Feature[];
- prices: Price[];
+ optionsListInput: FeatureOptions[];
+ features: Feature[];
+ prices: Price[];
}) => {
- let newOptionsList: FeatureOptions[] = [];
- for (const options of optionsListInput) {
- const feature = features.find(
- (feature) => feature.id === options.feature_id,
- );
+ let newOptionsList: FeatureOptions[] = [];
+ for (const options of optionsListInput) {
+ const feature = features.find(
+ (feature) => feature.id === options.feature_id,
+ );
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${options.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 400,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${options.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 400,
+ });
+ }
- let quantity = options?.quantity;
- if (!nullish(quantity)) {
- const prepaidPrice = prices.find(
- (p) =>
- getBillingType(p.config!) == BillingType.UsageInAdvance &&
- feature.internal_id ==
- (p.config as UsagePriceConfig).internal_feature_id,
- );
+ let quantity = options?.quantity;
+ if (!nullish(quantity)) {
+ const prepaidPrice = prices.find(
+ (p) =>
+ getBillingType(p.config!) == BillingType.UsageInAdvance &&
+ feature.internal_id ==
+ (p.config as UsagePriceConfig).internal_feature_id,
+ );
- if (!prepaidPrice) {
- throw new RecaseError({
- message: `No prepaid price found for feature ${feature.id}`,
- code: ErrCode.FeatureNotFound,
- statusCode: 400,
- });
- }
+ if (!prepaidPrice) {
+ throw new RecaseError({
+ message: `No prepaid price found for feature ${feature.id}`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 400,
+ });
+ }
- let config = prepaidPrice.config as UsagePriceConfig;
+ let config = prepaidPrice.config as UsagePriceConfig;
- let dividedQuantity = new Decimal(options.quantity!)
- .div(config.billing_units || 1)
- .ceil()
- .toNumber();
+ let dividedQuantity = new Decimal(options.quantity!)
+ .div(config.billing_units || 1)
+ .ceil()
+ .toNumber();
- quantity = dividedQuantity;
- }
+ quantity = dividedQuantity;
+ }
- newOptionsList.push({
- ...options,
- internal_feature_id: feature.internal_id,
- quantity,
- });
- }
+ newOptionsList.push({
+ ...options,
+ internal_feature_id: feature.internal_id,
+ quantity,
+ });
+ }
- return newOptionsList;
+ return newOptionsList;
};
export const getFullCusProductData = async ({
- req,
- db,
- org,
- features,
- customerId,
- customerData,
- productId,
- entityId,
- productIds,
- itemsInput,
- env,
- optionsListInput,
- freeTrialInput,
- isCustom = false,
- logger,
- version,
- entityData,
+ req,
+ db,
+ org,
+ features,
+ customerId,
+ customerData,
+ productId,
+ entityId,
+ productIds,
+ itemsInput,
+ env,
+ optionsListInput,
+ freeTrialInput,
+ isCustom = false,
+ logger,
+ version,
+ entityData,
}: {
- req: ExtendedRequest;
- db: DrizzleCli;
- org: Organization;
- features: Feature[];
- customerId: string;
- customerData?: Customer;
- productId?: string;
- productIds?: string[];
- itemsInput: ProductItem[];
- env: AppEnv;
- optionsListInput: FeatureOptions[];
- freeTrialInput: FreeTrial | null;
- isCustom?: boolean;
- logger: any;
- version?: number;
- entityId?: string;
- entityData?: EntityData;
+ req: ExtendedRequest;
+ db: DrizzleCli;
+ org: Organization;
+ features: Feature[];
+ customerId: string;
+ customerData?: Customer;
+ productId?: string;
+ productIds?: string[];
+ itemsInput: ProductItem[];
+ env: AppEnv;
+ optionsListInput: FeatureOptions[];
+ freeTrialInput: FreeTrial | null;
+ isCustom?: boolean;
+ logger: any;
+ version?: number;
+ entityId?: string;
+ entityData?: EntityData;
}) => {
- // 1. Get customer, product, org & features
- const { customer, products, cusProducts } = await getCustomerAndProducts({
- req,
- db,
- org,
- features,
- customerId,
- customerData,
- productId,
- productIds,
- env,
- logger,
- version,
+ // 1. Get customer, product, org & features
+ const { customer, products, cusProducts } = await getCustomerAndProducts({
+ req,
+ db,
+ org,
+ features,
+ customerId,
+ customerData,
+ productId,
+ productIds,
+ env,
+ logger,
+ version,
- entityId,
- entityData,
- });
+ entityId,
+ entityData,
+ });
- if (!isCustom) {
- let freeTrial = null;
- let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
+ if (!isCustom) {
+ let freeTrial = null;
+ let freeTrialProduct = products.find((p) => notNullish(p.free_trial));
- if (freeTrialProduct) {
- freeTrial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: freeTrialProduct.free_trial,
- productId: freeTrialProduct.id,
- fingerprint: customer.fingerprint,
- internalCustomerId: customer.internal_id,
- multipleAllowed: org.config.multiple_trials,
- });
- }
+ if (freeTrialProduct) {
+ freeTrial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: freeTrialProduct.free_trial,
+ productId: freeTrialProduct.id,
+ fingerprint: customer.fingerprint,
+ internalCustomerId: customer.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ });
+ }
- return {
- customer,
- products,
- org,
- features,
- optionsList: mapOptionsList({
- optionsListInput,
- features,
- prices: products.map((p) => p.prices).flat() as Price[],
- }),
- prices: products.map((p) => p.prices).flat() as Price[],
- entitlements: products
- .map((p) => getEntsWithFeature(p.entitlements, features))
- .flat() as EntitlementWithFeature[],
- freeTrial,
- cusProducts,
- entities: customer.entities,
- entityId: entityId,
- internalEntityId: entityId
- ? customer.entities.find(
- (e) => e.id === entityId || e.internal_id === entityId,
- )?.internal_id
- : undefined,
- };
- }
+ return {
+ customer,
+ products,
+ org,
+ features,
+ optionsList: mapOptionsList({
+ optionsListInput,
+ features,
+ prices: products.map((p) => p.prices).flat() as Price[],
+ }),
+ prices: products.map((p) => p.prices).flat() as Price[],
+ entitlements: products
+ .map((p) => getEntsWithFeature(p.entitlements, features))
+ .flat() as EntitlementWithFeature[],
+ freeTrial,
+ cusProducts,
+ entities: customer.entities,
+ entityId: entityId,
+ internalEntityId: entityId
+ ? customer.entities.find(
+ (e) => e.id === entityId || e.internal_id === entityId,
+ )?.internal_id
+ : undefined,
+ };
+ }
- if (products.length > 1) {
- throw new RecaseError({
- message: "Cannot attach multiple products when is_custom is true",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (products.length > 1) {
+ throw new RecaseError({
+ message: "Cannot attach multiple products when is_custom is true",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Get cur main product
- const product = products[0];
+ // Get cur main product
+ const product = products[0];
- const { curMainProduct } = getExistingCusProducts({
- product,
- cusProducts,
- });
+ const { curMainProduct } = getExistingCusProducts({
+ product,
+ cusProducts,
+ });
- let curPrices: Price[] = product!.prices;
- let curEnts: Entitlement[] = product!.entitlements.map((e: Entitlement) => {
- return {
- ...e,
- feature: features.find((f) => f.internal_id === e.internal_feature_id),
- };
- });
+ let curPrices: Price[] = product!.prices;
+ let curEnts: Entitlement[] = product!.entitlements.map((e: Entitlement) => {
+ return {
+ ...e,
+ feature: features.find((f) => f.internal_id === e.internal_feature_id),
+ };
+ });
- if (curMainProduct?.product.id === product.id) {
- curPrices = getPricesForCusProduct({
- cusProduct: curMainProduct as FullCusProduct,
- });
+ if (curMainProduct?.product.id === product.id) {
+ curPrices = getPricesForCusProduct({
+ cusProduct: curMainProduct as FullCusProduct,
+ });
- curEnts = curMainProduct!.customer_entitlements.map((e) => e.entitlement);
- }
+ curEnts = curMainProduct!.customer_entitlements.map((e) => e.entitlement);
+ }
- let { prices, entitlements } = await handleNewProductItems({
- db,
- curPrices,
- curEnts,
- newItems: itemsInput,
- features,
- product,
- logger,
- isCustom: true,
- });
+ let { prices, entitlements } = await handleNewProductItems({
+ db,
+ curPrices,
+ curEnts,
+ newItems: itemsInput,
+ features,
+ product,
+ logger,
+ isCustom: true,
+ });
- const freeTrial = await handleNewFreeTrial({
- db,
- curFreeTrial: product!.free_trial,
- newFreeTrial: freeTrialInput || null,
- internalProductId: product!.internal_id,
- isCustom,
- });
+ const freeTrial = await handleNewFreeTrial({
+ db,
+ curFreeTrial: product!.free_trial,
+ newFreeTrial: freeTrialInput || null,
+ internalProductId: product!.internal_id,
+ isCustom,
+ });
- const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: freeTrial,
- productId: product.id,
- fingerprint: customer.fingerprint,
- internalCustomerId: customer.internal_id,
- multipleAllowed: org.config.multiple_trials,
- });
+ const uniqueFreeTrial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: freeTrial,
+ productId: product.id,
+ fingerprint: customer.fingerprint,
+ internalCustomerId: customer.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ });
- return {
- customer,
- products,
- org,
- features,
- optionsList: mapOptionsList({
- optionsListInput,
- features,
- prices,
- }),
- prices: prices as Price[],
- entitlements: entitlements as EntitlementWithFeature[],
- freeTrial: uniqueFreeTrial,
- cusProducts,
- entities: customer.entities,
- entityId: entityId,
- internalEntityId: entityId
- ? customer.entities.find(
- (e) => e.id === entityId || e.internal_id === entityId,
- )?.internal_id
- : undefined,
- };
+ return {
+ customer,
+ products,
+ org,
+ features,
+ optionsList: mapOptionsList({
+ optionsListInput,
+ features,
+ prices,
+ }),
+ prices: prices as Price[],
+ entitlements: entitlements as EntitlementWithFeature[],
+ freeTrial: uniqueFreeTrial,
+ cusProducts,
+ entities: customer.entities,
+ entityId: entityId,
+ internalEntityId: entityId
+ ? customer.entities.find(
+ (e) => e.id === entityId || e.internal_id === entityId,
+ )?.internal_id
+ : undefined,
+ };
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts
index a66978036..eadf7967e 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/CusEntitlementService.ts
@@ -1,22 +1,22 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- AppEnv,
- CusProduct,
- CusProductStatus,
- Customer,
- CustomerEntitlement,
- customerPrices,
- customers,
- entitlements,
- ErrCode,
- features,
- FullCusEntWithProduct,
- FullCustomerEntitlement,
- prices,
- Replaceable,
- ResetCusEnt,
- Rollover,
+ AppEnv,
+ CusProduct,
+ CusProductStatus,
+ Customer,
+ CustomerEntitlement,
+ customerPrices,
+ customers,
+ entitlements,
+ ErrCode,
+ features,
+ FullCusEntWithProduct,
+ FullCustomerEntitlement,
+ prices,
+ Replaceable,
+ ResetCusEnt,
+ Rollover,
} from "@autumn/shared";
import { customerEntitlements } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
@@ -25,229 +25,232 @@ import { customerProducts } from "@autumn/shared";
import { buildConflictUpdateColumns } from "@/db/dbUtils.js";
export class CusEntService {
- static async upsert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: CustomerEntitlement[];
- }) {
- if (Array.isArray(data) && data.length == 0) return;
+ static async upsert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: CustomerEntitlement[];
+ }) {
+ if (Array.isArray(data) && data.length == 0) return;
- const updateColumns = buildConflictUpdateColumns(customerEntitlements, [
- "id",
- ]);
- await db
- .insert(customerEntitlements)
- .values(data as any)
- .onConflictDoUpdate({
- target: customerEntitlements.id,
- set: updateColumns,
- });
- }
+ const updateColumns = buildConflictUpdateColumns(customerEntitlements, [
+ "id",
+ ]);
+ await db
+ .insert(customerEntitlements)
+ .values(data as any)
+ .onConflictDoUpdate({
+ target: customerEntitlements.id,
+ set: updateColumns,
+ });
+ }
- static async getByFeature({
- db,
- internalFeatureId,
- }: {
- db: DrizzleCli;
- internalFeatureId: string;
- }) {
- const data = await db
- .select()
- .from(customerEntitlements)
- .where(eq(customerEntitlements.internal_feature_id, internalFeatureId))
- .limit(10);
+ static async getByFeature({
+ db,
+ internalFeatureId,
+ }: {
+ db: DrizzleCli;
+ internalFeatureId: string;
+ }) {
+ const data = await db
+ .select()
+ .from(customerEntitlements)
+ .where(eq(customerEntitlements.internal_feature_id, internalFeatureId))
+ .limit(10);
- return data as FullCustomerEntitlement[];
- }
+ return data as FullCustomerEntitlement[];
+ }
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: CustomerEntitlement[];
- }) {
- if (Array.isArray(data) && data.length === 0) {
- return;
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: CustomerEntitlement[];
+ }) {
+ if (Array.isArray(data) && data.length === 0) {
+ return;
+ }
- await db.insert(customerEntitlements).values(data as any); // DRIZZLE TYPE REFACTOR
- }
+ await db.insert(customerEntitlements).values(data as any); // DRIZZLE TYPE REFACTOR
+ }
- static async getActiveResetPassed({
- db,
- customDateUnix,
- batchSize = 1000,
- }: {
- db: DrizzleCli;
- customDateUnix?: number;
- batchSize?: number;
- }) {
- const allResults: FullCusEntWithProduct[] = [];
- let offset = 0;
- let hasMore = true;
+ static async getActiveResetPassed({
+ db,
+ customDateUnix,
+ batchSize = 1000,
+ }: {
+ db: DrizzleCli;
+ customDateUnix?: number;
+ batchSize?: number;
+ }) {
+ const allResults: FullCusEntWithProduct[] = [];
+ let offset = 0;
+ let hasMore = true;
- while (hasMore) {
- const data = await db
- .select()
- .from(customerEntitlements)
- .innerJoin(
- customerProducts,
- eq(customerEntitlements.customer_product_id, customerProducts.id)
- )
- .innerJoin(
- entitlements,
- eq(customerEntitlements.entitlement_id, entitlements.id)
- )
- .innerJoin(
- features,
- eq(entitlements.internal_feature_id, features.internal_id)
- )
- .innerJoin(
- customers,
- eq(customerEntitlements.internal_customer_id, customers.internal_id)
- )
- .where(
- and(
- eq(customerProducts.status, CusProductStatus.Active),
- lt(customerEntitlements.next_reset_at, customDateUnix ?? Date.now())
- )
- )
- .limit(batchSize)
- .offset(offset);
+ while (hasMore) {
+ const data = await db
+ .select()
+ .from(customerEntitlements)
+ .innerJoin(
+ customerProducts,
+ eq(customerEntitlements.customer_product_id, customerProducts.id),
+ )
+ .innerJoin(
+ entitlements,
+ eq(customerEntitlements.entitlement_id, entitlements.id),
+ )
+ .innerJoin(
+ features,
+ eq(entitlements.internal_feature_id, features.internal_id),
+ )
+ .innerJoin(
+ customers,
+ eq(customerEntitlements.internal_customer_id, customers.internal_id),
+ )
+ .where(
+ and(
+ eq(customerProducts.status, CusProductStatus.Active),
+ lt(
+ customerEntitlements.next_reset_at,
+ customDateUnix ?? Date.now(),
+ ),
+ ),
+ )
+ .limit(batchSize)
+ .offset(offset);
- if (data.length === 0) {
- hasMore = false;
- } else {
- const mappedData = data.map((item) => ({
- ...item.customer_entitlements,
- entitlement: {
- ...item.entitlements,
- feature: item.features,
- },
- customer_product: item.customer_products,
- customer: item.customers,
- replaceables: [],
- rollovers: [],
- })) as ResetCusEnt[];
+ if (data.length === 0) {
+ hasMore = false;
+ } else {
+ const mappedData = data.map((item) => ({
+ ...item.customer_entitlements,
+ entitlement: {
+ ...item.entitlements,
+ feature: item.features,
+ },
+ customer_product: item.customer_products,
+ customer: item.customers,
+ replaceables: [],
+ rollovers: [],
+ })) as ResetCusEnt[];
- allResults.push(...mappedData);
- offset += batchSize;
- hasMore = data.length === batchSize;
- console.log(`Fetched ${allResults.length} entitlements to reset`);
- }
- }
+ allResults.push(...mappedData);
+ offset += batchSize;
+ hasMore = data.length === batchSize;
+ console.log(`Fetched ${allResults.length} entitlements to reset`);
+ }
+ }
- return allResults as ResetCusEnt[];
- }
+ return allResults as ResetCusEnt[];
+ }
- static async update({
- db,
- id,
- updates,
- }: {
- db: DrizzleCli;
- id: string;
- updates: Partial;
- }) {
- const data = await db
- .update(customerEntitlements)
- .set(updates as any)
- .where(eq(customerEntitlements.id, id))
- .returning();
+ static async update({
+ db,
+ id,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ updates: Partial;
+ }) {
+ const data = await db
+ .update(customerEntitlements)
+ .set(updates as any)
+ .where(eq(customerEntitlements.id, id))
+ .returning();
- return data;
- }
+ return data;
+ }
- static async getStrict({
- db,
- id,
- orgId,
- env,
- withCusProduct,
- }: {
- db: DrizzleCli;
- id: string;
- orgId: string;
- env: AppEnv;
- withCusProduct?: boolean;
- }) {
- const data = await db.query.customerEntitlements.findFirst({
- where: eq(customerEntitlements.id, id),
- with: {
- entitlement: {
- with: {
- feature: true,
- },
- },
- replaceables: true,
- rollovers: true,
- customer_product: withCusProduct || undefined,
- customer: true,
- },
- });
+ static async getStrict({
+ db,
+ id,
+ orgId,
+ env,
+ withCusProduct,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ orgId: string;
+ env: AppEnv;
+ withCusProduct?: boolean;
+ }) {
+ const data = await db.query.customerEntitlements.findFirst({
+ where: eq(customerEntitlements.id, id),
+ with: {
+ entitlement: {
+ with: {
+ feature: true,
+ },
+ },
+ replaceables: true,
+ rollovers: true,
+ customer_product: withCusProduct || undefined,
+ customer: true,
+ },
+ });
- if (
- !data ||
- !data.customer ||
- data.customer.org_id !== orgId ||
- data.customer.env !== env
- ) {
- throw new RecaseError({
- message: "Customer entitlement not found",
- code: ErrCode.CustomerEntitlementNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (
+ !data ||
+ !data.customer ||
+ data.customer.org_id !== orgId ||
+ data.customer.env !== env
+ ) {
+ throw new RecaseError({
+ message: "Customer entitlement not found",
+ code: ErrCode.CustomerEntitlementNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- return data as FullCustomerEntitlement & {
- customer: Customer;
- customer_product?: CusProduct;
- // replaceables?: Replaceable[];
- };
- }
+ return data as FullCustomerEntitlement & {
+ customer: Customer;
+ customer_product?: CusProduct;
+ // replaceables?: Replaceable[];
+ };
+ }
- static async increment({
- db,
- id,
- amount,
- }: {
- db: DrizzleCli;
- id: string;
- amount: number;
- }) {
- const data = await db
- .update(customerEntitlements)
- .set({ balance: sql`${customerEntitlements.balance} + ${amount}` })
- .where(eq(customerEntitlements.id, id))
- .returning();
+ static async increment({
+ db,
+ id,
+ amount,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ amount: number;
+ }) {
+ const data = await db
+ .update(customerEntitlements)
+ .set({ balance: sql`${customerEntitlements.balance} + ${amount}` })
+ .where(eq(customerEntitlements.id, id))
+ .returning();
- return data;
- }
+ return data;
+ }
- static async decrement({
- db,
- id,
- amount,
- }: {
- db: DrizzleCli;
- id: string;
- amount: number;
- }) {
- const data = await db
- .update(customerEntitlements)
- .set({ balance: sql`${customerEntitlements.balance} - ${amount}` })
- .where(eq(customerEntitlements.id, id))
- .returning();
+ static async decrement({
+ db,
+ id,
+ amount,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ amount: number;
+ }) {
+ const data = await db
+ .update(customerEntitlements)
+ .set({ balance: sql`${customerEntitlements.balance} - ${amount}` })
+ .where(eq(customerEntitlements.id, id))
+ .returning();
- return data;
- }
+ return data;
+ }
- static async delete({ db, id }: { db: DrizzleCli; id: string }) {
- await db
- .delete(customerEntitlements)
- .where(eq(customerEntitlements.id, id));
- }
+ static async delete({ db, id }: { db: DrizzleCli; id: string }) {
+ await db
+ .delete(customerEntitlements)
+ .where(eq(customerEntitlements.id, id));
+ }
}
diff --git a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts
index 2abddbc40..110378618 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/RepService.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/RepService.ts
@@ -3,41 +3,41 @@ import { InsertReplaceable, Replaceable, replaceables } from "@autumn/shared";
import { eq, inArray } from "drizzle-orm";
export class RepService {
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: InsertReplaceable[];
- }) {
- if (data.length === 0) return [];
- const inserted = await db.insert(replaceables).values(data).returning();
- return inserted as Replaceable[];
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: InsertReplaceable[];
+ }) {
+ if (data.length === 0) return [];
+ const inserted = await db.insert(replaceables).values(data).returning();
+ return inserted as Replaceable[];
+ }
- static async update({
- db,
- id,
- data,
- }: {
- db: DrizzleCli;
- id: string;
- data: any;
- }) {
- const updated = await db
- .update(replaceables)
- .set(data)
- .where(eq(replaceables.id, id))
- .returning();
- return updated as Replaceable[];
- }
+ static async update({
+ db,
+ id,
+ data,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ data: any;
+ }) {
+ const updated = await db
+ .update(replaceables)
+ .set(data)
+ .where(eq(replaceables.id, id))
+ .returning();
+ return updated as Replaceable[];
+ }
- static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- if (ids.length === 0) return [];
- const deleted = await db
- .delete(replaceables)
- .where(inArray(replaceables.id, ids))
- .returning();
- return deleted as Replaceable[];
- }
+ static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ if (ids.length === 0) return [];
+ const deleted = await db
+ .delete(replaceables)
+ .where(inArray(replaceables.id, ids))
+ .returning();
+ return deleted as Replaceable[];
+ }
}
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts
index db21c72e5..0b60720c4 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntHelpers.ts
@@ -3,17 +3,17 @@
import { Feature, FullCustomerEntitlement } from "@autumn/shared";
export const getMinNextResetAtCusEnt = ({
- cusEnts,
- feature,
+ cusEnts,
+ feature,
}: {
- cusEnts: FullCustomerEntitlement[];
- feature: Feature;
+ cusEnts: FullCustomerEntitlement[];
+ feature: Feature;
}) => {
- return cusEnts
- .filter(
- (cusEnt) => cusEnt.entitlement.internal_feature_id == feature.internal_id
- )
- .reduce((min, cusEnt) => {
- return Math.min(min, cusEnt.next_reset_at || Infinity);
- }, Infinity);
+ return cusEnts
+ .filter(
+ (cusEnt) => cusEnt.entitlement.internal_feature_id == feature.internal_id,
+ )
+ .reduce((min, cusEnt) => {
+ return Math.min(min, cusEnt.next_reset_at || Infinity);
+ }, Infinity);
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts
index ffb460abc..ce88bac38 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts
@@ -1,476 +1,476 @@
import {
- AllowanceType,
- AppEnv,
- BillingType,
- CusProductStatus,
- Customer,
- EntInterval,
- entIntervalsDifferent,
- entIntervalToValue,
- Entitlement,
- EntitlementWithFeature,
- Entity,
- Feature,
- FeatureOptions,
- FeatureType,
- FullCusProduct,
- FullCustomerEntitlement,
- FullCustomerPrice,
- Organization,
- Price,
- UsagePriceConfig,
+ AllowanceType,
+ AppEnv,
+ BillingType,
+ CusProductStatus,
+ Customer,
+ EntInterval,
+ entIntervalsDifferent,
+ entIntervalToValue,
+ Entitlement,
+ EntitlementWithFeature,
+ Entity,
+ Feature,
+ FeatureOptions,
+ FeatureType,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ Organization,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import {
- getBillingType,
- getEntOptions,
+ getBillingType,
+ getEntOptions,
} from "@/internal/products/prices/priceUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- getEntityBalance,
- getSummedEntityBalances,
+ getEntityBalance,
+ getSummedEntityBalances,
} from "./entBalanceUtils.js";
import { Decimal } from "decimal.js";
import { logger } from "better-auth";
export const getCusEntMasterBalance = ({
- cusEnt,
- entities,
+ cusEnt,
+ entities,
}: {
- cusEnt: FullCustomerEntitlement;
- entities: Entity[];
+ cusEnt: FullCustomerEntitlement;
+ entities: Entity[];
}) => {
- let ent = cusEnt.entitlement;
- let feature = ent.feature;
+ let ent = cusEnt.entitlement;
+ let feature = ent.feature;
- if (notNullish(ent.entity_feature_id)) {
- let totalBalance = Object.values(cusEnt.entities || {}).reduce(
- (acc, curr) => {
- return acc + curr.balance;
- },
- 0
- );
+ if (notNullish(ent.entity_feature_id)) {
+ let totalBalance = Object.values(cusEnt.entities || {}).reduce(
+ (acc, curr) => {
+ return acc + curr.balance;
+ },
+ 0,
+ );
- let totalAdjustment = Object.values(cusEnt.entities || {}).reduce(
- (acc, curr) => {
- return acc + curr.adjustment;
- },
- 0
- );
+ let totalAdjustment = Object.values(cusEnt.entities || {}).reduce(
+ (acc, curr) => {
+ return acc + curr.adjustment;
+ },
+ 0,
+ );
- return {
- balance: totalBalance,
- adjustment: totalAdjustment,
- count: Object.values(cusEnt.entities || {}).length,
- };
- }
+ return {
+ balance: totalBalance,
+ adjustment: totalAdjustment,
+ count: Object.values(cusEnt.entities || {}).length,
+ };
+ }
- // Get unused count
+ // Get unused count
- let unusedCount =
- entities &&
- entities.filter(
- (entity) =>
- entity.internal_feature_id == feature.internal_id && entity.deleted
- ).length;
+ let unusedCount =
+ entities &&
+ entities.filter(
+ (entity) =>
+ entity.internal_feature_id == feature.internal_id && entity.deleted,
+ ).length;
- return {
- balance: cusEnt.balance,
- adjustment: cusEnt.adjustment,
- count: 1,
- unused: unusedCount,
- };
+ return {
+ balance: cusEnt.balance,
+ adjustment: cusEnt.adjustment,
+ count: 1,
+ unused: unusedCount,
+ };
};
export const getCusEntBalance = ({
- cusEnt,
- entityId,
+ cusEnt,
+ entityId,
}: {
- cusEnt: FullCustomerEntitlement;
- entityId?: string | null;
+ cusEnt: FullCustomerEntitlement;
+ entityId?: string | null;
}) => {
- let entitlement = cusEnt.entitlement;
- let ent = cusEnt.entitlement;
- let feature = ent.feature;
+ let entitlement = cusEnt.entitlement;
+ let ent = cusEnt.entitlement;
+ let feature = ent.feature;
- if (notNullish(entitlement.entity_feature_id)) {
- if (nullish(entityId)) {
- return getSummedEntityBalances({
- cusEnt,
- });
- }
+ if (notNullish(entitlement.entity_feature_id)) {
+ if (nullish(entityId)) {
+ return getSummedEntityBalances({
+ cusEnt,
+ });
+ }
- return {
- ...getEntityBalance({
- cusEnt,
- entityId: entityId!,
- }),
- unused: 0,
- count: 1,
- };
- }
+ return {
+ ...getEntityBalance({
+ cusEnt,
+ entityId: entityId!,
+ }),
+ unused: 0,
+ count: 1,
+ };
+ }
- return {
- balance: cusEnt.balance,
- adjustment: cusEnt.adjustment,
- unused: cusEnt.replaceables?.length || 0,
- count: 1,
- };
+ return {
+ balance: cusEnt.balance,
+ adjustment: cusEnt.adjustment,
+ unused: cusEnt.replaceables?.length || 0,
+ count: 1,
+ };
};
// Get related cusPrice
export const getRelatedCusPrice = (
- cusEnt: FullCustomerEntitlement,
- cusPrices: FullCustomerPrice[]
+ cusEnt: FullCustomerEntitlement,
+ cusPrices: FullCustomerPrice[],
) => {
- return cusPrices.find((cusPrice) => {
- let productMatch =
- cusPrice.customer_product_id == cusEnt.customer_product_id;
+ return cusPrices.find((cusPrice) => {
+ let productMatch =
+ cusPrice.customer_product_id == cusEnt.customer_product_id;
- let entMatch = cusPrice.price.entitlement_id == cusEnt.entitlement.id;
+ let entMatch = cusPrice.price.entitlement_id == cusEnt.entitlement.id;
- return productMatch && entMatch;
- });
+ return productMatch && entMatch;
+ });
};
// 3. Perform deductions and update customer balance
export const updateCusEntInStripe = async ({
- cusEnt,
- cusPrices,
- org,
- env,
- customer,
- amountUsed,
- eventId,
+ cusEnt,
+ cusPrices,
+ org,
+ env,
+ customer,
+ amountUsed,
+ eventId,
}: {
- cusEnt: FullCustomerEntitlement;
- cusPrices: FullCustomerPrice[];
- org: Organization;
- env: AppEnv;
- customer: Customer;
- amountUsed: number;
- eventId: string;
+ cusEnt: FullCustomerEntitlement;
+ cusPrices: FullCustomerPrice[];
+ org: Organization;
+ env: AppEnv;
+ customer: Customer;
+ amountUsed: number;
+ eventId: string;
}) => {
- const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
+ const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
- if (!relatedCusPrice) {
- return;
- }
+ if (!relatedCusPrice) {
+ return;
+ }
- // Send event to Stripe
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ // Send event to Stripe
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- await stripeCli.billing.meterEvents.create({
- event_name: relatedCusPrice.price.id!,
- payload: {
- stripe_customer_id: customer.processor.id,
- value: amountUsed.toString(),
- },
- identifier: eventId,
- });
- console.log(` ✅ Stripe event sent, amount: (${amountUsed})`);
+ await stripeCli.billing.meterEvents.create({
+ event_name: relatedCusPrice.price.id!,
+ payload: {
+ stripe_customer_id: customer.processor.id,
+ value: amountUsed.toString(),
+ },
+ identifier: eventId,
+ });
+ console.log(` ✅ Stripe event sent, amount: (${amountUsed})`);
};
// Get balance
export const getResetBalance = ({
- entitlement,
- options,
- relatedPrice,
- productQuantity,
+ entitlement,
+ options,
+ relatedPrice,
+ productQuantity,
}: {
- entitlement: Entitlement;
- options: FeatureOptions | undefined | null;
- relatedPrice?: Price | null;
- productQuantity?: number;
+ entitlement: Entitlement;
+ options: FeatureOptions | undefined | null;
+ relatedPrice?: Price | null;
+ productQuantity?: number;
}) => {
- // 1. No related price
- if (!relatedPrice) {
- return (entitlement.allowance || 0) * (productQuantity || 1);
- }
+ // 1. No related price
+ if (!relatedPrice) {
+ return (entitlement.allowance || 0) * (productQuantity || 1);
+ }
- let config = relatedPrice.config as UsagePriceConfig;
+ let config = relatedPrice.config as UsagePriceConfig;
- let billingType = getBillingType(config);
- if (billingType != BillingType.UsageInAdvance) {
- return entitlement.allowance || 0;
- }
+ let billingType = getBillingType(config);
+ if (billingType != BillingType.UsageInAdvance) {
+ return entitlement.allowance || 0;
+ }
- let quantity = options?.quantity;
- let billingUnits = (relatedPrice.config as UsagePriceConfig).billing_units;
- if (nullish(quantity) || nullish(billingUnits)) {
- return entitlement.allowance || 0;
- }
+ let quantity = options?.quantity;
+ let billingUnits = (relatedPrice.config as UsagePriceConfig).billing_units;
+ if (nullish(quantity) || nullish(billingUnits)) {
+ return entitlement.allowance || 0;
+ }
- try {
- return (entitlement.allowance || 0) + quantity! * billingUnits!;
- } catch (error) {
- console.log(
- "WARNING: Failed to return quantity * billing units, returning allowance..."
- );
- return entitlement.allowance || 0;
- }
+ try {
+ return (entitlement.allowance || 0) + quantity! * billingUnits!;
+ } catch (error) {
+ console.log(
+ "WARNING: Failed to return quantity * billing units, returning allowance...",
+ );
+ return entitlement.allowance || 0;
+ }
};
export const getUnlimitedAndUsageAllowed = ({
- cusEnts,
- internalFeatureId,
- includeUsageLimit = true,
+ cusEnts,
+ internalFeatureId,
+ includeUsageLimit = true,
}: {
- cusEnts: FullCustomerEntitlement[];
- internalFeatureId: string;
- includeUsageLimit?: boolean;
+ cusEnts: FullCustomerEntitlement[];
+ internalFeatureId: string;
+ includeUsageLimit?: boolean;
}) => {
- // Unlimited
+ // Unlimited
- const unlimited = cusEnts.some(
- (cusEnt) =>
- cusEnt.internal_feature_id === internalFeatureId &&
- (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited ||
- cusEnt.unlimited)
- );
+ const unlimited = cusEnts.some(
+ (cusEnt) =>
+ cusEnt.internal_feature_id === internalFeatureId &&
+ (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited ||
+ cusEnt.unlimited),
+ );
- const usageAllowed = cusEnts.some(
- (ent) =>
- ent.internal_feature_id === internalFeatureId &&
- ent.usage_allowed &&
- (includeUsageLimit ? nullish(ent.entitlement.usage_limit) : true)
- );
+ const usageAllowed = cusEnts.some(
+ (ent) =>
+ ent.internal_feature_id === internalFeatureId &&
+ ent.usage_allowed &&
+ (includeUsageLimit ? nullish(ent.entitlement.usage_limit) : true),
+ );
- return { unlimited, usageAllowed };
+ return { unlimited, usageAllowed };
};
export const getFeatureBalance = ({
- cusEnts,
- internalFeatureId,
- entityId,
+ cusEnts,
+ internalFeatureId,
+ entityId,
}: {
- cusEnts: FullCustomerEntitlement[];
- internalFeatureId: string;
- entityId?: string;
+ cusEnts: FullCustomerEntitlement[];
+ internalFeatureId: string;
+ entityId?: string;
}) => {
- let balance = 0;
+ let balance = 0;
- const { unlimited } = getUnlimitedAndUsageAllowed({
- cusEnts,
- internalFeatureId,
- });
+ const { unlimited } = getUnlimitedAndUsageAllowed({
+ cusEnts,
+ internalFeatureId,
+ });
- if (unlimited) {
- return null;
- }
+ if (unlimited) {
+ return null;
+ }
- for (const cusEnt of cusEnts) {
- if (cusEnt.internal_feature_id !== internalFeatureId) {
- continue;
- }
+ for (const cusEnt of cusEnts) {
+ if (cusEnt.internal_feature_id !== internalFeatureId) {
+ continue;
+ }
- if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
- return null;
- }
+ if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
+ return null;
+ }
- // 1. If feature entity exists...
- let cusEntBalance = cusEnt.balance!;
+ // 1. If feature entity exists...
+ let cusEntBalance = cusEnt.balance!;
- // If entity feature id exists, then it is grouped...
- let entityFeatureId = cusEnt.entitlement.entity_feature_id;
+ // If entity feature id exists, then it is grouped...
+ let entityFeatureId = cusEnt.entitlement.entity_feature_id;
- if (notNullish(entityFeatureId)) {
- if (notNullish(entityId)) {
- let { balance: entityBalance } = getEntityBalance({
- cusEnt,
- entityId: entityId!,
- });
- cusEntBalance = entityBalance!;
- } else {
- let summed = getSummedEntityBalances({
- cusEnt,
- });
- cusEntBalance = summed.balance;
- }
- }
+ if (notNullish(entityFeatureId)) {
+ if (notNullish(entityId)) {
+ let { balance: entityBalance } = getEntityBalance({
+ cusEnt,
+ entityId: entityId!,
+ });
+ cusEntBalance = entityBalance!;
+ } else {
+ let summed = getSummedEntityBalances({
+ cusEnt,
+ });
+ cusEntBalance = summed.balance;
+ }
+ }
- balance += cusEntBalance;
+ balance += cusEntBalance;
- // 2. If no entityId provided, use main balance
- }
+ // 2. If no entityId provided, use main balance
+ }
- return balance;
+ return balance;
};
export const getPaidFeatureBalance = ({
- cusEnts,
- internalFeatureId,
+ cusEnts,
+ internalFeatureId,
}: {
- cusEnts: FullCustomerEntitlement[];
- internalFeatureId: string;
+ cusEnts: FullCustomerEntitlement[];
+ internalFeatureId: string;
}) => {
- let paidAllowance = 0;
- try {
- for (const cusEnt of cusEnts) {
- if (cusEnt.internal_feature_id !== internalFeatureId) continue;
+ let paidAllowance = 0;
+ try {
+ for (const cusEnt of cusEnts) {
+ if (cusEnt.internal_feature_id !== internalFeatureId) continue;
- if (notNullish(cusEnt.entitlement.usage_limit)) {
- paidAllowance = new Decimal(paidAllowance)
- .plus(cusEnt.entitlement.usage_limit!)
- .minus(cusEnt.entitlement.allowance || 0)
- .toNumber();
- }
- }
- } catch (error) {
- logger.error(`Failed to get paid feature balance`, { error });
- }
+ if (notNullish(cusEnt.entitlement.usage_limit)) {
+ paidAllowance = new Decimal(paidAllowance)
+ .plus(cusEnt.entitlement.usage_limit!)
+ .minus(cusEnt.entitlement.allowance || 0)
+ .toNumber();
+ }
+ }
+ } catch (error) {
+ logger.error(`Failed to get paid feature balance`, { error });
+ }
- return paidAllowance;
+ return paidAllowance;
};
export const cusEntsContainFeature = ({
- cusEnts,
- feature,
+ cusEnts,
+ feature,
}: {
- cusEnts: FullCustomerEntitlement[];
- feature: Feature;
+ cusEnts: FullCustomerEntitlement[];
+ feature: Feature;
}) => {
- return cusEnts.some(
- (cusEnt) => cusEnt.internal_feature_id === feature.internal_id!
- );
+ return cusEnts.some(
+ (cusEnt) => cusEnt.internal_feature_id === feature.internal_id!,
+ );
};
export const getTotalNegativeBalance = ({
- cusEnt,
- balance,
- entities,
- billingUnits,
+ cusEnt,
+ balance,
+ entities,
+ billingUnits,
}: {
- cusEnt: FullCustomerEntitlement;
- balance: number;
- entities: Record;
- billingUnits?: number;
+ cusEnt: FullCustomerEntitlement;
+ balance: number;
+ entities: Record;
+ billingUnits?: number;
}) => {
- let entityFeatureId = cusEnt.entitlement.entity_feature_id;
+ let entityFeatureId = cusEnt.entitlement.entity_feature_id;
- if (nullish(entityFeatureId)) {
- return balance;
- }
+ if (nullish(entityFeatureId)) {
+ return balance;
+ }
- let totalNegative = 0;
- for (const group in entities) {
- if (entities[group].balance < 0) {
- let balance = entities[group].balance;
- if (billingUnits) {
- balance = new Decimal(balance)
- .div(billingUnits)
- .round()
- .mul(billingUnits)
- .toNumber();
- }
- totalNegative += balance;
- }
- }
+ let totalNegative = 0;
+ for (const group in entities) {
+ if (entities[group].balance < 0) {
+ let balance = entities[group].balance;
+ if (billingUnits) {
+ balance = new Decimal(balance)
+ .div(billingUnits)
+ .round()
+ .mul(billingUnits)
+ .toNumber();
+ }
+ totalNegative += balance;
+ }
+ }
- if (totalNegative == 0) {
- if (Object.values(entities).length > 0) {
- let entityBalances = Object.values(entities).map((e) => e.balance || 0);
- return Math.min(...entityBalances);
- } else {
- return cusEnt.entitlement.allowance || 0;
- }
- }
+ if (totalNegative == 0) {
+ if (Object.values(entities).length > 0) {
+ let entityBalances = Object.values(entities).map((e) => e.balance || 0);
+ return Math.min(...entityBalances);
+ } else {
+ return cusEnt.entitlement.allowance || 0;
+ }
+ }
- return totalNegative;
+ return totalNegative;
};
// GET EXISTING USAGE
export const getExistingUsageFromCusProducts = ({
- entitlement,
- cusProducts,
- entities,
- carryExistingUsages = false,
- internalEntityId,
+ entitlement,
+ cusProducts,
+ entities,
+ carryExistingUsages = false,
+ internalEntityId,
}: {
- entitlement: EntitlementWithFeature;
- cusProducts?: FullCusProduct[];
- entities: Entity[];
- carryExistingUsages?: boolean;
- internalEntityId?: string;
+ entitlement: EntitlementWithFeature;
+ cusProducts?: FullCusProduct[];
+ entities: Entity[];
+ carryExistingUsages?: boolean;
+ internalEntityId?: string;
}) => {
- if (!entitlement || entitlement.feature.type === FeatureType.Boolean) {
- return 0;
- }
+ if (!entitlement || entitlement.feature.type === FeatureType.Boolean) {
+ return 0;
+ }
- // Existing usage should also include entities
- let entityUsage = entities.reduce((acc, entity) => {
- if (entity.internal_feature_id !== entitlement.internal_feature_id) {
- return acc;
- }
+ // Existing usage should also include entities
+ let entityUsage = entities.reduce((acc, entity) => {
+ if (entity.internal_feature_id !== entitlement.internal_feature_id) {
+ return acc;
+ }
- return acc + 1;
- }, 0);
+ return acc + 1;
+ }, 0);
- if (entityUsage > 0) {
- return entityUsage;
- }
+ if (entityUsage > 0) {
+ return entityUsage;
+ }
- let existingUsage = 0;
+ let existingUsage = 0;
- // NOTE: Assuming that feature entitlements are unique to each main product...
- let existingCusEnt = cusProducts
- ?.filter(
- (cp) =>
- (cp.status === CusProductStatus.Active ||
- cp.status === CusProductStatus.PastDue) &&
- !cp.product.is_add_on &&
- (internalEntityId
- ? cp.internal_entity_id === internalEntityId
- : nullish(cp.internal_entity_id))
- )
- .flatMap((cp) => cp.customer_entitlements)
- .find((ce) => ce.internal_feature_id === entitlement.internal_feature_id);
+ // NOTE: Assuming that feature entitlements are unique to each main product...
+ let existingCusEnt = cusProducts
+ ?.filter(
+ (cp) =>
+ (cp.status === CusProductStatus.Active ||
+ cp.status === CusProductStatus.PastDue) &&
+ !cp.product.is_add_on &&
+ (internalEntityId
+ ? cp.internal_entity_id === internalEntityId
+ : nullish(cp.internal_entity_id)),
+ )
+ .flatMap((cp) => cp.customer_entitlements)
+ .find((ce) => ce.internal_feature_id === entitlement.internal_feature_id);
- if (
- !existingCusEnt ||
- (!entitlement.carry_from_previous && !carryExistingUsages)
- ) {
- return existingUsage;
- }
+ if (
+ !existingCusEnt ||
+ (!entitlement.carry_from_previous && !carryExistingUsages)
+ ) {
+ return existingUsage;
+ }
- if (
- nullish(existingCusEnt.balance) ||
- existingCusEnt.entitlement.allowance_type === AllowanceType.Unlimited
- ) {
- return existingUsage;
- }
+ if (
+ nullish(existingCusEnt.balance) ||
+ existingCusEnt.entitlement.allowance_type === AllowanceType.Unlimited
+ ) {
+ return existingUsage;
+ }
- // Get options
- let cusProduct = cusProducts?.find(
- (cp) => cp.id === existingCusEnt.customer_product_id
- );
- let options = getEntOptions(
- cusProduct?.options || [],
- existingCusEnt.entitlement
- );
- let price = getRelatedCusPrice(
- existingCusEnt,
- cusProduct?.customer_prices || []
- );
- let existingAllowance = getResetBalance({
- entitlement: existingCusEnt.entitlement,
- options: options,
- relatedPrice: price?.price,
- });
+ // Get options
+ let cusProduct = cusProducts?.find(
+ (cp) => cp.id === existingCusEnt.customer_product_id,
+ );
+ let options = getEntOptions(
+ cusProduct?.options || [],
+ existingCusEnt.entitlement,
+ );
+ let price = getRelatedCusPrice(
+ existingCusEnt,
+ cusProduct?.customer_prices || [],
+ );
+ let existingAllowance = getResetBalance({
+ entitlement: existingCusEnt.entitlement,
+ options: options,
+ relatedPrice: price?.price,
+ });
- let { balance, adjustment, count, unused } = getCusEntMasterBalance({
- cusEnt: existingCusEnt as any,
- entities: entities,
- });
+ let { balance, adjustment, count, unused } = getCusEntMasterBalance({
+ cusEnt: existingCusEnt as any,
+ entities: entities,
+ });
- existingUsage = existingAllowance! - balance!;
- if (unused && unused > 0) {
- existingUsage -= unused;
- }
+ existingUsage = existingAllowance! - balance!;
+ if (unused && unused > 0) {
+ existingUsage -= unused;
+ }
- return existingUsage;
+ return existingUsage;
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts
index 720813bb9..fcd371681 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts
@@ -1,100 +1,100 @@
import { notNullish } from "@/utils/genUtils.js";
import {
- Entity,
- EntityWithFeature,
- Feature,
- FullCusEntWithFullCusProduct,
- FullCustomerEntitlement,
+ Entity,
+ EntityWithFeature,
+ Feature,
+ FullCusEntWithFullCusProduct,
+ FullCustomerEntitlement,
} from "@autumn/shared";
export const cusEntMatchesEntity = ({
- cusEnt,
- entity,
- features,
+ cusEnt,
+ entity,
+ features,
}: {
- cusEnt: FullCusEntWithFullCusProduct;
- entity?: Entity;
- features?: Feature[];
+ cusEnt: FullCusEntWithFullCusProduct;
+ entity?: Entity;
+ features?: Feature[];
}) => {
- if (!entity) return true;
+ if (!entity) return true;
- let cusProductMatch = true;
+ let cusProductMatch = true;
- if (notNullish(cusEnt.customer_product?.internal_entity_id)) {
- cusProductMatch =
- cusEnt.customer_product.internal_entity_id === entity.internal_id;
- }
+ if (notNullish(cusEnt.customer_product?.internal_entity_id)) {
+ cusProductMatch =
+ cusEnt.customer_product.internal_entity_id === entity.internal_id;
+ }
- let entityFeatureIdMatch = true;
- // let feature = features?.find(
- // (f) => f.id == cusEnt.entitlement.entity_feature_id,
- // );
+ let entityFeatureIdMatch = true;
+ // let feature = features?.find(
+ // (f) => f.id == cusEnt.entitlement.entity_feature_id,
+ // );
- if (notNullish(cusEnt.entitlement.entity_feature_id)) {
- entityFeatureIdMatch =
- cusEnt.entitlement.entity_feature_id == entity.feature_id;
- }
+ if (notNullish(cusEnt.entitlement.entity_feature_id)) {
+ entityFeatureIdMatch =
+ cusEnt.entitlement.entity_feature_id == entity.feature_id;
+ }
- return cusProductMatch && entityFeatureIdMatch;
+ return cusProductMatch && entityFeatureIdMatch;
};
export const cusEntMatchesFeature = ({
- cusEnt,
- feature,
+ cusEnt,
+ feature,
}: {
- cusEnt: FullCustomerEntitlement;
- feature: Feature;
+ cusEnt: FullCustomerEntitlement;
+ feature: Feature;
}) => {
- return cusEnt.entitlement.feature.internal_id === feature.internal_id;
+ return cusEnt.entitlement.feature.internal_id === feature.internal_id;
};
export const findMainCusEntForFeature = ({
- cusEnts,
- feature,
+ cusEnts,
+ feature,
}: {
- cusEnts: FullCustomerEntitlement[];
- feature: Feature;
+ cusEnts: FullCustomerEntitlement[];
+ feature: Feature;
}) => {
- let mainCusEnt = cusEnts.find(
- (e: any) => e.entitlement.feature.internal_id === feature.internal_id
- );
+ let mainCusEnt = cusEnts.find(
+ (e: any) => e.entitlement.feature.internal_id === feature.internal_id,
+ );
- return mainCusEnt;
+ return mainCusEnt;
};
export const findLinkedCusEnts = ({
- cusEnts,
- feature,
+ cusEnts,
+ feature,
}: {
- cusEnts: FullCustomerEntitlement[];
- feature: Feature;
+ cusEnts: FullCustomerEntitlement[];
+ feature: Feature;
}) => {
- return cusEnts.filter(
- (e: any) => e.entitlement.entity_feature_id === feature.id
- );
+ return cusEnts.filter(
+ (e: any) => e.entitlement.entity_feature_id === feature.id,
+ );
};
export const findCusEnt = ({
- feature,
- cusEnts,
- onlyUsageAllowed = false,
- entity,
- features,
+ feature,
+ cusEnts,
+ onlyUsageAllowed = false,
+ entity,
+ features,
}: {
- feature: Feature;
- cusEnts: FullCustomerEntitlement[];
- onlyUsageAllowed?: boolean;
- entity?: Entity;
- features?: Feature[];
+ feature: Feature;
+ cusEnts: FullCustomerEntitlement[];
+ onlyUsageAllowed?: boolean;
+ entity?: Entity;
+ features?: Feature[];
}) => {
- return cusEnts.find((ce: any) => {
- let featureMatch =
- ce.entitlement.feature.internal_id === feature.internal_id;
+ return cusEnts.find((ce: any) => {
+ let featureMatch =
+ ce.entitlement.feature.internal_id === feature.internal_id;
- let entityMatch = cusEntMatchesEntity({ cusEnt: ce, entity, features });
+ let entityMatch = cusEntMatchesEntity({ cusEnt: ce, entity, features });
- let usageMatch = onlyUsageAllowed ? ce.usage_allowed : true;
+ let usageMatch = onlyUsageAllowed ? ce.usage_allowed : true;
- return featureMatch && entityMatch && usageMatch;
- });
+ return featureMatch && entityMatch && usageMatch;
+ });
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts
index 8ae6319e3..4ae5815a8 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts
@@ -1,20 +1,20 @@
import {
- CustomerEntitlement,
- CustomerEntitlementSchema,
- EntInterval,
- EntitlementWithFeature,
- Entity,
- Feature,
- FeatureType,
- FullCusEntWithFullCusProduct,
- FullCusProduct,
- Price,
+ CustomerEntitlement,
+ CustomerEntitlementSchema,
+ EntInterval,
+ EntitlementWithFeature,
+ Entity,
+ Feature,
+ FeatureType,
+ FullCusEntWithFullCusProduct,
+ FullCusProduct,
+ Price,
} from "@autumn/shared";
import {
- getRelatedCusPrice,
- getResetBalance,
- getUnlimitedAndUsageAllowed,
+ getRelatedCusPrice,
+ getResetBalance,
+ getUnlimitedAndUsageAllowed,
} from "../cusEntUtils.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
@@ -22,257 +22,257 @@ import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
import { sortCusEntsForDeduction } from "@autumn/shared";
export const getExistingCusEntAndUsage = async ({
- curCusProduct,
- entitlement,
- relatedPrice,
+ curCusProduct,
+ entitlement,
+ relatedPrice,
}: {
- entitlement: EntitlementWithFeature;
- curCusProduct?: FullCusProduct;
- relatedPrice?: Price;
+ entitlement: EntitlementWithFeature;
+ curCusProduct?: FullCusProduct;
+ relatedPrice?: Price;
}) => {
- if (!curCusProduct) {
- return { cusEnt: null, usage: null };
- }
+ if (!curCusProduct) {
+ return { cusEnt: null, usage: null };
+ }
- // 1. If there is only one cus ent, return it and usage
- let similarCusEnts = curCusProduct.customer_entitlements.filter(
- (ce) => ce.internal_feature_id === entitlement.internal_feature_id
- // &&
- // ce.entitlement.interval === entitlement.interval
- );
+ // 1. If there is only one cus ent, return it and usage
+ let similarCusEnts = curCusProduct.customer_entitlements.filter(
+ (ce) => ce.internal_feature_id === entitlement.internal_feature_id,
+ // &&
+ // ce.entitlement.interval === entitlement.interval
+ );
- console.log("Entitlement:", entitlement.feature_id, entitlement.interval);
- console.log(
- "Similar entitlements:",
- similarCusEnts.map(
- (ce) =>
- `${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})`
- )
- );
+ console.log("Entitlement:", entitlement.feature_id, entitlement.interval);
+ console.log(
+ "Similar entitlements:",
+ similarCusEnts.map(
+ (ce) =>
+ `${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})`,
+ ),
+ );
- if (similarCusEnts.length === 1) {
- return { cusEnt: similarCusEnts[0], usage: null };
- }
+ if (similarCusEnts.length === 1) {
+ return { cusEnt: similarCusEnts[0], usage: null };
+ }
};
export const getExistingUsages = ({
- curCusProduct,
- entities,
- features,
+ curCusProduct,
+ entities,
+ features,
}: {
- curCusProduct: FullCusProduct;
- entities: Entity[];
- features: Feature[];
+ curCusProduct: FullCusProduct;
+ entities: Entity[];
+ features: Feature[];
}) => {
- let usages: Record<
- string,
- {
- feature_id: string;
- interval: EntInterval;
- interval_count: number;
- usage: number;
- entityUsages: Record | null;
- fromEntities: boolean;
- }
- > = {};
+ let usages: Record<
+ string,
+ {
+ feature_id: string;
+ interval: EntInterval;
+ interval_count: number;
+ usage: number;
+ entityUsages: Record | null;
+ fromEntities: boolean;
+ }
+ > = {};
- let cusPrices = curCusProduct?.customer_prices || [];
+ let cusPrices = curCusProduct?.customer_prices || [];
- // Get entityUsage
- for (const entity of entities) {
- let feature = features.find(
- (f) => f.internal_id === entity.internal_feature_id
- );
- let key = `${feature?.id}-${EntInterval.Lifetime}-1`;
+ // Get entityUsage
+ for (const entity of entities) {
+ let feature = features.find(
+ (f) => f.internal_id === entity.internal_feature_id,
+ );
+ let key = `${feature?.id}-${EntInterval.Lifetime}-1`;
- if (!usages[key]) {
- usages[key] = {
- feature_id: feature?.id || "",
- interval: EntInterval.Lifetime,
- interval_count: 1,
- usage: 0,
- entityUsages: null,
- fromEntities: true,
- };
- }
+ if (!usages[key]) {
+ usages[key] = {
+ feature_id: feature?.id || "",
+ interval: EntInterval.Lifetime,
+ interval_count: 1,
+ usage: 0,
+ entityUsages: null,
+ fromEntities: true,
+ };
+ }
- usages[key].usage += 1;
- }
+ usages[key].usage += 1;
+ }
- for (const cusEnt of curCusProduct?.customer_entitlements || []) {
- let ent = cusEnt.entitlement;
- let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
- let feature = ent.feature;
- if (feature.type == FeatureType.Boolean) continue;
+ for (const cusEnt of curCusProduct?.customer_entitlements || []) {
+ let ent = cusEnt.entitlement;
+ let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
+ let feature = ent.feature;
+ if (feature.type == FeatureType.Boolean) continue;
- let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
- cusEnts: curCusProduct.customer_entitlements,
- internalFeatureId: ent.internal_feature_id!,
- });
+ let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
+ cusEnts: curCusProduct.customer_entitlements,
+ internalFeatureId: ent.internal_feature_id!,
+ });
- if (unlimited) continue;
+ if (unlimited) continue;
- if (!usages[key]) {
- usages[key] = {
- feature_id: feature.id,
- interval: ent.interval || EntInterval.Lifetime,
- interval_count: ent.interval_count || 1,
- usage: 0,
- entityUsages: null,
- fromEntities: false,
- };
- }
+ if (!usages[key]) {
+ usages[key] = {
+ feature_id: feature.id,
+ interval: ent.interval || EntInterval.Lifetime,
+ interval_count: ent.interval_count || 1,
+ usage: 0,
+ entityUsages: null,
+ fromEntities: false,
+ };
+ }
- if (usages[key].fromEntities) {
- continue;
- }
+ if (usages[key].fromEntities) {
+ continue;
+ }
- // 1. To check, does ent options work with multiple features?
- let relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
- let options = getEntOptions(curCusProduct.options, ent);
+ // 1. To check, does ent options work with multiple features?
+ let relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
+ let options = getEntOptions(curCusProduct.options, ent);
- let resetBalance = getResetBalance({
- entitlement: ent,
- options,
- relatedPrice: relatedCusPrice?.price,
- // productQuantity: curCusProduct.quantity,
- });
+ let resetBalance = getResetBalance({
+ entitlement: ent,
+ options,
+ relatedPrice: relatedCusPrice?.price,
+ // productQuantity: curCusProduct.quantity,
+ });
- usages[key].usage += resetBalance! - cusEnt.balance!;
+ usages[key].usage += resetBalance! - cusEnt.balance!;
- if (notNullish(cusEnt.entities)) {
- if (!usages[key].entityUsages) {
- usages[key].entityUsages = {};
- }
+ if (notNullish(cusEnt.entities)) {
+ if (!usages[key].entityUsages) {
+ usages[key].entityUsages = {};
+ }
- for (const entityId in cusEnt.entities) {
- if (nullish(usages[key].entityUsages[entityId])) {
- usages[key].entityUsages[entityId] = 0;
- }
+ for (const entityId in cusEnt.entities) {
+ if (nullish(usages[key].entityUsages[entityId])) {
+ usages[key].entityUsages[entityId] = 0;
+ }
- usages[key].entityUsages[entityId] +=
- resetBalance! - cusEnt.entities[entityId].balance!;
- }
- }
- }
+ usages[key].entityUsages[entityId] +=
+ resetBalance! - cusEnt.entities[entityId].balance!;
+ }
+ }
+ }
- return usages;
+ return usages;
};
export const addExistingUsagesToCusEnts = ({
- cusEnts,
- entitlements,
- curCusProduct,
- carryExistingUsages = false,
- printLogs = false,
- isDowngrade = false,
- entities,
- features,
+ cusEnts,
+ entitlements,
+ curCusProduct,
+ carryExistingUsages = false,
+ printLogs = false,
+ isDowngrade = false,
+ entities,
+ features,
}: {
- cusEnts: CustomerEntitlement[];
- entitlements: EntitlementWithFeature[];
- curCusProduct: FullCusProduct;
- carryExistingUsages?: boolean;
- printLogs?: boolean;
- isDowngrade?: boolean;
- entities: Entity[];
- features: Feature[];
+ cusEnts: CustomerEntitlement[];
+ entitlements: EntitlementWithFeature[];
+ curCusProduct: FullCusProduct;
+ carryExistingUsages?: boolean;
+ printLogs?: boolean;
+ isDowngrade?: boolean;
+ entities: Entity[];
+ features: Feature[];
}) => {
- if (isDowngrade) {
- return cusEnts;
- }
+ if (isDowngrade) {
+ return cusEnts;
+ }
- let existingUsages = getExistingUsages({
- curCusProduct,
- entities,
- features,
- });
+ let existingUsages = getExistingUsages({
+ curCusProduct,
+ entities,
+ features,
+ });
- let fullCusEnts = cusEnts.map((ce) => {
- let entitlement = entitlements.find((e) => e.id === ce.entitlement_id!);
- return { ...ce, entitlement, customer_product: curCusProduct };
- }) as FullCusEntWithFullCusProduct[];
+ let fullCusEnts = cusEnts.map((ce) => {
+ let entitlement = entitlements.find((e) => e.id === ce.entitlement_id!);
+ return { ...ce, entitlement, customer_product: curCusProduct };
+ }) as FullCusEntWithFullCusProduct[];
- // Sort cusEnts
- sortCusEntsForDeduction(fullCusEnts);
+ // Sort cusEnts
+ sortCusEntsForDeduction(fullCusEnts);
- printLogs = false;
- if (printLogs) {
- console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS");
- console.log("Existing usages:", existingUsages);
- console.log(
- "Sorted cusEnts:",
- fullCusEnts.map(
- (ce) =>
- `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}`
- )
- );
- }
+ printLogs = false;
+ if (printLogs) {
+ console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS");
+ console.log("Existing usages:", existingUsages);
+ console.log(
+ "Sorted cusEnts:",
+ fullCusEnts.map(
+ (ce) =>
+ `${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}`,
+ ),
+ );
+ }
- for (const key in existingUsages) {
- let usage = existingUsages[key].usage;
- let entityUsages = existingUsages[key].entityUsages;
+ for (const key in existingUsages) {
+ let usage = existingUsages[key].usage;
+ let entityUsages = existingUsages[key].entityUsages;
- const {
- feature_id = "",
- interval = "",
- interval_count = 1,
- } = existingUsages[key] || {};
+ const {
+ feature_id = "",
+ interval = "",
+ interval_count = 1,
+ } = existingUsages[key] || {};
- for (const cusEnt of fullCusEnts) {
- let ent = cusEnt.entitlement;
- let fromEntities = existingUsages[key].fromEntities;
+ for (const cusEnt of fullCusEnts) {
+ let ent = cusEnt.entitlement;
+ let fromEntities = existingUsages[key].fromEntities;
- // if (cusEntKey !== key) continue;
- const isSameFeature = cusEnt.feature_id == feature_id;
+ // if (cusEntKey !== key) continue;
+ const isSameFeature = cusEnt.feature_id == feature_id;
- if (!isSameFeature) continue;
+ if (!isSameFeature) continue;
- let shouldCarry =
- ent.carry_from_previous || carryExistingUsages || fromEntities;
+ let shouldCarry =
+ ent.carry_from_previous || carryExistingUsages || fromEntities;
- if (!shouldCarry) continue;
+ if (!shouldCarry) continue;
- if (notNullish(entityUsages)) {
- for (const entityId in entityUsages) {
- let { toDeduct, newEntities } = performDeductionOnCusEnt({
- cusEnt,
- toDeduct: entityUsages[entityId],
- allowNegativeBalance: cusEnt.usage_allowed ?? false,
- entityId,
- });
+ if (notNullish(entityUsages)) {
+ for (const entityId in entityUsages) {
+ let { toDeduct, newEntities } = performDeductionOnCusEnt({
+ cusEnt,
+ toDeduct: entityUsages[entityId],
+ allowNegativeBalance: cusEnt.usage_allowed ?? false,
+ entityId,
+ });
- existingUsages[key].entityUsages![entityId] = toDeduct;
+ existingUsages[key].entityUsages![entityId] = toDeduct;
- if (nullish(cusEnt.entities![entityId])) {
- cusEnt.entities![entityId] = {
- id: entityId,
- balance: 0,
- adjustment: 0,
- };
- }
+ if (nullish(cusEnt.entities![entityId])) {
+ cusEnt.entities![entityId] = {
+ id: entityId,
+ balance: 0,
+ adjustment: 0,
+ };
+ }
- cusEnt.entities![entityId]!.balance = newEntities![entityId]!.balance;
- }
- } else {
- let { newBalance, toDeduct } = performDeductionOnCusEnt({
- cusEnt,
- toDeduct: usage,
- allowNegativeBalance: cusEnt.usage_allowed ?? false,
- });
- usage = toDeduct;
- cusEnt.balance = newBalance;
- }
+ cusEnt.entities![entityId]!.balance = newEntities![entityId]!.balance;
+ }
+ } else {
+ let { newBalance, toDeduct } = performDeductionOnCusEnt({
+ cusEnt,
+ toDeduct: usage,
+ allowNegativeBalance: cusEnt.usage_allowed ?? false,
+ });
+ usage = toDeduct;
+ cusEnt.balance = newBalance;
+ }
- if (printLogs) {
- console.log("--------------------------------");
- console.log("Key:", key);
- console.log("New cus ent balance:", cusEnt.balance, cusEnt.entities);
- console.log("Existing usages:", existingUsages);
- }
- }
- }
+ if (printLogs) {
+ console.log("--------------------------------");
+ console.log("Key:", key);
+ console.log("New cus ent balance:", cusEnt.balance, cusEnt.entities);
+ console.log("Existing usages:", existingUsages);
+ }
+ }
+ }
- // console.log("Full cusEnts:", fullCusEnts);
- return fullCusEnts.map((ce) => CustomerEntitlementSchema.parse(ce));
+ // console.log("Full cusEnts:", fullCusEnts);
+ return fullCusEnts.map((ce) => CustomerEntitlementSchema.parse(ce));
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.ts
index aac7267d1..ad3457b5e 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.ts
@@ -1,50 +1,50 @@
import {
- FullCustomerEntitlement,
- InsertReplaceable,
- Replaceable,
+ FullCustomerEntitlement,
+ InsertReplaceable,
+ Replaceable,
} from "@autumn/shared";
export const replaceEntityInCusEnt = ({
- cusEnt,
- entityId,
- replaceable,
+ cusEnt,
+ entityId,
+ replaceable,
}: {
- cusEnt: FullCustomerEntitlement;
- entityId: string;
- replaceable: Replaceable | InsertReplaceable;
+ cusEnt: FullCustomerEntitlement;
+ entityId: string;
+ replaceable: Replaceable | InsertReplaceable;
}) => {
- let newEntities = structuredClone(cusEnt.entities) || {};
- newEntities[replaceable.id] = newEntities[entityId];
+ let newEntities = structuredClone(cusEnt.entities) || {};
+ newEntities[replaceable.id] = newEntities[entityId];
- delete newEntities[entityId];
+ delete newEntities[entityId];
- return { newEntities };
+ return { newEntities };
};
export const deleteEntityFromCusEnt = ({
- cusEnt,
- entityId,
+ cusEnt,
+ entityId,
}: {
- cusEnt: FullCustomerEntitlement;
- entityId: string;
+ cusEnt: FullCustomerEntitlement;
+ entityId: string;
}) => {
- let newEntities = structuredClone(cusEnt.entities) || {};
- delete newEntities[entityId];
+ let newEntities = structuredClone(cusEnt.entities) || {};
+ delete newEntities[entityId];
- return { newEntities };
+ return { newEntities };
};
export const removeReplaceablesFromCusEnt = ({
- cusEnt,
- replaceableIds,
+ cusEnt,
+ replaceableIds,
}: {
- cusEnt: FullCustomerEntitlement;
- replaceableIds: string[];
+ cusEnt: FullCustomerEntitlement;
+ replaceableIds: string[];
}) => {
- let newEntities = structuredClone(cusEnt.entities) || {};
- for (const replaceableId of replaceableIds) {
- delete newEntities[replaceableId];
- }
+ let newEntities = structuredClone(cusEnt.entities) || {};
+ for (const replaceableId of replaceableIds) {
+ delete newEntities[replaceableId];
+ }
- return { newEntities };
+ return { newEntities };
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts
index 573dc1eb6..827ee0ff2 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.ts
@@ -1,133 +1,133 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- FullCustomerEntitlement,
- Rollover,
- RolloverConfig,
- rollovers,
+ FullCustomerEntitlement,
+ Rollover,
+ RolloverConfig,
+ rollovers,
} from "@autumn/shared";
import { and, eq, gte, inArray } from "drizzle-orm";
import { performMaximumClearing } from "./rolloverUtils.js";
import { buildConflictUpdateColumns } from "@/db/dbUtils.js";
export class RolloverService {
- static async update({
- db,
- id,
- updates,
- }: {
- db: DrizzleCli;
- id: string;
- updates: Partial;
- }) {
- if (!updates.balance && !updates.entities) return [];
+ static async update({
+ db,
+ id,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ updates: Partial;
+ }) {
+ if (!updates.balance && !updates.entities) return [];
- const data = await db
- .update(rollovers)
- .set(updates as any)
- .where(eq(rollovers.id, id))
- .returning();
+ const data = await db
+ .update(rollovers)
+ .set(updates as any)
+ .where(eq(rollovers.id, id))
+ .returning();
- return data;
- }
+ return data;
+ }
- static async upsert({ db, rows }: { db: DrizzleCli; rows: Rollover[] }) {
- if (Array.isArray(rows) && rows.length == 0) return;
+ static async upsert({ db, rows }: { db: DrizzleCli; rows: Rollover[] }) {
+ if (Array.isArray(rows) && rows.length == 0) return;
- const updateColumns = buildConflictUpdateColumns(rollovers, ["id"]);
- await db
- .insert(rollovers)
- .values(rows as any)
- .onConflictDoUpdate({
- target: rollovers.id,
- set: updateColumns,
- });
- }
+ const updateColumns = buildConflictUpdateColumns(rollovers, ["id"]);
+ await db
+ .insert(rollovers)
+ .values(rows as any)
+ .onConflictDoUpdate({
+ target: rollovers.id,
+ set: updateColumns,
+ });
+ }
- // static async bulkUpdate({ db, rows }: { db: DrizzleCli; rows: Rollover[] }) {
- // if (rows.length === 0) return [];
+ // static async bulkUpdate({ db, rows }: { db: DrizzleCli; rows: Rollover[] }) {
+ // if (rows.length === 0) return [];
- // const results = [];
- // for (const row of rows) {
- // const result = await this.update({
- // db,
- // id: row.id,
- // updates: row,
- // });
- // results.push(...result);
- // }
- // return results;
- // }
+ // const results = [];
+ // for (const row of rows) {
+ // const result = await this.update({
+ // db,
+ // id: row.id,
+ // updates: row,
+ // });
+ // results.push(...result);
+ // }
+ // return results;
+ // }
- static async getCurrentRollovers({
- db,
- cusEntID,
- }: {
- db: DrizzleCli;
- cusEntID: string;
- }) {
- return await db
- .select()
- .from(rollovers)
- .where(
- and(
- eq(rollovers.cus_ent_id, cusEntID),
- gte(rollovers.expires_at, new Date().getTime())
- )
- );
- }
+ static async getCurrentRollovers({
+ db,
+ cusEntID,
+ }: {
+ db: DrizzleCli;
+ cusEntID: string;
+ }) {
+ return await db
+ .select()
+ .from(rollovers)
+ .where(
+ and(
+ eq(rollovers.cus_ent_id, cusEntID),
+ gte(rollovers.expires_at, new Date().getTime()),
+ ),
+ );
+ }
- static async insert({
- db,
- rows,
- // rolloverConfig,
- fullCusEnt,
- // cusEntID,
- // entityMode,
- }: {
- db: DrizzleCli;
- rows: Rollover[];
- // rolloverConfig: RolloverConfig;
- fullCusEnt: FullCustomerEntitlement;
- // cusEntID: string;
- // entityMode: boolean;
- }) {
- if (rows.length === 0) return {};
+ static async insert({
+ db,
+ rows,
+ // rolloverConfig,
+ fullCusEnt,
+ // cusEntID,
+ // entityMode,
+ }: {
+ db: DrizzleCli;
+ rows: Rollover[];
+ // rolloverConfig: RolloverConfig;
+ fullCusEnt: FullCustomerEntitlement;
+ // cusEntID: string;
+ // entityMode: boolean;
+ }) {
+ if (rows.length === 0) return {};
- await db
- .insert(rollovers)
- .values(rows as any)
- .returning();
+ await db
+ .insert(rollovers)
+ .values(rows as any)
+ .returning();
- let curRollovers = [...fullCusEnt.rollovers, ...rows];
+ let curRollovers = [...fullCusEnt.rollovers, ...rows];
- let { toDelete, toUpdate } = performMaximumClearing({
- rows: curRollovers as Rollover[],
- cusEnt: fullCusEnt,
- });
+ let { toDelete, toUpdate } = performMaximumClearing({
+ rows: curRollovers as Rollover[],
+ cusEnt: fullCusEnt,
+ });
- if (toDelete.length > 0) {
- await RolloverService.delete({ db, ids: toDelete });
- }
+ if (toDelete.length > 0) {
+ await RolloverService.delete({ db, ids: toDelete });
+ }
- if (toUpdate.length > 0) {
- await RolloverService.upsert({ db, rows: toUpdate });
- }
+ if (toUpdate.length > 0) {
+ await RolloverService.upsert({ db, rows: toUpdate });
+ }
- // Return latest rollovers...?
- curRollovers = curRollovers.filter((r) => toDelete.includes(r.id));
- curRollovers = curRollovers.map((r) => {
- let updatedRow = toUpdate.find((u) => u.id === r.id);
- if (updatedRow) {
- return updatedRow;
- }
- return r;
- });
+ // Return latest rollovers...?
+ curRollovers = curRollovers.filter((r) => toDelete.includes(r.id));
+ curRollovers = curRollovers.map((r) => {
+ let updatedRow = toUpdate.find((u) => u.id === r.id);
+ if (updatedRow) {
+ return updatedRow;
+ }
+ return r;
+ });
- return curRollovers;
- }
+ return curRollovers;
+ }
- static async delete({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- if (ids.length === 0) return;
- const data = await db.delete(rollovers).where(inArray(rollovers.id, ids));
- }
+ static async delete({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ if (ids.length === 0) return;
+ const data = await db.delete(rollovers).where(inArray(rollovers.id, ids));
+ }
}
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts
index 92fce5265..d089e814e 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/getNewProductRollovers.ts
@@ -1,138 +1,139 @@
import { generateId, nullish } from "@/utils/genUtils.js";
import {
- CustomerEntitlement,
- EntitlementWithFeature,
- FullCusProduct,
- FullCustomerEntitlement,
- Rollover,
- RolloverConfig,
+ CustomerEntitlement,
+ EntitlementWithFeature,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ Rollover,
+ RolloverConfig,
} from "@autumn/shared";
import { RolloverService } from "./RolloverService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { calculateNextExpiry } from "./rolloverUtils.js";
export const getNewProductRollovers = async ({
- curCusProduct,
- cusEnts: newCusEnts,
- entitlements,
- db,
- logger,
+ curCusProduct,
+ cusEnts: newCusEnts,
+ entitlements,
+ db,
+ logger,
}: {
- curCusProduct: FullCusProduct;
- cusEnts: CustomerEntitlement[];
- entitlements: EntitlementWithFeature[];
- db: DrizzleCli;
- logger: any;
+ curCusProduct: FullCusProduct;
+ cusEnts: CustomerEntitlement[];
+ entitlements: EntitlementWithFeature[];
+ db: DrizzleCli;
+ logger: any;
}) => {
- if (!curCusProduct) return [];
- if (!curCusProduct.id) return [];
- try {
- let rolloverOperations: {
- // rolloverConfig: RolloverConfig;
- toInsert: Rollover[];
- cusEnt: FullCustomerEntitlement;
- // cusEntId: string;
- // toUpdate: Rollover[];
- // entityMode: boolean;
- }[] = [];
+ if (!curCusProduct) return [];
+ if (!curCusProduct.id) return [];
+ try {
+ let rolloverOperations: {
+ // rolloverConfig: RolloverConfig;
+ toInsert: Rollover[];
+ cusEnt: FullCustomerEntitlement;
+ // cusEntId: string;
+ // toUpdate: Rollover[];
+ // entityMode: boolean;
+ }[] = [];
- // let newRollovers: Rollover[] = [];
+ // let newRollovers: Rollover[] = [];
- let oldCusEnts = curCusProduct.customer_entitlements;
+ let oldCusEnts = curCusProduct.customer_entitlements;
- for (const newCusEnt of newCusEnts) {
- let newRollovers: Rollover[] = [];
- let newEnt = entitlements.find((e) => e.id === newCusEnt.entitlement_id);
- let oldCusEnt = oldCusEnts.find(
- (e) => e.entitlement.internal_feature_id === newEnt?.internal_feature_id
- );
- let oldEnt = oldCusEnt?.entitlement;
+ for (const newCusEnt of newCusEnts) {
+ let newRollovers: Rollover[] = [];
+ let newEnt = entitlements.find((e) => e.id === newCusEnt.entitlement_id);
+ let oldCusEnt = oldCusEnts.find(
+ (e) =>
+ e.entitlement.internal_feature_id === newEnt?.internal_feature_id,
+ );
+ let oldEnt = oldCusEnt?.entitlement;
- if (!oldCusEnt || !newEnt?.rollover) continue;
+ if (!oldCusEnt || !newEnt?.rollover) continue;
- // Do not handle case where user is upgrading from non-entity to entity or vice versa
- if (newEnt?.entity_feature_id && !oldEnt?.entity_feature_id) {
- continue;
- }
- if (!newEnt?.entity_feature_id && oldEnt?.entity_feature_id) {
- continue;
- }
+ // Do not handle case where user is upgrading from non-entity to entity or vice versa
+ if (newEnt?.entity_feature_id && !oldEnt?.entity_feature_id) {
+ continue;
+ }
+ if (!newEnt?.entity_feature_id && oldEnt?.entity_feature_id) {
+ continue;
+ }
- // Bring over current balance (if greater > 0), and any existing rollover
- // if (
- // oldCusEnt.balance &&
- // oldCusEnt.balance > 0 &&
- // !oldCusEnt.entitlement.entity_feature_id &&
- // rollover
- // ) {
- // newRollovers.push({
- // id: generateId("roll"),
- // cus_ent_id: newCusEnt.id,
- // balance: oldCusEnt.balance,
- // entities: {},
- // usage: 0,
- // expires_at: calculateNextExpiry(Date.now(), rollover),
- // });
- // } else if (
- // oldCusEnt.entitlement.entity_feature_id &&
- // oldCusEnt.entities
- // ) {
- // const entityRollovers = Object.keys(oldCusEnt.entities || {}).reduce(
- // (acc, entityId) => {
- // const entityBalance = oldCusEnt.entities?.[entityId];
- // if (entityBalance && entityBalance.balance > 0) {
- // acc[entityId] = {
- // id: entityId,
- // balance: entityBalance.balance || 0,
- // usage: 0,
- // };
- // }
- // return acc;
- // },
- // {} as Record
- // );
+ // Bring over current balance (if greater > 0), and any existing rollover
+ // if (
+ // oldCusEnt.balance &&
+ // oldCusEnt.balance > 0 &&
+ // !oldCusEnt.entitlement.entity_feature_id &&
+ // rollover
+ // ) {
+ // newRollovers.push({
+ // id: generateId("roll"),
+ // cus_ent_id: newCusEnt.id,
+ // balance: oldCusEnt.balance,
+ // entities: {},
+ // usage: 0,
+ // expires_at: calculateNextExpiry(Date.now(), rollover),
+ // });
+ // } else if (
+ // oldCusEnt.entitlement.entity_feature_id &&
+ // oldCusEnt.entities
+ // ) {
+ // const entityRollovers = Object.keys(oldCusEnt.entities || {}).reduce(
+ // (acc, entityId) => {
+ // const entityBalance = oldCusEnt.entities?.[entityId];
+ // if (entityBalance && entityBalance.balance > 0) {
+ // acc[entityId] = {
+ // id: entityId,
+ // balance: entityBalance.balance || 0,
+ // usage: 0,
+ // };
+ // }
+ // return acc;
+ // },
+ // {} as Record
+ // );
- // if (Object.keys(entityRollovers).length > 0) {
- // newRollovers.push({
- // id: generateId("roll"),
- // cus_ent_id: newCusEnt.id,
- // balance: 0,
- // entities: entityRollovers,
- // usage: 0,
- // expires_at: calculateNextExpiry(Date.now(), rollover),
- // });
- // }
- // }
+ // if (Object.keys(entityRollovers).length > 0) {
+ // newRollovers.push({
+ // id: generateId("roll"),
+ // cus_ent_id: newCusEnt.id,
+ // balance: 0,
+ // entities: entityRollovers,
+ // usage: 0,
+ // expires_at: calculateNextExpiry(Date.now(), rollover),
+ // });
+ // }
+ // }
- let curRollovers = oldCusEnt.rollovers;
+ let curRollovers = oldCusEnt.rollovers;
- for (const curRollover of curRollovers) {
- newRollovers.push({
- ...curRollover,
- id: generateId("roll"),
- cus_ent_id: newCusEnt.id,
- });
- }
+ for (const curRollover of curRollovers) {
+ newRollovers.push({
+ ...curRollover,
+ id: generateId("roll"),
+ cus_ent_id: newCusEnt.id,
+ });
+ }
- console.log(`Feature ${newEnt?.feature_id} rollovers:`, newRollovers);
+ console.log(`Feature ${newEnt?.feature_id} rollovers:`, newRollovers);
- // // Add this entitlement's rollover operations
- rolloverOperations.push({
- toInsert: newRollovers,
- cusEnt: {
- ...newCusEnt,
- entitlement: newEnt,
- rollovers: [],
- replaceables: [],
- },
- });
- }
+ // // Add this entitlement's rollover operations
+ rolloverOperations.push({
+ toInsert: newRollovers,
+ cusEnt: {
+ ...newCusEnt,
+ entitlement: newEnt,
+ rollovers: [],
+ replaceables: [],
+ },
+ });
+ }
- return rolloverOperations;
- } catch (error) {
- logger.error(`Failed to handle new product rollovers:`, {
- error,
- });
- return [];
- }
+ return rolloverOperations;
+ } catch (error) {
+ logger.error(`Failed to handle new product rollovers:`, {
+ error,
+ });
+ return [];
+ }
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.ts
index a7c923b8f..6950a251a 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.ts
@@ -3,128 +3,128 @@ import { FullCusEntWithFullCusProduct, Rollover } from "@autumn/shared";
import { RolloverService } from "./RolloverService.js";
export const deductFromCusRollovers = async ({
- toDeduct,
- deductParams,
- cusEnt,
+ toDeduct,
+ deductParams,
+ cusEnt,
}: {
- toDeduct: number;
- deductParams: RolloverDeductParams;
- cusEnt: FullCusEntWithFullCusProduct;
+ toDeduct: number;
+ deductParams: RolloverDeductParams;
+ cusEnt: FullCusEntWithFullCusProduct;
}) => {
- if (toDeduct == 0) {
- return toDeduct;
- }
+ if (toDeduct == 0) {
+ return toDeduct;
+ }
- let updates = {
- toInsert: [] as Rollover[],
- toUpdate: [] as Rollover[],
- };
- let rollovers = getSortedRollovers({
- cusEnts: [cusEnt],
- featureId: deductParams.feature.id,
- entityId: deductParams.entity?.id,
- });
+ let updates = {
+ toInsert: [] as Rollover[],
+ toUpdate: [] as Rollover[],
+ };
+ let rollovers = getSortedRollovers({
+ cusEnts: [cusEnt],
+ featureId: deductParams.feature.id,
+ entityId: deductParams.entity?.id,
+ });
- if (deductParams.entity) {
- for (let rollover of rollovers) {
- let entityRollover = rollover.entities[deductParams.entity.id];
- if (entityRollover) {
- if (entityRollover.balance >= toDeduct) {
- entityRollover.balance -= toDeduct;
- entityRollover.usage += toDeduct;
+ if (deductParams.entity) {
+ for (let rollover of rollovers) {
+ let entityRollover = rollover.entities[deductParams.entity.id];
+ if (entityRollover) {
+ if (entityRollover.balance >= toDeduct) {
+ entityRollover.balance -= toDeduct;
+ entityRollover.usage += toDeduct;
- updates.toUpdate.push(rollover);
- toDeduct = 0;
- break;
- } else {
- if (entityRollover.balance > 0) {
- let deductedAmount = entityRollover.balance;
- toDeduct -= entityRollover.balance;
- entityRollover.balance = 0;
- entityRollover.usage += deductedAmount;
- updates.toUpdate.push(rollover);
- }
- }
- }
- }
- } else {
- for (let rollover of rollovers) {
- if (rollover.balance >= toDeduct) {
- rollover = {
- ...rollover,
- balance: rollover.balance - toDeduct,
- usage: rollover.usage + toDeduct,
- };
+ updates.toUpdate.push(rollover);
+ toDeduct = 0;
+ break;
+ } else {
+ if (entityRollover.balance > 0) {
+ let deductedAmount = entityRollover.balance;
+ toDeduct -= entityRollover.balance;
+ entityRollover.balance = 0;
+ entityRollover.usage += deductedAmount;
+ updates.toUpdate.push(rollover);
+ }
+ }
+ }
+ }
+ } else {
+ for (let rollover of rollovers) {
+ if (rollover.balance >= toDeduct) {
+ rollover = {
+ ...rollover,
+ balance: rollover.balance - toDeduct,
+ usage: rollover.usage + toDeduct,
+ };
- updates.toUpdate.push(rollover);
- toDeduct = 0;
+ updates.toUpdate.push(rollover);
+ toDeduct = 0;
- break;
- } else {
- if (rollover.balance > 0) {
- toDeduct -= rollover.balance;
- rollover = {
- ...rollover,
- usage: rollover.usage + rollover.balance,
- balance: 0,
- };
+ break;
+ } else {
+ if (rollover.balance > 0) {
+ toDeduct -= rollover.balance;
+ rollover = {
+ ...rollover,
+ usage: rollover.usage + rollover.balance,
+ balance: 0,
+ };
- updates.toUpdate.push(rollover);
- }
- }
- }
- }
+ updates.toUpdate.push(rollover);
+ }
+ }
+ }
+ }
- await RolloverService.upsert({
- db: deductParams.db,
- rows: updates.toUpdate,
- });
+ await RolloverService.upsert({
+ db: deductParams.db,
+ rows: updates.toUpdate,
+ });
- return toDeduct;
+ return toDeduct;
};
export const getSortedRollovers = ({
- cusEnts,
- featureId,
- entityId,
+ cusEnts,
+ featureId,
+ entityId,
}: {
- cusEnts: FullCusEntWithFullCusProduct[];
- featureId: string;
- entityId?: string;
+ cusEnts: FullCusEntWithFullCusProduct[];
+ featureId: string;
+ entityId?: string;
}) => {
- if (!entityId)
- return cusEnts
- .filter((cusEnt) => {
- return cusEnt.feature_id === featureId;
- })
- .flatMap((cusEnt) => {
- return cusEnt.rollovers;
- })
- .sort((a, b) => {
- if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
- if (a.expires_at && !b.expires_at) return -1;
- if (!a.expires_at && b.expires_at) return 1;
- return 0;
- });
- else {
- return cusEnts
- .filter((cusEnt) => {
- return (
- cusEnt.feature_id === featureId &&
- cusEnt.entities &&
- cusEnt.entities[entityId]
- );
- })
- .flatMap((cusEnt) => {
- return cusEnt.rollovers.filter((x) => {
- return x.entities[entityId];
- });
- })
- .sort((a, b) => {
- if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
- if (a.expires_at && !b.expires_at) return -1;
- if (!a.expires_at && b.expires_at) return 1;
- return 0;
- });
- }
+ if (!entityId)
+ return cusEnts
+ .filter((cusEnt) => {
+ return cusEnt.feature_id === featureId;
+ })
+ .flatMap((cusEnt) => {
+ return cusEnt.rollovers;
+ })
+ .sort((a, b) => {
+ if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
+ if (a.expires_at && !b.expires_at) return -1;
+ if (!a.expires_at && b.expires_at) return 1;
+ return 0;
+ });
+ else {
+ return cusEnts
+ .filter((cusEnt) => {
+ return (
+ cusEnt.feature_id === featureId &&
+ cusEnt.entities &&
+ cusEnt.entities[entityId]
+ );
+ })
+ .flatMap((cusEnt) => {
+ return cusEnt.rollovers.filter((x) => {
+ return x.entities[entityId];
+ });
+ })
+ .sort((a, b) => {
+ if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
+ if (a.expires_at && !b.expires_at) return -1;
+ if (!a.expires_at && b.expires_at) return 1;
+ return 0;
+ });
+ }
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.ts
index 01b326b62..e2b5ffe90 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.ts
@@ -1,242 +1,242 @@
import {
- FullCustomerEntitlement,
- RolloverConfig,
- EntityRolloverBalance,
- RolloverDuration,
- Rollover,
+ FullCustomerEntitlement,
+ RolloverConfig,
+ EntityRolloverBalance,
+ RolloverDuration,
+ Rollover,
} from "@autumn/shared";
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
import { addMonths } from "date-fns";
import { Decimal } from "decimal.js";
export const getRolloverUpdates = ({
- cusEnt,
- nextResetAt,
+ cusEnt,
+ nextResetAt,
}: {
- cusEnt: FullCustomerEntitlement;
- nextResetAt: number;
+ cusEnt: FullCustomerEntitlement;
+ nextResetAt: number;
}) => {
- let update: {
- toDelete: string[];
- toInsert: Rollover[];
- toUpdate: Rollover[];
- } = {
- toDelete: [],
- toInsert: [],
- toUpdate: [],
- };
- let ent = cusEnt.entitlement;
- let shouldRollover =
- cusEnt.balance && cusEnt.balance > 0 && notNullish(ent.rollover);
+ let update: {
+ toDelete: string[];
+ toInsert: Rollover[];
+ toUpdate: Rollover[];
+ } = {
+ toDelete: [],
+ toInsert: [],
+ toUpdate: [],
+ };
+ let ent = cusEnt.entitlement;
+ let shouldRollover =
+ cusEnt.balance && cusEnt.balance > 0 && notNullish(ent.rollover);
- if (!shouldRollover) return update;
+ if (!shouldRollover) return update;
- let nextExpiry = calculateNextExpiry(nextResetAt, ent.rollover!);
+ let nextExpiry = calculateNextExpiry(nextResetAt, ent.rollover!);
- let newRollover: Rollover = {
- id: generateId("roll"),
- cus_ent_id: cusEnt.id,
- balance: 0,
- usage: 0,
- expires_at: nextExpiry,
- entities: {},
- };
+ let newRollover: Rollover = {
+ id: generateId("roll"),
+ cus_ent_id: cusEnt.id,
+ balance: 0,
+ usage: 0,
+ expires_at: nextExpiry,
+ entities: {},
+ };
- if (notNullish(ent.entity_feature_id)) {
- for (const entityId in cusEnt.entities) {
- let entRollover = cusEnt.entities[entityId].balance;
+ if (notNullish(ent.entity_feature_id)) {
+ for (const entityId in cusEnt.entities) {
+ let entRollover = cusEnt.entities[entityId].balance;
- if (entRollover > 0) {
- newRollover.entities[entityId] = {
- id: entityId,
- balance: entRollover,
- usage: 0,
- };
- }
- }
+ if (entRollover > 0) {
+ newRollover.entities[entityId] = {
+ id: entityId,
+ balance: entRollover,
+ usage: 0,
+ };
+ }
+ }
- update.toInsert.push(newRollover);
- } else {
- let balance = cusEnt.balance!;
- if (balance > 0) {
- newRollover.balance = balance;
- update.toInsert.push(newRollover);
- }
- }
+ update.toInsert.push(newRollover);
+ } else {
+ let balance = cusEnt.balance!;
+ if (balance > 0) {
+ newRollover.balance = balance;
+ update.toInsert.push(newRollover);
+ }
+ }
- return update;
+ return update;
};
export const calculateNextExpiry = (
- nextResetAt: number,
- config: RolloverConfig
+ nextResetAt: number,
+ config: RolloverConfig,
) => {
- if (nullish(config)) {
- return null;
- }
+ if (nullish(config)) {
+ return null;
+ }
- if (config.duration == RolloverDuration.Forever) return null;
+ if (config.duration == RolloverDuration.Forever) return null;
- return addMonths(nextResetAt, config.length).getTime();
+ return addMonths(nextResetAt, config.length).getTime();
};
export function performMaximumClearing({
- rows,
- // rolloverConfig,
- cusEnt,
- // cusEntID,
- // entityMode,
+ rows,
+ // rolloverConfig,
+ cusEnt,
+ // cusEntID,
+ // entityMode,
}: {
- rows: Rollover[];
- // rolloverConfig: RolloverConfig;
- cusEnt: FullCustomerEntitlement;
- // cusEntID: string;
- // entityMode: boolean;
+ rows: Rollover[];
+ // rolloverConfig: RolloverConfig;
+ cusEnt: FullCustomerEntitlement;
+ // cusEntID: string;
+ // entityMode: boolean;
}) {
- let rolloverConfig = cusEnt.entitlement.rollover;
+ let rolloverConfig = cusEnt.entitlement.rollover;
- if (!rolloverConfig) {
- return { toDelete: [], toUpdate: [] };
- }
+ if (!rolloverConfig) {
+ return { toDelete: [], toUpdate: [] };
+ }
- if (rolloverConfig.max == null) {
- return { toDelete: [], toUpdate: [] };
- }
+ if (rolloverConfig.max == null) {
+ return { toDelete: [], toUpdate: [] };
+ }
- let total = 0;
- let toDelete: string[] = [];
- let toUpdate: Rollover[] = [];
+ let total = 0;
+ let toDelete: string[] = [];
+ let toUpdate: Rollover[] = [];
- // look through each row
- // if entityMode is true, then look through each entity
- // otherwise look at balance
+ // look through each row
+ // if entityMode is true, then look through each entity
+ // otherwise look at balance
- // sort by the oldest first
- // add the balance of the oldest to the total
- // if the total is greater than or equal to the max, then:
- // subtract the max from the total, if theres a difference then instantiate the updated row object and push to toUpdate
- // if theres no difference, then push to toDelete
- // move to the next row
- // if the total is less than the max, then
- // move to the next row
+ // sort by the oldest first
+ // add the balance of the oldest to the total
+ // if the total is greater than or equal to the max, then:
+ // subtract the max from the total, if theres a difference then instantiate the updated row object and push to toUpdate
+ // if theres no difference, then push to toDelete
+ // move to the next row
+ // if the total is less than the max, then
+ // move to the next row
- rows.sort((a, b) => {
- if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
- if (a.expires_at && !b.expires_at) return -1;
- if (!a.expires_at && b.expires_at) return 1;
- return 0;
- });
+ rows.sort((a, b) => {
+ if (a.expires_at && b.expires_at) return a.expires_at - b.expires_at;
+ if (a.expires_at && !b.expires_at) return -1;
+ if (!a.expires_at && b.expires_at) return 1;
+ return 0;
+ });
- let ent = cusEnt.entitlement;
- let entityMode = !!ent.entity_feature_id;
+ let ent = cusEnt.entitlement;
+ let entityMode = !!ent.entity_feature_id;
- if (!entityMode) {
- let totalRolloverBalance = rows.reduce((acc, row) => acc + row.balance, 0);
- let toDeduct = new Decimal(totalRolloverBalance).sub(rolloverConfig.max);
+ if (!entityMode) {
+ let totalRolloverBalance = rows.reduce((acc, row) => acc + row.balance, 0);
+ let toDeduct = new Decimal(totalRolloverBalance).sub(rolloverConfig.max);
- if (toDeduct.lt(0)) return { toDelete: [], toUpdate: [] };
+ if (toDeduct.lt(0)) return { toDelete: [], toUpdate: [] };
- let toUpdate: Rollover[] = [];
- let toDelete: string[] = [];
+ let toUpdate: Rollover[] = [];
+ let toDelete: string[] = [];
- for (const row of rows) {
- let curBalance = new Decimal(row.balance);
- let newBalance = curBalance;
- if (curBalance.gte(toDeduct)) {
- newBalance = newBalance.sub(toDeduct);
- toDeduct = new Decimal(0);
+ for (const row of rows) {
+ let curBalance = new Decimal(row.balance);
+ let newBalance = curBalance;
+ if (curBalance.gte(toDeduct)) {
+ newBalance = newBalance.sub(toDeduct);
+ toDeduct = new Decimal(0);
- toUpdate.push({ ...row, balance: newBalance.toNumber() });
- } else {
- newBalance = new Decimal(0);
- toDeduct = toDeduct.sub(curBalance);
+ toUpdate.push({ ...row, balance: newBalance.toNumber() });
+ } else {
+ newBalance = new Decimal(0);
+ toDeduct = toDeduct.sub(curBalance);
- toDelete.push(row.id);
- }
+ toDelete.push(row.id);
+ }
- if (toDeduct.lte(0)) break;
- }
+ if (toDeduct.lte(0)) break;
+ }
- return { toDelete, toUpdate };
- } else {
- const allEntityIds = new Set();
- rows.forEach((row) => {
- if (row.entities && Array.isArray(row.entities)) {
- row.entities.forEach((entity: any) => {
- if (entity.id) {
- allEntityIds.add(entity.id);
- }
- });
- }
- });
+ return { toDelete, toUpdate };
+ } else {
+ const allEntityIds = new Set();
+ rows.forEach((row) => {
+ if (row.entities && Array.isArray(row.entities)) {
+ row.entities.forEach((entity: any) => {
+ if (entity.id) {
+ allEntityIds.add(entity.id);
+ }
+ });
+ }
+ });
- const entityTotals = new Map();
- allEntityIds.forEach((id) => entityTotals.set(id, 0));
- let entityIdToTotal: Record = {};
- rows.forEach((row) => {
- for (const entityId in row.entities) {
- entityIdToTotal[entityId] =
- (entityIdToTotal[entityId] || 0) + row.entities[entityId].balance;
- }
- });
+ const entityTotals = new Map();
+ allEntityIds.forEach((id) => entityTotals.set(id, 0));
+ let entityIdToTotal: Record = {};
+ rows.forEach((row) => {
+ for (const entityId in row.entities) {
+ entityIdToTotal[entityId] =
+ (entityIdToTotal[entityId] || 0) + row.entities[entityId].balance;
+ }
+ });
- // console.log(`id to total:`, entityIdToTotal);
+ // console.log(`id to total:`, entityIdToTotal);
- let toUpdate: Rollover[] = [];
- let toDelete: string[] = [];
+ let toUpdate: Rollover[] = [];
+ let toDelete: string[] = [];
- // Non entity mode
- for (const row of rows) {
- let update = structuredClone(row);
- let shouldUpdate = false;
+ // Non entity mode
+ for (const row of rows) {
+ let update = structuredClone(row);
+ let shouldUpdate = false;
- for (const entityId in entityIdToTotal) {
- let entityTotal = entityIdToTotal[entityId];
- let toDeduct = new Decimal(entityTotal).sub(rolloverConfig.max);
+ for (const entityId in entityIdToTotal) {
+ let entityTotal = entityIdToTotal[entityId];
+ let toDeduct = new Decimal(entityTotal).sub(rolloverConfig.max);
- if (toDeduct.lte(0) || !row.entities[entityId]) continue;
- // console.log(`Entity ${entityId}, deducting ${toDeduct.toNumber()}`);
+ if (toDeduct.lte(0) || !row.entities[entityId]) continue;
+ // console.log(`Entity ${entityId}, deducting ${toDeduct.toNumber()}`);
- let curBalance = new Decimal(row.entities[entityId].balance);
- let newBalance = curBalance;
+ let curBalance = new Decimal(row.entities[entityId].balance);
+ let newBalance = curBalance;
- if (curBalance.gte(toDeduct)) {
- newBalance = newBalance.sub(toDeduct);
- entityIdToTotal[entityId] = 0;
- shouldUpdate = true;
- update.entities[entityId] = {
- id: entityId,
- balance: newBalance.toNumber(),
- usage: 0,
- };
- } else {
- newBalance = new Decimal(0);
- entityIdToTotal[entityId] = toDeduct.sub(curBalance).toNumber();
- shouldUpdate = true;
- update.entities[entityId] = {
- id: entityId,
- balance: 0,
- usage: 0,
- };
- }
- }
- // console.log(`Max clearing for row ${row.id}`);
- // console.log(`Update:`, update.entities);
+ if (curBalance.gte(toDeduct)) {
+ newBalance = newBalance.sub(toDeduct);
+ entityIdToTotal[entityId] = 0;
+ shouldUpdate = true;
+ update.entities[entityId] = {
+ id: entityId,
+ balance: newBalance.toNumber(),
+ usage: 0,
+ };
+ } else {
+ newBalance = new Decimal(0);
+ entityIdToTotal[entityId] = toDeduct.sub(curBalance).toNumber();
+ shouldUpdate = true;
+ update.entities[entityId] = {
+ id: entityId,
+ balance: 0,
+ usage: 0,
+ };
+ }
+ }
+ // console.log(`Max clearing for row ${row.id}`);
+ // console.log(`Update:`, update.entities);
- // If all keys are 0, then delete the row
- if (
- Object.values(update.entities).every(
- (entity: EntityRolloverBalance) => entity.balance === 0
- )
- ) {
- toDelete.push(row.id);
- } else if (shouldUpdate) {
- toUpdate.push(update);
- }
- }
+ // If all keys are 0, then delete the row
+ if (
+ Object.values(update.entities).every(
+ (entity: EntityRolloverBalance) => entity.balance === 0,
+ )
+ ) {
+ toDelete.push(row.id);
+ } else if (shouldUpdate) {
+ toUpdate.push(update);
+ }
+ }
- return { toDelete, toUpdate };
- }
+ return { toDelete, toUpdate };
+ }
}
// For each entity ID, perform maximum clearing...
diff --git a/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts
index a18fb668e..6c71d0424 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/entBalanceUtils.ts
@@ -3,47 +3,47 @@ import { nullish } from "@/utils/genUtils.js";
import { FullCustomerEntitlement } from "@autumn/shared";
export const getEntityBalance = ({
- cusEnt,
- entityId,
+ cusEnt,
+ entityId,
}: {
- cusEnt: FullCustomerEntitlement;
- entityId: string;
+ cusEnt: FullCustomerEntitlement;
+ entityId: string;
}) => {
- let entityBalance = cusEnt.entities?.[entityId!]?.balance;
- let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0;
+ let entityBalance = cusEnt.entities?.[entityId!]?.balance;
+ let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0;
- if (nullish(entityBalance)) return { balance: 0, adjustment: 0 };
+ if (nullish(entityBalance)) return { balance: 0, adjustment: 0 };
- return {
- balance: entityBalance,
- adjustment,
- };
+ return {
+ balance: entityBalance,
+ adjustment,
+ };
};
export const getSummedEntityBalances = ({
- cusEnt,
+ cusEnt,
}: {
- cusEnt: FullCustomerEntitlement;
+ cusEnt: FullCustomerEntitlement;
}) => {
- if (nullish(cusEnt.entities)) {
- return {
- balance: 0,
- adjustment: 0,
- unused: 0,
- count: 0,
- };
- }
+ if (nullish(cusEnt.entities)) {
+ return {
+ balance: 0,
+ adjustment: 0,
+ unused: 0,
+ count: 0,
+ };
+ }
- return {
- balance: Object.values(cusEnt.entities!).reduce(
- (acc, curr) => acc + curr.balance,
- 0
- ),
- adjustment: Object.values(cusEnt.entities!).reduce(
- (acc, curr) => acc + curr.adjustment,
- 0
- ),
- unused: 0,
- count: Object.values(cusEnt.entities!).length,
- };
+ return {
+ balance: Object.values(cusEnt.entities!).reduce(
+ (acc, curr) => acc + curr.balance,
+ 0,
+ ),
+ adjustment: Object.values(cusEnt.entities!).reduce(
+ (acc, curr) => acc + curr.adjustment,
+ 0,
+ ),
+ unused: 0,
+ count: Object.values(cusEnt.entities!).length,
+ };
};
diff --git a/server/src/internal/customers/cusProducts/cusEnts/groupByUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/groupByUtils.ts
index 6c04a904a..ce599d4e3 100644
--- a/server/src/internal/customers/cusProducts/cusEnts/groupByUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusEnts/groupByUtils.ts
@@ -1,6 +1,4 @@
-import {
- FullCustomerEntitlement,
-} from "@autumn/shared";
+import { FullCustomerEntitlement } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
export const getResetBalancesUpdate = ({
@@ -29,4 +27,4 @@ export const getResetBalancesUpdate = ({
}
return update;
-};
\ No newline at end of file
+};
diff --git a/server/src/internal/customers/cusProducts/cusPrices/CusPriceService.ts b/server/src/internal/customers/cusProducts/cusPrices/CusPriceService.ts
index f82d4fa17..a1fe84afb 100644
--- a/server/src/internal/customers/cusProducts/cusPrices/CusPriceService.ts
+++ b/server/src/internal/customers/cusProducts/cusPrices/CusPriceService.ts
@@ -1,100 +1,100 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- CustomerPrice,
- FullCustomerEntitlement,
- FullCustomerPrice,
+ CustomerPrice,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
} from "@autumn/shared";
import { customerPrices } from "@autumn/shared";
import { eq } from "drizzle-orm";
export class CusPriceService {
- static async getRelatedToCusEnt({
- db,
- cusEnt,
- }: {
- db: DrizzleCli;
- cusEnt: FullCustomerEntitlement;
- }) {
- const customerPricesData = await db.query.customerPrices.findMany({
- where: eq(customerPrices.customer_product_id, cusEnt.customer_product_id),
- with: {
- price: true,
- },
- });
+ static async getRelatedToCusEnt({
+ db,
+ cusEnt,
+ }: {
+ db: DrizzleCli;
+ cusEnt: FullCustomerEntitlement;
+ }) {
+ const customerPricesData = await db.query.customerPrices.findMany({
+ where: eq(customerPrices.customer_product_id, cusEnt.customer_product_id),
+ with: {
+ price: true,
+ },
+ });
- const matchingCustomerPrice = customerPricesData.find(
- (cp) => cp.price?.entitlement_id === cusEnt.entitlement.id
- ) as FullCustomerPrice | undefined;
+ const matchingCustomerPrice = customerPricesData.find(
+ (cp) => cp.price?.entitlement_id === cusEnt.entitlement.id,
+ ) as FullCustomerPrice | undefined;
- return matchingCustomerPrice || null;
- }
+ return matchingCustomerPrice || null;
+ }
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: CustomerPrice[] | CustomerPrice;
- }) {
- if (Array.isArray(data) && data.length == 0) {
- return;
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: CustomerPrice[] | CustomerPrice;
+ }) {
+ if (Array.isArray(data) && data.length == 0) {
+ return;
+ }
- const inserted = await db
- .insert(customerPrices)
- .values(data as any)
- .returning();
- return inserted as CustomerPrice[];
- }
+ const inserted = await db
+ .insert(customerPrices)
+ .values(data as any)
+ .returning();
+ return inserted as CustomerPrice[];
+ }
- static async getByCustomerProductId({
- db,
- customerProductId,
- }: {
- db: DrizzleCli;
- customerProductId: string;
- }) {
- const data = await db.query.customerPrices.findMany({
- where: eq(customerPrices.customer_product_id, customerProductId),
- with: {
- price: true,
- },
- });
+ static async getByCustomerProductId({
+ db,
+ customerProductId,
+ }: {
+ db: DrizzleCli;
+ customerProductId: string;
+ }) {
+ const data = await db.query.customerPrices.findMany({
+ where: eq(customerPrices.customer_product_id, customerProductId),
+ with: {
+ price: true,
+ },
+ });
- return data as FullCustomerPrice[];
- }
+ return data as FullCustomerPrice[];
+ }
- static async delete({ db, id }: { db: DrizzleCli; id: string }) {
- const deleted = await db
- .delete(customerPrices)
- .where(eq(customerPrices.id, id))
- .returning();
- return deleted;
- }
+ static async delete({ db, id }: { db: DrizzleCli; id: string }) {
+ const deleted = await db
+ .delete(customerPrices)
+ .where(eq(customerPrices.id, id))
+ .returning();
+ return deleted;
+ }
- static async update({
- db,
- id,
- updates,
- }: {
- db: DrizzleCli;
- id: string;
- updates: Partial;
- }) {
- const updated = await db
- .update(customerPrices)
- .set(updates)
- .where(eq(customerPrices.id, id))
- .returning();
+ static async update({
+ db,
+ id,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ updates: Partial;
+ }) {
+ const updated = await db
+ .update(customerPrices)
+ .set(updates)
+ .where(eq(customerPrices.id, id))
+ .returning();
- // Ensure exactly one record was updated
- if (updated.length !== 1) {
- throw new Error(
- `Expected to update exactly 1 record, but updated ${updated.length} records`
- );
- }
+ // Ensure exactly one record was updated
+ if (updated.length !== 1) {
+ throw new Error(
+ `Expected to update exactly 1 record, but updated ${updated.length} records`,
+ );
+ }
- return updated[0];
- }
+ return updated[0];
+ }
}
diff --git a/server/src/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.ts b/server/src/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.ts
index 06e4b80b1..4be16c05b 100644
--- a/server/src/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusPrices/convertCusPriceUtils.ts
@@ -1,14 +1,14 @@
import { FullCustomerPrice, UsagePriceConfig } from "@autumn/shared";
export const featureToCusPrice = ({
- internalFeatureId,
- cusPrices,
+ internalFeatureId,
+ cusPrices,
}: {
- internalFeatureId: string;
- cusPrices: FullCustomerPrice[];
+ internalFeatureId: string;
+ cusPrices: FullCustomerPrice[];
}) => {
- return cusPrices.find((cusPrice) => {
- const config = cusPrice.price.config as UsagePriceConfig;
- return config.internal_feature_id === internalFeatureId;
- });
+ return cusPrices.find((cusPrice) => {
+ const config = cusPrice.price.config as UsagePriceConfig;
+ return config.internal_feature_id === internalFeatureId;
+ });
};
diff --git a/server/src/internal/customers/cusProducts/cusPrices/cusPriceUtils.ts b/server/src/internal/customers/cusProducts/cusPrices/cusPriceUtils.ts
index 1169c1690..41e39ed42 100644
--- a/server/src/internal/customers/cusProducts/cusPrices/cusPriceUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusPrices/cusPriceUtils.ts
@@ -1,12 +1,12 @@
import {
- ErrCode,
- FullCusProduct,
- FullCustomerEntitlement,
- FullCustomerPrice,
- getFeatureInvoiceDescription,
- OnDecrease,
- Price,
- UsagePriceConfig,
+ ErrCode,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ getFeatureInvoiceDescription,
+ OnDecrease,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import { getTotalNegativeBalance } from "../cusEnts/cusEntUtils.js";
import { Decimal } from "decimal.js";
@@ -14,103 +14,103 @@ import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
export const getRelatedCusEnt = ({
- cusPrice,
- cusEnts,
+ cusPrice,
+ cusEnts,
}: {
- cusPrice: FullCustomerPrice;
- cusEnts: FullCustomerEntitlement[];
+ cusPrice: FullCustomerPrice;
+ cusEnts: FullCustomerEntitlement[];
}) => {
- let config = cusPrice.price.config as UsagePriceConfig;
- if (!config) {
- console.log("No config found for cusPrice", cusPrice);
- return null;
- }
+ let config = cusPrice.price.config as UsagePriceConfig;
+ if (!config) {
+ console.log("No config found for cusPrice", cusPrice);
+ return null;
+ }
- const cusEnt = cusEnts.find(
- (ce) =>
- ce.customer_product_id == cusPrice.customer_product_id &&
- ce.entitlement.id == cusPrice.price.entitlement_id,
- );
+ const cusEnt = cusEnts.find(
+ (ce) =>
+ ce.customer_product_id == cusPrice.customer_product_id &&
+ ce.entitlement.id == cusPrice.price.entitlement_id,
+ );
- return cusEnt;
+ return cusEnt;
};
// Get overage for a cusPrice
export const getCusPriceUsage = ({
- cusPrice,
- price,
- cusProduct,
- logger,
- withProdPrefix = true,
+ cusPrice,
+ price,
+ cusProduct,
+ logger,
+ withProdPrefix = true,
}: {
- cusPrice?: FullCustomerPrice;
- price?: Price;
- cusProduct: FullCusProduct;
- logger: any;
- withProdPrefix?: boolean;
+ cusPrice?: FullCustomerPrice;
+ price?: Price;
+ cusProduct: FullCusProduct;
+ logger: any;
+ withProdPrefix?: boolean;
}) => {
- if (!cusPrice) {
- cusPrice = cusProduct.customer_prices.find(
- (cp) => cp.price.id == price!.id,
- );
- if (!cusPrice) {
- throw new RecaseError({
- message: `getCusPriceUsage: No cusPrice found for price: ${price!.id}`,
- code: ErrCode.CusPriceNotFound,
- });
- }
- }
+ if (!cusPrice) {
+ cusPrice = cusProduct.customer_prices.find(
+ (cp) => cp.price.id == price!.id,
+ );
+ if (!cusPrice) {
+ throw new RecaseError({
+ message: `getCusPriceUsage: No cusPrice found for price: ${price!.id}`,
+ code: ErrCode.CusPriceNotFound,
+ });
+ }
+ }
- // 1. Get related cusEnt
- const cusEnt = getRelatedCusEnt({
- cusPrice,
- cusEnts: cusProduct.customer_entitlements,
- });
- const config = cusPrice.price.config as UsagePriceConfig;
+ // 1. Get related cusEnt
+ const cusEnt = getRelatedCusEnt({
+ cusPrice,
+ cusEnts: cusProduct.customer_entitlements,
+ });
+ const config = cusPrice.price.config as UsagePriceConfig;
- if (!cusEnt) {
- logger.warn(`No cusEnt found for cusPrice: ${cusPrice.id}`);
- return {
- amount: 0,
- description: "",
- usage: 0,
- overage: 0,
- roundedUsage: 0,
- };
- }
+ if (!cusEnt) {
+ logger.warn(`No cusEnt found for cusPrice: ${cusPrice.id}`);
+ return {
+ amount: 0,
+ description: "",
+ usage: 0,
+ overage: 0,
+ roundedUsage: 0,
+ };
+ }
- // 2. Get overage
- const totalNegativeBalance = getTotalNegativeBalance({
- cusEnt: cusEnt as any,
- balance: cusEnt.balance!,
- entities: cusEnt.entities!,
- billingUnits:
- (cusPrice.price.config as UsagePriceConfig).billing_units || 1,
- });
+ // 2. Get overage
+ const totalNegativeBalance = getTotalNegativeBalance({
+ cusEnt: cusEnt as any,
+ balance: cusEnt.balance!,
+ entities: cusEnt.entities!,
+ billingUnits:
+ (cusPrice.price.config as UsagePriceConfig).billing_units || 1,
+ });
- const allowance = cusEnt.entitlement.allowance || 0;
- const usage = new Decimal(allowance).minus(totalNegativeBalance).toNumber();
- const billingUnits = config.billing_units || 1;
+ const allowance = cusEnt.entitlement.allowance || 0;
+ const usage = new Decimal(allowance).minus(totalNegativeBalance).toNumber();
+ const billingUnits = config.billing_units || 1;
- const roundedQuantity =
- Math.ceil(new Decimal(usage).div(billingUnits).toNumber()) * billingUnits;
+ const roundedQuantity =
+ Math.ceil(new Decimal(usage).div(billingUnits).toNumber()) * billingUnits;
- const amount = getPriceForOverage(cusPrice.price, -totalNegativeBalance);
+ const amount = getPriceForOverage(cusPrice.price, -totalNegativeBalance);
- let description = getFeatureInvoiceDescription({
- feature: cusEnt.entitlement.feature,
- usage,
- });
+ let description = getFeatureInvoiceDescription({
+ feature: cusEnt.entitlement.feature,
+ usage,
+ });
- if (withProdPrefix) {
- description = `${cusProduct.product.name} - ${description}`;
- }
+ if (withProdPrefix) {
+ description = `${cusProduct.product.name} - ${description}`;
+ }
- return {
- usage, // total usage
- overage: -totalNegativeBalance, // usage that's past the allowance
- roundedUsage: roundedQuantity, // usage rounded to the nearest billing unit
- amount,
- description,
- };
+ return {
+ usage, // total usage
+ overage: -totalNegativeBalance, // usage that's past the allowance
+ roundedUsage: roundedQuantity, // usage rounded to the nearest billing unit
+ amount,
+ description,
+ };
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts
index 042c8cd4e..6a1a5df9c 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts
@@ -1,22 +1,22 @@
import {
- APIVersion,
- AppEnv,
- AttachScenario,
- CusProductResponseSchema,
- CusProductStatus,
- Entity,
- FixedPriceConfig,
- FullCusProduct,
- FullCustomer,
- Organization,
- PriceType,
- Subscription,
- TierInfinite,
- UsagePriceConfig,
+ APIVersion,
+ AppEnv,
+ AttachScenario,
+ CusProductResponseSchema,
+ CusProductStatus,
+ Entity,
+ FixedPriceConfig,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
+ PriceType,
+ Subscription,
+ TierInfinite,
+ UsagePriceConfig,
} from "@autumn/shared";
import {
- getPriceOptions,
- getUsageTier,
+ getPriceOptions,
+ getUsageTier,
} from "@/internal/products/prices/priceUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { CusProductService, RELEVANT_STATUSES } from "./CusProductService.js";
@@ -41,609 +41,610 @@ import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js
// 1. Cancel cusProductSubscriptions
// CAN DELETE
export const cancelCusProductSubscriptions = async ({
- cusProduct,
- org,
- env,
- excludeIds,
- expireImmediately = true,
- logger,
- prorate = true,
+ cusProduct,
+ org,
+ env,
+ excludeIds,
+ expireImmediately = true,
+ logger,
+ prorate = true,
}: {
- cusProduct: FullCusProduct;
- org: Organization;
- env: AppEnv;
- excludeIds?: string[];
- expireImmediately?: boolean;
- logger: any;
- prorate?: boolean;
+ cusProduct: FullCusProduct;
+ org: Organization;
+ env: AppEnv;
+ excludeIds?: string[];
+ expireImmediately?: boolean;
+ logger: any;
+ prorate?: boolean;
}) => {
- // 1. Cancel all subscriptions
- const stripeCli = createStripeCli({
- org: org,
- env: env,
- });
+ // 1. Cancel all subscriptions
+ const stripeCli = createStripeCli({
+ org: org,
+ env: env,
+ });
- let latestSubEnd: number | undefined;
- if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) {
- let stripeSubs = await getStripeSubs({
- stripeCli,
- subIds: cusProduct.subscription_ids,
- });
+ let latestSubEnd: number | undefined;
+ if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) {
+ let stripeSubs = await getStripeSubs({
+ stripeCli,
+ subIds: cusProduct.subscription_ids,
+ });
- latestSubEnd = stripeSubs?.[0]?.items.data[0].current_period_end;
- }
+ latestSubEnd = stripeSubs?.[0]?.items.data[0].current_period_end;
+ }
- const cancelStripeSub = async (subId: string) => {
- if (excludeIds && excludeIds.includes(subId)) {
- return;
- }
+ const cancelStripeSub = async (subId: string) => {
+ if (excludeIds && excludeIds.includes(subId)) {
+ return;
+ }
- try {
- if (expireImmediately) {
- await stripeCli.subscriptions.cancel(subId, {
- prorate: prorate,
- });
- } else {
- await stripeCli.subscriptions.update(subId, {
- cancel_at: latestSubEnd || undefined,
- cancel_at_period_end: latestSubEnd ? undefined : true,
- });
- }
+ try {
+ if (expireImmediately) {
+ await stripeCli.subscriptions.cancel(subId, {
+ prorate: prorate,
+ });
+ } else {
+ await stripeCli.subscriptions.update(subId, {
+ cancel_at: latestSubEnd || undefined,
+ cancel_at_period_end: latestSubEnd ? undefined : true,
+ });
+ }
- logger.info(`Cancelled stripe subscription ${subId}, org: ${org.slug}`);
- } catch (error: any) {
- if (error.code != "resource_missing") {
- console.log(
- `Error canceling stripe subscription ${error.code}: ${error.message}`
- );
- } // else subscription probably already cancelled
- }
- };
+ logger.info(`Cancelled stripe subscription ${subId}, org: ${org.slug}`);
+ } catch (error: any) {
+ if (error.code != "resource_missing") {
+ console.log(
+ `Error canceling stripe subscription ${error.code}: ${error.message}`,
+ );
+ } // else subscription probably already cancelled
+ }
+ };
- if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) {
- const batchCancel = [];
- for (const subId of cusProduct.subscription_ids) {
- batchCancel.push(cancelStripeSub(subId));
- }
- await Promise.all(batchCancel);
- return true;
- }
+ if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) {
+ const batchCancel = [];
+ for (const subId of cusProduct.subscription_ids) {
+ batchCancel.push(cancelStripeSub(subId));
+ }
+ await Promise.all(batchCancel);
+ return true;
+ }
- return false;
+ return false;
};
export const getDefaultProduct = async ({
- req,
- productGroup,
+ req,
+ productGroup,
}: {
- req: ExtendedRequest;
- productGroup: string;
+ req: ExtendedRequest;
+ productGroup: string;
}) => {
- const { db, org, env, logger } = req;
- const defaultProducts = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ const { db, org, env, logger } = req;
+ const defaultProducts = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- let defaultProd = defaultProducts.find(
- (p) =>
- p.group === productGroup && !isDefaultTrialFullProduct({ product: p })
- );
+ let defaultProd = defaultProducts.find(
+ (p) =>
+ p.group === productGroup && !isDefaultTrialFullProduct({ product: p }),
+ );
- return defaultProd;
+ return defaultProd;
};
export const activateDefaultProduct = async ({
- req,
- productGroup,
- fullCus,
- curCusProduct,
+ req,
+ productGroup,
+ fullCus,
+ curCusProduct,
}: {
- req: ExtendedRequest;
- productGroup: string;
- fullCus: FullCustomer;
- curCusProduct?: FullCusProduct;
+ req: ExtendedRequest;
+ productGroup: string;
+ fullCus: FullCustomer;
+ curCusProduct?: FullCusProduct;
}) => {
- const { db, org, env, logger } = req;
- // 1. Expire current product
- const defaultProducts = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ const { db, org, env, logger } = req;
+ // 1. Expire current product
+ const defaultProducts = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- // Look for a paid default trial first, then fall back to free default
- let defaultProd = defaultProducts.find(
- (p) => p.group === productGroup && isDefaultTrialFullProduct({ product: p })
- );
+ // Look for a paid default trial first, then fall back to free default
+ let defaultProd = defaultProducts.find(
+ (p) =>
+ p.group === productGroup && isDefaultTrialFullProduct({ product: p }),
+ );
- let defaultableProducts = {
- free: defaultProducts.filter(
- (p) => p.group === productGroup && isFreeProduct(p.prices)
- ),
- paid: defaultProducts.filter(
- (p) =>
- p.group === productGroup && isDefaultTrialFullProduct({ product: p })
- ),
- };
+ let defaultableProducts = {
+ free: defaultProducts.filter(
+ (p) => p.group === productGroup && isFreeProduct(p.prices),
+ ),
+ paid: defaultProducts.filter(
+ (p) =>
+ p.group === productGroup && isDefaultTrialFullProduct({ product: p }),
+ ),
+ };
- // console.log("Found defaultable products:", {
- // free: defaultableProducts.free.map((p) => p.name),
- // paid: defaultableProducts.paid.map((p) => p.name),
- // });
+ // console.log("Found defaultable products:", {
+ // free: defaultableProducts.free.map((p) => p.name),
+ // paid: defaultableProducts.paid.map((p) => p.name),
+ // });
- if (defaultableProducts.paid.length > 0) {
- defaultProd = defaultableProducts.paid[0];
- } else if (defaultableProducts.free.length > 0) {
- defaultProd = defaultableProducts.free[0];
- } else {
- return false;
- }
+ if (defaultableProducts.paid.length > 0) {
+ defaultProd = defaultableProducts.paid[0];
+ } else if (defaultableProducts.free.length > 0) {
+ defaultProd = defaultableProducts.free[0];
+ } else {
+ return false;
+ }
- if (curCusProduct?.internal_product_id == defaultProd.internal_id) {
- return false;
- }
+ if (curCusProduct?.internal_product_id == defaultProd.internal_id) {
+ return false;
+ }
- const stripeCli = createStripeCli({ org, env });
- let defaultIsFree = isFreeProduct(defaultProd.prices);
- let isDefaultTrial = isDefaultTrialFullProduct({ product: defaultProd });
+ const stripeCli = createStripeCli({ org, env });
+ let defaultIsFree = isFreeProduct(defaultProd.prices);
+ let isDefaultTrial = isDefaultTrialFullProduct({ product: defaultProd });
- // Initialize Stripe customer and products if needed (for paid non-trial products)
- if (!defaultIsFree && !isDefaultTrial) {
- await initStripeCusAndProducts({
- db,
- org,
- env,
- customer: fullCus,
- products: [defaultProd],
- logger,
- });
- }
+ // Initialize Stripe customer and products if needed (for paid non-trial products)
+ if (!defaultIsFree && !isDefaultTrial) {
+ await initStripeCusAndProducts({
+ db,
+ org,
+ env,
+ customer: fullCus,
+ products: [defaultProd],
+ logger,
+ });
+ }
- if (!isDefaultTrial) {
- const existingDefaultProduct = fullCus.customer_products.find(
- (cp) =>
- cp.product.internal_id === defaultProd!.internal_id &&
- (cp.status === CusProductStatus.Active ||
- cp.status === CusProductStatus.PastDue ||
- cp.status === CusProductStatus.Trialing)
- );
+ if (!isDefaultTrial) {
+ const existingDefaultProduct = fullCus.customer_products.find(
+ (cp) =>
+ cp.product.internal_id === defaultProd!.internal_id &&
+ (cp.status === CusProductStatus.Active ||
+ cp.status === CusProductStatus.PastDue ||
+ cp.status === CusProductStatus.Trialing),
+ );
- if (existingDefaultProduct) {
- logger.info(
- `Default product ${defaultProd!.name} already exists for customer`
- );
- return false;
- }
+ if (existingDefaultProduct) {
+ logger.info(
+ `Default product ${defaultProd!.name} already exists for customer`,
+ );
+ return false;
+ }
- await handleAddProduct({
- req,
- attachParams: newCusToAttachParams({
- req,
- newCus: fullCus,
- products: [defaultProd],
- stripeCli,
- }),
- });
+ await handleAddProduct({
+ req,
+ attachParams: newCusToAttachParams({
+ req,
+ newCus: fullCus,
+ products: [defaultProd],
+ stripeCli,
+ }),
+ });
- // await createFullCusProduct({
- // db,
- // attachParams: {
- // org,
- // customer,
- // product: defaultProd,
- // prices: defaultProd.prices,
- // entitlements: defaultProd.entitlements,
- // freeTrial: defaultProd.free_trial || null,
- // optionsList: [],
- // entities: [],
- // features: [],
- // replaceables: [],
- // },
- // scenario: AttachScenario.New,
- // logger,
- // });
+ // await createFullCusProduct({
+ // db,
+ // attachParams: {
+ // org,
+ // customer,
+ // product: defaultProd,
+ // prices: defaultProd.prices,
+ // entitlements: defaultProd.entitlements,
+ // freeTrial: defaultProd.free_trial || null,
+ // optionsList: [],
+ // entities: [],
+ // features: [],
+ // replaceables: [],
+ // },
+ // scenario: AttachScenario.New,
+ // logger,
+ // });
- // console.log(` ✅ activated default product: ${defaultProd.group}`);
- return true;
- } else if (isDefaultTrial && defaultableProducts.free.length > 0) {
- defaultProd = defaultableProducts.free[0];
+ // console.log(` ✅ activated default product: ${defaultProd.group}`);
+ return true;
+ } else if (isDefaultTrial && defaultableProducts.free.length > 0) {
+ defaultProd = defaultableProducts.free[0];
- // Check if the free default product already exists to prevent duplicates
- const existingFreeProduct = fullCus.customer_products.find(
- (cp) =>
- cp.product.internal_id === defaultProd!.internal_id &&
- (cp.status === CusProductStatus.Active ||
- cp.status === CusProductStatus.PastDue)
- );
+ // Check if the free default product already exists to prevent duplicates
+ const existingFreeProduct = fullCus.customer_products.find(
+ (cp) =>
+ cp.product.internal_id === defaultProd!.internal_id &&
+ (cp.status === CusProductStatus.Active ||
+ cp.status === CusProductStatus.PastDue),
+ );
- if (existingFreeProduct) {
- logger.info(
- `Free default product ${defaultProd!.name} already exists for customer`
- );
- return false;
- }
+ if (existingFreeProduct) {
+ logger.info(
+ `Free default product ${defaultProd!.name} already exists for customer`,
+ );
+ return false;
+ }
- await handleAddProduct({
- req,
- attachParams: newCusToAttachParams({
- req,
- newCus: fullCus,
- products: [defaultProd],
- stripeCli,
- }),
- config: getDefaultAttachConfig(),
- });
+ await handleAddProduct({
+ req,
+ attachParams: newCusToAttachParams({
+ req,
+ newCus: fullCus,
+ products: [defaultProd],
+ stripeCli,
+ }),
+ config: getDefaultAttachConfig(),
+ });
- return true;
- }
+ return true;
+ }
};
export const expireAndActivate = async ({
- req,
- cusProduct,
- fullCus,
+ req,
+ cusProduct,
+ fullCus,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
}) => {
- const { db, org, env, logger } = req;
- // 1. Expire current product
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: { status: CusProductStatus.Expired, ended_at: Date.now() },
- });
+ const { db, org, env, logger } = req;
+ // 1. Expire current product
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: { status: CusProductStatus.Expired, ended_at: Date.now() },
+ });
- // Check if it's one time product
- let prices = cusProductToPrices({ cusProduct });
- let product = cusProduct.product;
- const isOneOffOrAddOn = product.is_add_on || isOneOff(prices);
+ // Check if it's one time product
+ let prices = cusProductToPrices({ cusProduct });
+ let product = cusProduct.product;
+ const isOneOffOrAddOn = product.is_add_on || isOneOff(prices);
- if (isOneOffOrAddOn || notNullish(cusProduct.internal_entity_id)) {
- return;
- }
+ if (isOneOffOrAddOn || notNullish(cusProduct.internal_entity_id)) {
+ return;
+ }
- await activateDefaultProduct({
- req,
- productGroup: cusProduct.product.group,
- fullCus,
- });
+ await activateDefaultProduct({
+ req,
+ productGroup: cusProduct.product.group,
+ fullCus,
+ });
};
export const activateFutureProduct = async ({
- req,
- cusProduct,
+ req,
+ cusProduct,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
}) => {
- const { db, org, env, logger } = req;
+ const { db, org, env, logger } = req;
- let cusProducts = await CusProductService.list({
- db,
- internalCustomerId: cusProduct.internal_customer_id,
- inStatuses: [CusProductStatus.Scheduled],
- });
+ let cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId: cusProduct.internal_customer_id,
+ inStatuses: [CusProductStatus.Scheduled],
+ });
- let { curScheduledProduct: futureProduct } = getExistingCusProducts({
- product: cusProduct.product,
- cusProducts,
- internalEntityId: cusProduct.internal_entity_id,
- });
+ let { curScheduledProduct: futureProduct } = getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts,
+ internalEntityId: cusProduct.internal_entity_id,
+ });
- if (!futureProduct) {
- return false;
- }
+ if (!futureProduct) {
+ return false;
+ }
- await CusProductService.update({
- db,
- cusProductId: futureProduct.id,
- updates: { status: CusProductStatus.Active },
- });
+ await CusProductService.update({
+ db,
+ cusProductId: futureProduct.id,
+ updates: { status: CusProductStatus.Active },
+ });
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: cusProduct.internal_customer_id,
- org,
- env,
- customerId: null,
- scenario: AttachScenario.New,
- cusProduct: futureProduct,
- logger,
- });
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: cusProduct.internal_customer_id,
+ org,
+ env,
+ customerId: null,
+ scenario: AttachScenario.New,
+ cusProduct: futureProduct,
+ logger,
+ });
- return futureProduct;
+ return futureProduct;
- // if (subIsPrematurelyCanceled(subscription)) {
- // console.log(
- // " 🔔 Subscription prematurely canceled, deleting scheduled products"
- // );
+ // if (subIsPrematurelyCanceled(subscription)) {
+ // console.log(
+ // " 🔔 Subscription prematurely canceled, deleting scheduled products"
+ // );
- // await deleteScheduledIds({
- // stripeCli,
- // scheduledIds: futureProduct.scheduled_ids || [],
- // });
- // await CusProductService.delete({
- // db,
- // cusProductId: futureProduct.id,
- // });
- // return false;
- // } else {
- // await CusProductService.update({
- // db,
- // cusProductId: futureProduct.id,
- // updates: { status: CusProductStatus.Active },
- // });
+ // await deleteScheduledIds({
+ // stripeCli,
+ // scheduledIds: futureProduct.scheduled_ids || [],
+ // });
+ // await CusProductService.delete({
+ // db,
+ // cusProductId: futureProduct.id,
+ // });
+ // return false;
+ // } else {
+ // await CusProductService.update({
+ // db,
+ // cusProductId: futureProduct.id,
+ // updates: { status: CusProductStatus.Active },
+ // });
- // await addProductsUpdatedWebhookTask({
- // req,
- // internalCustomerId: cusProduct.internal_customer_id,
- // org,
- // env,
- // customerId: null,
- // scenario: AttachScenario.New,
- // cusProduct: futureProduct,
- // logger,
- // });
+ // await addProductsUpdatedWebhookTask({
+ // req,
+ // internalCustomerId: cusProduct.internal_customer_id,
+ // org,
+ // env,
+ // customerId: null,
+ // scenario: AttachScenario.New,
+ // cusProduct: futureProduct,
+ // logger,
+ // });
- // return true;
- // }
+ // return true;
+ // }
};
// GET CUS ENTS FROM CUS PRODUCTS
export const processFullCusProduct = ({
- cusProduct,
- subs,
- org,
- entities = [],
- apiVersion,
+ cusProduct,
+ subs,
+ org,
+ entities = [],
+ apiVersion,
}: {
- cusProduct: FullCusProduct;
- org: Organization;
- subs?: Subscription[];
- entities?: Entity[];
- apiVersion: number;
+ cusProduct: FullCusProduct;
+ org: Organization;
+ subs?: Subscription[];
+ entities?: Entity[];
+ apiVersion: number;
}) => {
- // Process prices
+ // Process prices
- const prices = cusProduct.customer_prices.map((cp) => {
- let price = cp.price;
+ const prices = cusProduct.customer_prices.map((cp) => {
+ let price = cp.price;
- if (price.config?.type == PriceType.Fixed) {
- let config = price.config as FixedPriceConfig;
- return {
- amount: config.amount,
- interval: config.interval,
- };
- } else {
- let config = price.config as UsagePriceConfig;
- let priceOptions = getPriceOptions(price, cusProduct.options);
- let usageTier = getUsageTier(price, priceOptions?.quantity!);
- let cusEnt = getRelatedCusEnt({
- cusPrice: cp,
- cusEnts: cusProduct.customer_entitlements,
- });
+ if (price.config?.type == PriceType.Fixed) {
+ let config = price.config as FixedPriceConfig;
+ return {
+ amount: config.amount,
+ interval: config.interval,
+ };
+ } else {
+ let config = price.config as UsagePriceConfig;
+ let priceOptions = getPriceOptions(price, cusProduct.options);
+ let usageTier = getUsageTier(price, priceOptions?.quantity!);
+ let cusEnt = getRelatedCusEnt({
+ cusPrice: cp,
+ cusEnts: cusProduct.customer_entitlements,
+ });
- let ent = cusEnt?.entitlement;
+ let ent = cusEnt?.entitlement;
- let singleTier = ent?.allowance == 0 && config.usage_tiers.length == 1;
+ let singleTier = ent?.allowance == 0 && config.usage_tiers.length == 1;
- if (singleTier) {
- return {
- amount: usageTier.amount,
- interval: config.interval,
- quantity: priceOptions?.quantity,
- };
- } else {
- // Add allowance to tiers
- let allowance = ent?.allowance;
- let tiers;
+ if (singleTier) {
+ return {
+ amount: usageTier.amount,
+ interval: config.interval,
+ quantity: priceOptions?.quantity,
+ };
+ } else {
+ // Add allowance to tiers
+ let allowance = ent?.allowance;
+ let tiers;
- if (notNullish(allowance) && allowance! > 0) {
- tiers = [
- {
- to: allowance,
- amount: 0,
- },
- ...config.usage_tiers.map((tier) => {
- let isLastTier = tier.to == -1 || tier.to == TierInfinite;
- return {
- to: isLastTier ? tier.to : Number(tier.to) + allowance!,
- amount: tier.amount,
- };
- }),
- ];
- } else {
- tiers = config.usage_tiers.map((tier) => {
- let isLastTier = tier.to == -1 || tier.to == TierInfinite;
- return {
- to: isLastTier ? tier.to : Number(tier.to) + allowance!,
- amount: tier.amount,
- };
- });
- }
+ if (notNullish(allowance) && allowance! > 0) {
+ tiers = [
+ {
+ to: allowance,
+ amount: 0,
+ },
+ ...config.usage_tiers.map((tier) => {
+ let isLastTier = tier.to == -1 || tier.to == TierInfinite;
+ return {
+ to: isLastTier ? tier.to : Number(tier.to) + allowance!,
+ amount: tier.amount,
+ };
+ }),
+ ];
+ } else {
+ tiers = config.usage_tiers.map((tier) => {
+ let isLastTier = tier.to == -1 || tier.to == TierInfinite;
+ return {
+ to: isLastTier ? tier.to : Number(tier.to) + allowance!,
+ amount: tier.amount,
+ };
+ });
+ }
- return {
- tiers: tiers,
- name: "",
- quantity: priceOptions?.quantity,
- };
- }
- }
- });
+ return {
+ tiers: tiers,
+ name: "",
+ quantity: priceOptions?.quantity,
+ };
+ }
+ }
+ });
- const trialing =
- cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
+ const trialing =
+ cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
- const subIds = cusProduct.subscription_ids;
- let stripeSubData = {};
+ const subIds = cusProduct.subscription_ids;
+ let stripeSubData = {};
- if (
- subIds &&
- subIds.length > 0 &&
- org.config.api_version >= BREAK_API_VERSION
- ) {
- let baseSub = subs?.find(
- (s) => s.id == subIds[0] || (s as Subscription).stripe_id == subIds[0]
- );
- stripeSubData = {
- current_period_end: baseSub?.current_period_end
- ? baseSub.current_period_end * 1000
- : null,
- current_period_start: baseSub?.current_period_start
- ? baseSub.current_period_start * 1000
- : null,
- };
- }
+ if (
+ subIds &&
+ subIds.length > 0 &&
+ org.config.api_version >= BREAK_API_VERSION
+ ) {
+ let baseSub = subs?.find(
+ (s) => s.id == subIds[0] || (s as Subscription).stripe_id == subIds[0],
+ );
+ stripeSubData = {
+ current_period_end: baseSub?.current_period_end
+ ? baseSub.current_period_end * 1000
+ : null,
+ current_period_start: baseSub?.current_period_start
+ ? baseSub.current_period_start * 1000
+ : null,
+ };
+ }
- if (!subIds && trialing) {
- stripeSubData = {
- current_period_start: cusProduct.starts_at,
- current_period_end: cusProduct.trial_ends_at,
- };
- }
+ if (!subIds && trialing) {
+ stripeSubData = {
+ current_period_start: cusProduct.starts_at,
+ current_period_end: cusProduct.trial_ends_at,
+ };
+ }
- if (apiVersion >= APIVersion.v1_1) {
- if ((!subIds || subIds.length == 0) && trialing) {
- stripeSubData = {
- current_period_start: cusProduct.starts_at,
- current_period_end: cusProduct.trial_ends_at,
- };
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ if ((!subIds || subIds.length == 0) && trialing) {
+ stripeSubData = {
+ current_period_start: cusProduct.starts_at,
+ current_period_end: cusProduct.trial_ends_at,
+ };
+ }
- return CusProductResponseSchema.parse({
- id: cusProduct.product.id,
- name: cusProduct.product.name,
- group: cusProduct.product.group || null,
- status: trialing ? CusProductStatus.Trialing : cusProduct.status,
- // created_at: cusProduct.created_at,
- canceled_at: cusProduct.canceled_at,
- is_default: cusProduct.product.is_default || false,
- is_add_on: cusProduct.product.is_add_on || false,
+ return CusProductResponseSchema.parse({
+ id: cusProduct.product.id,
+ name: cusProduct.product.name,
+ group: cusProduct.product.group || null,
+ status: trialing ? CusProductStatus.Trialing : cusProduct.status,
+ // created_at: cusProduct.created_at,
+ canceled_at: cusProduct.canceled_at,
+ is_default: cusProduct.product.is_default || false,
+ is_add_on: cusProduct.product.is_add_on || false,
- stripe_subscription_ids: cusProduct.subscription_ids || [],
- started_at: cusProduct.starts_at,
- // entity_id: cusProduct.entity_id,
- entity_id: cusProduct.internal_entity_id
- ? entities?.find((e) => e.internal_id == cusProduct.internal_entity_id)
- ?.id
- : cusProduct.entity_id || undefined,
+ stripe_subscription_ids: cusProduct.subscription_ids || [],
+ started_at: cusProduct.starts_at,
+ // entity_id: cusProduct.entity_id,
+ entity_id: cusProduct.internal_entity_id
+ ? entities?.find((e) => e.internal_id == cusProduct.internal_entity_id)
+ ?.id
+ : cusProduct.entity_id || undefined,
- ...stripeSubData,
- });
- } else {
- let cusProductResponse = {
- id: cusProduct.product.id,
- name: cusProduct.product.name,
- group: cusProduct.product.group,
- status: trialing ? CusProductStatus.Trialing : cusProduct.status,
- created_at: cusProduct.created_at,
- canceled_at: cusProduct.canceled_at,
- processor: {
- type: cusProduct.processor?.type,
- subscription_id: cusProduct.processor?.subscription_id || null,
- },
- subscription_ids: cusProduct.subscription_ids || [],
- prices: prices,
- starts_at: cusProduct.starts_at,
+ ...stripeSubData,
+ });
+ } else {
+ let cusProductResponse = {
+ id: cusProduct.product.id,
+ name: cusProduct.product.name,
+ group: cusProduct.product.group,
+ status: trialing ? CusProductStatus.Trialing : cusProduct.status,
+ created_at: cusProduct.created_at,
+ canceled_at: cusProduct.canceled_at,
+ processor: {
+ type: cusProduct.processor?.type,
+ subscription_id: cusProduct.processor?.subscription_id || null,
+ },
+ subscription_ids: cusProduct.subscription_ids || [],
+ prices: prices,
+ starts_at: cusProduct.starts_at,
- ...stripeSubData,
- // prices: cusProduct.customer_prices,
- // entitlements: cusProduct.customer_entitlements,
- };
+ ...stripeSubData,
+ // prices: cusProduct.customer_prices,
+ // entitlements: cusProduct.customer_entitlements,
+ };
- return cusProductResponse;
- }
+ return cusProductResponse;
+ }
};
// GET CUSTOMER PRODUCT & ORG IN PARALLEL
export const fullCusProductToProduct = (cusProduct: FullCusProduct) => {
- return {
- ...cusProduct.product,
- prices: cusProduct.customer_prices.map((cp) => cp.price),
- entitlements: cusProduct.customer_entitlements.map((ce) => ce.entitlement),
- };
+ return {
+ ...cusProduct.product,
+ prices: cusProduct.customer_prices.map((cp) => cp.price),
+ entitlements: cusProduct.customer_entitlements.map((ce) => ce.entitlement),
+ };
};
export const searchCusProducts = ({
- productId,
- internalProductId,
- cusProducts,
- status,
+ productId,
+ internalProductId,
+ cusProducts,
+ status,
}: {
- productId?: string;
- internalProductId?: string;
- cusProducts: FullCusProduct[];
- status?: CusProductStatus;
+ productId?: string;
+ internalProductId?: string;
+ cusProducts: FullCusProduct[];
+ status?: CusProductStatus;
}) => {
- if (!cusProducts) {
- return undefined;
- }
- return cusProducts.find((cusProduct: FullCusProduct) => {
- let prodIdMatch = false;
- if (productId) {
- prodIdMatch = cusProduct.product.id === productId;
- } else if (internalProductId) {
- prodIdMatch = cusProduct.product.internal_id === internalProductId;
- }
- return prodIdMatch && (status ? cusProduct.status === status : true);
- });
+ if (!cusProducts) {
+ return undefined;
+ }
+ return cusProducts.find((cusProduct: FullCusProduct) => {
+ let prodIdMatch = false;
+ if (productId) {
+ prodIdMatch = cusProduct.product.id === productId;
+ } else if (internalProductId) {
+ prodIdMatch = cusProduct.product.internal_id === internalProductId;
+ }
+ return prodIdMatch && (status ? cusProduct.status === status : true);
+ });
};
export const getMainCusProduct = async ({
- db,
- internalCustomerId,
- productGroup,
+ db,
+ internalCustomerId,
+ productGroup,
}: {
- db: DrizzleCli;
- internalCustomerId: string;
- productGroup?: string;
+ db: DrizzleCli;
+ internalCustomerId: string;
+ productGroup?: string;
}) => {
- let cusProducts = await CusProductService.list({
- db,
- internalCustomerId,
- inStatuses: RELEVANT_STATUSES,
- });
+ let cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId,
+ inStatuses: RELEVANT_STATUSES,
+ });
- let mainCusProduct = cusProducts.find(
- (cusProduct: FullCusProduct) =>
- !cusProduct.product.is_add_on &&
- (productGroup ? cusProduct.product.group === productGroup : true)
- );
+ let mainCusProduct = cusProducts.find(
+ (cusProduct: FullCusProduct) =>
+ !cusProduct.product.is_add_on &&
+ (productGroup ? cusProduct.product.group === productGroup : true),
+ );
- return mainCusProduct as FullCusProduct;
+ return mainCusProduct as FullCusProduct;
};
export const getCusProductsWithStripeSubId = ({
- cusProducts,
- stripeSubId,
- curCusProductId,
+ cusProducts,
+ stripeSubId,
+ curCusProductId,
}: {
- cusProducts: FullCusProduct[];
- stripeSubId: string;
- curCusProductId?: string;
+ cusProducts: FullCusProduct[];
+ stripeSubId: string;
+ curCusProductId?: string;
}) => {
- return cusProducts.filter(
- (cusProduct) =>
- cusProduct.subscription_ids?.includes(stripeSubId) &&
- cusProduct.id !== curCusProductId
- );
+ return cusProducts.filter(
+ (cusProduct) =>
+ cusProduct.subscription_ids?.includes(stripeSubId) &&
+ cusProduct.id !== curCusProductId,
+ );
};
export const getFeatureQuantity = ({
- cusProduct,
- internalFeatureId,
+ cusProduct,
+ internalFeatureId,
}: {
- cusProduct: FullCusProduct;
- internalFeatureId: string;
+ cusProduct: FullCusProduct;
+ internalFeatureId: string;
}) => {
- const options = cusProduct.options;
- const option = options.find(
- (o) => o.internal_feature_id == internalFeatureId
- );
- return nullish(option?.quantity) ? 1 : option?.quantity!;
+ const options = cusProduct.options;
+ const option = options.find(
+ (o) => o.internal_feature_id == internalFeatureId,
+ );
+ return nullish(option?.quantity) ? 1 : option?.quantity!;
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.ts b/server/src/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.ts
index 4ddb8b6b4..ce87865c5 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.ts
@@ -1,5 +1,5 @@
import { FullCusProduct } from "@autumn/shared";
export const isCanceled = ({ cusProduct }: { cusProduct: FullCusProduct }) => {
- return cusProduct.canceled;
+ return cusProduct.canceled;
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/convertCusProduct.ts b/server/src/internal/customers/cusProducts/cusProductUtils/convertCusProduct.ts
index 0957f7b04..e8fc0adfa 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils/convertCusProduct.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils/convertCusProduct.ts
@@ -2,106 +2,106 @@ import Stripe from "stripe";
import { FullCusEntWithFullCusProduct, FullCusProduct } from "@autumn/shared";
import {
- getStripeSchedules,
- getStripeSubs,
+ getStripeSchedules,
+ getStripeSubs,
} from "@/external/stripe/stripeSubUtils.js";
export const cusProductsToSchedules = ({
- cusProducts,
- stripeCli,
+ cusProducts,
+ stripeCli,
}: {
- cusProducts: (FullCusProduct | undefined)[];
- stripeCli: Stripe;
+ cusProducts: (FullCusProduct | undefined)[];
+ stripeCli: Stripe;
}) => {
- let scheduleIds: string[] = [];
- for (const cusProduct of cusProducts) {
- if (cusProduct) {
- scheduleIds.push(...(cusProduct.scheduled_ids || []));
- }
- }
+ let scheduleIds: string[] = [];
+ for (const cusProduct of cusProducts) {
+ if (cusProduct) {
+ scheduleIds.push(...(cusProduct.scheduled_ids || []));
+ }
+ }
- return getStripeSchedules({
- stripeCli,
- scheduleIds,
- });
+ return getStripeSchedules({
+ stripeCli,
+ scheduleIds,
+ });
};
export const cusProductToSchedule = async ({
- cusProduct,
- stripeCli,
+ cusProduct,
+ stripeCli,
}: {
- cusProduct: FullCusProduct;
- stripeCli: Stripe;
+ cusProduct: FullCusProduct;
+ stripeCli: Stripe;
}) => {
- const subScheduleIds = cusProduct?.scheduled_ids || [];
- if (subScheduleIds.length === 0) {
- return null;
- }
+ const subScheduleIds = cusProduct?.scheduled_ids || [];
+ if (subScheduleIds.length === 0) {
+ return null;
+ }
- const schedule = await stripeCli.subscriptionSchedules.retrieve(
- subScheduleIds[0],
- {
- expand: ["phases.items.price"],
- }
- );
+ const schedule = await stripeCli.subscriptionSchedules.retrieve(
+ subScheduleIds[0],
+ {
+ expand: ["phases.items.price"],
+ },
+ );
- if (schedule.status == "canceled" || schedule.status == "released") {
- return undefined;
- }
+ if (schedule.status == "canceled" || schedule.status == "released") {
+ return undefined;
+ }
- return schedule;
+ return schedule;
};
export const cusProductToSub = async ({
- cusProduct,
- stripeCli,
+ cusProduct,
+ stripeCli,
}: {
- cusProduct?: FullCusProduct;
- stripeCli: Stripe;
+ cusProduct?: FullCusProduct;
+ stripeCli: Stripe;
}) => {
- const subId = cusProduct?.subscription_ids?.[0];
- if (!subId) {
- return undefined;
- }
- const sub = await stripeCli.subscriptions.retrieve(subId, {
- expand: ["items.data.price.tiers", "discounts.coupon.applies_to"],
- });
+ const subId = cusProduct?.subscription_ids?.[0];
+ if (!subId) {
+ return undefined;
+ }
+ const sub = await stripeCli.subscriptions.retrieve(subId, {
+ expand: ["items.data.price.tiers", "discounts.coupon.applies_to"],
+ });
- return sub;
+ return sub;
};
export const cusProductsToStripeSubs = ({
- cusProducts,
- stripeCli,
+ cusProducts,
+ stripeCli,
}: {
- cusProducts: FullCusProduct[];
- stripeCli: Stripe;
+ cusProducts: FullCusProduct[];
+ stripeCli: Stripe;
}) => {
- return getStripeSubs({
- stripeCli,
- subIds: cusProducts.flatMap((p: any) => p.subscription_ids || []),
- });
+ return getStripeSubs({
+ stripeCli,
+ subIds: cusProducts.flatMap((p: any) => p.subscription_ids || []),
+ });
};
export const cusProductToCusEnt = ({
- cusProduct,
- featureId,
+ cusProduct,
+ featureId,
}: {
- cusProduct: FullCusProduct;
- featureId: string;
+ cusProduct: FullCusProduct;
+ featureId: string;
}) => {
- let cusEnts = cusProduct.customer_entitlements;
+ let cusEnts = cusProduct.customer_entitlements;
- let fullCusEnt = cusEnts.find(
- (ce) => ce.entitlement.feature_id === featureId
- );
+ let fullCusEnt = cusEnts.find(
+ (ce) => ce.entitlement.feature_id === featureId,
+ );
- if (fullCusEnt) {
- return {
- ...fullCusEnt,
- customer_product: cusProduct,
- } as FullCusEntWithFullCusProduct;
- }
+ if (fullCusEnt) {
+ return {
+ ...fullCusEnt,
+ customer_product: cusProduct,
+ } as FullCusEntWithFullCusProduct;
+ }
- return undefined;
+ return undefined;
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts b/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts
index a3ed13e1b..d79b3e603 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils/findCusProduct.ts
@@ -3,40 +3,40 @@ import { FullCusProduct, FullCustomer } from "@autumn/shared";
import { ACTIVE_STATUSES, CusProductService } from "../CusProductService.js";
export const getActiveCusProduct = ({
- fullCus,
- cusProducts,
- productId,
+ fullCus,
+ cusProducts,
+ productId,
}: {
- fullCus?: FullCustomer;
- cusProducts?: FullCusProduct[];
- productId: string;
+ fullCus?: FullCustomer;
+ cusProducts?: FullCusProduct[];
+ productId: string;
}) => {
- if (fullCus) {
- return fullCus.customer_products.find(
- (cusProduct: FullCusProduct) =>
- cusProduct.product.id === productId &&
- ACTIVE_STATUSES.includes(cusProduct.status)
- );
- }
+ if (fullCus) {
+ return fullCus.customer_products.find(
+ (cusProduct: FullCusProduct) =>
+ cusProduct.product.id === productId &&
+ ACTIVE_STATUSES.includes(cusProduct.status),
+ );
+ }
- return undefined;
+ return undefined;
};
export const findCusProductById = async ({
- db,
- internalCustomerId,
- productId,
+ db,
+ internalCustomerId,
+ productId,
}: {
- db: DrizzleCli;
- internalCustomerId: string;
- productId: string;
+ db: DrizzleCli;
+ internalCustomerId: string;
+ productId: string;
}) => {
- let cusProducts = await CusProductService.list({
- db,
- internalCustomerId,
- });
+ let cusProducts = await CusProductService.list({
+ db,
+ internalCustomerId,
+ });
- return cusProducts.find(
- (cusProduct: FullCusProduct) => cusProduct.product.id === productId
- );
+ return cusProducts.find(
+ (cusProduct: FullCusProduct) => cusProduct.product.id === productId,
+ );
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.ts b/server/src/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.ts
index 98cca2ec8..2f9a2f1c0 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.ts
@@ -8,58 +8,58 @@ import { Product } from "@autumn/shared";
import { ACTIVE_STATUSES } from "../CusProductService.js";
export const getExistingCusProducts = ({
- product,
- cusProducts,
- internalEntityId,
+ product,
+ cusProducts,
+ internalEntityId,
}: {
- product: Product;
- cusProducts: FullCusProduct[];
- internalEntityId?: string | null;
+ product: Product;
+ cusProducts: FullCusProduct[];
+ internalEntityId?: string | null;
}) => {
- if (!cusProducts || cusProducts.length === 0) {
- return {
- curMainProduct: undefined,
- curSameProduct: undefined,
- curScheduledProduct: undefined,
- };
- }
+ if (!cusProducts || cusProducts.length === 0) {
+ return {
+ curMainProduct: undefined,
+ curSameProduct: undefined,
+ curScheduledProduct: undefined,
+ };
+ }
- let curMainProduct = cusProducts.find((cp: any) => {
- let sameGroup = cp.product.group === product.group;
- let isMain = !cp.product.is_add_on;
- let isActive =
- cp.status == CusProductStatus.Active ||
- cp.status == CusProductStatus.PastDue;
+ let curMainProduct = cusProducts.find((cp: any) => {
+ let sameGroup = cp.product.group === product.group;
+ let isMain = !cp.product.is_add_on;
+ let isActive =
+ cp.status == CusProductStatus.Active ||
+ cp.status == CusProductStatus.PastDue;
- let oneOff = isOneOff(cp.customer_prices.map((cp: any) => cp.price));
+ let oneOff = isOneOff(cp.customer_prices.map((cp: any) => cp.price));
- let sameEntity = internalEntityId
- ? cp.internal_entity_id === internalEntityId
- : nullish(cp.internal_entity_id);
+ let sameEntity = internalEntityId
+ ? cp.internal_entity_id === internalEntityId
+ : nullish(cp.internal_entity_id);
- return sameGroup && isMain && isActive && !oneOff && sameEntity;
- });
+ return sameGroup && isMain && isActive && !oneOff && sameEntity;
+ });
- const curSameProduct = cusProducts!.find((cp: any) => {
- const idMatch = cp.product.id === product.id;
- const entityMatch = internalEntityId
- ? cp.internal_entity_id === internalEntityId
- : nullish(cp.internal_entity_id);
+ const curSameProduct = cusProducts!.find((cp: any) => {
+ const idMatch = cp.product.id === product.id;
+ const entityMatch = internalEntityId
+ ? cp.internal_entity_id === internalEntityId
+ : nullish(cp.internal_entity_id);
- const isActive = ACTIVE_STATUSES.includes(cp.status);
+ const isActive = ACTIVE_STATUSES.includes(cp.status);
- return idMatch && entityMatch;
- });
+ return idMatch && entityMatch;
+ });
- const curScheduledProduct = cusProducts!.find(
- (cp: any) =>
- cp.status === CusProductStatus.Scheduled &&
- cp.product.group === product.group &&
- !cp.product.is_add_on &&
- (internalEntityId
- ? cp.internal_entity_id === internalEntityId
- : nullish(cp.internal_entity_id))
- );
+ const curScheduledProduct = cusProducts!.find(
+ (cp: any) =>
+ cp.status === CusProductStatus.Scheduled &&
+ cp.product.group === product.group &&
+ !cp.product.is_add_on &&
+ (internalEntityId
+ ? cp.internal_entity_id === internalEntityId
+ : nullish(cp.internal_entity_id)),
+ );
- return { curMainProduct, curSameProduct, curScheduledProduct };
+ return { curMainProduct, curSameProduct, curScheduledProduct };
};
diff --git a/server/src/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.ts b/server/src/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.ts
index b590aaafb..a21919e7e 100644
--- a/server/src/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.ts
+++ b/server/src/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.ts
@@ -4,8 +4,8 @@ import { cusProductToPrices } from "@autumn/shared";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
export type ScheduledStartStatusPair = {
- starts_at: number;
- status: CusProductStatus;
+ starts_at: number;
+ status: CusProductStatus;
};
/**
@@ -13,37 +13,37 @@ export type ScheduledStartStatusPair = {
* Results are sorted by starts_at ascending.
*/
export const getUniqueUpcomingSchedulePairs = ({
- cusProducts,
- now,
- includePast = false,
+ cusProducts,
+ now,
+ includePast = false,
}: {
- cusProducts: FullCusProduct[];
- now?: number;
- /** If true, include scheduled items with starts_at <= now */
- includePast?: boolean;
+ cusProducts: FullCusProduct[];
+ now?: number;
+ /** If true, include scheduled items with starts_at <= now */
+ includePast?: boolean;
}): number[] => {
- const currentTime = now ?? Date.now();
- const uniqueKeys = new Set();
- const pairs: ScheduledStartStatusPair[] = [];
+ const currentTime = now ?? Date.now();
+ const uniqueKeys = new Set();
+ const pairs: ScheduledStartStatusPair[] = [];
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const free = isFreeProduct(prices);
- const oneOff = isOneOff(prices);
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const free = isFreeProduct(prices);
+ const oneOff = isOneOff(prices);
- if (cusProduct.status !== CusProductStatus.Scheduled) continue;
- if (typeof cusProduct.starts_at !== "number") continue;
- if (!includePast && cusProduct.starts_at <= currentTime) continue;
- // if (free || oneOff) continue;
+ if (cusProduct.status !== CusProductStatus.Scheduled) continue;
+ if (typeof cusProduct.starts_at !== "number") continue;
+ if (!includePast && cusProduct.starts_at <= currentTime) continue;
+ // if (free || oneOff) continue;
- const key = `${cusProduct.starts_at}|${cusProduct.status}`;
- if (uniqueKeys.has(key)) continue;
- uniqueKeys.add(key);
- pairs.push({ starts_at: cusProduct.starts_at, status: cusProduct.status });
- }
+ const key = `${cusProduct.starts_at}|${cusProduct.status}`;
+ if (uniqueKeys.has(key)) continue;
+ uniqueKeys.add(key);
+ pairs.push({ starts_at: cusProduct.starts_at, status: cusProduct.status });
+ }
- pairs.sort((a, b) => a.starts_at - b.starts_at);
- return pairs.map((p) => p.starts_at);
+ pairs.sort((a, b) => a.starts_at - b.starts_at);
+ return pairs.map((p) => p.starts_at);
};
// export const logSchedulePairs = ({
diff --git a/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts b/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts
index 8687ad941..768ac0a2b 100644
--- a/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts
+++ b/server/src/internal/customers/cusProducts/insertCusProduct/initCusEnt/initNextResetAt.ts
@@ -4,74 +4,74 @@ import { subtractFromUnixTillAligned } from "@/internal/products/prices/billingI
import { formatUnixToDate } from "@/utils/genUtils.js";
import { getNextEntitlementReset } from "@/utils/timeUtils.js";
import {
- EntInterval,
- AllowanceType,
- EntitlementWithFeature,
- FeatureType,
- FreeTrial,
+ EntInterval,
+ AllowanceType,
+ EntitlementWithFeature,
+ FeatureType,
+ FreeTrial,
} from "@autumn/shared";
import { UTCDate } from "@date-fns/utc";
export const initNextResetAt = ({
- entitlement,
- nextResetAt,
- trialEndsAt,
- freeTrial,
- anchorToUnix,
- now,
+ entitlement,
+ nextResetAt,
+ trialEndsAt,
+ freeTrial,
+ anchorToUnix,
+ now,
}: {
- entitlement: EntitlementWithFeature;
- nextResetAt?: number;
- trialEndsAt?: number;
- freeTrial: FreeTrial | null;
- anchorToUnix?: number;
- now: number;
+ entitlement: EntitlementWithFeature;
+ nextResetAt?: number;
+ trialEndsAt?: number;
+ freeTrial: FreeTrial | null;
+ anchorToUnix?: number;
+ now: number;
}) => {
- // 1. If entitlement is boolean, or unlimited, or lifetime, then next reset at is null
- if (
- entitlement.feature.type === FeatureType.Boolean ||
- entitlement.allowance_type === AllowanceType.Unlimited ||
- entitlement.interval == EntInterval.Lifetime
- ) {
- return null;
- }
+ // 1. If entitlement is boolean, or unlimited, or lifetime, then next reset at is null
+ if (
+ entitlement.feature.type === FeatureType.Boolean ||
+ entitlement.allowance_type === AllowanceType.Unlimited ||
+ entitlement.interval == EntInterval.Lifetime
+ ) {
+ return null;
+ }
- // 2. If nextResetAt is provided, return it...
- if (nextResetAt) {
- return nextResetAt;
- }
+ // 2. If nextResetAt is provided, return it...
+ if (nextResetAt) {
+ return nextResetAt;
+ }
- // 3. Calculate next reset at...
- let nextResetAtCalculated = null;
- let trialEndTimestamp = trialEndsAt
- ? Math.round(trialEndsAt / 1000)
- : freeTrial
- ? freeTrialToStripeTimestamp({ freeTrial, now })
- : null;
+ // 3. Calculate next reset at...
+ let nextResetAtCalculated = null;
+ let trialEndTimestamp = trialEndsAt
+ ? Math.round(trialEndsAt / 1000)
+ : freeTrial
+ ? freeTrialToStripeTimestamp({ freeTrial, now })
+ : null;
- if (
- freeTrial &&
- applyTrialToEntitlement(entitlement, freeTrial) &&
- trialEndTimestamp
- ) {
- nextResetAtCalculated = new UTCDate(trialEndTimestamp! * 1000);
- }
+ if (
+ freeTrial &&
+ applyTrialToEntitlement(entitlement, freeTrial) &&
+ trialEndTimestamp
+ ) {
+ nextResetAtCalculated = new UTCDate(trialEndTimestamp! * 1000);
+ }
- let resetInterval = entitlement.interval as EntInterval;
+ let resetInterval = entitlement.interval as EntInterval;
- nextResetAtCalculated = getNextEntitlementReset(
- nextResetAtCalculated || new UTCDate(now),
- resetInterval,
- entitlement.interval_count || 1
- ).getTime();
+ nextResetAtCalculated = getNextEntitlementReset(
+ nextResetAtCalculated || new UTCDate(now),
+ resetInterval,
+ entitlement.interval_count || 1,
+ ).getTime();
- // If anchorToUnix, align next reset at to anchorToUnix...
- if (anchorToUnix && nextResetAtCalculated) {
- nextResetAtCalculated = subtractFromUnixTillAligned({
- targetUnix: anchorToUnix,
- originalUnix: nextResetAtCalculated,
- });
- }
+ // If anchorToUnix, align next reset at to anchorToUnix...
+ if (anchorToUnix && nextResetAtCalculated) {
+ nextResetAtCalculated = subtractFromUnixTillAligned({
+ targetUnix: anchorToUnix,
+ originalUnix: nextResetAtCalculated,
+ });
+ }
- return nextResetAtCalculated;
+ return nextResetAtCalculated;
};
diff --git a/server/src/internal/customers/cusRouter.ts b/server/src/internal/customers/cusRouter.ts
index 1092dad20..e1ae573cc 100644
--- a/server/src/internal/customers/cusRouter.ts
+++ b/server/src/internal/customers/cusRouter.ts
@@ -29,24 +29,24 @@ export const cusRouter: Router = Router();
cusRouter.get("", handleBatchCustomers);
cusRouter.post("/all/search", async (req: any, res: any) => {
- try {
- const { search, page_size = 50, page = 1, last_item, filters } = req.body;
+ try {
+ const { search, page_size = 50, page = 1, last_item, filters } = req.body;
- const { data: customers, count } = await CusSearchService.search({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- search,
- filters,
- lastItem: last_item,
- pageNumber: page,
- pageSize: page_size,
- });
+ const { data: customers, count } = await CusSearchService.search({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ search,
+ filters,
+ lastItem: last_item,
+ pageNumber: page,
+ pageSize: page_size,
+ });
- res.status(200).json({ customers, totalCount: Number(count) });
- } catch (error) {
- handleRequestError({ req, error, res, action: "search customers" });
- }
+ res.status(200).json({ customers, totalCount: Number(count) });
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "search customers" });
+ }
});
cusRouter.post("", handlePostCustomerRequest);
@@ -59,8 +59,8 @@ cusRouter.post("/:customer_id", handleUpdateCustomer);
// Update customer entitlement directly
cusRouter.post(
- "/:customer_id/entitlements/:customer_entitlement_id",
- handleUpdateEntitlement
+ "/:customer_id/entitlements/:customer_entitlement_id",
+ handleUpdateEntitlement,
);
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
@@ -71,92 +71,92 @@ cusRouter.post("/:customer_id/balances", handleUpdateBalances);
// );
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
- try {
- let returnUrl = req.query.return_url;
+ try {
+ let returnUrl = req.query.return_url;
- const customerId = req.params.customer_id;
+ const customerId = req.params.customer_id;
- const [org, customer] = await Promise.all([
- OrgService.getFromReq(req),
- CusService.get({
- db: req.db,
- idOrInternalId: customerId,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ const [org, customer] = await Promise.all([
+ OrgService.getFromReq(req),
+ CusService.get({
+ db: req.db,
+ idOrInternalId: customerId,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const stripeCli = createStripeCli({ org, env: req.env });
+ const stripeCli = createStripeCli({ org, env: req.env });
- if (!customer.processor?.id) {
- let newCus;
- try {
- newCus = await createStripeCusIfNotExists({
- db: req.db,
- org,
- env: req.env,
- customer,
- logger: req.logtail,
- });
- } catch (error: any) {
- throw new RecaseError({
- message: `Failed to create Stripe customer`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- } finally {
- if (!newCus) {
- throw new RecaseError({
- message: `Failed to create Stripe customer`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (!customer.processor?.id) {
+ let newCus;
+ try {
+ newCus = await createStripeCusIfNotExists({
+ db: req.db,
+ org,
+ env: req.env,
+ customer,
+ logger: req.logtail,
+ });
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Failed to create Stripe customer`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ } finally {
+ if (!newCus) {
+ throw new RecaseError({
+ message: `Failed to create Stripe customer`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- const portal = await stripeCli.billingPortal.sessions.create({
- customer: newCus.id,
- return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
- });
+ const portal = await stripeCli.billingPortal.sessions.create({
+ customer: newCus.id,
+ return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
+ });
- if (org.api_version >= APIVersion.v1_1) {
- return res.status(200).json({
- customer_id: customer.id,
- url: portal.url,
- });
- } else {
- return res.status(200).json({
- url: portal.url,
- });
- }
- }
- }
+ if (org.api_version >= APIVersion.v1_1) {
+ return res.status(200).json({
+ customer_id: customer.id,
+ url: portal.url,
+ });
+ } else {
+ return res.status(200).json({
+ url: portal.url,
+ });
+ }
+ }
+ }
- const portal = await stripeCli.billingPortal.sessions.create({
- customer: customer.processor.id,
- return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
- });
+ const portal = await stripeCli.billingPortal.sessions.create({
+ customer: customer.processor.id,
+ return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
+ });
- if (org.api_version >= APIVersion.v1_1) {
- res.status(200).json({
- customer_id: customer.id,
- url: portal.url,
- });
- } else {
- res.status(200).json({
- url: portal.url,
- });
- }
- } catch (error) {
- handleRequestError({ req, error, res, action: "get billing portal" });
- }
+ if (org.api_version >= APIVersion.v1_1) {
+ res.status(200).json({
+ customer_id: customer.id,
+ url: portal.url,
+ });
+ } else {
+ res.status(200).json({
+ url: portal.url,
+ });
+ }
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "get billing portal" });
+ }
});
cusRouter.post("/:customer_id/billing_portal", handleCreateBillingPortal);
diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts
index e70d671dd..cf225d542 100644
--- a/server/src/internal/customers/cusUtils/createNewCustomer.ts
+++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts
@@ -6,13 +6,13 @@ import { isFreeProduct } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- CreateCustomer,
- CreateCustomerSchema,
- ErrCode,
- BillingInterval,
- AttachScenario,
- FullCustomer,
- FullProduct,
+ CreateCustomer,
+ CreateCustomerSchema,
+ ErrCode,
+ BillingInterval,
+ AttachScenario,
+ FullCustomer,
+ FullProduct,
} from "@autumn/shared";
import { Customer } from "@autumn/shared";
import { createFullCusProduct } from "../add-product/createFullCusProduct.js";
@@ -22,221 +22,221 @@ import { initStripeCusAndProducts } from "../handlers/handleCreateCustomer.js";
import { generateId } from "@/utils/genUtils.js";
import {
- newCusToAttachParams,
- newCusToInsertParams,
+ newCusToAttachParams,
+ newCusToInsertParams,
} from "../attach/attachUtils/attachParams/convertToParams.js";
import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js";
export const getGroupToDefaultProd = async ({
- defaultProds,
+ defaultProds,
}: {
- defaultProds: FullProduct[];
+ defaultProds: FullProduct[];
}) => {
- const groups = new Set(defaultProds.map((p) => p.group));
- const groupToDefaultProd: Record = {};
+ const groups = new Set(defaultProds.map((p) => p.group));
+ const groupToDefaultProd: Record = {};
- for (const group of groups) {
- const defaultProdsInGroup = defaultProds.filter((p) => p.group === group);
+ for (const group of groups) {
+ const defaultProdsInGroup = defaultProds.filter((p) => p.group === group);
- if (defaultProdsInGroup.length === 0) continue;
+ if (defaultProdsInGroup.length === 0) continue;
- defaultProdsInGroup.sort((a, b) => {
- // 1. If a is default trial, go first
- if (isDefaultTrialFullProduct({ product: a })) return -1;
+ defaultProdsInGroup.sort((a, b) => {
+ // 1. If a is default trial, go first
+ if (isDefaultTrialFullProduct({ product: a })) return -1;
- if (!isFreeProduct(a.prices)) return -1;
+ if (!isFreeProduct(a.prices)) return -1;
- return 0;
- });
+ return 0;
+ });
- groupToDefaultProd[group] = defaultProdsInGroup[0];
- }
+ groupToDefaultProd[group] = defaultProdsInGroup[0];
+ }
- return groupToDefaultProd;
+ return groupToDefaultProd;
};
export const createNewCustomer = async ({
- req,
- customer,
- nextResetAt,
- createDefaultProducts = true,
+ req,
+ customer,
+ nextResetAt,
+ createDefaultProducts = true,
}: {
- req: ExtendedRequest;
- customer: CreateCustomer;
- nextResetAt?: number;
- createDefaultProducts?: boolean;
+ req: ExtendedRequest;
+ customer: CreateCustomer;
+ nextResetAt?: number;
+ createDefaultProducts?: boolean;
}) => {
- const { db, org, env, logger } = req;
+ const { db, org, env, logger } = req;
- logger.info(
- `Creating customer: ${customer.email || customer.id}, org: ${org.slug}`
- );
+ logger.info(
+ `Creating customer: ${customer.email || customer.id}, org: ${org.slug}`,
+ );
- const defaultProds = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ const defaultProds = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- const nonFreeProds = defaultProds.filter(
- (p) =>
- !isFreeProduct(p.prices) && !isDefaultTrialFullProduct({ product: p })
- );
- // const freeProds = defaultProds.filter((p) => isFreeProduct(p.prices));
- // const defaultPaidTrialProd = nonFreeProds.find((p) =>
- // isDefaultTrialFullProduct({ product: p })
- // );
+ const nonFreeProds = defaultProds.filter(
+ (p) =>
+ !isFreeProduct(p.prices) && !isDefaultTrialFullProduct({ product: p }),
+ );
+ // const freeProds = defaultProds.filter((p) => isFreeProduct(p.prices));
+ // const defaultPaidTrialProd = nonFreeProds.find((p) =>
+ // isDefaultTrialFullProduct({ product: p })
+ // );
- const parsedCustomer = CreateCustomerSchema.parse(customer);
+ const parsedCustomer = CreateCustomerSchema.parse(customer);
- const internalId = generateId("cus");
- const customerData: Customer = {
- ...parsedCustomer,
+ const internalId = generateId("cus");
+ const customerData: Customer = {
+ ...parsedCustomer,
- name: parsedCustomer.name || "",
- email:
- nonFreeProds.length > 0 && !parsedCustomer.email
- ? `${parsedCustomer.id || internalId}@invoices.useautumn.com`
- : parsedCustomer.email || "",
+ name: parsedCustomer.name || "",
+ email:
+ nonFreeProds.length > 0 && !parsedCustomer.email
+ ? `${parsedCustomer.id || internalId}@invoices.useautumn.com`
+ : parsedCustomer.email || "",
- metadata: parsedCustomer.metadata || {},
- internal_id: internalId,
- org_id: org.id,
- created_at: Date.now(),
- env,
- processor: parsedCustomer.stripe_id
- ? {
- id: parsedCustomer.stripe_id,
- type: "stripe",
- }
- : undefined,
- };
+ metadata: parsedCustomer.metadata || {},
+ internal_id: internalId,
+ org_id: org.id,
+ created_at: Date.now(),
+ env,
+ processor: parsedCustomer.stripe_id
+ ? {
+ id: parsedCustomer.stripe_id,
+ type: "stripe",
+ }
+ : undefined,
+ };
- // Check if stripeCli exists
- if (nonFreeProds.length > 0) {
- createStripeCli({ org, env });
- }
+ // Check if stripeCli exists
+ if (nonFreeProds.length > 0) {
+ createStripeCli({ org, env });
+ }
- const newCustomer = await CusService.insert({
- db,
- data: customerData,
- });
+ const newCustomer = await CusService.insert({
+ db,
+ data: customerData,
+ });
- if (!newCustomer) {
- throw new RecaseError({
- code: ErrCode.InternalError,
- message: "CusService.insert returned null",
- });
- }
+ if (!newCustomer) {
+ throw new RecaseError({
+ code: ErrCode.InternalError,
+ message: "CusService.insert returned null",
+ });
+ }
- if (!createDefaultProducts) {
- return newCustomer;
- }
+ if (!createDefaultProducts) {
+ return newCustomer;
+ }
- await addCustomerCreatedTask({
- req,
- internalCustomerId: newCustomer.internal_id,
- org,
- env,
- });
+ await addCustomerCreatedTask({
+ req,
+ internalCustomerId: newCustomer.internal_id,
+ org,
+ env,
+ });
- const groupToDefaultProd = await getGroupToDefaultProd({
- defaultProds,
- });
+ const groupToDefaultProd = await getGroupToDefaultProd({
+ defaultProds,
+ });
- for (const group in groupToDefaultProd) {
- const defaultProd = groupToDefaultProd[group];
+ for (const group in groupToDefaultProd) {
+ const defaultProd = groupToDefaultProd[group];
- if (!isFreeProduct(defaultProd.prices)) {
- let stripeCli = null;
+ if (!isFreeProduct(defaultProd.prices)) {
+ let stripeCli = null;
- stripeCli = createStripeCli({ org, env });
- await initStripeCusAndProducts({
- db,
- org,
- env,
- customer: newCustomer,
- products: nonFreeProds,
- logger,
- });
+ stripeCli = createStripeCli({ org, env });
+ await initStripeCusAndProducts({
+ db,
+ org,
+ env,
+ customer: newCustomer,
+ products: nonFreeProds,
+ logger,
+ });
- await handleAddProduct({
- req,
- attachParams: newCusToAttachParams({
- req,
- newCus: newCustomer as FullCustomer,
- products: [defaultProd],
- stripeCli,
- freeTrial: defaultProd.free_trial || null,
- }),
- });
- } else {
- await createFullCusProduct({
- db,
- attachParams: newCusToInsertParams({
- req,
- newCus: newCustomer,
- product: defaultProd,
- freeTrial: defaultProd?.free_trial || null,
- }),
- nextResetAt,
- anchorToUnix: org.config.anchor_start_of_month
- ? getNextStartOfMonthUnix({
- interval: BillingInterval.Month,
- intervalCount: 1,
- })
- : undefined,
- scenario: AttachScenario.New,
- logger,
- });
- }
- }
+ await handleAddProduct({
+ req,
+ attachParams: newCusToAttachParams({
+ req,
+ newCus: newCustomer as FullCustomer,
+ products: [defaultProd],
+ stripeCli,
+ freeTrial: defaultProd.free_trial || null,
+ }),
+ });
+ } else {
+ await createFullCusProduct({
+ db,
+ attachParams: newCusToInsertParams({
+ req,
+ newCus: newCustomer,
+ product: defaultProd,
+ freeTrial: defaultProd?.free_trial || null,
+ }),
+ nextResetAt,
+ anchorToUnix: org.config.anchor_start_of_month
+ ? getNextStartOfMonthUnix({
+ interval: BillingInterval.Month,
+ intervalCount: 1,
+ })
+ : undefined,
+ scenario: AttachScenario.New,
+ logger,
+ });
+ }
+ }
- // if (nonFreeProds.length > 0) {
- // const stripeCli = createStripeCli({ org, env });
+ // if (nonFreeProds.length > 0) {
+ // const stripeCli = createStripeCli({ org, env });
- // await initStripeCusAndProducts({
- // db,
- // org,
- // env,
- // customer: newCustomer,
- // products: nonFreeProds,
- // logger,
- // });
+ // await initStripeCusAndProducts({
+ // db,
+ // org,
+ // env,
+ // customer: newCustomer,
+ // products: nonFreeProds,
+ // logger,
+ // });
- // await handleAddProduct({
- // req,
- // attachParams: newCusToAttachParams({
- // req,
- // newCus: newCustomer as FullCustomer,
- // products: nonFreeProds,
- // stripeCli,
- // freeTrial: defaultPaidTrialProd?.free_trial || null,
- // }),
- // });
- // }
+ // await handleAddProduct({
+ // req,
+ // attachParams: newCusToAttachParams({
+ // req,
+ // newCus: newCustomer as FullCustomer,
+ // products: nonFreeProds,
+ // stripeCli,
+ // freeTrial: defaultPaidTrialProd?.free_trial || null,
+ // }),
+ // });
+ // }
- // if (!defaultPaidTrialProd) {
- // for (const product of freeProds) {
- // await createFullCusProduct({
- // db,
- // attachParams: newCusToInsertParams({
- // req,
- // newCus: newCustomer,
- // product,
- // }),
- // nextResetAt,
- // anchorToUnix: org.config.anchor_start_of_month
- // ? getNextStartOfMonthUnix({
- // interval: BillingInterval.Month,
- // intervalCount: 1,
- // })
- // : undefined,
- // scenario: AttachScenario.New,
- // logger,
- // });
- // }
- // }
+ // if (!defaultPaidTrialProd) {
+ // for (const product of freeProds) {
+ // await createFullCusProduct({
+ // db,
+ // attachParams: newCusToInsertParams({
+ // req,
+ // newCus: newCustomer,
+ // product,
+ // }),
+ // nextResetAt,
+ // anchorToUnix: org.config.anchor_start_of_month
+ // ? getNextStartOfMonthUnix({
+ // interval: BillingInterval.Month,
+ // intervalCount: 1,
+ // })
+ // : undefined,
+ // scenario: AttachScenario.New,
+ // logger,
+ // });
+ // }
+ // }
- return newCustomer;
+ return newCustomer;
};
diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts
index 3d974736b..420b8b088 100644
--- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts
+++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.ts
@@ -1,152 +1,152 @@
import {
- CreditSchemaItem,
- CusEntResponse,
- CusEntResponseSchema,
- CusEntResponseV2,
- CusRollover,
- Feature,
- FeatureType,
- FullCustomerEntitlement,
+ CreditSchemaItem,
+ CusEntResponse,
+ CusEntResponseSchema,
+ CusEntResponseV2,
+ CusRollover,
+ Feature,
+ FeatureType,
+ FullCustomerEntitlement,
} from "@autumn/shared";
import { CusFeatureBalance } from "./getCusBalances.js";
import {
- getCusFeatureType,
- isCreditSystem,
+ getCusFeatureType,
+ isCreditSystem,
} from "@/internal/features/featureUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const sumValues = (
- entList: CusEntResponse[],
- key: keyof CusEntResponse
+ entList: CusEntResponse[],
+ key: keyof CusEntResponse,
) => {
- return entList.reduce((acc, curr) => {
- if (curr[key]) {
- return acc + Number(curr[key]);
- }
+ return entList.reduce((acc, curr) => {
+ if (curr[key]) {
+ return acc + Number(curr[key]);
+ }
- return acc;
- }, 0);
+ return acc;
+ }, 0);
};
export const getEarliestNextResetAt = (entList: CusEntResponse[]) => {
- let earliest = entList.reduce((acc, curr) => {
- if (curr.next_reset_at && curr.next_reset_at < acc) {
- return curr.next_reset_at;
- }
+ let earliest = entList.reduce((acc, curr) => {
+ if (curr.next_reset_at && curr.next_reset_at < acc) {
+ return curr.next_reset_at;
+ }
- return acc;
- }, Infinity);
+ return acc;
+ }, Infinity);
- return earliest == Infinity ? null : earliest;
+ return earliest == Infinity ? null : earliest;
};
export const featuresToObject = ({
- features,
- entList,
+ features,
+ entList,
}: {
- features: Feature[];
- entList: CusEntResponse[];
+ features: Feature[];
+ entList: CusEntResponse[];
}) => {
- let featureObject: Record = {};
+ let featureObject: Record = {};
- for (let entRes of entList) {
- let feature = features.find((f) => f.id == entRes.feature_id)!;
- let featureType = getCusFeatureType({ feature });
+ for (let entRes of entList) {
+ let feature = features.find((f) => f.id == entRes.feature_id)!;
+ let featureType = getCusFeatureType({ feature });
- let featureId = feature.id;
- let unlimited = entRes.unlimited;
- let relatedEnts = entList.filter((e) => e.feature_id == featureId);
+ let featureId = feature.id;
+ let unlimited = entRes.unlimited;
+ let relatedEnts = entList.filter((e) => e.feature_id == featureId);
- if (featureObject[featureId]) {
- continue;
- }
+ if (featureObject[featureId]) {
+ continue;
+ }
- let includedUsage = sumValues(relatedEnts, "included_usage");
- let usageLimit: number | undefined = sumValues(relatedEnts, "usage_limit");
- if (notNullish(usageLimit) && usageLimit === includedUsage) {
- usageLimit = undefined;
- }
+ let includedUsage = sumValues(relatedEnts, "included_usage");
+ let usageLimit: number | undefined = sumValues(relatedEnts, "usage_limit");
+ if (notNullish(usageLimit) && usageLimit === includedUsage) {
+ usageLimit = undefined;
+ }
- // console.log(`Feature ${featureId} list:`, relatedEnts);
+ // console.log(`Feature ${featureId} list:`, relatedEnts);
- let hasRollovers = relatedEnts.some((e) => notNullish(e.rollovers));
- let rollovers = hasRollovers
- ? (relatedEnts
- .flatMap((e) => e.rollovers)
- .filter(notNullish) as CusRollover[])
- : undefined;
- let cusFeature: CusEntResponseV2 = {
- id: featureId,
- name: feature.name,
- type: featureType,
- unlimited,
- balance: unlimited ? null : sumValues(relatedEnts, "balance"),
- usage: sumValues(relatedEnts, "usage"),
- included_usage: sumValues(relatedEnts, "included_usage"),
- usage_limit: usageLimit,
+ let hasRollovers = relatedEnts.some((e) => notNullish(e.rollovers));
+ let rollovers = hasRollovers
+ ? (relatedEnts
+ .flatMap((e) => e.rollovers)
+ .filter(notNullish) as CusRollover[])
+ : undefined;
+ let cusFeature: CusEntResponseV2 = {
+ id: featureId,
+ name: feature.name,
+ type: featureType,
+ unlimited,
+ balance: unlimited ? null : sumValues(relatedEnts, "balance"),
+ usage: sumValues(relatedEnts, "usage"),
+ included_usage: sumValues(relatedEnts, "included_usage"),
+ usage_limit: usageLimit,
- next_reset_at: getEarliestNextResetAt(relatedEnts),
- interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple",
- interval_count:
- relatedEnts.length == 1 ? relatedEnts[0].interval_count : null,
- overage_allowed: relatedEnts.some((e) => e.overage_allowed),
- breakdown:
- !unlimited && relatedEnts.length > 1
- ? relatedEnts.map((e) => ({
- interval: e.interval!,
- interval_count: e.interval_count,
- balance: e.balance,
- usage: e.usage,
- included_usage: e.included_usage,
- next_reset_at: e.next_reset_at,
- }))
- : undefined,
- credit_schema: isCreditSystem({ feature })
- ? feature.config?.schema?.map((s: CreditSchemaItem) => ({
- feature_id: s.metered_feature_id,
- credit_amount: s.credit_amount,
- }))
- : undefined,
+ next_reset_at: getEarliestNextResetAt(relatedEnts),
+ interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple",
+ interval_count:
+ relatedEnts.length == 1 ? relatedEnts[0].interval_count : null,
+ overage_allowed: relatedEnts.some((e) => e.overage_allowed),
+ breakdown:
+ !unlimited && relatedEnts.length > 1
+ ? relatedEnts.map((e) => ({
+ interval: e.interval!,
+ interval_count: e.interval_count,
+ balance: e.balance,
+ usage: e.usage,
+ included_usage: e.included_usage,
+ next_reset_at: e.next_reset_at,
+ }))
+ : undefined,
+ credit_schema: isCreditSystem({ feature })
+ ? feature.config?.schema?.map((s: CreditSchemaItem) => ({
+ feature_id: s.metered_feature_id,
+ credit_amount: s.credit_amount,
+ }))
+ : undefined,
- rollovers,
- };
+ rollovers,
+ };
- featureObject[featureId] = cusFeature;
- }
+ featureObject[featureId] = cusFeature;
+ }
- return featureObject;
+ return featureObject;
};
export const balancesToFeatureResponse = ({
- cusEnts,
- balances,
+ cusEnts,
+ balances,
}: {
- cusEnts: FullCustomerEntitlement[];
- balances: CusFeatureBalance[];
+ cusEnts: FullCustomerEntitlement[];
+ balances: CusFeatureBalance[];
}) => {
- let features = cusEnts.map((cusEnt) => cusEnt.entitlement.feature);
+ let features = cusEnts.map((cusEnt) => cusEnt.entitlement.feature);
- let entList: any = balances.map((b) => {
- let isBoolean =
- features.find((f: Feature) => f.id == b.feature_id)?.type ==
- FeatureType.Boolean;
+ let entList: any = balances.map((b) => {
+ let isBoolean =
+ features.find((f: Feature) => f.id == b.feature_id)?.type ==
+ FeatureType.Boolean;
- if (b.unlimited || isBoolean) {
- return b;
- }
+ if (b.unlimited || isBoolean) {
+ return b;
+ }
- return CusEntResponseSchema.parse({
- ...b,
- usage: b.used,
- included_usage: b.allowance,
- overage_allowed: b.overage_allowed,
- });
- });
+ return CusEntResponseSchema.parse({
+ ...b,
+ usage: b.used,
+ included_usage: b.allowance,
+ overage_allowed: b.overage_allowed,
+ });
+ });
- entList = featuresToObject({
- features,
- entList,
- });
+ entList = featuresToObject({
+ features,
+ entList,
+ });
- return entList;
+ return entList;
};
diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts
index e8acd526d..0376cefa9 100644
--- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts
+++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.ts
@@ -1,13 +1,13 @@
import {
- APIVersion,
- EntInterval,
- EntitlementWithFeature,
- Entity,
- FeatureType,
- FullCusProduct,
- FullCustomerEntitlement,
- FullCustomerPrice,
- Organization,
+ APIVersion,
+ EntInterval,
+ EntitlementWithFeature,
+ Entity,
+ FeatureType,
+ FullCusProduct,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ Organization,
} from "@autumn/shared";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
@@ -15,364 +15,364 @@ import { notNullish, notNullOrUndefined } from "@/utils/genUtils.js";
import { BREAK_API_VERSION } from "@/utils/constants.js";
import {
- getRelatedCusPrice,
- getResetBalance,
- getUnlimitedAndUsageAllowed,
+ getRelatedCusPrice,
+ getResetBalance,
+ getUnlimitedAndUsageAllowed,
} from "../../cusProducts/cusEnts/cusEntUtils.js";
import { getCusEntBalance } from "@autumn/shared";
export interface CusFeatureBalance {
- feature_id: string;
- unlimited?: boolean;
- interval?: EntInterval;
- balance?: number | null;
- total?: number | null;
- adjustment?: number | null;
- used?: number | null;
- unused?: number | null;
- next_reset_at?: number | null;
- allowance?: number | null;
- overage_allowed?: boolean | null;
+ feature_id: string;
+ unlimited?: boolean;
+ interval?: EntInterval;
+ balance?: number | null;
+ total?: number | null;
+ adjustment?: number | null;
+ used?: number | null;
+ unused?: number | null;
+ next_reset_at?: number | null;
+ allowance?: number | null;
+ overage_allowed?: boolean | null;
}
export const getV1EntitlementsRes = ({
- org,
- cusEnt,
- isBoolean,
- unlimited,
- ent,
+ org,
+ cusEnt,
+ isBoolean,
+ unlimited,
+ ent,
}: {
- org: Organization;
- cusEnt: FullCustomerEntitlement;
- isBoolean: boolean;
- unlimited: boolean;
- ent: EntitlementWithFeature;
+ org: Organization;
+ cusEnt: FullCustomerEntitlement;
+ isBoolean: boolean;
+ unlimited: boolean;
+ ent: EntitlementWithFeature;
}) => {
- let res: any = {
- feature_id: ent.feature.id,
- unlimited: isBoolean ? undefined : unlimited,
- interval: isBoolean || unlimited ? null : ent.interval || undefined,
- balance: isBoolean ? undefined : unlimited ? null : 0,
- total: isBoolean || unlimited ? undefined : 0,
- adjustment: isBoolean || unlimited ? undefined : 0,
- used: isBoolean ? undefined : unlimited ? null : 0,
- unused: 0,
- };
+ let res: any = {
+ feature_id: ent.feature.id,
+ unlimited: isBoolean ? undefined : unlimited,
+ interval: isBoolean || unlimited ? null : ent.interval || undefined,
+ balance: isBoolean ? undefined : unlimited ? null : 0,
+ total: isBoolean || unlimited ? undefined : 0,
+ adjustment: isBoolean || unlimited ? undefined : 0,
+ used: isBoolean ? undefined : unlimited ? null : 0,
+ unused: 0,
+ };
- if (org.config.api_version >= BREAK_API_VERSION) {
- res.next_reset_at =
- isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
- res.allowance = isBoolean || unlimited ? undefined : 0;
- res.usage_limit = isBoolean || unlimited ? undefined : 0;
- }
+ if (org.config.api_version >= BREAK_API_VERSION) {
+ res.next_reset_at =
+ isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
+ res.allowance = isBoolean || unlimited ? undefined : 0;
+ res.usage_limit = isBoolean || unlimited ? undefined : 0;
+ }
- return res;
+ return res;
};
export const getRolloverFields = ({
- cusEnt,
- entityId,
+ cusEnt,
+ entityId,
}: {
- cusEnt: FullCustomerEntitlement;
- entityId?: string;
+ cusEnt: FullCustomerEntitlement;
+ entityId?: string;
}) => {
- let hasRollover = notNullish(cusEnt.entitlement.rollover);
- if (!hasRollover) {
- return null;
- }
+ let hasRollover = notNullish(cusEnt.entitlement.rollover);
+ if (!hasRollover) {
+ return null;
+ }
- let rollovers = cusEnt.rollovers || [];
+ let rollovers = cusEnt.rollovers || [];
- if (cusEnt.entitlement.entity_feature_id) {
- if (entityId) {
- return rollovers.reduce(
- (acc, rollover) => {
- if (rollover.entities[entityId]) {
- return {
- balance: acc.balance + rollover.entities[entityId].balance,
- usage: acc.usage + rollover.entities[entityId].usage,
- rollovers: [
- ...acc.rollovers,
- {
- balance: rollover.entities[entityId].balance,
- usage: rollover.entities[entityId].usage,
- expires_at: rollover.expires_at,
- },
- ],
- };
- }
- return acc;
- },
- {
- balance: 0,
- usage: 0,
- rollovers: [] as any[],
- }
- );
- } else {
- return rollovers.reduce(
- (acc, rollover) => {
- let newBalance = 0;
- let newUsage = 0;
+ if (cusEnt.entitlement.entity_feature_id) {
+ if (entityId) {
+ return rollovers.reduce(
+ (acc, rollover) => {
+ if (rollover.entities[entityId]) {
+ return {
+ balance: acc.balance + rollover.entities[entityId].balance,
+ usage: acc.usage + rollover.entities[entityId].usage,
+ rollovers: [
+ ...acc.rollovers,
+ {
+ balance: rollover.entities[entityId].balance,
+ usage: rollover.entities[entityId].usage,
+ expires_at: rollover.expires_at,
+ },
+ ],
+ };
+ }
+ return acc;
+ },
+ {
+ balance: 0,
+ usage: 0,
+ rollovers: [] as any[],
+ },
+ );
+ } else {
+ return rollovers.reduce(
+ (acc, rollover) => {
+ let newBalance = 0;
+ let newUsage = 0;
- for (const entityId in rollover.entities) {
- newBalance += rollover.entities[entityId].balance;
- newUsage += rollover.entities[entityId].usage;
- }
+ for (const entityId in rollover.entities) {
+ newBalance += rollover.entities[entityId].balance;
+ newUsage += rollover.entities[entityId].usage;
+ }
- return {
- balance: acc.balance + newBalance,
- usage: acc.usage + newUsage,
- rollovers: [
- ...acc.rollovers,
- {
- balance: newBalance,
- usage: newUsage,
- expires_at: rollover.expires_at,
- },
- ],
- };
- },
- {
- balance: 0,
- usage: 0,
- rollovers: [] as any[],
- }
- );
- }
- } else {
- return rollovers.reduce(
- (acc, rollover) => {
- return {
- balance: acc.balance + rollover.balance,
- usage: acc.usage + rollover.usage,
- rollovers: [
- ...acc.rollovers,
- {
- balance: rollover.balance,
- usage: rollover.usage,
- expires_at: rollover.expires_at,
- },
- ],
- };
- },
- {
- balance: 0,
- usage: 0,
- rollovers: [] as any[],
- }
- );
- }
+ return {
+ balance: acc.balance + newBalance,
+ usage: acc.usage + newUsage,
+ rollovers: [
+ ...acc.rollovers,
+ {
+ balance: newBalance,
+ usage: newUsage,
+ expires_at: rollover.expires_at,
+ },
+ ],
+ };
+ },
+ {
+ balance: 0,
+ usage: 0,
+ rollovers: [] as any[],
+ },
+ );
+ }
+ } else {
+ return rollovers.reduce(
+ (acc, rollover) => {
+ return {
+ balance: acc.balance + rollover.balance,
+ usage: acc.usage + rollover.usage,
+ rollovers: [
+ ...acc.rollovers,
+ {
+ balance: rollover.balance,
+ usage: rollover.usage,
+ expires_at: rollover.expires_at,
+ },
+ ],
+ };
+ },
+ {
+ balance: 0,
+ usage: 0,
+ rollovers: [] as any[],
+ },
+ );
+ }
};
// IMPORTANT FUNCTION
export const getCusBalances = async ({
- cusEntsWithCusProduct,
- cusPrices,
- org,
- entity,
- apiVersion,
+ cusEntsWithCusProduct,
+ cusPrices,
+ org,
+ entity,
+ apiVersion,
}: {
- cusEntsWithCusProduct: (FullCustomerEntitlement & {
- customer_product: FullCusProduct;
- })[];
- cusPrices: FullCustomerPrice[];
- org: Organization;
- entity?: Entity;
- apiVersion: number;
+ cusEntsWithCusProduct: (FullCustomerEntitlement & {
+ customer_product: FullCusProduct;
+ })[];
+ cusPrices: FullCustomerPrice[];
+ org: Organization;
+ entity?: Entity;
+ apiVersion: number;
}) => {
- const data: Record = {};
- const features = cusEntsWithCusProduct.map(
- (cusEnt) => cusEnt.entitlement.feature
- );
- const cusEntsFiltered = cusEntsWithCusProduct.filter((cusEnt) => {
- const ent: EntitlementWithFeature = cusEnt.entitlement;
+ const data: Record = {};
+ const features = cusEntsWithCusProduct.map(
+ (cusEnt) => cusEnt.entitlement.feature,
+ );
+ const cusEntsFiltered = cusEntsWithCusProduct.filter((cusEnt) => {
+ const ent: EntitlementWithFeature = cusEnt.entitlement;
- if (!entity) return true;
+ if (!entity) return true;
- if (
- notNullish(ent.entity_feature_id) &&
- entity?.feature_id != ent.entity_feature_id
- ) {
- return false;
- }
+ if (
+ notNullish(ent.entity_feature_id) &&
+ entity?.feature_id != ent.entity_feature_id
+ ) {
+ return false;
+ }
- return true;
- });
+ return true;
+ });
- for (const cusEnt of cusEntsFiltered) {
- const cusProduct = cusEnt.customer_product;
- const feature = cusEnt.entitlement.feature;
- const ent: EntitlementWithFeature = cusEnt.entitlement;
- let key = `${ent.interval || "no-interval"}-${ent.interval_count || 1}-${feature.id}`;
+ for (const cusEnt of cusEntsFiltered) {
+ const cusProduct = cusEnt.customer_product;
+ const feature = cusEnt.entitlement.feature;
+ const ent: EntitlementWithFeature = cusEnt.entitlement;
+ let key = `${ent.interval || "no-interval"}-${ent.interval_count || 1}-${feature.id}`;
- // 1. Handle boolean
- let isBoolean = feature.type == FeatureType.Boolean;
+ // 1. Handle boolean
+ let isBoolean = feature.type == FeatureType.Boolean;
- const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
- cusEnts: cusEntsWithCusProduct,
- internalFeatureId: feature.internal_id!,
- });
+ const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
+ cusEnts: cusEntsWithCusProduct,
+ internalFeatureId: feature.internal_id!,
+ });
- // 1. Initialize balance object
- if (!data[key] && apiVersion == APIVersion.v1) {
- data[key] = getV1EntitlementsRes({
- org,
- cusEnt,
- isBoolean,
- unlimited,
- ent,
- });
- } else if (!data[key]) {
- if (isBoolean) {
- data[key] = {
- feature_id: feature.id,
- };
- } else if (unlimited) {
- data[key] = {
- feature_id: feature.id,
- unlimited: true,
- };
- } else {
- data[key] = {
- feature_id: feature.id,
- unlimited: isBoolean ? undefined : unlimited,
- interval:
- isBoolean || unlimited ? undefined : ent.interval || undefined,
- // interval_count:
- // isBoolean || unlimited
- // ? undefined
- // : ent.interval_count || undefined,
+ // 1. Initialize balance object
+ if (!data[key] && apiVersion == APIVersion.v1) {
+ data[key] = getV1EntitlementsRes({
+ org,
+ cusEnt,
+ isBoolean,
+ unlimited,
+ ent,
+ });
+ } else if (!data[key]) {
+ if (isBoolean) {
+ data[key] = {
+ feature_id: feature.id,
+ };
+ } else if (unlimited) {
+ data[key] = {
+ feature_id: feature.id,
+ unlimited: true,
+ };
+ } else {
+ data[key] = {
+ feature_id: feature.id,
+ unlimited: isBoolean ? undefined : unlimited,
+ interval:
+ isBoolean || unlimited ? undefined : ent.interval || undefined,
+ // interval_count:
+ // isBoolean || unlimited
+ // ? undefined
+ // : ent.interval_count || undefined,
- balance: isBoolean ? undefined : unlimited ? null : 0,
- total: isBoolean || unlimited ? undefined : 0,
- adjustment: isBoolean || unlimited ? undefined : 0,
- used: isBoolean ? undefined : unlimited ? null : 0,
- unused: 0,
- overage_allowed: usageAllowed,
- };
+ balance: isBoolean ? undefined : unlimited ? null : 0,
+ total: isBoolean || unlimited ? undefined : 0,
+ adjustment: isBoolean || unlimited ? undefined : 0,
+ used: isBoolean ? undefined : unlimited ? null : 0,
+ unused: 0,
+ overage_allowed: usageAllowed,
+ };
- if (org.config.api_version >= BREAK_API_VERSION) {
- data[key].next_reset_at =
- isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
- data[key].allowance = isBoolean || unlimited ? undefined : 0;
- data[key].usage_limit = isBoolean || unlimited ? undefined : 0;
- data[key].interval_count = ent.interval_count || 1;
- }
- }
- }
+ if (org.config.api_version >= BREAK_API_VERSION) {
+ data[key].next_reset_at =
+ isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
+ data[key].allowance = isBoolean || unlimited ? undefined : 0;
+ data[key].usage_limit = isBoolean || unlimited ? undefined : 0;
+ data[key].interval_count = ent.interval_count || 1;
+ }
+ }
+ }
- if (isBoolean || unlimited) {
- continue;
- }
+ if (isBoolean || unlimited) {
+ continue;
+ }
- let { balance, adjustment, count, unused } = getCusEntBalance({
- cusEnt,
- entityId: entity?.id,
- });
+ let { balance, adjustment, count, unused } = getCusEntBalance({
+ cusEnt,
+ entityId: entity?.id,
+ });
- data[key].balance += balance || 0;
- data[key].adjustment += adjustment || 0;
+ data[key].balance += balance || 0;
+ data[key].adjustment += adjustment || 0;
- let total =
- (getResetBalance({
- entitlement: ent,
- options: getEntOptions(cusProduct.options, ent),
- relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
- productQuantity: cusProduct.quantity || 1,
- }) || 0) * count;
+ let total =
+ (getResetBalance({
+ entitlement: ent,
+ options: getEntOptions(cusProduct.options, ent),
+ relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
+ productQuantity: cusProduct.quantity || 1,
+ }) || 0) * count;
- data[key].total += total;
- data[key].unused += unused || 0;
+ data[key].total += total;
+ data[key].unused += unused || 0;
- let rollover = getRolloverFields({
- cusEnt,
- entityId: entity?.id,
- });
+ let rollover = getRolloverFields({
+ cusEnt,
+ entityId: entity?.id,
+ });
- if (rollover) {
- data[key].balance += rollover.balance;
- data[key].total += rollover.balance + rollover.usage;
- data[key].rollovers = rollover.rollovers;
- }
+ if (rollover) {
+ data[key].balance += rollover.balance;
+ data[key].total += rollover.balance + rollover.usage;
+ data[key].rollovers = rollover.rollovers;
+ }
- if (org.config.api_version >= BREAK_API_VERSION) {
- if (
- !data[key].next_reset_at ||
- (cusEnt.next_reset_at && cusEnt.next_reset_at < data[key].next_reset_at)
- ) {
- data[key].next_reset_at = cusEnt.next_reset_at;
- }
+ if (org.config.api_version >= BREAK_API_VERSION) {
+ if (
+ !data[key].next_reset_at ||
+ (cusEnt.next_reset_at && cusEnt.next_reset_at < data[key].next_reset_at)
+ ) {
+ data[key].next_reset_at = cusEnt.next_reset_at;
+ }
- const resetBalance = getResetBalance({
- entitlement: ent,
- options: getEntOptions(cusProduct.options, ent),
- relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
- productQuantity: cusProduct.quantity || 1,
- });
+ const resetBalance = getResetBalance({
+ entitlement: ent,
+ options: getEntOptions(cusProduct.options, ent),
+ relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
+ productQuantity: cusProduct.quantity || 1,
+ });
- data[key].allowance += (resetBalance || 0) * count;
+ data[key].allowance += (resetBalance || 0) * count;
- let usageLimit = ent.usage_limit;
+ let usageLimit = ent.usage_limit;
- if (notNullish(usageLimit)) {
- data[key].usage_limit += usageLimit;
- } else {
- data[key].usage_limit += resetBalance || 0;
- }
- }
- }
+ if (notNullish(usageLimit)) {
+ data[key].usage_limit += usageLimit;
+ } else {
+ data[key].usage_limit += resetBalance || 0;
+ }
+ }
+ }
- const balances = Object.values(data);
+ const balances = Object.values(data);
- for (const balance of balances) {
- if (
- notNullOrUndefined(balance.total) &&
- notNullOrUndefined(balance.balance)
- ) {
- balance.used =
- balance.total +
- balance.adjustment -
- balance.balance -
- (balance.unused || 0);
+ for (const balance of balances) {
+ if (
+ notNullOrUndefined(balance.total) &&
+ notNullOrUndefined(balance.balance)
+ ) {
+ balance.used =
+ balance.total +
+ balance.adjustment -
+ balance.balance -
+ (balance.unused || 0);
- delete balance.total;
- delete balance.adjustment;
- }
- delete balance.unused;
- }
+ delete balance.total;
+ delete balance.adjustment;
+ }
+ delete balance.unused;
+ }
- // Sort balances
- if (org.api_version == APIVersion.v1) {
- balances.sort((a: any, b: any) => {
- let featureA = features.find((f) => f.id == a.feature_id);
- let featureB = features.find((f) => f.id == b.feature_id);
+ // Sort balances
+ if (org.api_version == APIVersion.v1) {
+ balances.sort((a: any, b: any) => {
+ let featureA = features.find((f) => f.id == a.feature_id);
+ let featureB = features.find((f) => f.id == b.feature_id);
- if (
- featureA?.type == FeatureType.Boolean &&
- featureB?.type != FeatureType.Boolean
- ) {
- return -1;
- } else if (
- featureA?.type != FeatureType.Boolean &&
- featureB?.type == FeatureType.Boolean
- ) {
- return 1;
- }
+ if (
+ featureA?.type == FeatureType.Boolean &&
+ featureB?.type != FeatureType.Boolean
+ ) {
+ return -1;
+ } else if (
+ featureA?.type != FeatureType.Boolean &&
+ featureB?.type == FeatureType.Boolean
+ ) {
+ return 1;
+ }
- if (a.unlimited && !b.unlimited) {
- return -1;
- } else if (!a.unlimited && b.unlimited) {
- return 1;
- }
+ if (a.unlimited && !b.unlimited) {
+ return -1;
+ } else if (!a.unlimited && b.unlimited) {
+ return 1;
+ }
- return a.feature_id.localeCompare(b.feature_id);
- });
- }
+ return a.feature_id.localeCompare(b.feature_id);
+ });
+ }
- // if (org.api_version == APIVersion.v1) {
+ // if (org.api_version == APIVersion.v1) {
- // }
+ // }
- return balances as CusFeatureBalance[];
+ return balances as CusFeatureBalance[];
};
diff --git a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.ts b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.ts
index 0e19f5dfb..f20a2628b 100644
--- a/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.ts
+++ b/server/src/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.ts
@@ -1,38 +1,38 @@
import { balancesToFeatureResponse } from "./balancesToFeatureResponse.js";
import {
- FullCusProduct,
- Organization,
- Entity,
- APIVersion,
+ FullCusProduct,
+ Organization,
+ Entity,
+ APIVersion,
} from "@autumn/shared";
import { cusProductsToCusEnts, cusProductsToCusPrices } from "@autumn/shared";
import { getCusBalances } from "./getCusBalances.js";
export const getCusFeaturesResponse = async ({
- cusProducts,
- org,
- entity,
- apiVersion,
+ cusProducts,
+ org,
+ entity,
+ apiVersion,
}: {
- cusProducts: FullCusProduct[];
- org: Organization;
- entity?: Entity;
- apiVersion: APIVersion;
+ cusProducts: FullCusProduct[];
+ org: Organization;
+ entity?: Entity;
+ apiVersion: APIVersion;
}) => {
- let cusEnts = cusProductsToCusEnts({ cusProducts }) as any;
+ let cusEnts = cusProductsToCusEnts({ cusProducts }) as any;
- const balances = await getCusBalances({
- cusEntsWithCusProduct: cusEnts,
- cusPrices: cusProductsToCusPrices({
- cusProducts,
- }),
- org,
- entity,
- apiVersion,
- });
+ const balances = await getCusBalances({
+ cusEntsWithCusProduct: cusEnts,
+ cusPrices: cusProductsToCusPrices({
+ cusProducts,
+ }),
+ org,
+ entity,
+ apiVersion,
+ });
- return balancesToFeatureResponse({
- cusEnts,
- balances,
- });
+ return balancesToFeatureResponse({
+ cusEnts,
+ balances,
+ });
};
diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts
index 184aeb7e5..f862724a6 100644
--- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts
+++ b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts
@@ -1,218 +1,218 @@
import {
- getPriceOptions,
- getUsageTier,
+ getPriceOptions,
+ getUsageTier,
} from "@/internal/products/prices/priceUtils.js";
import { BREAK_API_VERSION } from "@/utils/constants.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- FullCusProduct,
- Organization,
- Subscription,
- PriceType,
- FixedPriceConfig,
- UsagePriceConfig,
- TierInfinite,
- APIVersion,
- CusProductResponseSchema,
- CusProductStatus,
- Entity,
- Feature,
+ FullCusProduct,
+ Organization,
+ Subscription,
+ PriceType,
+ FixedPriceConfig,
+ UsagePriceConfig,
+ TierInfinite,
+ APIVersion,
+ CusProductResponseSchema,
+ CusProductStatus,
+ Entity,
+ Feature,
} from "@autumn/shared";
import Stripe from "stripe";
import { getRelatedCusEnt } from "../../cusProducts/cusPrices/cusPriceUtils.js";
import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js";
import {
- getProductItemResponse,
- getProductResponse,
+ getProductItemResponse,
+ getProductResponse,
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
const getQuantityData = ({ cusProduct }: { cusProduct: FullCusProduct }) => {
- return {
- prepaid_quantities: cusProduct.options.map((o) => {
- return {
- quantity: o.quantity,
- feature_id: o.feature_id,
- };
- }),
- };
+ return {
+ prepaid_quantities: cusProduct.options.map((o) => {
+ return {
+ quantity: o.quantity,
+ feature_id: o.feature_id,
+ };
+ }),
+ };
};
export const getCusProductResponse = async ({
- cusProduct,
- subs,
- org,
- apiVersion,
- features,
- entity,
+ cusProduct,
+ subs,
+ org,
+ apiVersion,
+ features,
+ entity,
}: {
- cusProduct: FullCusProduct;
- org: Organization;
- subs?: Subscription[];
- apiVersion: number;
- features: Feature[];
- entity?: Entity;
+ cusProduct: FullCusProduct;
+ org: Organization;
+ subs?: Subscription[];
+ apiVersion: number;
+ features: Feature[];
+ entity?: Entity;
}) => {
- // Process prices
+ // Process prices
- const prices = cusProduct.customer_prices.map((cp) => {
- let price = cp.price;
+ const prices = cusProduct.customer_prices.map((cp) => {
+ let price = cp.price;
- if (price.config?.type == PriceType.Fixed) {
- let config = price.config as FixedPriceConfig;
- return {
- amount: config.amount,
- interval: config.interval,
- };
- } else {
- let config = price.config as UsagePriceConfig;
- let priceOptions = getPriceOptions(price, cusProduct.options);
- let usageTier = getUsageTier(price, priceOptions?.quantity!);
- let cusEnt = getRelatedCusEnt({
- cusPrice: cp,
- cusEnts: cusProduct.customer_entitlements,
- });
+ if (price.config?.type == PriceType.Fixed) {
+ let config = price.config as FixedPriceConfig;
+ return {
+ amount: config.amount,
+ interval: config.interval,
+ };
+ } else {
+ let config = price.config as UsagePriceConfig;
+ let priceOptions = getPriceOptions(price, cusProduct.options);
+ let usageTier = getUsageTier(price, priceOptions?.quantity!);
+ let cusEnt = getRelatedCusEnt({
+ cusPrice: cp,
+ cusEnts: cusProduct.customer_entitlements,
+ });
- let ent = cusEnt?.entitlement;
+ let ent = cusEnt?.entitlement;
- let singleTier = ent?.allowance == 0 && config.usage_tiers.length == 1;
+ let singleTier = ent?.allowance == 0 && config.usage_tiers.length == 1;
- if (singleTier) {
- return {
- amount: usageTier.amount,
- interval: config.interval,
- quantity: priceOptions?.quantity,
- };
- } else {
- // Add allowance to tiers
- let allowance = ent?.allowance;
- let tiers;
+ if (singleTier) {
+ return {
+ amount: usageTier.amount,
+ interval: config.interval,
+ quantity: priceOptions?.quantity,
+ };
+ } else {
+ // Add allowance to tiers
+ let allowance = ent?.allowance;
+ let tiers;
- if (notNullish(allowance) && allowance! > 0) {
- tiers = [
- {
- to: allowance,
- amount: 0,
- },
- ...config.usage_tiers.map((tier) => {
- let isLastTier = tier.to == -1 || tier.to == TierInfinite;
- return {
- to: isLastTier ? tier.to : Number(tier.to) + allowance!,
- amount: tier.amount,
- };
- }),
- ];
- } else {
- tiers = config.usage_tiers.map((tier) => {
- let isLastTier = tier.to == -1 || tier.to == TierInfinite;
- return {
- to: isLastTier ? tier.to : Number(tier.to) + allowance!,
- amount: tier.amount,
- };
- });
- }
+ if (notNullish(allowance) && allowance! > 0) {
+ tiers = [
+ {
+ to: allowance,
+ amount: 0,
+ },
+ ...config.usage_tiers.map((tier) => {
+ let isLastTier = tier.to == -1 || tier.to == TierInfinite;
+ return {
+ to: isLastTier ? tier.to : Number(tier.to) + allowance!,
+ amount: tier.amount,
+ };
+ }),
+ ];
+ } else {
+ tiers = config.usage_tiers.map((tier) => {
+ let isLastTier = tier.to == -1 || tier.to == TierInfinite;
+ return {
+ to: isLastTier ? tier.to : Number(tier.to) + allowance!,
+ amount: tier.amount,
+ };
+ });
+ }
- return {
- tiers: tiers,
- name: "",
- quantity: priceOptions?.quantity,
- };
- }
- }
- });
+ return {
+ tiers: tiers,
+ name: "",
+ quantity: priceOptions?.quantity,
+ };
+ }
+ }
+ });
- const trialing =
- cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
+ const trialing =
+ cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
- const subIds = cusProduct.subscription_ids;
- let stripeSubData = {};
+ const subIds = cusProduct.subscription_ids;
+ let stripeSubData = {};
- if (
- subIds &&
- subIds.length > 0 &&
- org.config.api_version >= BREAK_API_VERSION
- ) {
- let baseSub = subs?.find(
- (s) => s.id == subIds[0] || (s as Subscription).stripe_id == subIds[0]
- );
- stripeSubData = {
- current_period_end: baseSub?.current_period_end
- ? baseSub.current_period_end * 1000
- : null,
- current_period_start: baseSub?.current_period_start
- ? baseSub.current_period_start * 1000
- : null,
- };
- }
+ if (
+ subIds &&
+ subIds.length > 0 &&
+ org.config.api_version >= BREAK_API_VERSION
+ ) {
+ let baseSub = subs?.find(
+ (s) => s.id == subIds[0] || (s as Subscription).stripe_id == subIds[0],
+ );
+ stripeSubData = {
+ current_period_end: baseSub?.current_period_end
+ ? baseSub.current_period_end * 1000
+ : null,
+ current_period_start: baseSub?.current_period_start
+ ? baseSub.current_period_start * 1000
+ : null,
+ };
+ }
- if (!subIds && trialing) {
- stripeSubData = {
- current_period_start: cusProduct.starts_at,
- current_period_end: cusProduct.trial_ends_at,
- };
- }
+ if (!subIds && trialing) {
+ stripeSubData = {
+ current_period_start: cusProduct.starts_at,
+ current_period_end: cusProduct.trial_ends_at,
+ };
+ }
- if (apiVersion >= APIVersion.v1_1) {
- if ((!subIds || subIds.length == 0) && trialing) {
- stripeSubData = {
- current_period_start: cusProduct.starts_at,
- current_period_end: cusProduct.trial_ends_at,
- };
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ if ((!subIds || subIds.length == 0) && trialing) {
+ stripeSubData = {
+ current_period_start: cusProduct.starts_at,
+ current_period_end: cusProduct.trial_ends_at,
+ };
+ }
- const fullProduct = fullCusProductToProduct(cusProduct);
- const v2Product = await getProductResponse({
- product: fullProduct,
- features,
- withDisplay: false,
- options: cusProduct.options,
- });
+ const fullProduct = fullCusProductToProduct(cusProduct);
+ const v2Product = await getProductResponse({
+ product: fullProduct,
+ features,
+ withDisplay: false,
+ options: cusProduct.options,
+ });
- return CusProductResponseSchema.parse({
- id: fullProduct.id,
- name: fullProduct.name,
- group: fullProduct.group || null,
- status: trialing ? CusProductStatus.Trialing : cusProduct.status,
- canceled_at: cusProduct.canceled_at || null,
+ return CusProductResponseSchema.parse({
+ id: fullProduct.id,
+ name: fullProduct.name,
+ group: fullProduct.group || null,
+ status: trialing ? CusProductStatus.Trialing : cusProduct.status,
+ canceled_at: cusProduct.canceled_at || null,
- // canceled: cusProduct.canceled || false,
- // trialing: trialing ? true : false,
+ // canceled: cusProduct.canceled || false,
+ // trialing: trialing ? true : false,
- is_default: fullProduct.is_default || false,
- is_add_on: fullProduct.is_add_on || false,
- version: fullProduct.version,
- quantity: cusProduct.quantity,
+ is_default: fullProduct.is_default || false,
+ is_add_on: fullProduct.is_add_on || false,
+ version: fullProduct.version,
+ quantity: cusProduct.quantity,
- // stripe_subscription_ids: cusProduct.subscription_ids || [],
- started_at: cusProduct.starts_at,
- entity_id: entity?.id || cusProduct.entity_id || undefined,
- // entity_id: cusProduct.internal_entity_id
- // ? entities?.find(
- // (e: Entity) => e.internal_id == cusProduct.internal_entity_id
- // )?.id
- // : cusProduct.entity_id || undefined,
+ // stripe_subscription_ids: cusProduct.subscription_ids || [],
+ started_at: cusProduct.starts_at,
+ entity_id: entity?.id || cusProduct.entity_id || undefined,
+ // entity_id: cusProduct.internal_entity_id
+ // ? entities?.find(
+ // (e: Entity) => e.internal_id == cusProduct.internal_entity_id
+ // )?.id
+ // : cusProduct.entity_id || undefined,
- ...stripeSubData,
- items: v2Product.items,
- });
- } else {
- let cusProductResponse = {
- id: cusProduct.product.id,
- name: cusProduct.product.name,
- group: cusProduct.product.group,
- status: trialing ? CusProductStatus.Trialing : cusProduct.status,
- created_at: cusProduct.created_at,
- canceled_at: cusProduct.canceled_at,
- processor: {
- type: cusProduct.processor?.type,
- subscription_id: cusProduct.processor?.subscription_id || null,
- },
- subscription_ids: cusProduct.subscription_ids || [],
- prices: prices,
- starts_at: cusProduct.starts_at,
+ ...stripeSubData,
+ items: v2Product.items,
+ });
+ } else {
+ let cusProductResponse = {
+ id: cusProduct.product.id,
+ name: cusProduct.product.name,
+ group: cusProduct.product.group,
+ status: trialing ? CusProductStatus.Trialing : cusProduct.status,
+ created_at: cusProduct.created_at,
+ canceled_at: cusProduct.canceled_at,
+ processor: {
+ type: cusProduct.processor?.type,
+ subscription_id: cusProduct.processor?.subscription_id || null,
+ },
+ subscription_ids: cusProduct.subscription_ids || [],
+ prices: prices,
+ starts_at: cusProduct.starts_at,
- ...stripeSubData,
- };
+ ...stripeSubData,
+ };
- return cusProductResponse;
- }
+ return cusProductResponse;
+ }
};
diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts
index 99c062b5c..503c69826 100644
--- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts
+++ b/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts
@@ -1,96 +1,96 @@
import {
- ACTIVE_STATUSES,
- APIVersion,
- CusProductResponse,
- Entity,
- Feature,
- Organization,
+ ACTIVE_STATUSES,
+ APIVersion,
+ CusProductResponse,
+ Entity,
+ Feature,
+ Organization,
} from "@autumn/shared";
import { getCusProductResponse } from "./getCusProductResponse.js";
const mergeCusProductResponses = ({
- cusProductResponses,
+ cusProductResponses,
}: {
- cusProductResponses: CusProductResponse[];
+ cusProductResponses: CusProductResponse[];
}) => {
- const getProductKey = (product: CusProductResponse) => {
- let status = ACTIVE_STATUSES.includes(product.status)
- ? "active"
- : product.status;
- return `${product.id}:${status}`;
- };
+ const getProductKey = (product: CusProductResponse) => {
+ let status = ACTIVE_STATUSES.includes(product.status)
+ ? "active"
+ : product.status;
+ return `${product.id}:${status}`;
+ };
- const record: Record = {};
+ const record: Record = {};
- for (const curr of cusProductResponses) {
- const key = getProductKey(curr);
- const latest = record[key];
+ for (const curr of cusProductResponses) {
+ const key = getProductKey(curr);
+ const latest = record[key];
- const currStartedAt = curr.started_at;
+ const currStartedAt = curr.started_at;
- record[key] = {
- ...(latest || curr),
- version: Math.max(latest?.version || 1, curr?.version || 1),
- canceled_at: curr.canceled_at
- ? curr.canceled_at
- : latest?.canceled_at || null,
- started_at: latest?.started_at
- ? Math.min(latest?.started_at, currStartedAt)
- : currStartedAt,
- quantity: (latest?.quantity || 0) + (curr?.quantity || 0),
- };
- }
+ record[key] = {
+ ...(latest || curr),
+ version: Math.max(latest?.version || 1, curr?.version || 1),
+ canceled_at: curr.canceled_at
+ ? curr.canceled_at
+ : latest?.canceled_at || null,
+ started_at: latest?.started_at
+ ? Math.min(latest?.started_at, currStartedAt)
+ : currStartedAt,
+ quantity: (latest?.quantity || 0) + (curr?.quantity || 0),
+ };
+ }
- return Object.values(record);
+ return Object.values(record);
};
export const processFullCusProducts = async ({
- fullCusProducts,
- subs,
- org,
- entity,
- apiVersion,
- features,
+ fullCusProducts,
+ subs,
+ org,
+ entity,
+ apiVersion,
+ features,
}: {
- fullCusProducts: any;
- subs: any;
- org: Organization;
- entity?: Entity;
- apiVersion: number;
- features: Feature[];
+ fullCusProducts: any;
+ subs: any;
+ org: Organization;
+ entity?: Entity;
+ apiVersion: number;
+ features: Feature[];
}) => {
- // Process full cus products
- let main = [];
- let addOns = [];
- for (const cusProduct of fullCusProducts) {
- let processed = await getCusProductResponse({
- cusProduct,
- subs,
- org,
- entity,
- apiVersion,
- features,
- });
+ // Process full cus products
+ let main = [];
+ let addOns = [];
+ for (const cusProduct of fullCusProducts) {
+ let processed = await getCusProductResponse({
+ cusProduct,
+ subs,
+ org,
+ entity,
+ apiVersion,
+ features,
+ });
- let isAddOn = cusProduct.product.is_add_on;
- if (isAddOn) {
- addOns.push(processed);
- } else {
- main.push(processed);
- }
- }
+ let isAddOn = cusProduct.product.is_add_on;
+ if (isAddOn) {
+ addOns.push(processed);
+ } else {
+ main.push(processed);
+ }
+ }
- if (apiVersion >= APIVersion.v1_1) {
- main = mergeCusProductResponses({
- cusProductResponses: main as CusProductResponse[],
- });
- addOns = mergeCusProductResponses({
- cusProductResponses: addOns as CusProductResponse[],
- });
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ main = mergeCusProductResponses({
+ cusProductResponses: main as CusProductResponse[],
+ });
+ addOns = mergeCusProductResponses({
+ cusProductResponses: addOns as CusProductResponse[],
+ });
+ }
- return {
- main: main,
- addOns: addOns,
- };
+ return {
+ main: main,
+ addOns: addOns,
+ };
};
diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts
index 39136c1d6..138fb8693 100644
--- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts
+++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts
@@ -4,30 +4,30 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { AppEnv, CusExpand, FullCustomer, Organization } from "@autumn/shared";
export const getCusPaymentMethodRes = async ({
- org,
- env,
- fullCus,
- expand,
+ org,
+ env,
+ fullCus,
+ expand,
}: {
- org: Organization;
- env: AppEnv;
- fullCus: FullCustomer;
- expand: CusExpand[];
+ org: Organization;
+ env: AppEnv;
+ fullCus: FullCustomer;
+ expand: CusExpand[];
}) => {
- if (!expand?.includes(CusExpand.PaymentMethod)) {
- return undefined;
- }
+ if (!expand?.includes(CusExpand.PaymentMethod)) {
+ return undefined;
+ }
- let stripeCli = createStripeCli({
- org,
- env,
- });
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ });
- let paymentMethod = await getCusPaymentMethod({
- stripeCli,
- stripeId: fullCus.processor?.id,
- errorIfNone: false,
- });
+ let paymentMethod = await getCusPaymentMethod({
+ stripeCli,
+ stripeId: fullCus.processor?.id,
+ errorIfNone: false,
+ });
- return paymentMethod;
+ return paymentMethod;
};
diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusReferrals.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusReferrals.ts
index c1bf640e0..b369d4952 100644
--- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusReferrals.ts
+++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusReferrals.ts
@@ -4,36 +4,36 @@ import { CusExpand, FullCustomer } from "@autumn/shared";
import { CusReferralResponseSchema } from "@autumn/shared";
export const getCusReferrals = async ({
- db,
- fullCus,
- expand,
+ db,
+ fullCus,
+ expand,
}: {
- db: DrizzleCli;
- fullCus: FullCustomer;
- expand?: CusExpand[];
+ db: DrizzleCli;
+ fullCus: FullCustomer;
+ expand?: CusExpand[];
}) => {
- if (!expand?.includes(CusExpand.Referrals)) {
- return undefined;
- }
+ if (!expand?.includes(CusExpand.Referrals)) {
+ return undefined;
+ }
- let referred = await RewardRedemptionService.getByReferrer({
- db,
- internalCustomerId: fullCus.internal_id,
- withCustomer: true,
- withRewardProgram: true,
- limit: 100,
- });
+ let referred = await RewardRedemptionService.getByReferrer({
+ db,
+ internalCustomerId: fullCus.internal_id,
+ withCustomer: true,
+ withRewardProgram: true,
+ limit: 100,
+ });
- return referred.map((r) =>
- CusReferralResponseSchema.parse({
- program_id: r.reward_program?.id,
- customer: {
- id: r.customer.id,
- name: r.customer.name,
- email: r.customer.email,
- },
- reward_applied: r.applied,
- created_at: r.created_at,
- }),
- );
+ return referred.map((r) =>
+ CusReferralResponseSchema.parse({
+ program_id: r.reward_program?.id,
+ customer: {
+ id: r.customer.id,
+ name: r.customer.name,
+ email: r.customer.email,
+ },
+ reward_applied: r.applied,
+ created_at: r.created_at,
+ }),
+ );
};
diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts
index 81a5b9bd7..f438d4b01 100644
--- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts
+++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts
@@ -2,91 +2,91 @@ import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- AppEnv,
- CouponDurationType,
- CusExpand,
- FullCustomer,
- Organization,
- RewardType,
- Subscription,
+ AppEnv,
+ CouponDurationType,
+ CusExpand,
+ FullCustomer,
+ Organization,
+ RewardType,
+ Subscription,
} from "@autumn/shared";
import Stripe from "stripe";
export const getCusRewards = async ({
- org,
- env,
- fullCus,
- subIds,
- expand,
+ org,
+ env,
+ fullCus,
+ subIds,
+ expand,
}: {
- org: Organization;
- env: AppEnv;
- fullCus: FullCustomer;
- subIds?: string[];
- expand?: CusExpand[];
+ org: Organization;
+ env: AppEnv;
+ fullCus: FullCustomer;
+ subIds?: string[];
+ expand?: CusExpand[];
}) => {
- if (!expand?.includes(CusExpand.Rewards)) {
- return undefined;
- }
+ if (!expand?.includes(CusExpand.Rewards)) {
+ return undefined;
+ }
- if (!fullCus.processor?.id) {
- return undefined;
- }
+ if (!fullCus.processor?.id) {
+ return undefined;
+ }
- let stripeCli = createStripeCli({
- org,
- env,
- });
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ });
- const [stripeCus, stripeSubs] = await Promise.all([
- stripeCli.customers.retrieve(
- fullCus.processor?.id!,
- ) as Promise,
- getStripeSubs({
- stripeCli,
- subIds,
- expand: ["discounts"],
- }),
- ]);
+ const [stripeCus, stripeSubs] = await Promise.all([
+ stripeCli.customers.retrieve(
+ fullCus.processor?.id!,
+ ) as Promise,
+ getStripeSubs({
+ stripeCli,
+ subIds,
+ expand: ["discounts"],
+ }),
+ ]);
- let stripeDiscounts: Stripe.Discount[] = stripeSubs?.flatMap(
- (s) => s.discounts,
- ) as Stripe.Discount[];
+ let stripeDiscounts: Stripe.Discount[] = stripeSubs?.flatMap(
+ (s) => s.discounts,
+ ) as Stripe.Discount[];
- if (stripeCus.discount) {
- stripeDiscounts.push(stripeCus.discount);
- }
+ if (stripeCus.discount) {
+ stripeDiscounts.push(stripeCus.discount);
+ }
- let rewards = {
- discounts: stripeDiscounts.map((d) => {
- let duration_type: CouponDurationType;
- let duration_value = 0;
- if (d.coupon?.duration === "forever") {
- duration_type = CouponDurationType.Forever;
- } else if (d.coupon?.duration === "once") {
- duration_type = CouponDurationType.OneOff;
- } else if (d.coupon?.duration === "repeating") {
- duration_type = CouponDurationType.Months;
- duration_value = d.coupon?.duration_in_months || 0;
- } else {
- duration_type = CouponDurationType.OneOff;
- }
- return {
- id: d.coupon?.id,
- name: d.coupon?.name ?? "",
- type: d.coupon?.amount_off
- ? RewardType.FixedDiscount
- : RewardType.PercentageDiscount,
- discount_value: d.coupon?.amount_off || d.coupon?.percent_off || 0,
- currency: d.coupon?.currency ?? null,
- start: d.start ?? null,
- end: d.end ?? null,
- subscription_id: d.subscription ?? null,
- duration_type,
- duration_value,
- };
- }),
- };
+ let rewards = {
+ discounts: stripeDiscounts.map((d) => {
+ let duration_type: CouponDurationType;
+ let duration_value = 0;
+ if (d.coupon?.duration === "forever") {
+ duration_type = CouponDurationType.Forever;
+ } else if (d.coupon?.duration === "once") {
+ duration_type = CouponDurationType.OneOff;
+ } else if (d.coupon?.duration === "repeating") {
+ duration_type = CouponDurationType.Months;
+ duration_value = d.coupon?.duration_in_months || 0;
+ } else {
+ duration_type = CouponDurationType.OneOff;
+ }
+ return {
+ id: d.coupon?.id,
+ name: d.coupon?.name ?? "",
+ type: d.coupon?.amount_off
+ ? RewardType.FixedDiscount
+ : RewardType.PercentageDiscount,
+ discount_value: d.coupon?.amount_off || d.coupon?.percent_off || 0,
+ currency: d.coupon?.currency ?? null,
+ start: d.start ?? null,
+ end: d.end ?? null,
+ subscription_id: d.subscription ?? null,
+ duration_type,
+ duration_value,
+ };
+ }),
+ };
- return rewards;
+ return rewards;
};
diff --git a/server/src/internal/customers/cusUtils/cusUtils.ts b/server/src/internal/customers/cusUtils/cusUtils.ts
index fea88176f..22be1db9d 100644
--- a/server/src/internal/customers/cusUtils/cusUtils.ts
+++ b/server/src/internal/customers/cusUtils/cusUtils.ts
@@ -1,15 +1,15 @@
import { z } from "zod";
import {
- CusExpand,
- Customer,
- CustomerData,
- ErrCode,
- Feature,
- FullCustomer,
- Invoice,
- InvoiceResponse,
- Organization,
- sortCusEntsForDeduction,
+ CusExpand,
+ Customer,
+ CustomerData,
+ ErrCode,
+ Feature,
+ FullCustomer,
+ Invoice,
+ InvoiceResponse,
+ Organization,
+ sortCusEntsForDeduction,
} from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
@@ -23,159 +23,159 @@ import { processInvoice } from "@/internal/invoices/InvoiceService.js";
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
export const updateCustomerDetails = async ({
- db,
- customer,
- customerData,
- org,
- logger,
+ db,
+ customer,
+ customerData,
+ org,
+ logger,
}: {
- db: DrizzleCli;
- customer: any;
- customerData?: CustomerData;
- org: Organization;
- logger: any;
+ db: DrizzleCli;
+ customer: any;
+ customerData?: CustomerData;
+ org: Organization;
+ logger: any;
}) => {
- let updates: any = {};
- if (!customer.name && customerData?.name) {
- updates.name = customerData.name;
- }
- if (!customer.email && customerData?.email) {
- // Check that email is valid, if not skip...
- if (z.string().email().safeParse(customerData.email).error) {
- logger.info(`Invalid email ${customerData.email}, skipping update`);
- } else {
- updates.email = customerData.email;
- }
- }
+ let updates: any = {};
+ if (!customer.name && customerData?.name) {
+ updates.name = customerData.name;
+ }
+ if (!customer.email && customerData?.email) {
+ // Check that email is valid, if not skip...
+ if (z.string().email().safeParse(customerData.email).error) {
+ logger.info(`Invalid email ${customerData.email}, skipping update`);
+ } else {
+ updates.email = customerData.email;
+ }
+ }
- if (Object.keys(updates).length > 0) {
- logger.info(`Updating customer details:`, {
- data: updates,
- });
- await CusService.update({
- db,
- internalCusId: customer.internal_id,
- update: updates,
- });
- customer = { ...customer, ...updates };
+ if (Object.keys(updates).length > 0) {
+ logger.info(`Updating customer details:`, {
+ data: updates,
+ });
+ await CusService.update({
+ db,
+ internalCusId: customer.internal_id,
+ update: updates,
+ });
+ customer = { ...customer, ...updates };
- await refreshCusCache({
- db,
- customerId: customer.id!,
- org: org,
- env: customer.env,
- });
- }
+ await refreshCusCache({
+ db,
+ customerId: customer.id!,
+ org: org,
+ env: customer.env,
+ });
+ }
- return customer;
+ return customer;
};
export const getCusInvoices = async ({
- db,
- internalCustomerId,
- invoices,
- limit = 10,
- withItems = false,
- features,
+ db,
+ internalCustomerId,
+ invoices,
+ limit = 10,
+ withItems = false,
+ features,
}: {
- db: DrizzleCli;
- internalCustomerId: string;
- invoices?: Invoice[];
- limit?: number;
- withItems?: boolean;
- features?: Feature[];
+ db: DrizzleCli;
+ internalCustomerId: string;
+ invoices?: Invoice[];
+ limit?: number;
+ withItems?: boolean;
+ features?: Feature[];
}): Promise => {
- const finalInvoices = notNullish(invoices)
- ? invoices
- : await InvoiceService.list({
- db,
- internalCustomerId,
- limit,
- });
+ const finalInvoices = notNullish(invoices)
+ ? invoices
+ : await InvoiceService.list({
+ db,
+ internalCustomerId,
+ limit,
+ });
- const processedInvoices = finalInvoices!.map((i) =>
- processInvoice({
- invoice: i,
- withItems,
- features,
- })
- );
+ const processedInvoices = finalInvoices!.map((i) =>
+ processInvoice({
+ invoice: i,
+ withItems,
+ features,
+ }),
+ );
- return processedInvoices;
+ return processedInvoices;
};
// IMPORTANT FUNCTION
export const getCusEntsInFeatures = async ({
- customer,
- internalFeatureIds,
- logger,
- reverseOrder = false,
+ customer,
+ internalFeatureIds,
+ logger,
+ reverseOrder = false,
}: {
- customer: FullCustomer;
- internalFeatureIds?: string[];
- logger: any;
- reverseOrder?: boolean;
+ customer: FullCustomer;
+ internalFeatureIds?: string[];
+ logger: any;
+ reverseOrder?: boolean;
}) => {
- let cusProducts = customer.customer_products;
+ let cusProducts = customer.customer_products;
- // This is important, attaching customer_product to cus ent is used elsewhere, don't delete.
- let cusEnts = cusProducts.flatMap((cusProduct) => {
- return cusProduct.customer_entitlements.map((cusEnt) => ({
- ...cusEnt,
- customer_product: cusProduct,
- }));
- });
+ // This is important, attaching customer_product to cus ent is used elsewhere, don't delete.
+ let cusEnts = cusProducts.flatMap((cusProduct) => {
+ return cusProduct.customer_entitlements.map((cusEnt) => ({
+ ...cusEnt,
+ customer_product: cusProduct,
+ }));
+ });
- let cusPrices = cusProducts.flatMap((cusProduct) => {
- return cusProduct.customer_prices || [];
- });
+ let cusPrices = cusProducts.flatMap((cusProduct) => {
+ return cusProduct.customer_prices || [];
+ });
- if (internalFeatureIds) {
- cusEnts = cusEnts.filter((cusEnt) =>
- internalFeatureIds.includes(cusEnt.entitlement.internal_feature_id)
- );
- }
+ if (internalFeatureIds) {
+ cusEnts = cusEnts.filter((cusEnt) =>
+ internalFeatureIds.includes(cusEnt.entitlement.internal_feature_id),
+ );
+ }
- if (customer.entity) {
- let entity = customer.entity;
- cusEnts = cusEnts.filter(
- (cusEnt) =>
- nullish(cusEnt.customer_product.internal_entity_id) ||
- cusEnt.customer_product.internal_entity_id === entity.internal_id
- );
- }
+ if (customer.entity) {
+ let entity = customer.entity;
+ cusEnts = cusEnts.filter(
+ (cusEnt) =>
+ nullish(cusEnt.customer_product.internal_entity_id) ||
+ cusEnt.customer_product.internal_entity_id === entity.internal_id,
+ );
+ }
- sortCusEntsForDeduction(cusEnts, reverseOrder);
+ sortCusEntsForDeduction(cusEnts, reverseOrder);
- return { cusEnts, cusPrices };
+ return { cusEnts, cusPrices };
};
export const parseCusExpand = (expand?: string): CusExpand[] => {
- if (expand) {
- let options = expand.split(",");
- let result: CusExpand[] = [];
- for (const option of options) {
- if (!Object.values(CusExpand).includes(option as CusExpand)) {
- throw new RecaseError({
- message: `Invalid expand option: ${option}`,
- code: ErrCode.InvalidExpand,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- result.push(option as CusExpand);
- }
- return result;
- } else {
- return [];
- }
+ if (expand) {
+ let options = expand.split(",");
+ let result: CusExpand[] = [];
+ for (const option of options) {
+ if (!Object.values(CusExpand).includes(option as CusExpand)) {
+ throw new RecaseError({
+ message: `Invalid expand option: ${option}`,
+ code: ErrCode.InvalidExpand,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ result.push(option as CusExpand);
+ }
+ return result;
+ } else {
+ return [];
+ }
};
export const newCusToFullCus = ({ newCus }: { newCus: Customer }) => {
- let fullCus: FullCustomer = {
- ...newCus,
- customer_products: [],
- entities: [],
- };
+ let fullCus: FullCustomer = {
+ ...newCus,
+ customer_products: [],
+ entities: [],
+ };
- return fullCus;
+ return fullCus;
};
diff --git a/server/src/internal/customers/cusUtils/getCustomerDetails.ts b/server/src/internal/customers/cusUtils/getCustomerDetails.ts
index e99339f11..118fc465c 100644
--- a/server/src/internal/customers/cusUtils/getCustomerDetails.ts
+++ b/server/src/internal/customers/cusUtils/getCustomerDetails.ts
@@ -1,19 +1,19 @@
import { BREAK_API_VERSION } from "@/utils/constants.js";
import {
- AppEnv,
- CusProductStatus,
- FullCusProduct,
- APIVersion,
- CusResponseSchema,
- CustomerResponseSchema,
- CusEntResponseSchema,
- FeatureType,
- Feature,
- Organization,
- FullCustomer,
- CusExpand,
- RewardResponse,
- EntityResponseSchema,
+ AppEnv,
+ CusProductStatus,
+ FullCusProduct,
+ APIVersion,
+ CusResponseSchema,
+ CustomerResponseSchema,
+ CusEntResponseSchema,
+ FeatureType,
+ Feature,
+ Organization,
+ FullCustomer,
+ CusExpand,
+ RewardResponse,
+ EntityResponseSchema,
} from "@autumn/shared";
import { getCusInvoices } from "./cusUtils.js";
@@ -30,173 +30,173 @@ import { getCusRewards } from "./cusResponseUtils/getCusRewards.js";
import { getCusPaymentMethodRes } from "./cusResponseUtils/getCusPaymentMethodRes.js";
export const getCustomerDetails = async ({
- db,
- customer,
- features,
- org,
- env,
- params = {},
- logger,
- cusProducts,
- expand,
- reqApiVersion,
+ db,
+ customer,
+ features,
+ org,
+ env,
+ params = {},
+ logger,
+ cusProducts,
+ expand,
+ reqApiVersion,
}: {
- db: DrizzleCli;
- customer: FullCustomer;
- features: Feature[];
- org: Organization;
- env: AppEnv;
- params?: any;
- logger: any;
- cusProducts: FullCusProduct[];
- expand: CusExpand[];
- reqApiVersion?: number;
+ db: DrizzleCli;
+ customer: FullCustomer;
+ features: Feature[];
+ org: Organization;
+ env: AppEnv;
+ params?: any;
+ logger: any;
+ cusProducts: FullCusProduct[];
+ expand: CusExpand[];
+ reqApiVersion?: number;
}) => {
- let apiVersion = orgToVersion({
- org,
- reqApiVersion,
- });
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion,
+ });
- let withRewards = expand.includes(CusExpand.Rewards);
+ let withRewards = expand.includes(CusExpand.Rewards);
- let inStatuses = org.config.include_past_due
- ? [CusProductStatus.Active, CusProductStatus.PastDue]
- : [CusProductStatus.Active];
+ let inStatuses = org.config.include_past_due
+ ? [CusProductStatus.Active, CusProductStatus.PastDue]
+ : [CusProductStatus.Active];
- let cusEnts = cusProductsToCusEnts({ cusProducts, inStatuses }) as any;
+ let cusEnts = cusProductsToCusEnts({ cusProducts, inStatuses }) as any;
- const balances = await getCusBalances({
- cusEntsWithCusProduct: cusEnts,
- cusPrices: cusProductsToCusPrices({ cusProducts, inStatuses }),
- org,
- apiVersion,
- });
+ const balances = await getCusBalances({
+ cusEntsWithCusProduct: cusEnts,
+ cusPrices: cusProductsToCusPrices({ cusProducts, inStatuses }),
+ org,
+ apiVersion,
+ });
- let subIds = cusProducts.flatMap(
- (cp: FullCusProduct) => cp.subscription_ids || []
- );
+ let subIds = cusProducts.flatMap(
+ (cp: FullCusProduct) => cp.subscription_ids || [],
+ );
- const subs = customer.subscriptions || [];
- const { main, addOns } = await processFullCusProducts({
- fullCusProducts: cusProducts,
- subs,
- org,
- apiVersion,
- features,
- });
+ const subs = customer.subscriptions || [];
+ const { main, addOns } = await processFullCusProducts({
+ fullCusProducts: cusProducts,
+ subs,
+ org,
+ apiVersion,
+ features,
+ });
- if (apiVersion >= APIVersion.v1_1) {
- let entList: any = balances.map((b) => {
- let isBoolean =
- features.find((f: Feature) => f.id == b.feature_id)?.type ==
- FeatureType.Boolean;
- if (b.unlimited || isBoolean) {
- return b;
- }
+ if (apiVersion >= APIVersion.v1_1) {
+ let entList: any = balances.map((b) => {
+ let isBoolean =
+ features.find((f: Feature) => f.id == b.feature_id)?.type ==
+ FeatureType.Boolean;
+ if (b.unlimited || isBoolean) {
+ return b;
+ }
- return CusEntResponseSchema.parse({
- ...b,
- usage: b.used,
- included_usage: b.allowance,
- });
- });
+ return CusEntResponseSchema.parse({
+ ...b,
+ usage: b.used,
+ included_usage: b.allowance,
+ });
+ });
- let products: any = [...main, ...addOns];
+ let products: any = [...main, ...addOns];
- if (apiVersion >= APIVersion.v1_2) {
- entList = featuresToObject({
- features,
- entList,
- });
- }
+ if (apiVersion >= APIVersion.v1_2) {
+ entList = featuresToObject({
+ features,
+ entList,
+ });
+ }
- let withInvoices = expand.includes(CusExpand.Invoices);
+ let withInvoices = expand.includes(CusExpand.Invoices);
- let rewards: RewardResponse | undefined = await getCusRewards({
- org,
- env,
- fullCus: customer,
- subIds,
- expand,
- });
+ let rewards: RewardResponse | undefined = await getCusRewards({
+ org,
+ env,
+ fullCus: customer,
+ subIds,
+ expand,
+ });
- let referrals = await getCusReferrals({
- db,
- fullCus: customer,
- expand,
- });
+ let referrals = await getCusReferrals({
+ db,
+ fullCus: customer,
+ expand,
+ });
- let paymentMethod = await getCusPaymentMethodRes({
- org,
- env,
- fullCus: customer,
- expand,
- });
+ let paymentMethod = await getCusPaymentMethodRes({
+ org,
+ env,
+ fullCus: customer,
+ expand,
+ });
- let cusResponse = {
- ...CusResponseSchema.parse({
- ...customer,
- stripe_id: customer.processor?.id,
- features: entList,
- products,
- // invoices: withInvoices ? invoices : undefined,
- invoices: withInvoices
- ? invoicesToResponse({
- invoices: customer.invoices || [],
- logger,
- })
- : undefined,
- trials_used: expand.includes(CusExpand.TrialsUsed)
- ? customer.trials_used
- : undefined,
- rewards: withRewards ? rewards : undefined,
- metadata: customer.metadata,
- entities: expand.includes(CusExpand.Entities)
- ? customer.entities.map((e) =>
- EntityResponseSchema.parse({
- id: e.id,
- name: e.name,
- customer_id: customer.id,
- feature_id: e.feature_id,
- created_at: e.created_at,
- env: customer.env,
- })
- )
- : undefined,
- referrals,
- payment_method: paymentMethod,
- }),
- };
+ let cusResponse = {
+ ...CusResponseSchema.parse({
+ ...customer,
+ stripe_id: customer.processor?.id,
+ features: entList,
+ products,
+ // invoices: withInvoices ? invoices : undefined,
+ invoices: withInvoices
+ ? invoicesToResponse({
+ invoices: customer.invoices || [],
+ logger,
+ })
+ : undefined,
+ trials_used: expand.includes(CusExpand.TrialsUsed)
+ ? customer.trials_used
+ : undefined,
+ rewards: withRewards ? rewards : undefined,
+ metadata: customer.metadata,
+ entities: expand.includes(CusExpand.Entities)
+ ? customer.entities.map((e) =>
+ EntityResponseSchema.parse({
+ id: e.id,
+ name: e.name,
+ customer_id: customer.id,
+ feature_id: e.feature_id,
+ created_at: e.created_at,
+ env: customer.env,
+ }),
+ )
+ : undefined,
+ referrals,
+ payment_method: paymentMethod,
+ }),
+ };
- if (params?.with_autumn_id === "true") {
- return {
- ...cusResponse,
- autumn_id: customer.internal_id,
- };
- } else {
- return cusResponse;
- }
- } else {
- let withItems = org.config.api_version >= BREAK_API_VERSION;
+ if (params?.with_autumn_id === "true") {
+ return {
+ ...cusResponse,
+ autumn_id: customer.internal_id,
+ };
+ } else {
+ return cusResponse;
+ }
+ } else {
+ let withItems = org.config.api_version >= BREAK_API_VERSION;
- const processedInvoices = await getCusInvoices({
- db,
- internalCustomerId: customer.internal_id,
- invoices: customer.invoices,
- limit: 20,
- withItems,
- features,
- });
+ const processedInvoices = await getCusInvoices({
+ db,
+ internalCustomerId: customer.internal_id,
+ invoices: customer.invoices,
+ limit: 20,
+ withItems,
+ features,
+ });
- return {
- customer: CustomerResponseSchema.parse(customer),
- products: main,
- add_ons: addOns,
- entitlements: balances,
- invoices: processedInvoices,
- trials_used: expand.includes(CusExpand.TrialsUsed)
- ? customer.trials_used
- : undefined,
- };
- }
+ return {
+ customer: CustomerResponseSchema.parse(customer),
+ products: main,
+ add_ons: addOns,
+ entitlements: balances,
+ invoices: processedInvoices,
+ trials_used: expand.includes(CusExpand.TrialsUsed)
+ ? customer.trials_used
+ : undefined,
+ };
+ }
};
diff --git a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts
index c22a9756f..f061b37d0 100644
--- a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts
+++ b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts
@@ -3,173 +3,173 @@ import { handleCreateCustomer } from "../handlers/handleCreateCustomer.js";
import { CusService } from "../CusService.js";
import {
- AppEnv,
- CusExpand,
- CusProductStatus,
- CustomerData,
- EntityData,
- ErrCode,
- Feature,
- FullCustomer,
- Organization,
+ AppEnv,
+ CusExpand,
+ CusProductStatus,
+ CustomerData,
+ EntityData,
+ ErrCode,
+ Feature,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { autoCreateEntity } from "@/internal/entities/handlers/handleCreateEntity/autoCreateEntity.js";
import {
- deleteCusCache,
- refreshCusCache,
+ deleteCusCache,
+ refreshCusCache,
} from "../cusCache/updateCachedCus.js";
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
export const getOrCreateCustomer = async ({
- req,
- customerId,
- customerData,
- inStatuses = [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- skipGet = false,
- withEntities = false,
- expand,
+ req,
+ customerId,
+ customerData,
+ inStatuses = [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ skipGet = false,
+ withEntities = false,
+ expand,
- // Entity stuff
- entityId,
- entityData,
- withCache = false,
+ // Entity stuff
+ entityId,
+ entityData,
+ withCache = false,
}: {
- req: ExtendedRequest;
- customerId: string;
- customerData?: CustomerData;
- inStatuses?: CusProductStatus[];
- skipGet?: boolean;
- withEntities?: boolean;
- expand?: CusExpand[];
- entityId?: string;
- entityData?: EntityData;
- withCache?: boolean;
+ req: ExtendedRequest;
+ customerId: string;
+ customerData?: CustomerData;
+ inStatuses?: CusProductStatus[];
+ skipGet?: boolean;
+ withEntities?: boolean;
+ expand?: CusExpand[];
+ entityId?: string;
+ entityData?: EntityData;
+ withCache?: boolean;
}): Promise => {
- let customer;
+ let customer;
- const { db, org, features, env, logtail: logger } = req;
+ const { db, org, features, env, logtail: logger } = req;
- if (!withEntities) {
- withEntities = expand?.includes(CusExpand.Entities) || false;
- }
+ if (!withEntities) {
+ withEntities = expand?.includes(CusExpand.Entities) || false;
+ }
- if (!skipGet) {
- if (withCache) {
- customer = await getCusWithCache({
- db,
- idOrInternalId: customerId,
- org,
- env,
- entityId,
- expand: expand as CusExpand[],
- logger,
- });
- } else {
- customer = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- inStatuses,
- withEntities,
- entityId,
- expand,
- allowNotFound: true,
- withSubs: true,
- });
- }
- }
+ if (!skipGet) {
+ if (withCache) {
+ customer = await getCusWithCache({
+ db,
+ idOrInternalId: customerId,
+ org,
+ env,
+ entityId,
+ expand: expand as CusExpand[],
+ logger,
+ });
+ } else {
+ customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ inStatuses,
+ withEntities,
+ entityId,
+ expand,
+ allowNotFound: true,
+ withSubs: true,
+ });
+ }
+ }
- if (!customer) {
- try {
- customer = await handleCreateCustomer({
- req,
- cusData: {
- id: customerId,
- name: customerData?.name,
- email: customerData?.email,
- fingerprint: customerData?.fingerprint,
- metadata: customerData?.metadata || {},
- stripe_id: customerData?.stripe_id,
- },
- });
+ if (!customer) {
+ try {
+ customer = await handleCreateCustomer({
+ req,
+ cusData: {
+ id: customerId,
+ name: customerData?.name,
+ email: customerData?.email,
+ fingerprint: customerData?.fingerprint,
+ metadata: customerData?.metadata || {},
+ stripe_id: customerData?.stripe_id,
+ },
+ });
- customer = await CusService.getFull({
- db,
- idOrInternalId: customerId || customer!.internal_id,
- orgId: org.id,
- env,
- inStatuses,
- withEntities,
- entityId,
- expand,
- withSubs: true,
- });
+ customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId || customer!.internal_id,
+ orgId: org.id,
+ env,
+ inStatuses,
+ withEntities,
+ entityId,
+ expand,
+ withSubs: true,
+ });
- await deleteCusCache({
- db,
- customerId: customer.id!,
- org,
- env,
- });
- } catch (error: any) {
- if (error?.data?.code == "23505") {
- customer = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- inStatuses,
- withEntities,
- entityId,
- expand,
- withSubs: true,
- });
- } else {
- throw error;
- }
- }
- }
+ await deleteCusCache({
+ db,
+ customerId: customer.id!,
+ org,
+ env,
+ });
+ } catch (error: any) {
+ if (error?.data?.code == "23505") {
+ customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ inStatuses,
+ withEntities,
+ entityId,
+ expand,
+ withSubs: true,
+ });
+ } else {
+ throw error;
+ }
+ }
+ }
- customer = await updateCustomerDetails({
- db,
- customer,
- customerData,
- org,
- logger,
- });
+ customer = await updateCustomerDetails({
+ db,
+ customer,
+ customerData,
+ org,
+ logger,
+ });
- if (entityId && !customer.entity) {
- logger.info(`Auto creating entity ${entityId} for customer ${customerId}`);
+ if (entityId && !customer.entity) {
+ logger.info(`Auto creating entity ${entityId} for customer ${customerId}`);
- let newEntity = await autoCreateEntity({
- req,
- customer,
- entityId,
- entityData: {
- id: entityId,
- name: entityData?.name,
- feature_id: entityData?.feature_id!,
- },
- logger,
- });
+ let newEntity = await autoCreateEntity({
+ req,
+ customer,
+ entityId,
+ entityData: {
+ id: entityId,
+ name: entityData?.name,
+ feature_id: entityData?.feature_id!,
+ },
+ logger,
+ });
- customer.entities = [...(customer.entities || []), newEntity];
- customer.entity = newEntity;
+ customer.entities = [...(customer.entities || []), newEntity];
+ customer.entity = newEntity;
- await refreshCusCache({
- db,
- customerId: customer.id!,
- org,
- env: customer.env,
- });
- }
+ await refreshCusCache({
+ db,
+ customerId: customer.id!,
+ org,
+ env: customer.env,
+ });
+ }
- return customer as FullCustomer;
+ return customer as FullCustomer;
};
diff --git a/server/src/internal/customers/getFullCusQuery.ts b/server/src/internal/customers/getFullCusQuery.ts
index 318872e63..a44697f32 100644
--- a/server/src/internal/customers/getFullCusQuery.ts
+++ b/server/src/internal/customers/getFullCusQuery.ts
@@ -3,16 +3,16 @@ import { CusProductStatus } from "@autumn/shared";
import { sql, SQL } from "drizzle-orm";
const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
- const withStatusFilter = () => {
- return inStatuses
- ? sql`AND cp.status = ANY(ARRAY[${sql.join(
- inStatuses.map((status) => sql`${status}`),
- sql`, `
- )}])`
- : sql``;
- };
+ const withStatusFilter = () => {
+ return inStatuses
+ ? sql`AND cp.status = ANY(ARRAY[${sql.join(
+ inStatuses.map((status) => sql`${status}`),
+ sql`, `,
+ )}])`
+ : sql``;
+ };
- return sql`
+ return sql`
customer_products_with_prices AS (
SELECT
cp.*,
@@ -80,11 +80,11 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
};
const buildEntitiesCTE = (withEntities: boolean) => {
- if (!withEntities) {
- return sql``;
- }
+ if (!withEntities) {
+ return sql``;
+ }
- return sql`
+ return sql`
customer_entities AS (
SELECT
COALESCE(
@@ -99,11 +99,11 @@ const buildEntitiesCTE = (withEntities: boolean) => {
};
const buildEntityCTE = (entityId?: string) => {
- if (!entityId) {
- return sql``;
- }
+ if (!entityId) {
+ return sql``;
+ }
- return sql`
+ return sql`
entity_record AS (
SELECT * FROM entities e
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
@@ -116,15 +116,15 @@ const buildEntityCTE = (entityId?: string) => {
};
const buildTrialsUsedCTE = (
- withTrialsUsed: boolean,
- orgId: string,
- env: AppEnv
+ withTrialsUsed: boolean,
+ orgId: string,
+ env: AppEnv,
) => {
- if (!withTrialsUsed) {
- return sql``;
- }
+ if (!withTrialsUsed) {
+ return sql``;
+ }
- return sql`
+ return sql`
customer_trials_used AS (
SELECT
COALESCE(
@@ -147,14 +147,14 @@ const buildTrialsUsedCTE = (
};
const buildSubscriptionsCTE = (
- withSubs: boolean,
- inStatuses?: CusProductStatus[]
+ withSubs: boolean,
+ inStatuses?: CusProductStatus[],
) => {
- if (!withSubs) {
- return sql``;
- }
+ if (!withSubs) {
+ return sql``;
+ }
- return sql`
+ return sql`
customer_subscriptions AS (
SELECT
COALESCE(
@@ -171,15 +171,15 @@ const buildSubscriptionsCTE = (
};
const buildInvoicesCTE = (hasEntityCTE: boolean) => {
- let entityFilter = hasEntityCTE
- ? sql`AND (
+ let entityFilter = hasEntityCTE
+ ? sql`AND (
NOT EXISTS (SELECT 1 FROM entity_record)
OR i.internal_entity_id = (SELECT internal_id FROM entity_record LIMIT 1)
OR i.internal_entity_id IS NULL
)`
- : sql``;
+ : sql``;
- return sql`
+ return sql`
customer_invoices AS (
SELECT
COALESCE(
@@ -195,21 +195,21 @@ const buildInvoicesCTE = (hasEntityCTE: boolean) => {
};
export const getFullCusQuery = (
- idOrInternalId: string,
- orgId: string,
- env: AppEnv,
- inStatuses: CusProductStatus[],
- includeInvoices: boolean,
- withEntities: boolean,
- withTrialsUsed: boolean,
- withSubs: boolean,
- withEvents: boolean,
- entityId?: string
+ idOrInternalId: string,
+ orgId: string,
+ env: AppEnv,
+ inStatuses: CusProductStatus[],
+ includeInvoices: boolean,
+ withEntities: boolean,
+ withTrialsUsed: boolean,
+ withSubs: boolean,
+ withEvents: boolean,
+ entityId?: string,
) => {
- const sqlChunks: SQL[] = [];
+ const sqlChunks: SQL[] = [];
- // Step 1: Get customer record
- sqlChunks.push(sql`
+ // Step 1: Get customer record
+ sqlChunks.push(sql`
WITH customer_record AS (
SELECT * FROM customers c
WHERE (
@@ -222,45 +222,45 @@ export const getFullCusQuery = (
)
`);
- // Step 2: Get entities
- if (withEntities) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(buildEntitiesCTE(withEntities));
- }
+ // Step 2: Get entities
+ if (withEntities) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(buildEntitiesCTE(withEntities));
+ }
- // Step 3: Get entity
- if (entityId) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(buildEntityCTE(entityId));
- }
+ // Step 3: Get entity
+ if (entityId) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(buildEntityCTE(entityId));
+ }
- // Add customer products CTE
- sqlChunks.push(sql`, `);
- // sqlChunks.push(buildCusProductsCTE(inStatuses));
- sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
+ // Add customer products CTE
+ sqlChunks.push(sql`, `);
+ // sqlChunks.push(buildCusProductsCTE(inStatuses));
+ sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
- // Conditionally add trials used CTE
- if (withTrialsUsed) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env));
- }
+ // Conditionally add trials used CTE
+ if (withTrialsUsed) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env));
+ }
- // Conditionally add subscriptions CTE
- if (withSubs) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses));
- }
+ // Conditionally add subscriptions CTE
+ if (withSubs) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses));
+ }
- // Conditionally add invoices CTE
- if (includeInvoices) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(buildInvoicesCTE(!!entityId));
- }
+ // Conditionally add invoices CTE
+ if (includeInvoices) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(buildInvoicesCTE(!!entityId));
+ }
- // Conditionally add events CTE
- if (withEvents) {
- sqlChunks.push(sql`, `);
- sqlChunks.push(sql`
+ // Conditionally add events CTE
+ if (withEvents) {
+ sqlChunks.push(sql`, `);
+ sqlChunks.push(sql`
customer_events AS (
SELECT
COALESCE(
@@ -281,11 +281,11 @@ export const getFullCusQuery = (
AND e.set_usage = false
)
`);
- }
+ }
- // Build final SELECT
- const selectFieldsChunks: SQL[] = [];
- selectFieldsChunks.push(sql`
+ // Build final SELECT
+ const selectFieldsChunks: SQL[] = [];
+ selectFieldsChunks.push(sql`
cr.*,
COALESCE(
(SELECT json_agg(cpwp) FROM customer_products_with_prices cpwp),
@@ -293,98 +293,98 @@ export const getFullCusQuery = (
) AS customer_products
`);
- // Add entities to SELECT if withEntities is true
- if (withEntities) {
- selectFieldsChunks.push(sql`,
+ // Add entities to SELECT if withEntities is true
+ if (withEntities) {
+ selectFieldsChunks.push(sql`,
(SELECT entities FROM customer_entities) AS entities`);
- }
+ }
- // Add entity to SELECT if entityId is provided
- if (entityId) {
- selectFieldsChunks.push(sql`,
+ // Add entity to SELECT if entityId is provided
+ if (entityId) {
+ selectFieldsChunks.push(sql`,
(SELECT row_to_json(er) FROM entity_record er LIMIT 1) AS entity`);
- }
+ }
- // Add trials used to SELECT if withTrialsUsed is true
- if (withTrialsUsed) {
- selectFieldsChunks.push(sql`,
+ // Add trials used to SELECT if withTrialsUsed is true
+ if (withTrialsUsed) {
+ selectFieldsChunks.push(sql`,
(SELECT trials_used FROM customer_trials_used) AS trials_used`);
- }
+ }
- // Add subscriptions to SELECT if withSubs is true
- if (withSubs) {
- selectFieldsChunks.push(sql`,
+ // Add subscriptions to SELECT if withSubs is true
+ if (withSubs) {
+ selectFieldsChunks.push(sql`,
(SELECT subscriptions FROM customer_subscriptions) AS subscriptions`);
- }
+ }
- if (includeInvoices) {
- selectFieldsChunks.push(sql`,
+ if (includeInvoices) {
+ selectFieldsChunks.push(sql`,
(SELECT invoices FROM customer_invoices) AS invoices`);
- }
+ }
- if (withEvents) {
- selectFieldsChunks.push(sql`,
+ if (withEvents) {
+ selectFieldsChunks.push(sql`,
(SELECT events FROM customer_events) AS events`);
- }
+ }
- sqlChunks.push(sql`
+ sqlChunks.push(sql`
SELECT ${sql.join(selectFieldsChunks, sql``)}
FROM customer_record cr
`);
- return sql.join(sqlChunks, sql``);
+ return sql.join(sqlChunks, sql``);
};
export const getPaginatedFullCusQuery = ({
- orgId,
- env,
- inStatuses,
- includeInvoices,
- withEntities,
- withTrialsUsed,
- withSubs,
- limit = 10,
- offset = 0,
- withEvents = false,
- entityId,
- internalCustomerIds,
+ orgId,
+ env,
+ inStatuses,
+ includeInvoices,
+ withEntities,
+ withTrialsUsed,
+ withSubs,
+ limit = 10,
+ offset = 0,
+ withEvents = false,
+ entityId,
+ internalCustomerIds,
}: {
- orgId: string;
- env: AppEnv;
- inStatuses?: CusProductStatus[];
- includeInvoices: boolean;
- withEntities: boolean;
- withTrialsUsed: boolean;
- withSubs: boolean;
- limit: number;
- offset: number;
- withEvents?: boolean;
- entityId?: string;
- internalCustomerIds?: string[];
+ orgId: string;
+ env: AppEnv;
+ inStatuses?: CusProductStatus[];
+ includeInvoices: boolean;
+ withEntities: boolean;
+ withTrialsUsed: boolean;
+ withSubs: boolean;
+ limit: number;
+ offset: number;
+ withEvents?: boolean;
+ entityId?: string;
+ internalCustomerIds?: string[];
}) => {
- const withStatusFilter = () => {
- return inStatuses?.length
- ? sql`AND cp.status = ANY(ARRAY[${sql.join(
- inStatuses.map((status) => sql`${status}`),
- sql`, `
- )}])`
- : sql``;
- };
+ const withStatusFilter = () => {
+ return inStatuses?.length
+ ? sql`AND cp.status = ANY(ARRAY[${sql.join(
+ inStatuses.map((status) => sql`${status}`),
+ sql`, `,
+ )}])`
+ : sql``;
+ };
- return sql`
+ return sql`
WITH customer_records AS (
SELECT c.*
FROM customers c
WHERE c.org_id = ${orgId}
AND c.env = ${env}
${
- internalCustomerIds && internalCustomerIds.length > 0
- ? sql`AND c.internal_id IN (${sql.join(
- internalCustomerIds.map((id) => sql`${id}`),
- sql`, `
- )})`
- : sql``
- }
+ internalCustomerIds && internalCustomerIds.length > 0
+ ? sql`AND c.internal_id IN (${sql.join(
+ internalCustomerIds.map((id) => sql`${id}`),
+ sql`, `,
+ )})`
+ : sql``
+ }
ORDER BY c.created_at DESC
LIMIT ${limit} OFFSET ${offset}
),
@@ -462,8 +462,8 @@ export const getPaginatedFullCusQuery = ({
)
${
- withSubs
- ? sql`, customer_subscriptions AS (
+ withSubs
+ ? sql`, customer_subscriptions AS (
SELECT
cpwp.internal_customer_id,
COALESCE(
@@ -474,12 +474,12 @@ export const getPaginatedFullCusQuery = ({
JOIN subscriptions s ON s.stripe_id = ANY(cpwp.subscription_ids)
GROUP BY cpwp.internal_customer_id
)`
- : sql``
- }
+ : sql``
+ }
${
- withEntities
- ? sql`, customer_entities AS (
+ withEntities
+ ? sql`, customer_entities AS (
SELECT
e.internal_customer_id,
COALESCE(
@@ -490,12 +490,12 @@ export const getPaginatedFullCusQuery = ({
WHERE e.internal_customer_id IN (SELECT internal_id FROM customer_records)
GROUP BY e.internal_customer_id
)`
- : sql``
- }
+ : sql``
+ }
${
- includeInvoices
- ? sql`, customer_invoices AS (
+ includeInvoices
+ ? sql`, customer_invoices AS (
SELECT
i.internal_customer_id,
COALESCE(
@@ -506,12 +506,12 @@ export const getPaginatedFullCusQuery = ({
WHERE i.internal_customer_id IN (SELECT internal_id FROM customer_records)
GROUP BY i.internal_customer_id
)`
- : sql``
- }
+ : sql``
+ }
${
- withTrialsUsed
- ? sql`, customer_trials_used AS (
+ withTrialsUsed
+ ? sql`, customer_trials_used AS (
SELECT
cp.internal_customer_id,
json_agg(json_build_object(
@@ -526,8 +526,8 @@ export const getPaginatedFullCusQuery = ({
AND cp.free_trial_id IS NOT NULL
GROUP BY cp.internal_customer_id
)`
- : sql``
- }
+ : sql``
+ }
SELECT
cr.*,
diff --git a/server/src/internal/customers/handlers/handleAddCouponToCus.ts b/server/src/internal/customers/handlers/handleAddCouponToCus.ts
index 467cc6925..b7fe49b1c 100644
--- a/server/src/internal/customers/handlers/handleAddCouponToCus.ts
+++ b/server/src/internal/customers/handlers/handleAddCouponToCus.ts
@@ -8,77 +8,77 @@ import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const handleAddCouponToCus = async (req: any, res: any) => {
- try {
- const { customer_id, coupon_id } = req.params;
- const { db, orgId, env, logtail: logger } = req;
+ try {
+ const { customer_id, coupon_id } = req.params;
+ const { db, orgId, env, logtail: logger } = req;
- const [org, customer, coupon] = await Promise.all([
- OrgService.getFromReq(req),
- CusService.get({
- db,
- idOrInternalId: customer_id,
- orgId,
- env,
- }),
- RewardService.get({
- db,
- idOrInternalId: coupon_id,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ const [org, customer, coupon] = await Promise.all([
+ OrgService.getFromReq(req),
+ CusService.get({
+ db,
+ idOrInternalId: customer_id,
+ orgId,
+ env,
+ }),
+ RewardService.get({
+ db,
+ idOrInternalId: coupon_id,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customer_id} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customer_id} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- if (!coupon) {
- throw new RecaseError({
- message: `Coupon ${coupon_id} not found`,
- code: ErrCode.RewardNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!coupon) {
+ throw new RecaseError({
+ message: `Coupon ${coupon_id} not found`,
+ code: ErrCode.RewardNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const stripeCli = createStripeCli({
- org,
- env,
- // apiVersion: "2025-02-24.acacia",
- legacyVersion: true,
- });
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ // apiVersion: "2025-02-24.acacia",
+ legacyVersion: true,
+ });
- await createStripeCusIfNotExists({
- db,
- org,
- env,
- customer,
- logger,
- });
+ await createStripeCusIfNotExists({
+ db,
+ org,
+ env,
+ customer,
+ logger,
+ });
- // Attach coupon to customer
- // curl https://api.stripe.com/v1/customers/cus_123456/discounts \
- // -u sk_test_your_key: \
- // -d coupon=COUPON_ID
+ // Attach coupon to customer
+ // curl https://api.stripe.com/v1/customers/cus_123456/discounts \
+ // -u sk_test_your_key: \
+ // -d coupon=COUPON_ID
- await stripeCli.rawRequest(
- "POST",
- `/v1/customers/${customer.processor.id}`,
- {
- coupon: coupon.id,
- }
- );
- // await stripeCli.customers.update(customer.processor.id, {
- // coupon: coupon.id,
+ await stripeCli.rawRequest(
+ "POST",
+ `/v1/customers/${customer.processor.id}`,
+ {
+ coupon: coupon.id,
+ },
+ );
+ // await stripeCli.customers.update(customer.processor.id, {
+ // coupon: coupon.id,
- // discounts: [{ coupon: coupon.id }],
- // });
+ // discounts: [{ coupon: coupon.id }],
+ // });
- res.status(200).json({ customer, coupon });
- } catch (error) {
- handleRequestError({ req, error, res, action: "add coupon to customer" });
- }
+ res.status(200).json({ customer, coupon });
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "add coupon to customer" });
+ }
};
diff --git a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts
index 160402a31..3b95ddf16 100644
--- a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts
+++ b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts
@@ -10,160 +10,160 @@ import { StatusCodes } from "http-status-codes";
import Stripe from "stripe";
const createDefaultBillingPortalConfiguration = async (stripeCli: Stripe) => {
- try {
- const configuration = await stripeCli.billingPortal.configurations.create({
- features: {
- customer_update: {
- allowed_updates: ["email", "address"],
- enabled: true,
- },
- invoice_history: {
- enabled: true,
- },
- payment_method_update: {
- enabled: true,
- },
- subscription_cancel: {
- enabled: true,
- },
- },
- // business_profile: {
- // privacy_policy_url: "https://example.com/privacy",
- // terms_of_service_url: "https://example.com/terms",
- // },
- });
- return configuration;
- } catch (error: any) {
- throw new RecaseError({
- message: `Failed to create billing portal configuration: ${error.message}`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ try {
+ const configuration = await stripeCli.billingPortal.configurations.create({
+ features: {
+ customer_update: {
+ allowed_updates: ["email", "address"],
+ enabled: true,
+ },
+ invoice_history: {
+ enabled: true,
+ },
+ payment_method_update: {
+ enabled: true,
+ },
+ subscription_cancel: {
+ enabled: true,
+ },
+ },
+ // business_profile: {
+ // privacy_policy_url: "https://example.com/privacy",
+ // terms_of_service_url: "https://example.com/terms",
+ // },
+ });
+ return configuration;
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Failed to create billing portal configuration: ${error.message}`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
};
export const handleCreateBillingPortal = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "create_billing_portal",
- handler: async (req: any, res: any) => {
- const customerId = req.params.customer_id;
- let returnUrl = req.body.return_url;
+ routeHandler({
+ req,
+ res,
+ action: "create_billing_portal",
+ handler: async (req: any, res: any) => {
+ const customerId = req.params.customer_id;
+ let returnUrl = req.body.return_url;
- const [org, customer] = await Promise.all([
- OrgService.getFromReq(req),
- CusService.get({
- db: req.db,
- idOrInternalId: customerId,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ const [org, customer] = await Promise.all([
+ OrgService.getFromReq(req),
+ CusService.get({
+ db: req.db,
+ idOrInternalId: customerId,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const stripeCli = createStripeCli({ org, env: req.env });
+ const stripeCli = createStripeCli({ org, env: req.env });
- // Determine the Stripe customer ID to use
- let stripeCustomerId: string;
+ // Determine the Stripe customer ID to use
+ let stripeCustomerId: string;
- if (!customer.processor?.id) {
- try {
- const newCus = await createStripeCusIfNotExists({
- db: req.db,
- org,
- env: req.env,
- customer,
- logger: req.logtail,
- });
+ if (!customer.processor?.id) {
+ try {
+ const newCus = await createStripeCusIfNotExists({
+ db: req.db,
+ org,
+ env: req.env,
+ customer,
+ logger: req.logtail,
+ });
- if (!newCus) {
- throw new RecaseError({
- message: `Failed to create Stripe customer`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (!newCus) {
+ throw new RecaseError({
+ message: `Failed to create Stripe customer`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- stripeCustomerId = newCus.id;
- } catch (error: any) {
- throw new RecaseError({
- message: `Failed to create Stripe customer`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
- } else {
- stripeCustomerId = customer.processor.id;
- }
+ stripeCustomerId = newCus.id;
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Failed to create Stripe customer`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
+ } else {
+ stripeCustomerId = customer.processor.id;
+ }
- // Create billing portal session
- let portal;
- try {
- portal = await stripeCli.billingPortal.sessions.create({
- customer: stripeCustomerId,
- return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
- });
- } catch (error: any) {
- console.log(`Code: ${error.code}, Message: ${error.message}`);
+ // Create billing portal session
+ let portal;
+ try {
+ portal = await stripeCli.billingPortal.sessions.create({
+ customer: stripeCustomerId,
+ return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
+ });
+ } catch (error: any) {
+ console.log(`Code: ${error.code}, Message: ${error.message}`);
- // Check if the error is due to missing default configuration
- if (
- error.message &&
- error.message.includes("default configuration has not been created")
- ) {
- try {
- // Create a default billing portal configuration
- req.logtail?.info(
- `Creating default billing portal configuration for customer ${customer.id}`
- );
+ // Check if the error is due to missing default configuration
+ if (
+ error.message &&
+ error.message.includes("default configuration has not been created")
+ ) {
+ try {
+ // Create a default billing portal configuration
+ req.logtail?.info(
+ `Creating default billing portal configuration for customer ${customer.id}`,
+ );
- const configuration =
- await createDefaultBillingPortalConfiguration(stripeCli);
+ const configuration =
+ await createDefaultBillingPortalConfiguration(stripeCli);
- req.logtail?.info(
- "Successfully created billing portal configuration",
- {
- configurationId: configuration.id,
- orgId: org.id,
- }
- );
+ req.logtail?.info(
+ "Successfully created billing portal configuration",
+ {
+ configurationId: configuration.id,
+ orgId: org.id,
+ },
+ );
- // Retry creating the portal session with the new configuration
- portal = await stripeCli.billingPortal.sessions.create({
- customer: stripeCustomerId,
- return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
- configuration: configuration.id,
- });
- } catch (configError: any) {
- req.logtail?.error(
- "Failed to create billing portal configuration",
- {
- error: configError.message,
- orgId: org.id,
- }
- );
- throw new RecaseError({
- message: `Failed to create billing portal configuration: ${configError.message}`,
- code: ErrCode.StripeError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
- } else {
- throw error;
- }
- }
+ // Retry creating the portal session with the new configuration
+ portal = await stripeCli.billingPortal.sessions.create({
+ customer: stripeCustomerId,
+ return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
+ configuration: configuration.id,
+ });
+ } catch (configError: any) {
+ req.logtail?.error(
+ "Failed to create billing portal configuration",
+ {
+ error: configError.message,
+ orgId: org.id,
+ },
+ );
+ throw new RecaseError({
+ message: `Failed to create billing portal configuration: ${configError.message}`,
+ code: ErrCode.StripeError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
+ } else {
+ throw error;
+ }
+ }
- res.status(200).json({
- customer_id: customer.id,
- url: portal.url,
- });
- },
- });
+ res.status(200).json({
+ customer_id: customer.id,
+ url: portal.url,
+ });
+ },
+ });
diff --git a/server/src/internal/customers/handlers/handleCreateCustomer.ts b/server/src/internal/customers/handlers/handleCreateCustomer.ts
index 568cd3666..97ddba755 100644
--- a/server/src/internal/customers/handlers/handleCreateCustomer.ts
+++ b/server/src/internal/customers/handlers/handleCreateCustomer.ts
@@ -1,13 +1,13 @@
import { CusService } from "@/internal/customers/CusService.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- AppEnv,
- CreateCustomer,
- CreateCustomerSchema,
- Customer,
- ErrCode,
- FullProduct,
- Organization,
+ AppEnv,
+ CreateCustomer,
+ CreateCustomerSchema,
+ Customer,
+ ErrCode,
+ FullProduct,
+ Organization,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { notNullish } from "@/utils/genUtils.js";
@@ -18,189 +18,189 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { createNewCustomer } from "../cusUtils/createNewCustomer.js";
export const initStripeCusAndProducts = async ({
- db,
- org,
- env,
- customer,
- products,
- logger,
+ db,
+ org,
+ env,
+ customer,
+ products,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- customer: Customer;
- products: FullProduct[];
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ customer: Customer;
+ products: FullProduct[];
+ logger: any;
}) => {
- const batchInit: any[] = [
- createStripeCusIfNotExists({
- db,
- org,
- env,
- customer,
- logger,
- }),
- ];
+ const batchInit: any[] = [
+ createStripeCusIfNotExists({
+ db,
+ org,
+ env,
+ customer,
+ logger,
+ }),
+ ];
- for (const product of products) {
- batchInit.push(
- initProductInStripe({
- db,
- org,
- env,
- logger,
- product,
- })
- );
- }
+ for (const product of products) {
+ batchInit.push(
+ initProductInStripe({
+ db,
+ org,
+ env,
+ logger,
+ product,
+ }),
+ );
+ }
- await Promise.all(batchInit);
+ await Promise.all(batchInit);
};
const handleIdIsNull = async ({
- req,
- newCus,
- createDefaultProducts,
+ req,
+ newCus,
+ createDefaultProducts,
}: {
- req: ExtendedRequest;
- newCus: CreateCustomer;
- createDefaultProducts?: boolean;
+ req: ExtendedRequest;
+ newCus: CreateCustomer;
+ createDefaultProducts?: boolean;
}) => {
- const { db, org, env, logger } = req;
+ const { db, org, env, logger } = req;
- // 1. ID is null
- if (!newCus.email) {
- throw new RecaseError({
- message: "Email is required when `id` is null",
- code: ErrCode.InvalidCustomer,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // 1. ID is null
+ if (!newCus.email) {
+ throw new RecaseError({
+ message: "Email is required when `id` is null",
+ code: ErrCode.InvalidCustomer,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // 2. Check if email already exists
- let existingCustomers = await CusService.getByEmail({
- db,
- email: newCus.email,
- orgId: org.id,
- env,
- });
+ // 2. Check if email already exists
+ let existingCustomers = await CusService.getByEmail({
+ db,
+ email: newCus.email,
+ orgId: org.id,
+ env,
+ });
- if (existingCustomers.length > 0) {
- for (const existingCustomer of existingCustomers) {
- if (existingCustomer.id === null) {
- logger.info(
- `Create customer by email: ${newCus.email} already exists, skipping...`
- );
- return existingCustomer;
- }
- }
+ if (existingCustomers.length > 0) {
+ for (const existingCustomer of existingCustomers) {
+ if (existingCustomer.id === null) {
+ logger.info(
+ `Create customer by email: ${newCus.email} already exists, skipping...`,
+ );
+ return existingCustomer;
+ }
+ }
- throw new RecaseError({
- message: `Email ${newCus.email} already exists`,
- code: ErrCode.DuplicateCustomerId,
- statusCode: StatusCodes.CONFLICT,
- });
- }
+ throw new RecaseError({
+ message: `Email ${newCus.email} already exists`,
+ code: ErrCode.DuplicateCustomerId,
+ statusCode: StatusCodes.CONFLICT,
+ });
+ }
- const createdCustomer = await createNewCustomer({
- req,
- customer: newCus,
- createDefaultProducts,
- });
+ const createdCustomer = await createNewCustomer({
+ req,
+ customer: newCus,
+ createDefaultProducts,
+ });
- return createdCustomer;
+ return createdCustomer;
};
// CAN ALSO USE DURING MIGRATION...
export const handleCreateCustomerWithId = async ({
- req,
- newCus,
- createDefaultProducts = true,
+ req,
+ newCus,
+ createDefaultProducts = true,
}: {
- req: ExtendedRequest;
- newCus: CreateCustomer;
- createDefaultProducts?: boolean;
+ req: ExtendedRequest;
+ newCus: CreateCustomer;
+ createDefaultProducts?: boolean;
}) => {
- const { db, org, env, logger } = req;
+ const { db, org, env, logger } = req;
- // 1. Get by ID
- let existingCustomer = await CusService.get({
- db,
- idOrInternalId: newCus.id!,
- orgId: org.id,
- env,
- });
+ // 1. Get by ID
+ let existingCustomer = await CusService.get({
+ db,
+ idOrInternalId: newCus.id!,
+ orgId: org.id,
+ env,
+ });
- if (existingCustomer) {
- logger.info(
- `Customer already exists, skipping creation: ${existingCustomer.id}`
- );
- return existingCustomer;
- }
+ if (existingCustomer) {
+ logger.info(
+ `Customer already exists, skipping creation: ${existingCustomer.id}`,
+ );
+ return existingCustomer;
+ }
- // 2. Check if email exists
- if (notNullish(newCus.email) && newCus.email !== "") {
- let cusWithEmail = await CusService.getByEmail({
- db,
- email: newCus.email!,
- orgId: org.id,
- env,
- });
+ // 2. Check if email exists
+ if (notNullish(newCus.email) && newCus.email !== "") {
+ let cusWithEmail = await CusService.getByEmail({
+ db,
+ email: newCus.email!,
+ orgId: org.id,
+ env,
+ });
- if (cusWithEmail.length === 1 && cusWithEmail[0].id === null) {
- logger.info(
- `POST /customers, email ${newCus.email} and ID null found, updating ID to ${newCus.id} (org: ${org.slug})`
- );
+ if (cusWithEmail.length === 1 && cusWithEmail[0].id === null) {
+ logger.info(
+ `POST /customers, email ${newCus.email} and ID null found, updating ID to ${newCus.id} (org: ${org.slug})`,
+ );
- let updatedCustomer = await CusService.update({
- db,
- internalCusId: cusWithEmail[0].internal_id,
- update: {
- id: newCus.id!,
- name: newCus.name,
- fingerprint: newCus.fingerprint,
- },
- });
+ let updatedCustomer = await CusService.update({
+ db,
+ internalCusId: cusWithEmail[0].internal_id,
+ update: {
+ id: newCus.id!,
+ name: newCus.name,
+ fingerprint: newCus.fingerprint,
+ },
+ });
- return updatedCustomer;
- }
- }
+ return updatedCustomer;
+ }
+ }
- // 2. Handle email step...
- return await createNewCustomer({
- req,
- customer: newCus,
- createDefaultProducts,
- });
+ // 2. Handle email step...
+ return await createNewCustomer({
+ req,
+ customer: newCus,
+ createDefaultProducts,
+ });
};
export const handleCreateCustomer = async ({
- req,
- cusData,
- createDefaultProducts = true,
+ req,
+ cusData,
+ createDefaultProducts = true,
}: {
- req: ExtendedRequest;
- cusData: CreateCustomer;
- createDefaultProducts?: boolean;
+ req: ExtendedRequest;
+ cusData: CreateCustomer;
+ createDefaultProducts?: boolean;
}) => {
- const newCus = CreateCustomerSchema.parse(cusData);
+ const newCus = CreateCustomerSchema.parse(cusData);
- // 1. If no ID and email is not NULL
- let createdCustomer;
+ // 1. If no ID and email is not NULL
+ let createdCustomer;
- if (newCus.id === null) {
- createdCustomer = await handleIdIsNull({
- req,
- newCus,
- createDefaultProducts,
- });
- } else {
- createdCustomer = await handleCreateCustomerWithId({
- req,
- newCus,
- createDefaultProducts,
- });
- }
+ if (newCus.id === null) {
+ createdCustomer = await handleIdIsNull({
+ req,
+ newCus,
+ createDefaultProducts,
+ });
+ } else {
+ createdCustomer = await handleCreateCustomerWithId({
+ req,
+ newCus,
+ createDefaultProducts,
+ });
+ }
- return createdCustomer;
+ return createdCustomer;
};
diff --git a/server/src/internal/customers/handlers/handleCusProductExpired.ts b/server/src/internal/customers/handlers/handleCusProductExpired.ts
index 684fe7a2a..16579ba4f 100644
--- a/server/src/internal/customers/handlers/handleCusProductExpired.ts
+++ b/server/src/internal/customers/handlers/handleCusProductExpired.ts
@@ -2,216 +2,216 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
- ACTIVE_STATUSES,
- CusProductService,
+ ACTIVE_STATUSES,
+ CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import {
- cancelCusProductSubscriptions,
- expireAndActivate,
- fullCusProductToProduct,
+ cancelCusProductSubscriptions,
+ expireAndActivate,
+ fullCusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- ErrCode,
- CusProductStatus,
- FullCusProduct,
- Organization,
- AppEnv,
- FullCustomer,
+ ErrCode,
+ CusProductStatus,
+ FullCusProduct,
+ Organization,
+ AppEnv,
+ FullCustomer,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { CusService } from "../CusService.js";
import { cusProductToPrices } from "@autumn/shared";
export const expireCusProduct = async ({
- req,
- cusProduct, // cus product to expire
- fullCus,
- expireImmediately = true,
- prorate,
+ req,
+ cusProduct, // cus product to expire
+ fullCus,
+ expireImmediately = true,
+ prorate,
}: {
- req: ExtendedRequest;
- cusProduct: FullCusProduct;
- fullCus: FullCustomer;
- expireImmediately: boolean;
- prorate: boolean;
+ req: ExtendedRequest;
+ cusProduct: FullCusProduct;
+ fullCus: FullCustomer;
+ expireImmediately: boolean;
+ prorate: boolean;
}) => {
- const { db, org, env, logger } = req;
- logger.info("--------------------------------");
- logger.info(
- `🔔 Expiring cutomer product (${
- expireImmediately ? "immediately" : "end of cycle"
- })`
- );
- logger.info(
- `Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`
- );
- logger.info(
- `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`
- );
+ const { db, org, env, logger } = req;
+ logger.info("--------------------------------");
+ logger.info(
+ `🔔 Expiring cutomer product (${
+ expireImmediately ? "immediately" : "end of cycle"
+ })`,
+ );
+ logger.info(
+ `Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`,
+ );
+ logger.info(
+ `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`,
+ );
- // if (cusProduct.status == CusProductStatus.Scheduled) {
- // await CusProductService.delete({
- // db,
- // cusProductId: cusProduct.id,
- // });
- // return;
- // }
+ // if (cusProduct.status == CusProductStatus.Scheduled) {
+ // await CusProductService.delete({
+ // db,
+ // cusProductId: cusProduct.id,
+ // });
+ // return;
+ // }
- // 1. If main product, can't expire if there's scheduled product
- let isMain = !cusProduct.product.is_add_on;
- let { curScheduledProduct: futureProduct } = getExistingCusProducts({
- product: cusProduct.product,
- cusProducts: fullCus.customer_products,
- internalEntityId: cusProduct.internal_entity_id,
- });
+ // 1. If main product, can't expire if there's scheduled product
+ let isMain = !cusProduct.product.is_add_on;
+ let { curScheduledProduct: futureProduct } = getExistingCusProducts({
+ product: cusProduct.product,
+ cusProducts: fullCus.customer_products,
+ internalEntityId: cusProduct.internal_entity_id,
+ });
- if (isMain) {
- if (
- cusProduct.canceled_at &&
- ACTIVE_STATUSES.includes(cusProduct.status) &&
- !expireImmediately
- ) {
- throw new RecaseError({
- message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isMain) {
+ if (
+ cusProduct.canceled_at &&
+ ACTIVE_STATUSES.includes(cusProduct.status) &&
+ !expireImmediately
+ ) {
+ throw new RecaseError({
+ message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (
- futureProduct &&
- !isFreeProduct(cusProductToPrices({ cusProduct: futureProduct }))
- ) {
- throw new RecaseError({
- message: `Please delete scheduled product ${futureProduct.product.name} first`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ if (
+ futureProduct &&
+ !isFreeProduct(cusProductToPrices({ cusProduct: futureProduct }))
+ ) {
+ throw new RecaseError({
+ message: `Please delete scheduled product ${futureProduct.product.name} first`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- // 2. If expire at cycle end, just cancel subscriptions
- if (!expireImmediately) {
- // 1. Check if already canceled
- await cancelCusProductSubscriptions({
- cusProduct,
- org,
- env,
- expireImmediately,
- logger,
- });
+ // 2. If expire at cycle end, just cancel subscriptions
+ if (!expireImmediately) {
+ // 1. Check if already canceled
+ await cancelCusProductSubscriptions({
+ cusProduct,
+ org,
+ env,
+ expireImmediately,
+ logger,
+ });
- if (isOneOff(cusProduct.customer_prices.map((p) => p.price))) {
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: { status: CusProductStatus.Expired },
- });
- } else {
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: { canceled_at: Date.now() },
- });
- }
+ if (isOneOff(cusProduct.customer_prices.map((p) => p.price))) {
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: { status: CusProductStatus.Expired },
+ });
+ } else {
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: { canceled_at: Date.now() },
+ });
+ }
- return;
- }
+ return;
+ }
- if (cusProduct.product.is_add_on) {
- await cancelCusProductSubscriptions({
- cusProduct,
- org,
- env,
- logger,
- });
+ if (cusProduct.product.is_add_on) {
+ await cancelCusProductSubscriptions({
+ cusProduct,
+ org,
+ env,
+ logger,
+ });
- await CusProductService.update({
- db,
- cusProductId: cusProduct.id,
- updates: {
- status: CusProductStatus.Expired,
- ended_at: Date.now(),
- },
- });
+ await CusProductService.update({
+ db,
+ cusProductId: cusProduct.id,
+ updates: {
+ status: CusProductStatus.Expired,
+ ended_at: Date.now(),
+ },
+ });
- return;
- }
+ return;
+ }
- // Remove scheduled products first...
- if (futureProduct) {
- await CusProductService.delete({
- db,
- cusProductId: futureProduct.id,
- });
- }
+ // Remove scheduled products first...
+ if (futureProduct) {
+ await CusProductService.delete({
+ db,
+ cusProductId: futureProduct.id,
+ });
+ }
- logger.info(`Expiring current product: ${cusProduct.product.name}`);
- await expireAndActivate({
- req,
- cusProduct,
- fullCus,
- });
+ logger.info(`Expiring current product: ${cusProduct.product.name}`);
+ await expireAndActivate({
+ req,
+ cusProduct,
+ fullCus,
+ });
- logger.info(`Cancelling stripe subscriptions`);
- await cancelCusProductSubscriptions({
- cusProduct,
- org,
- env,
- logger,
- prorate,
- });
+ logger.info(`Cancelling stripe subscriptions`);
+ await cancelCusProductSubscriptions({
+ cusProduct,
+ org,
+ env,
+ logger,
+ prorate,
+ });
- return;
+ return;
};
export const handleCusProductExpired = async (req: any, res: any) => {
- try {
- const { db } = req;
+ try {
+ const { db } = req;
- const customerProductId = req.params.customer_product_id;
- let cusProduct = await CusProductService.get({
- db,
- id: customerProductId,
- orgId: req.orgId,
- env: req.env,
- withCustomer: true,
- });
+ const customerProductId = req.params.customer_product_id;
+ let cusProduct = await CusProductService.get({
+ db,
+ id: customerProductId,
+ orgId: req.orgId,
+ env: req.env,
+ withCustomer: true,
+ });
- if (!cusProduct) {
- throw new RecaseError({
- message: `Cus product not found: ${customerProductId}`,
- code: ErrCode.CusProductNotFound,
- statusCode: 404,
- });
- }
+ if (!cusProduct) {
+ throw new RecaseError({
+ message: `Cus product not found: ${customerProductId}`,
+ code: ErrCode.CusProductNotFound,
+ statusCode: 404,
+ });
+ }
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId:
- cusProduct.customer!.id || cusProduct.customer!.internal_id,
- orgId: req.orgId,
- env: req.env,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId:
+ cusProduct.customer!.id || cusProduct.customer!.internal_id,
+ orgId: req.orgId,
+ env: req.env,
+ });
- await expireCusProduct({
- req,
- cusProduct,
- fullCus,
- expireImmediately: true,
- prorate: true,
- });
+ await expireCusProduct({
+ req,
+ cusProduct,
+ fullCus,
+ expireImmediately: true,
+ prorate: true,
+ });
- res.status(200).json({ message: "Product expired" });
- } catch (error) {
- handleRequestError({
- req,
- error,
- res,
- action: "update customer product",
- });
- }
+ res.status(200).json({ message: "Product expired" });
+ } catch (error) {
+ handleRequestError({
+ req,
+ error,
+ res,
+ action: "update customer product",
+ });
+ }
};
diff --git a/server/src/internal/customers/handlers/handleDeleteCustomer.ts b/server/src/internal/customers/handlers/handleDeleteCustomer.ts
index 7643ac83a..12ff519a3 100644
--- a/server/src/internal/customers/handlers/handleDeleteCustomer.ts
+++ b/server/src/internal/customers/handlers/handleDeleteCustomer.ts
@@ -9,89 +9,89 @@ import { AppEnv, ErrCode, Organization } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const deleteCusById = async ({
- db,
- org,
- customerId,
- env,
- logger,
- deleteInStripe = false,
+ db,
+ org,
+ customerId,
+ env,
+ logger,
+ deleteInStripe = false,
}: {
- db: DrizzleCli;
- org: Organization;
- customerId: string;
- env: AppEnv;
- logger: any;
- deleteInStripe?: boolean;
+ db: DrizzleCli;
+ org: Organization;
+ customerId: string;
+ env: AppEnv;
+ logger: any;
+ deleteInStripe?: boolean;
}) => {
- const orgId = org.id;
+ const orgId = org.id;
- const customer = await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId,
- env,
- });
+ const customer = await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ });
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let response = {
- customer,
- success: true,
- };
+ let response = {
+ customer,
+ success: true,
+ };
- try {
- if (customer.processor?.id && deleteInStripe) {
- await deleteStripeCustomer({
- org,
- env: env,
- stripeId: customer.processor.id,
- });
- }
- } catch (error: any) {
- console.log(
- `Couldn't delete ${chalk.yellow("stripe customer")} ${
- customer.processor.id
- }`,
- error?.message || error
- );
+ try {
+ if (customer.processor?.id && deleteInStripe) {
+ await deleteStripeCustomer({
+ org,
+ env: env,
+ stripeId: customer.processor.id,
+ });
+ }
+ } catch (error: any) {
+ console.log(
+ `Couldn't delete ${chalk.yellow("stripe customer")} ${
+ customer.processor.id
+ }`,
+ error?.message || error,
+ );
- response.success = false;
- }
+ response.success = false;
+ }
- await CusService.deleteByInternalId({
- db,
- internalId: customer.internal_id,
- orgId,
- env: env,
- });
+ await CusService.deleteByInternalId({
+ db,
+ internalId: customer.internal_id,
+ orgId,
+ env: env,
+ });
- return response;
+ return response;
};
export const handleDeleteCustomer = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "delete customer",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { env, logtail: logger, db, org } = req;
- const { delete_in_stripe } = req.query;
+ routeHandler({
+ req,
+ res,
+ action: "delete customer",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { env, logtail: logger, db, org } = req;
+ const { delete_in_stripe } = req.query;
- const data = await deleteCusById({
- db,
- org,
- customerId: req.params.customer_id,
- env,
- logger,
- deleteInStripe: delete_in_stripe === "true",
- });
+ const data = await deleteCusById({
+ db,
+ org,
+ customerId: req.params.customer_id,
+ env,
+ logger,
+ deleteInStripe: delete_in_stripe === "true",
+ });
- res.status(200).json(data);
- },
- });
+ res.status(200).json(data);
+ },
+ });
diff --git a/server/src/internal/customers/handlers/handleGetCustomer.ts b/server/src/internal/customers/handlers/handleGetCustomer.ts
index 9b5314c1c..748fb7583 100644
--- a/server/src/internal/customers/handlers/handleGetCustomer.ts
+++ b/server/src/internal/customers/handlers/handleGetCustomer.ts
@@ -7,62 +7,62 @@ import { orgToVersion } from "@/utils/versionUtils.js";
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
export const handleGetCustomer = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "get customer",
- handler: async () => {
- let customerId = req.params.customer_id;
- let { env, db, logtail: logger, org, features } = req;
- let { expand } = req.query;
+ routeHandler({
+ req,
+ res,
+ action: "get customer",
+ handler: async () => {
+ let customerId = req.params.customer_id;
+ let { env, db, logtail: logger, org, features } = req;
+ let { expand } = req.query;
- let expandArray = parseCusExpand(expand);
+ let expandArray = parseCusExpand(expand);
- let apiVersion = orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- });
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ });
- let getInvoices = apiVersion < APIVersion.v1_1;
- if (getInvoices) expandArray.push(CusExpand.Invoices);
+ let getInvoices = apiVersion < APIVersion.v1_1;
+ if (getInvoices) expandArray.push(CusExpand.Invoices);
- logger.info(`getting customer ${customerId} for org ${org.slug}`);
- const startTime = Date.now();
- const customer = await getCusWithCache({
- db,
- idOrInternalId: customerId,
- org,
- env,
- expand: expandArray,
- allowNotFound: true,
- logger,
- });
+ logger.info(`getting customer ${customerId} for org ${org.slug}`);
+ const startTime = Date.now();
+ const customer = await getCusWithCache({
+ db,
+ idOrInternalId: customerId,
+ org,
+ env,
+ expand: expandArray,
+ allowNotFound: true,
+ logger,
+ });
- logger.info(`get customer took ${Date.now() - startTime}ms`);
+ logger.info(`get customer took ${Date.now() - startTime}ms`);
- if (!customer) {
- req.logtail.warn(
- `GET /customers/${customerId}: not found | Org: ${org.slug}`
- );
- res.status(StatusCodes.NOT_FOUND).json({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- });
- return;
- }
+ if (!customer) {
+ req.logtail.warn(
+ `GET /customers/${customerId}: not found | Org: ${org.slug}`,
+ );
+ res.status(StatusCodes.NOT_FOUND).json({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ });
+ return;
+ }
- let cusData = await getCustomerDetails({
- db,
- customer,
- org,
- env: req.env,
- logger: req.logtail,
- cusProducts: customer.customer_products,
- expand: expandArray,
- features,
- reqApiVersion: req.apiVersion,
- });
+ let cusData = await getCustomerDetails({
+ db,
+ customer,
+ org,
+ env: req.env,
+ logger: req.logtail,
+ cusProducts: customer.customer_products,
+ expand: expandArray,
+ features,
+ reqApiVersion: req.apiVersion,
+ });
- res.status(200).json(cusData);
- },
- });
+ res.status(200).json(cusData);
+ },
+ });
diff --git a/server/src/internal/customers/handlers/handlePostCustomer.ts b/server/src/internal/customers/handlers/handlePostCustomer.ts
index bfbe89c1c..1d2359c43 100644
--- a/server/src/internal/customers/handlers/handlePostCustomer.ts
+++ b/server/src/internal/customers/handlers/handlePostCustomer.ts
@@ -8,77 +8,77 @@ import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js";
import { parseCusExpand } from "../cusUtils/cusUtils.js";
export const handlePostCustomerRequest = async (req: any, res: any) => {
- const logger = req.logtail;
- try {
- const data = req.body;
+ const logger = req.logtail;
+ try {
+ const data = req.body;
- const expand = parseCusExpand(req.query.expand);
- const { db, org, features } = req;
+ const expand = parseCusExpand(req.query.expand);
+ const { db, org, features } = req;
- if (data.customer_id) {
- throw new RecaseError({
- message:
- "use the `id` field instead of `customer_id` to specify the new customer's ID",
- code: ErrCode.InvalidInputs,
- });
- }
+ if (data.customer_id) {
+ throw new RecaseError({
+ message:
+ "use the `id` field instead of `customer_id` to specify the new customer's ID",
+ code: ErrCode.InvalidInputs,
+ });
+ }
- if (data.id === undefined) {
- throw new RecaseError({
- message: "`id` field must be either a string or null",
- code: ErrCode.InvalidInputs,
- });
- }
+ if (data.id === undefined) {
+ throw new RecaseError({
+ message: "`id` field must be either a string or null",
+ code: ErrCode.InvalidInputs,
+ });
+ }
- if (!data.id && !data.email) {
- throw new RecaseError({
- message: "ID or email is required",
- code: ErrCode.InvalidRequest,
- });
- }
+ if (!data.id && !data.email) {
+ throw new RecaseError({
+ message: "ID or email is required",
+ code: ErrCode.InvalidRequest,
+ });
+ }
- let customer = await getOrCreateCustomer({
- req,
- customerId: data.id,
- customerData: data,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- expand,
+ let customer = await getOrCreateCustomer({
+ req,
+ customerId: data.id,
+ customerData: data,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ expand,
- entityId: data.entity_id,
- entityData: data.entity_data,
- withCache: true,
- });
+ entityId: data.entity_id,
+ entityData: data.entity_data,
+ withCache: true,
+ });
- let cusDetails = await getCustomerDetails({
- db,
- customer,
- org,
- env: req.env,
- params: req.query,
- logger,
- cusProducts: customer.customer_products,
- expand,
- features,
- reqApiVersion: req.apiVersion,
- });
+ let cusDetails = await getCustomerDetails({
+ db,
+ customer,
+ org,
+ env: req.env,
+ params: req.query,
+ logger,
+ cusProducts: customer.customer_products,
+ expand,
+ features,
+ reqApiVersion: req.apiVersion,
+ });
- res.status(200).json(cusDetails);
- } catch (error: any) {
- if (
- error instanceof RecaseError &&
- error.code === ErrCode.DuplicateCustomerId
- ) {
- logger.warn(`POST /customers: ${error.message} (org: ${req.org?.slug})`);
- res.status(error.statusCode).json({
- message: error.message,
- code: error.code,
- });
- return;
- }
- handleRequestError({ req, error, res, action: "create customer" });
- }
+ res.status(200).json(cusDetails);
+ } catch (error: any) {
+ if (
+ error instanceof RecaseError &&
+ error.code === ErrCode.DuplicateCustomerId
+ ) {
+ logger.warn(`POST /customers: ${error.message} (org: ${req.org?.slug})`);
+ res.status(error.statusCode).json({
+ message: error.message,
+ code: error.code,
+ });
+ return;
+ }
+ handleRequestError({ req, error, res, action: "create customer" });
+ }
};
diff --git a/server/src/internal/customers/handlers/handleTransferProduct.ts b/server/src/internal/customers/handlers/handleTransferProduct.ts
index 47d8fdb45..5f2093fc2 100644
--- a/server/src/internal/customers/handlers/handleTransferProduct.ts
+++ b/server/src/internal/customers/handlers/handleTransferProduct.ts
@@ -11,142 +11,142 @@ import { ProductService } from "@/internal/products/ProductService.js";
import { deleteCusCache } from "../cusCache/updateCachedCus.js";
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
const TransferProductSchema = z.object({
- from_entity_id: z.string().nullish(),
- to_entity_id: z.string(),
- product_id: z.string(),
+ from_entity_id: z.string().nullish(),
+ to_entity_id: z.string(),
+ product_id: z.string(),
});
export const handleTransferProduct = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "transfer product",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { customer_id } = req.params;
- const { from_entity_id, to_entity_id, product_id } =
- TransferProductSchema.parse(req.body);
+ routeHandler({
+ req,
+ res,
+ action: "transfer product",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { customer_id } = req.params;
+ const { from_entity_id, to_entity_id, product_id } =
+ TransferProductSchema.parse(req.body);
- const customer = await CusService.getFull({
- idOrInternalId: customer_id,
- orgId: req.orgId,
- env: req.env,
- db: req.db,
- withEntities: true,
- // entityId: from_entity_id,
- });
+ const customer = await CusService.getFull({
+ idOrInternalId: customer_id,
+ orgId: req.orgId,
+ env: req.env,
+ db: req.db,
+ withEntities: true,
+ // entityId: from_entity_id,
+ });
- const product = await ProductService.get({
- id: product_id,
- orgId: req.orgId,
- env: req.env,
- db: req.db,
- });
+ const product = await ProductService.get({
+ id: product_id,
+ orgId: req.orgId,
+ env: req.env,
+ db: req.db,
+ });
- if (!product) {
- throw new RecaseError({
- code: ErrCode.ProductNotFound,
- message: `Product ${product_id} not found`,
- statusCode: 404,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ code: ErrCode.ProductNotFound,
+ message: `Product ${product_id} not found`,
+ statusCode: 404,
+ });
+ }
- const fromEntity = customer.entities.find(
- (e: any) => e.id === from_entity_id
- );
+ const fromEntity = customer.entities.find(
+ (e: any) => e.id === from_entity_id,
+ );
- const toEntity = customer.entities.find(
- (e: any) => e.id === to_entity_id
- );
+ const toEntity = customer.entities.find(
+ (e: any) => e.id === to_entity_id,
+ );
- // if (!fromEntity) {
- // throw new RecaseError({
- // code: ErrCode.EntityNotFound,
- // message: `Entity ${from_entity_id} not found`,
- // statusCode: 404,
- // });
- // }
+ // if (!fromEntity) {
+ // throw new RecaseError({
+ // code: ErrCode.EntityNotFound,
+ // message: `Entity ${from_entity_id} not found`,
+ // statusCode: 404,
+ // });
+ // }
- if (!toEntity) {
- throw new RecaseError({
- code: ErrCode.EntityNotFound,
- message: `Entity ${to_entity_id} not found`,
- statusCode: 404,
- });
- }
+ if (!toEntity) {
+ throw new RecaseError({
+ code: ErrCode.EntityNotFound,
+ message: `Entity ${to_entity_id} not found`,
+ statusCode: 404,
+ });
+ }
- const cusProduct = customer.customer_products.find(
- (cp: any) =>
- (fromEntity
- ? cp.internal_entity_id === fromEntity.internal_id
- : nullish(cp.internal_entity_id)) && cp.product.id === product_id
- );
+ const cusProduct = customer.customer_products.find(
+ (cp: any) =>
+ (fromEntity
+ ? cp.internal_entity_id === fromEntity.internal_id
+ : nullish(cp.internal_entity_id)) && cp.product.id === product_id,
+ );
- const toCusProduct = customer.customer_products.find(
- (cp: any) =>
- cp.internal_entity_id === toEntity.internal_id &&
- cp.product.group === product.group
- );
+ const toCusProduct = customer.customer_products.find(
+ (cp: any) =>
+ cp.internal_entity_id === toEntity.internal_id &&
+ cp.product.group === product.group,
+ );
- if (toCusProduct) {
- throw new RecaseError({
- code: ErrCode.ProductAlreadyExists,
- message: `Entity ${toEntity.id} already has product ${toCusProduct.product.name}`,
- statusCode: 400,
- });
- }
+ if (toCusProduct) {
+ throw new RecaseError({
+ code: ErrCode.ProductAlreadyExists,
+ message: `Entity ${toEntity.id} already has product ${toCusProduct.product.name}`,
+ statusCode: 400,
+ });
+ }
- if (!cusProduct) {
- throw new RecaseError({
- code: ErrCode.CusProductNotFound,
- message: `Product ${product_id} not found for entity ${from_entity_id}`,
- statusCode: 404,
- });
- }
+ if (!cusProduct) {
+ throw new RecaseError({
+ code: ErrCode.CusProductNotFound,
+ message: `Product ${product_id} not found for entity ${from_entity_id}`,
+ statusCode: 404,
+ });
+ }
- // 1. If cus product has quantity > 1, only transfer 1...
- if (cusProduct.quantity > 1) {
- await handleDecreaseAndTransfer({
- req: req,
- fullCus: customer,
- cusProduct: cusProduct,
- toEntity: toEntity,
- });
- } else {
- await CusProductService.update({
- db: req.db,
- cusProductId: cusProduct.id,
- updates: {
- entity_id: toEntity.id,
- internal_entity_id: toEntity.internal_id,
- },
- });
+ // 1. If cus product has quantity > 1, only transfer 1...
+ if (cusProduct.quantity > 1) {
+ await handleDecreaseAndTransfer({
+ req: req,
+ fullCus: customer,
+ cusProduct: cusProduct,
+ toEntity: toEntity,
+ });
+ } else {
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: cusProduct.id,
+ updates: {
+ entity_id: toEntity.id,
+ internal_entity_id: toEntity.internal_id,
+ },
+ });
- await addProductsUpdatedWebhookTask({
- req,
- internalCustomerId: customer.internal_id,
- org: req.org,
- env: req.env,
- customerId: customer.id || customer.internal_id,
- scenario: AttachScenario.New,
- cusProduct: {
- ...cusProduct,
- entity_id: toEntity.id,
- internal_entity_id: toEntity.internal_id,
- },
- logger: req.logger,
- });
- }
+ await addProductsUpdatedWebhookTask({
+ req,
+ internalCustomerId: customer.internal_id,
+ org: req.org,
+ env: req.env,
+ customerId: customer.id || customer.internal_id,
+ scenario: AttachScenario.New,
+ cusProduct: {
+ ...cusProduct,
+ entity_id: toEntity.id,
+ internal_entity_id: toEntity.internal_id,
+ },
+ logger: req.logger,
+ });
+ }
- await deleteCusCache({
- db: req.db,
- customerId: customer.id || customer.internal_id,
- org: req.org,
- env: req.env,
- });
+ await deleteCusCache({
+ db: req.db,
+ customerId: customer.id || customer.internal_id,
+ org: req.org,
+ env: req.env,
+ });
- res.status(200).json({
- // message: "Product transferred successfully",
- success: true,
- });
- },
- });
+ res.status(200).json({
+ // message: "Product transferred successfully",
+ success: true,
+ });
+ },
+ });
diff --git a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts
index bbf726be4..3247bfc3a 100644
--- a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts
+++ b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts
@@ -1,10 +1,10 @@
import {
- AttachScenario,
- Entity,
- ErrCode,
- FeatureType,
- FullCusProduct,
- FullCustomer,
+ AttachScenario,
+ Entity,
+ ErrCode,
+ FeatureType,
+ FullCusProduct,
+ FullCustomer,
} from "@autumn/shared";
import { createFullCusProduct } from "../../add-product/createFullCusProduct.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
@@ -13,93 +13,93 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- getRelatedCusPrice,
- getResetBalance,
+ getRelatedCusPrice,
+ getResetBalance,
} from "../../cusProducts/cusEnts/cusEntUtils.js";
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
import { CusEntService } from "../../cusProducts/cusEnts/CusEntitlementService.js";
import { CusProductService } from "../../cusProducts/CusProductService.js";
export const handleDecreaseAndTransfer = async ({
- req,
- fullCus,
- cusProduct,
- toEntity,
+ req,
+ fullCus,
+ cusProduct,
+ toEntity,
}: {
- req: ExtendedRequest;
- fullCus: FullCustomer;
- cusProduct: FullCusProduct;
- toEntity: Entity;
+ req: ExtendedRequest;
+ fullCus: FullCustomer;
+ cusProduct: FullCusProduct;
+ toEntity: Entity;
}) => {
- // 1. Create new cus product for entity...
- const { org, env } = req;
- const stripeCli = createStripeCli({ org, env });
- const product = cusProductToProduct({ cusProduct });
+ // 1. Create new cus product for entity...
+ const { org, env } = req;
+ const stripeCli = createStripeCli({ org, env });
+ const product = cusProductToProduct({ cusProduct });
- // Decrease quantity of cus product...
+ // Decrease quantity of cus product...
- const batchDecrement = [];
- for (const cusEnt of cusProduct.customer_entitlements) {
- const feature = cusEnt.entitlement.feature;
- if (feature.type == FeatureType.Boolean) continue;
+ const batchDecrement = [];
+ for (const cusEnt of cusProduct.customer_entitlements) {
+ const feature = cusEnt.entitlement.feature;
+ if (feature.type == FeatureType.Boolean) continue;
- const cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
+ const cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
- const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
- const resetBalance = getResetBalance({
- entitlement: cusEnt.entitlement,
- options: options,
- relatedPrice: cusPrice?.price,
- });
+ const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
+ const resetBalance = getResetBalance({
+ entitlement: cusEnt.entitlement,
+ options: options,
+ relatedPrice: cusPrice?.price,
+ });
- batchDecrement.push(
- CusEntService.decrement({
- db: req.db,
- id: cusEnt.id,
- amount: resetBalance,
- })
- );
- }
+ batchDecrement.push(
+ CusEntService.decrement({
+ db: req.db,
+ id: cusEnt.id,
+ amount: resetBalance,
+ }),
+ );
+ }
- await Promise.all(batchDecrement);
+ await Promise.all(batchDecrement);
- await CusProductService.update({
- db: req.db,
- cusProductId: cusProduct.id,
- updates: {
- quantity: cusProduct.quantity - 1,
- },
- });
+ await CusProductService.update({
+ db: req.db,
+ cusProductId: cusProduct.id,
+ updates: {
+ quantity: cusProduct.quantity - 1,
+ },
+ });
- const newCusProduct = await createFullCusProduct({
- db: req.db,
- logger: req.logger,
- trialEndsAt: cusProduct.trial_ends_at || undefined,
- subscriptionIds: cusProduct.subscription_ids || [],
- attachParams: attachToInsertParams(
- {
- req,
- customer: fullCus,
- products: [product],
- prices: product.prices,
- entitlements: product.entitlements,
- org: req.org,
- stripeCli: stripeCli,
- paymentMethod: null,
- freeTrial: cusProduct.free_trial || null,
- optionsList: cusProduct.options,
- scenario: AttachScenario.New,
- // scenario: AttachScenario.New,
+ const newCusProduct = await createFullCusProduct({
+ db: req.db,
+ logger: req.logger,
+ trialEndsAt: cusProduct.trial_ends_at || undefined,
+ subscriptionIds: cusProduct.subscription_ids || [],
+ attachParams: attachToInsertParams(
+ {
+ req,
+ customer: fullCus,
+ products: [product],
+ prices: product.prices,
+ entitlements: product.entitlements,
+ org: req.org,
+ stripeCli: stripeCli,
+ paymentMethod: null,
+ freeTrial: cusProduct.free_trial || null,
+ optionsList: cusProduct.options,
+ scenario: AttachScenario.New,
+ // scenario: AttachScenario.New,
- cusProducts: fullCus.customer_products,
- replaceables: [],
- entities: fullCus.entities,
- features: req.features,
- internalEntityId: toEntity.internal_id,
- entityId: toEntity.id,
- },
- product
- ),
- scenario: AttachScenario.New,
- });
+ cusProducts: fullCus.customer_products,
+ replaceables: [],
+ entities: fullCus.entities,
+ features: req.features,
+ internalEntityId: toEntity.internal_id,
+ entityId: toEntity.id,
+ },
+ product,
+ ),
+ scenario: AttachScenario.New,
+ });
};
diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts
index b12b937f7..182bd80f7 100644
--- a/server/src/internal/customers/handlers/handleUpdateBalances.ts
+++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts
@@ -8,8 +8,8 @@ import { StatusCodes } from "http-status-codes";
import { getCusEntsInFeatures } from "../cusUtils/cusUtils.js";
import { Decimal } from "decimal.js";
import {
- deductAllowanceFromCusEnt,
- deductFromUsageBasedCusEnt,
+ deductAllowanceFromCusEnt,
+ deductFromUsageBasedCusEnt,
} from "@/trigger/updateBalanceTask.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -19,281 +19,283 @@ import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntit
import { notNullish } from "@/utils/genUtils.js";
const getCusFeaturesAndOrg = async (req: any, customerId: string) => {
- // 1. Get customer
- const [customer, features, org] = await Promise.all([
- CusService.getFull({
- db: req.db,
- idOrInternalId: customerId,
- orgId: req.orgId,
- env: req.env,
- entityId: req.params.entity_id,
- }),
- FeatureService.getFromReq(req),
- OrgService.getFromReq(req),
- ]);
+ // 1. Get customer
+ const [customer, features, org] = await Promise.all([
+ CusService.getFull({
+ db: req.db,
+ idOrInternalId: customerId,
+ orgId: req.orgId,
+ env: req.env,
+ entityId: req.params.entity_id,
+ }),
+ FeatureService.getFromReq(req),
+ OrgService.getFromReq(req),
+ ]);
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- return { customer, features, org };
+ return { customer, features, org };
};
export const handleUpdateBalances = async (req: any, res: any) => {
- try {
- const logger = req.logtail;
- const cusId = req.params.customer_id;
- const { env, db, features } = req;
- const { balances } = req.body;
+ try {
+ const logger = req.logtail;
+ const cusId = req.params.customer_id;
+ const { env, db, features } = req;
+ const { balances } = req.body;
- if (!Array.isArray(balances)) {
- throw new RecaseError({
- message: "Balances must be an array",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!Array.isArray(balances)) {
+ throw new RecaseError({
+ message: "Balances must be an array",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const { customer, org } = await getCusFeaturesAndOrg(req, cusId);
+ const { customer, org } = await getCusFeaturesAndOrg(req, cusId);
- const featuresToUpdate = features.filter((f: any) =>
- balances.map((b: any) => b.feature_id).includes(f.id)
- );
+ const featuresToUpdate = features.filter((f: any) =>
+ balances.map((b: any) => b.feature_id).includes(f.id),
+ );
- if (featuresToUpdate.length === 0) {
- throw new RecaseError({
- message: "No valid features found to update",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (featuresToUpdate.length === 0) {
+ throw new RecaseError({
+ message: "No valid features found to update",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const { cusEnts, cusPrices } = await getCusEntsInFeatures({
- customer,
- internalFeatureIds: featuresToUpdate.map((f: any) => f.internal_id!),
- logger: req.logtail,
- });
+ const { cusEnts, cusPrices } = await getCusEntsInFeatures({
+ customer,
+ internalFeatureIds: featuresToUpdate.map((f: any) => f.internal_id!),
+ logger: req.logtail,
+ });
- logger.info("--------------------------------");
- logger.info(
- `REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${org.slug}`
- );
- logger.info(
- `Features to update: ${balances.map(
- (b: any) => `${b.feature_id} - ${b.unlimited ? "unlimited" : b.balance}`
- )}`
- );
+ logger.info("--------------------------------");
+ logger.info(
+ `REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${org.slug}`,
+ );
+ logger.info(
+ `Features to update: ${balances.map(
+ (b: any) =>
+ `${b.feature_id} - ${b.unlimited ? "unlimited" : b.balance}`,
+ )}`,
+ );
- // Get deductions for each feature
- const featureDeductions = [];
- for (const balance of balances) {
- if (!balance.feature_id) {
- throw new RecaseError({
- message: "Feature ID is required",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // Get deductions for each feature
+ const featureDeductions = [];
+ for (const balance of balances) {
+ if (!balance.feature_id) {
+ throw new RecaseError({
+ message: "Feature ID is required",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (typeof balance.balance !== "number" && balance.unlimited !== true) {
- throw new RecaseError({
- message: "Balance must be a number",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (typeof balance.balance !== "number" && balance.unlimited !== true) {
+ throw new RecaseError({
+ message: "Balance must be a number",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- const feature = featuresToUpdate.find(
- (f: any) => f.id === balance.feature_id
- );
+ const feature = featuresToUpdate.find(
+ (f: any) => f.id === balance.feature_id,
+ );
- if (balance.unlimited === true) {
- featureDeductions.push({
- feature,
- unlimited: true,
- toDeduct: 0,
- });
- continue;
- }
+ if (balance.unlimited === true) {
+ featureDeductions.push({
+ feature,
+ unlimited: true,
+ toDeduct: 0,
+ });
+ continue;
+ }
- let { unlimited } = getUnlimitedAndUsageAllowed({
- cusEnts,
- internalFeatureId: feature!.internal_id!,
- });
+ let { unlimited } = getUnlimitedAndUsageAllowed({
+ cusEnts,
+ internalFeatureId: feature!.internal_id!,
+ });
- if (unlimited) {
- throw new RecaseError({
- message: `Can't set balance for unlimited feature: ${feature!.id}`,
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (unlimited) {
+ throw new RecaseError({
+ message: `Can't set balance for unlimited feature: ${feature!.id}`,
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Get deductions
- let newBalance = balance.balance;
- let curBalance = new Decimal(0);
- let properties = structuredClone(balance);
- delete properties.feature_id;
- delete properties.balance;
+ // Get deductions
+ let newBalance = balance.balance;
+ let curBalance = new Decimal(0);
+ let properties = structuredClone(balance);
+ delete properties.feature_id;
+ delete properties.balance;
- for (const cusEnt of cusEnts) {
- let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
- let deductionIntCount = balance.interval_count || 1;
+ for (const cusEnt of cusEnts) {
+ let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
+ let deductionIntCount = balance.interval_count || 1;
- let intCountMatch = notNullish(balance.interval_count)
- ? cusEntIntCount === deductionIntCount
- : true;
+ let intCountMatch = notNullish(balance.interval_count)
+ ? cusEntIntCount === deductionIntCount
+ : true;
- let intMatch = notNullish(balance.interval)
- ? balance.interval === cusEnt.entitlement.interval
- : true;
+ let intMatch = notNullish(balance.interval)
+ ? balance.interval === cusEnt.entitlement.interval
+ : true;
- if (
- cusEnt.internal_feature_id !== feature!.internal_id! ||
- !intMatch ||
- !intCountMatch
- ) {
- continue;
- }
+ if (
+ cusEnt.internal_feature_id !== feature!.internal_id! ||
+ !intMatch ||
+ !intCountMatch
+ ) {
+ continue;
+ }
- let { balance: cusEntBalance } = getCusEntBalance({
- cusEnt,
- entityId: balance.entity_id,
- });
+ let { balance: cusEntBalance } = getCusEntBalance({
+ cusEnt,
+ entityId: balance.entity_id,
+ });
- curBalance = curBalance.add(new Decimal(cusEntBalance!));
- }
+ curBalance = curBalance.add(new Decimal(cusEntBalance!));
+ }
- let toDeduct = curBalance.sub(newBalance).toNumber();
+ let toDeduct = curBalance.sub(newBalance).toNumber();
- if (toDeduct == 0) {
- logger.info(`Skipping ${feature!.id} -- no change`);
- }
+ if (toDeduct == 0) {
+ logger.info(`Skipping ${feature!.id} -- no change`);
+ }
- featureDeductions.push({
- feature,
- toDeduct,
- properties,
- interval: balance.interval,
- intervalCount: balance.interval_count,
- });
- }
+ featureDeductions.push({
+ feature,
+ toDeduct,
+ properties,
+ interval: balance.interval,
+ intervalCount: balance.interval_count,
+ });
+ }
- const batchDeduct = [];
+ const batchDeduct = [];
- for (const featureDeduction of featureDeductions) {
- // 1. Deduct from allowance
- const performDeduction = async () => {
- let { toDeduct, feature, properties, interval } = featureDeduction;
+ for (const featureDeduction of featureDeductions) {
+ // 1. Deduct from allowance
+ const performDeduction = async () => {
+ let { toDeduct, feature, properties, interval } = featureDeduction;
- // Handle unlimited
- if (featureDeduction.unlimited) {
- // Get one active cusEnt and set unlimited to true
+ // Handle unlimited
+ if (featureDeduction.unlimited) {
+ // Get one active cusEnt and set unlimited to true
- const cusEnt = notNullish(interval)
- ? cusEnts.find((cusEnt) => {
- let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
- let deductionIntCount = featureDeduction.intervalCount || 1;
+ const cusEnt = notNullish(interval)
+ ? cusEnts.find((cusEnt) => {
+ let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
+ let deductionIntCount = featureDeduction.intervalCount || 1;
- return (
- cusEnt.internal_feature_id === feature!.internal_id! &&
- cusEnt.entitlement.interval === interval &&
- cusEntIntCount === deductionIntCount
- );
- })
- : cusEnts.find(
- (cusEnt) => cusEnt.internal_feature_id === feature!.internal_id!
- );
+ return (
+ cusEnt.internal_feature_id === feature!.internal_id! &&
+ cusEnt.entitlement.interval === interval &&
+ cusEntIntCount === deductionIntCount
+ );
+ })
+ : cusEnts.find(
+ (cusEnt) =>
+ cusEnt.internal_feature_id === feature!.internal_id!,
+ );
- if (!cusEnt) {
- logger.warn(
- `No active cus ent to set unlimited balance for feature: ${
- feature!.id
- }`
- );
- return;
- }
+ if (!cusEnt) {
+ logger.warn(
+ `No active cus ent to set unlimited balance for feature: ${
+ feature!.id
+ }`,
+ );
+ return;
+ }
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- unlimited: true,
- next_reset_at: null,
- },
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ unlimited: true,
+ next_reset_at: null,
+ },
+ });
- return;
- }
+ return;
+ }
- for (const cusEnt of cusEnts) {
- let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
- let deductionIntCount = featureDeduction.intervalCount || 1;
+ for (const cusEnt of cusEnts) {
+ let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
+ let deductionIntCount = featureDeduction.intervalCount || 1;
- let intCountMatch = notNullish(featureDeduction.intervalCount)
- ? cusEntIntCount === deductionIntCount
- : true;
+ let intCountMatch = notNullish(featureDeduction.intervalCount)
+ ? cusEntIntCount === deductionIntCount
+ : true;
- let intMatch = notNullish(featureDeduction.interval)
- ? featureDeduction.interval === cusEnt.entitlement.interval
- : true;
+ let intMatch = notNullish(featureDeduction.interval)
+ ? featureDeduction.interval === cusEnt.entitlement.interval
+ : true;
- if (
- cusEnt.internal_feature_id !==
- featureDeduction.feature!.internal_id! ||
- !intMatch ||
- !intCountMatch
- ) {
- continue;
- }
+ if (
+ cusEnt.internal_feature_id !==
+ featureDeduction.feature!.internal_id! ||
+ !intMatch ||
+ !intCountMatch
+ ) {
+ continue;
+ }
- toDeduct = await deductAllowanceFromCusEnt({
- toDeduct,
- deductParams: {
- db,
- feature: featureDeduction.feature!,
- env: req.env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties,
- },
- cusEnt,
- featureDeductions: [], // not important because not deducting credits
- willDeductCredits: false,
- });
- }
+ toDeduct = await deductAllowanceFromCusEnt({
+ toDeduct,
+ deductParams: {
+ db,
+ feature: featureDeduction.feature!,
+ env: req.env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties,
+ },
+ cusEnt,
+ featureDeductions: [], // not important because not deducting credits
+ willDeductCredits: false,
+ });
+ }
- if (toDeduct == 0) {
- return;
- }
+ if (toDeduct == 0) {
+ return;
+ }
- await deductFromUsageBasedCusEnt({
- toDeduct,
- cusEnts,
- deductParams: {
- db,
- feature: featureDeduction.feature!,
- env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties,
- },
- });
- };
- batchDeduct.push(performDeduction());
- }
- await Promise.all(batchDeduct);
+ await deductFromUsageBasedCusEnt({
+ toDeduct,
+ cusEnts,
+ deductParams: {
+ db,
+ feature: featureDeduction.feature!,
+ env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties,
+ },
+ });
+ };
+ batchDeduct.push(performDeduction());
+ }
+ await Promise.all(batchDeduct);
- logger.info(" ✅ Successfully updated balances");
+ logger.info(" ✅ Successfully updated balances");
- res.status(200).json({ success: true });
- } catch (error) {
- handleRequestError({ req, error, res, action: "update customer balances" });
- }
+ res.status(200).json({ success: true });
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "update customer balances" });
+ }
};
diff --git a/server/src/internal/customers/handlers/handleUpdateCustomer.ts b/server/src/internal/customers/handlers/handleUpdateCustomer.ts
index 4809a9623..2424c5390 100644
--- a/server/src/internal/customers/handlers/handleUpdateCustomer.ts
+++ b/server/src/internal/customers/handlers/handleUpdateCustomer.ts
@@ -12,138 +12,138 @@ import { ExtendedResponse } from "@/utils/models/Request.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
export const handleUpdateCustomer = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "POST/customers/:customer_id",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { orgId, env, db, org } = req;
- const customerId = req.params.customer_id;
- const [originalCustomer, features] = await Promise.all([
- CusService.get({
- db,
- idOrInternalId: customerId,
- orgId,
- env,
- }),
+ routeHandler({
+ req,
+ res,
+ action: "POST/customers/:customer_id",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { orgId, env, db, org } = req;
+ const customerId = req.params.customer_id;
+ const [originalCustomer, features] = await Promise.all([
+ CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ }),
- FeatureService.getFromReq(req),
- ]);
+ FeatureService.getFromReq(req),
+ ]);
- if (!originalCustomer) {
- throw new RecaseError({
- message: `Update customer: Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!originalCustomer) {
+ throw new RecaseError({
+ message: `Update customer: Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let newCusData: any = CreateCustomerSchema.parse(req.body);
+ let newCusData: any = CreateCustomerSchema.parse(req.body);
- if (req.body.id === null) {
- throw new RecaseError({
- message: `Update customer: Can't change customer ID to null`,
- code: ErrCode.InvalidUpdateCustomerParams,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (req.body.id === null) {
+ throw new RecaseError({
+ message: `Update customer: Can't change customer ID to null`,
+ code: ErrCode.InvalidUpdateCustomerParams,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (notNullish(newCusData.id) && originalCustomer.id !== newCusData.id) {
- // Fetch for existing customer
- const existingCustomer = await CusService.get({
- db: req.db,
- idOrInternalId: newCusData.id,
- orgId: req.orgId,
- env: req.env,
- });
+ if (notNullish(newCusData.id) && originalCustomer.id !== newCusData.id) {
+ // Fetch for existing customer
+ const existingCustomer = await CusService.get({
+ db: req.db,
+ idOrInternalId: newCusData.id,
+ orgId: req.orgId,
+ env: req.env,
+ });
- if (existingCustomer) {
- throw new RecaseError({
- message: `Update customer: Customer ${newCusData.id} already exists, can't change to this ID`,
- code: ErrCode.DuplicateCustomerId,
- statusCode: StatusCodes.CONFLICT,
- });
- }
- } else {
- delete newCusData.id;
- }
+ if (existingCustomer) {
+ throw new RecaseError({
+ message: `Update customer: Customer ${newCusData.id} already exists, can't change to this ID`,
+ code: ErrCode.DuplicateCustomerId,
+ statusCode: StatusCodes.CONFLICT,
+ });
+ }
+ } else {
+ delete newCusData.id;
+ }
- // Try to update stripe ID
- let stripeId = originalCustomer.processor?.id;
- let newStripeId = newCusData.stripe_id;
+ // Try to update stripe ID
+ let stripeId = originalCustomer.processor?.id;
+ let newStripeId = newCusData.stripe_id;
- if (notNullish(newStripeId) && stripeId !== newStripeId) {
- const stripeCli = createStripeCli({ org, env: req.env });
- await stripeCli.customers.retrieve(newStripeId);
+ if (notNullish(newStripeId) && stripeId !== newStripeId) {
+ const stripeCli = createStripeCli({ org, env: req.env });
+ await stripeCli.customers.retrieve(newStripeId);
- stripeId = newCusData.stripe_id;
- req.logger.info(
- `Updating customer's Stripe ID from ${originalCustomer.processor?.id} to ${stripeId}`
- );
- }
+ stripeId = newCusData.stripe_id;
+ req.logger.info(
+ `Updating customer's Stripe ID from ${originalCustomer.processor?.id} to ${stripeId}`,
+ );
+ }
- // 2. Check if customer email is being changed
- let oldMetadata = originalCustomer.metadata || {};
- let newMetadata = newCusData.metadata || {};
- for (let key in newMetadata) {
- if (newMetadata[key] === null) {
- delete newMetadata[key];
- delete oldMetadata[key];
- }
- }
+ // 2. Check if customer email is being changed
+ let oldMetadata = originalCustomer.metadata || {};
+ let newMetadata = newCusData.metadata || {};
+ for (let key in newMetadata) {
+ if (newMetadata[key] === null) {
+ delete newMetadata[key];
+ delete oldMetadata[key];
+ }
+ }
- let stripeUpdate = {
- email:
- originalCustomer.email !== newCusData.email
- ? newCusData.email
- : undefined,
- name:
- originalCustomer.name !== newCusData.name
- ? newCusData.name
- : undefined,
- };
+ let stripeUpdate = {
+ email:
+ originalCustomer.email !== newCusData.email
+ ? newCusData.email
+ : undefined,
+ name:
+ originalCustomer.name !== newCusData.name
+ ? newCusData.name
+ : undefined,
+ };
- if (Object.keys(stripeUpdate).length > 0 && stripeId) {
- const stripeCli = createStripeCli({ org, env: req.env });
- await stripeCli.customers.update(stripeId, stripeUpdate as any);
- }
+ if (Object.keys(stripeUpdate).length > 0 && stripeId) {
+ const stripeCli = createStripeCli({ org, env: req.env });
+ await stripeCli.customers.update(stripeId, stripeUpdate as any);
+ }
- await CusService.update({
- db: req.db,
- internalCusId: originalCustomer.internal_id,
- update: {
- ...newCusData,
- processor: newStripeId
- ? { id: newStripeId, type: ProcessorType.Stripe }
- : undefined,
- metadata: {
- ...oldMetadata,
- ...newMetadata,
- },
- },
- });
+ await CusService.update({
+ db: req.db,
+ internalCusId: originalCustomer.internal_id,
+ update: {
+ ...newCusData,
+ processor: newStripeId
+ ? { id: newStripeId, type: ProcessorType.Stripe }
+ : undefined,
+ metadata: {
+ ...oldMetadata,
+ ...newMetadata,
+ },
+ },
+ });
- let finalCustomer = await CusService.getFull({
- db,
- idOrInternalId: originalCustomer.internal_id,
- orgId: req.orgId,
- env: req.env,
- withEntities: true,
- });
+ let finalCustomer = await CusService.getFull({
+ db,
+ idOrInternalId: originalCustomer.internal_id,
+ orgId: req.orgId,
+ env: req.env,
+ withEntities: true,
+ });
- // res.status(200).json({ customer: updatedCustomer });
- let customerDetails = await getCustomerDetails({
- db,
- customer: finalCustomer,
- org,
- env: req.env,
- logger: req.logtail,
- cusProducts: finalCustomer.customer_products,
- expand: parseCusExpand(req.query.expand as string),
- features,
- reqApiVersion: req.apiVersion,
- });
+ // res.status(200).json({ customer: updatedCustomer });
+ let customerDetails = await getCustomerDetails({
+ db,
+ customer: finalCustomer,
+ org,
+ env: req.env,
+ logger: req.logtail,
+ cusProducts: finalCustomer.customer_products,
+ expand: parseCusExpand(req.query.expand as string),
+ features,
+ reqApiVersion: req.apiVersion,
+ });
- res.status(200).json(customerDetails);
- },
- });
+ res.status(200).json(customerDetails);
+ },
+ });
diff --git a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts
index 8a25358be..9c2360543 100644
--- a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts
+++ b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts
@@ -16,165 +16,165 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusProductService } from "../cusProducts/CusProductService.js";
const getCusOrgAndCusPrice = async ({
- db,
- req,
- cusEnt,
+ db,
+ req,
+ cusEnt,
}: {
- db: DrizzleCli;
- req: ExtendedRequest;
- cusEnt: FullCustomerEntitlement;
+ db: DrizzleCli;
+ req: ExtendedRequest;
+ cusEnt: FullCustomerEntitlement;
}) => {
- const [cusPrice, customer, org] = await Promise.all([
- CusPriceService.getRelatedToCusEnt({
- db,
- cusEnt,
- }),
- CusService.getByInternalId({
- db,
- internalId: cusEnt.internal_customer_id,
- }),
- OrgService.getFromReq(req),
- ]);
+ const [cusPrice, customer, org] = await Promise.all([
+ CusPriceService.getRelatedToCusEnt({
+ db,
+ cusEnt,
+ }),
+ CusService.getByInternalId({
+ db,
+ internalId: cusEnt.internal_customer_id,
+ }),
+ OrgService.getFromReq(req),
+ ]);
- return { cusPrice, customer, org };
+ return { cusPrice, customer, org };
};
export const handleUpdateEntitlement = async (req: any, res: any) => {
- try {
- const { db, logtail: logger } = req;
- const { customer_entitlement_id } = req.params;
- const { balance, next_reset_at, entity_id } = req.body;
+ try {
+ const { db, logtail: logger } = req;
+ const { customer_entitlement_id } = req.params;
+ const { balance, next_reset_at, entity_id } = req.body;
- if (isNaN(parseFloat(balance))) {
- throw new RecaseError({
- message: "Invalid balance",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isNaN(parseFloat(balance))) {
+ throw new RecaseError({
+ message: "Invalid balance",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (
- next_reset_at !== null &&
- (!Number.isInteger(next_reset_at) || next_reset_at < 0)
- ) {
- throw new RecaseError({
- message: "Next reset at must be a valid unix timestamp or null",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (
+ next_reset_at !== null &&
+ (!Number.isInteger(next_reset_at) || next_reset_at < 0)
+ ) {
+ throw new RecaseError({
+ message: "Next reset at must be a valid unix timestamp or null",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Check if org owns the entitlement
- const cusEnt = await CusEntService.getStrict({
- db,
- id: customer_entitlement_id,
- orgId: req.orgId,
- env: req.env,
- withCusProduct: true,
- });
+ // Check if org owns the entitlement
+ const cusEnt = await CusEntService.getStrict({
+ db,
+ id: customer_entitlement_id,
+ orgId: req.orgId,
+ env: req.env,
+ withCusProduct: true,
+ });
- const cusProduct = await CusProductService.get({
- db,
- id: cusEnt.customer_product_id,
- orgId: req.orgId,
- env: req.env,
- });
+ const cusProduct = await CusProductService.get({
+ db,
+ id: cusEnt.customer_product_id,
+ orgId: req.orgId,
+ env: req.env,
+ });
- if (balance < 0 && !cusEnt.usage_allowed) {
- throw new RecaseError({
- message: "Entitlement does not allow usage",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (balance < 0 && !cusEnt.usage_allowed) {
+ throw new RecaseError({
+ message: "Entitlement does not allow usage",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (cusEnt.unlimited) {
- throw new RecaseError({
- message: "Entitlement is unlimited",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (cusEnt.unlimited) {
+ throw new RecaseError({
+ message: "Entitlement is unlimited",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- let { balance: masterBalance } = getCusEntBalance({
- cusEnt,
- entityId: entity_id,
- });
+ let { balance: masterBalance } = getCusEntBalance({
+ cusEnt,
+ entityId: entity_id,
+ });
- const deducted = new Decimal(masterBalance!).minus(balance).toNumber();
+ const deducted = new Decimal(masterBalance!).minus(balance).toNumber();
- let originalBalance = structuredClone(masterBalance);
+ let originalBalance = structuredClone(masterBalance);
- let { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt({
- cusEnt: {
- ...cusEnt,
- customer_product: cusProduct!,
- },
- toDeduct: deducted,
- addAdjustment: true,
- allowNegativeBalance: cusEnt.usage_allowed || false,
- entityId: entity_id,
- });
+ let { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt({
+ cusEnt: {
+ ...cusEnt,
+ customer_product: cusProduct!,
+ },
+ toDeduct: deducted,
+ addAdjustment: true,
+ allowNegativeBalance: cusEnt.usage_allowed || false,
+ entityId: entity_id,
+ });
- let updates = {
- balance: newBalance,
- next_reset_at,
- entities: newEntities,
- adjustment: newAdjustment,
- };
+ let updates = {
+ balance: newBalance,
+ next_reset_at,
+ entities: newEntities,
+ adjustment: newAdjustment,
+ };
- const { cusPrice, customer, org } = await getCusOrgAndCusPrice({
- db,
- req,
- cusEnt,
- });
+ const { cusPrice, customer, org } = await getCusOrgAndCusPrice({
+ db,
+ req,
+ cusEnt,
+ });
- if (cusPrice && customer) {
- let fullCusProduct = await CusProductService.get({
- db,
- id: cusEnt.customer_product_id,
- orgId: req.orgId,
- env: req.env,
- });
+ if (cusPrice && customer) {
+ let fullCusProduct = await CusProductService.get({
+ db,
+ id: cusEnt.customer_product_id,
+ orgId: req.orgId,
+ env: req.env,
+ });
- const { newReplaceables, deletedReplaceables } = await adjustAllowance({
- db,
- env: req.env,
- org: org,
- affectedFeature: cusEnt.entitlement.feature,
- cusEnt: {
- ...cusEnt,
- customer_product: fullCusProduct!,
- },
- cusPrices: [cusPrice],
- customer: customer,
- originalBalance: originalBalance!,
- newBalance: balance,
- logger: req.logtail,
- });
+ const { newReplaceables, deletedReplaceables } = await adjustAllowance({
+ db,
+ env: req.env,
+ org: org,
+ affectedFeature: cusEnt.entitlement.feature,
+ cusEnt: {
+ ...cusEnt,
+ customer_product: fullCusProduct!,
+ },
+ cusPrices: [cusPrice],
+ customer: customer,
+ originalBalance: originalBalance!,
+ newBalance: balance,
+ logger: req.logtail,
+ });
- if (newReplaceables && newReplaceables.length > 0) {
- updates.balance = newBalance! - newReplaceables.length;
- }
+ if (newReplaceables && newReplaceables.length > 0) {
+ updates.balance = newBalance! - newReplaceables.length;
+ }
- if (deletedReplaceables && deletedReplaceables.length > 0) {
- updates.balance = newBalance! + deletedReplaceables.length;
- }
- }
+ if (deletedReplaceables && deletedReplaceables.length > 0) {
+ updates.balance = newBalance! + deletedReplaceables.length;
+ }
+ }
- await CusEntService.update({
- db,
- id: customer_entitlement_id,
- updates,
- });
+ await CusEntService.update({
+ db,
+ id: customer_entitlement_id,
+ updates,
+ });
- res.status(200).json({ success: true });
- } catch (error) {
- handleRequestError({
- req,
- error,
- res,
- action: "update customer entitlement",
- });
- }
+ res.status(200).json({ success: true });
+ } catch (error) {
+ handleRequestError({
+ req,
+ error,
+ res,
+ action: "update customer entitlement",
+ });
+ }
};
diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts
index 0591d0ccf..4b8355c3f 100644
--- a/server/src/internal/customers/internalCusRouter.ts
+++ b/server/src/internal/customers/internalCusRouter.ts
@@ -3,11 +3,11 @@ import { CusService } from "./CusService.js";
import { ProductService } from "../products/ProductService.js";
import {
- CusExpand,
- CusProductStatus,
- ErrCode,
- FullCusProduct,
- productToCusProduct,
+ CusExpand,
+ CusProductStatus,
+ ErrCode,
+ FullCusProduct,
+ productToCusProduct,
} from "@autumn/shared";
import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
@@ -27,340 +27,340 @@ import { ACTIVE_STATUSES } from "./cusProducts/CusProductService.js";
export const cusRouter: Router = Router();
cusRouter.post("/all/search", (req, res) =>
- routeHandler({
- req,
- res,
- action: "search customers",
- handler: async (req, res) => {
- const { search, page_size = 50, page = 1, last_item, filters } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "search customers",
+ handler: async (req, res) => {
+ const { search, page_size = 50, page = 1, last_item, filters } = req.body;
- const { data: customers, count } = await CusSearchService.search({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- search,
- filters,
- lastItem: last_item,
- pageNumber: page,
- pageSize: page_size,
- });
+ const { data: customers, count } = await CusSearchService.search({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ search,
+ filters,
+ lastItem: last_item,
+ pageNumber: page,
+ pageSize: page_size,
+ });
- res.status(200).json({ customers, totalCount: Number(count) });
- },
- })
+ res.status(200).json({ customers, totalCount: Number(count) });
+ },
+ }),
);
// Customer page
cusRouter.get("/:customer_id", async (req: any, res: any) => {
- try {
- const { db, org, features, env } = req;
- const { customer_id } = req.params;
- const orgId = req.orgId;
+ try {
+ const { db, org, features, env } = req;
+ const { customer_id } = req.params;
+ const orgId = req.orgId;
- const fullCus = await CusService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: customer_id,
- withEntities: true,
- expand: [CusExpand.Invoices],
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- CusProductStatus.Expired,
- ],
- });
+ const fullCus = await CusService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customer_id,
+ withEntities: true,
+ expand: [CusExpand.Invoices],
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ CusProductStatus.Expired,
+ ],
+ });
- res.status(200).json({
- customer: fullCus,
- // products: getLatestProducts(products),
- // versionCounts: getProductVersionCounts(products),
- // invoices,
- // features,
- // coupons,
- // events,
- // discount,
- // org,
- // entities,
- });
- } catch (error) {
- handleFrontendReqError({ req, error, res, action: "get customer data" });
- }
+ res.status(200).json({
+ customer: fullCus,
+ // products: getLatestProducts(products),
+ // versionCounts: getProductVersionCounts(products),
+ // invoices,
+ // features,
+ // coupons,
+ // events,
+ // discount,
+ // org,
+ // entities,
+ });
+ } catch (error) {
+ handleFrontendReqError({ req, error, res, action: "get customer data" });
+ }
});
cusRouter.get("/:customer_id/events", async (req: any, res: any) => {
- try {
- const { db, org, features, env } = req;
- const { customer_id } = req.params;
- const orgId = req.orgId;
+ try {
+ const { db, org, features, env } = req;
+ const { customer_id } = req.params;
+ const orgId = req.orgId;
- const customer = await CusService.get({
- db,
- orgId,
- env,
- idOrInternalId: customer_id,
- });
+ const customer = await CusService.get({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customer_id,
+ });
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const events = await EventService.getByCustomerId({
- db,
- internalCustomerId: customer.internal_id,
- env,
- orgId: orgId,
- });
+ const events = await EventService.getByCustomerId({
+ db,
+ internalCustomerId: customer.internal_id,
+ env,
+ orgId: orgId,
+ });
- res.status(200).json({ events });
- } catch (error) {
- handleFrontendReqError({ req, error, res, action: "get customer events" });
- }
+ res.status(200).json({ events });
+ } catch (error) {
+ handleFrontendReqError({ req, error, res, action: "get customer events" });
+ }
});
cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => {
- try {
- const { env, db, org } = req;
- const { customer_id } = req.params;
- const orgId = req.orgId;
+ try {
+ const { env, db, org } = req;
+ const { customer_id } = req.params;
+ const orgId = req.orgId;
- let internalCustomer = await CusService.get({
- db,
- orgId,
- env,
- idOrInternalId: customer_id,
- });
+ let internalCustomer = await CusService.get({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customer_id,
+ });
- if (!internalCustomer) {
- throw new RecaseError({
- message: "Customer not found",
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!internalCustomer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- // Get all redemptions for this customer
- let [referred, redeemed, stripeCus] = await Promise.all([
- RewardRedemptionService.getByReferrer({
- db,
- internalCustomerId: internalCustomer.internal_id,
- withCustomer: true,
- limit: 100,
- }),
- RewardRedemptionService.getByCustomer({
- db,
- internalCustomerId: internalCustomer.internal_id,
- withReferralCode: true,
- limit: 100,
- }),
- async () => {
- if (isStripeConnected({ org, env }) && internalCustomer.processor?.id) {
- const stripeCli = createStripeCli({ org, env });
- const stripeCus: any = await stripeCli.customers.retrieve(
- internalCustomer.processor.id
- );
- return stripeCus;
- }
- return null;
- },
- ]);
+ // Get all redemptions for this customer
+ let [referred, redeemed, stripeCus] = await Promise.all([
+ RewardRedemptionService.getByReferrer({
+ db,
+ internalCustomerId: internalCustomer.internal_id,
+ withCustomer: true,
+ limit: 100,
+ }),
+ RewardRedemptionService.getByCustomer({
+ db,
+ internalCustomerId: internalCustomer.internal_id,
+ withReferralCode: true,
+ limit: 100,
+ }),
+ async () => {
+ if (isStripeConnected({ org, env }) && internalCustomer.processor?.id) {
+ const stripeCli = createStripeCli({ org, env });
+ const stripeCus: any = await stripeCli.customers.retrieve(
+ internalCustomer.processor.id,
+ );
+ return stripeCus;
+ }
+ return null;
+ },
+ ]);
- let redeemedCustomerIds = redeemed.map(
- (redemption: any) => redemption.referral_code.internal_customer_id
- );
+ let redeemedCustomerIds = redeemed.map(
+ (redemption: any) => redemption.referral_code.internal_customer_id,
+ );
- let redeemedCustomers = await CusReadService.getInInternalIds({
- db,
- internalIds: redeemedCustomerIds,
- });
+ let redeemedCustomers = await CusReadService.getInInternalIds({
+ db,
+ internalIds: redeemedCustomerIds,
+ });
- for (const redemption of redeemed) {
- if (redemption.referral_code) {
- redemption.referral_code.customer = redeemedCustomers.find(
- (customer: any) =>
- customer.internal_id ===
- redemption.referral_code!.internal_customer_id
- );
- }
- }
+ for (const redemption of redeemed) {
+ if (redemption.referral_code) {
+ redemption.referral_code.customer = redeemedCustomers.find(
+ (customer: any) =>
+ customer.internal_id ===
+ redemption.referral_code!.internal_customer_id,
+ );
+ }
+ }
- const end = performance.now();
+ const end = performance.now();
- res.status(200).send({
- referred,
- redeemed,
- stripeCus,
- });
- } catch (error) {
- handleFrontendReqError({
- req,
- error,
- res,
- action: "get customer referrals",
- });
- }
+ res.status(200).send({
+ referred,
+ redeemed,
+ stripeCus,
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ req,
+ error,
+ res,
+ action: "get customer referrals",
+ });
+ }
});
cusRouter.post("/all/full_customers", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "get customer full customers",
- handler: async (req, res) => {
- const { db, org, env } = req;
- const { search, page_size = 50, page = 1, last_item, filters } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "get customer full customers",
+ handler: async (req, res) => {
+ const { db, org, env } = req;
+ const { search, page_size = 50, page = 1, last_item, filters } = req.body;
- const { data: customers, count } = await CusSearchService.search({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- search,
- filters,
- lastItem: last_item,
- pageNumber: page,
- pageSize: page_size,
- });
+ const { data: customers, count } = await CusSearchService.search({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ search,
+ filters,
+ lastItem: last_item,
+ pageNumber: page,
+ pageSize: page_size,
+ });
- const fullCustomers = await CusBatchService.getByInternalIds({
- db,
- org,
- env,
- internalCustomerIds: customers.map(
- (customer: any) => customer.internal_id
- ),
- });
+ const fullCustomers = await CusBatchService.getByInternalIds({
+ db,
+ org,
+ env,
+ internalCustomerIds: customers.map(
+ (customer: any) => customer.internal_id,
+ ),
+ });
- res.status(200).json({ fullCustomers });
- },
- })
+ res.status(200).json({ fullCustomers });
+ },
+ }),
);
cusRouter.get(
- "/:customer_id/product/:product_id",
- async (req: any, res: any) => {
- try {
- const { org, env, db, features, logtail: logger } = req;
- const { customer_id, product_id } = req.params;
- const { version, customer_product_id, entity_id } = req.query;
+ "/:customer_id/product/:product_id",
+ async (req: any, res: any) => {
+ try {
+ const { org, env, db, features, logtail: logger } = req;
+ const { customer_id, product_id } = req.params;
+ const { version, customer_product_id, entity_id } = req.query;
- const customer = await CusService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: customer_id,
- withEntities: true,
- entityId: entity_id,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- CusProductStatus.Expired,
- ],
- });
+ const customer = await CusService.getFull({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: customer_id,
+ withEntities: true,
+ entityId: entity_id,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ CusProductStatus.Expired,
+ ],
+ });
- if (!customer) {
- throw new RecaseError({
- message: "Customer not found",
- code: "CUSTOMER_NOT_FOUND",
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: "Customer not found",
+ code: "CUSTOMER_NOT_FOUND",
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let cusProducts = customer.customer_products;
- let entity = customer.entity;
+ let cusProducts = customer.customer_products;
+ let entity = customer.entity;
- let cusProduct = productToCusProduct({
- cusProducts,
- productId: product_id,
- internalEntityId: entity?.internal_id,
- version: version ? parseInt(version) : undefined,
- cusProductId: customer_product_id,
- inStatuses: ACTIVE_STATUSES,
- });
+ let cusProduct = productToCusProduct({
+ cusProducts,
+ productId: product_id,
+ internalEntityId: entity?.internal_id,
+ version: version ? parseInt(version) : undefined,
+ cusProductId: customer_product_id,
+ inStatuses: ACTIVE_STATUSES,
+ });
- let product = cusProduct
- ? cusProductToProduct({ cusProduct })
- : await ProductService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: product_id,
- version:
- version && Number.isInteger(parseInt(version))
- ? parseInt(version)
- : undefined,
- });
+ let product = cusProduct
+ ? cusProductToProduct({ cusProduct })
+ : await ProductService.getFull({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: product_id,
+ version:
+ version && Number.isInteger(parseInt(version))
+ ? parseInt(version)
+ : undefined,
+ });
- let productV2 = mapToProductV2({ product: product!, features });
- // let numVersions = await ProductService.getProductVersionCount({
- // db,
- // orgId: org.id,
- // env,
- // productId: product_id,
- // });
+ let productV2 = mapToProductV2({ product: product!, features });
+ // let numVersions = await ProductService.getProductVersionCount({
+ // db,
+ // orgId: org.id,
+ // env,
+ // productId: product_id,
+ // });
- res.status(200).json({
- cusProduct,
- product: productV2,
- // customer,
- // product: cusProduct
- // ? {
- // ...productV2,
- // options: cusProduct.options,
- // isActive: cusProduct.status === CusProductStatus.Active,
- // isCustom: cusProduct.is_custom,
- // isCanceled:
- // cusProduct.canceled_at !== null || cusProduct.canceled,
- // cusProductId: cusProduct.id,
- // }
- // : productV2,
- // features,
- // numVersions,
- // entities: customer.entities,
- // org: createOrgResponse({ org, env }),
- });
- } catch (error) {
- handleFrontendReqError({
- req,
- error,
- res,
- action: "get customer product",
- });
- }
- }
+ res.status(200).json({
+ cusProduct,
+ product: productV2,
+ // customer,
+ // product: cusProduct
+ // ? {
+ // ...productV2,
+ // options: cusProduct.options,
+ // isActive: cusProduct.status === CusProductStatus.Active,
+ // isCustom: cusProduct.is_custom,
+ // isCanceled:
+ // cusProduct.canceled_at !== null || cusProduct.canceled,
+ // cusProductId: cusProduct.id,
+ // }
+ // : productV2,
+ // features,
+ // numVersions,
+ // entities: customer.entities,
+ // org: createOrgResponse({ org, env }),
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ req,
+ error,
+ res,
+ action: "get customer product",
+ });
+ }
+ },
);
cusRouter.get("/:customer_id/sub", async (req: any, res: any) => {
- try {
- const { org, env, db } = req;
- const { customer_id } = req.params;
- const orgId = req.orgId;
+ try {
+ const { org, env, db } = req;
+ const { customer_id } = req.params;
+ const orgId = req.orgId;
- const fullCus = await CusService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: customer_id,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: customer_id,
+ });
- const subId = fullCus.customer_products.flatMap(
- (cp: FullCusProduct) => cp.subscription_ids || []
- )?.[0];
+ const subId = fullCus.customer_products.flatMap(
+ (cp: FullCusProduct) => cp.subscription_ids || [],
+ )?.[0];
- if (!subId) return res.status(200).json({ sub: undefined });
+ if (!subId) return res.status(200).json({ sub: undefined });
- const stripeCli = createStripeCli({ org, env });
- const sub = await stripeCli.subscriptions.retrieve(subId, {
- expand: ["discounts.coupon"],
- });
+ const stripeCli = createStripeCli({ org, env });
+ const sub = await stripeCli.subscriptions.retrieve(subId, {
+ expand: ["discounts.coupon"],
+ });
- res.status(200).json({ sub });
- } catch (error) {
- handleFrontendReqError({ req, error, res, action: "get customer rewards" });
- }
+ res.status(200).json({ sub });
+ } catch (error) {
+ handleFrontendReqError({ req, error, res, action: "get customer rewards" });
+ }
});
diff --git a/server/src/internal/customers/previews/checkProductUtils.ts b/server/src/internal/customers/previews/checkProductUtils.ts
index 027e4d2c3..a75958714 100644
--- a/server/src/internal/customers/previews/checkProductUtils.ts
+++ b/server/src/internal/customers/previews/checkProductUtils.ts
@@ -1,10 +1,10 @@
import {
- featurePricetoPricecnItem,
- getPriceText,
+ featurePricetoPricecnItem,
+ getPriceText,
} from "@/internal/products/pricecn/pricecnUtils.js";
import {
- isFeatureItem,
- isPriceItem,
+ isFeatureItem,
+ isPriceItem,
} from "@/internal/products/product-items/productItemUtils/getItemType.js";
import { Feature, Organization, ProductItem, ProductV2 } from "@autumn/shared";
@@ -14,88 +14,88 @@ import { notNullish } from "@/utils/genUtils.js";
import { getFeatureNameWithCapital } from "@/internal/features/utils/displayUtils.js";
export const getProductChargeText = ({
- product,
- org,
- features,
+ product,
+ org,
+ features,
}: {
- product: ProductV2;
- org: Organization;
- features: Feature[];
+ product: ProductV2;
+ org: Organization;
+ features: Feature[];
}) => {
- let basePrices = product.items.filter((i) => isPriceItem(i));
- let total = basePrices.reduce((acc, curr) => acc + curr.price!, 0);
+ let basePrices = product.items.filter((i) => isPriceItem(i));
+ let total = basePrices.reduce((acc, curr) => acc + curr.price!, 0);
- let itemStrs = [];
- if (total > 0) {
- itemStrs.push(
- formatCurrency({
- amount: total,
- defaultCurrency: org.default_currency!,
- }),
- );
- }
+ let itemStrs = [];
+ if (total > 0) {
+ itemStrs.push(
+ formatCurrency({
+ amount: total,
+ defaultCurrency: org.default_currency!,
+ }),
+ );
+ }
- let prepaidPrices = product.items.filter(
- (i) => isFeaturePriceItem(i) && i.usage_model == "prepaid",
- );
+ let prepaidPrices = product.items.filter(
+ (i) => isFeaturePriceItem(i) && i.usage_model == "prepaid",
+ );
- let prepaidStrings = prepaidPrices.map((i) => {
- let feature = features.find((f) => f.id === i.feature_id);
- let priceStr = formatTiers({
- tiers: i.tiers!,
- org,
- });
+ let prepaidStrings = prepaidPrices.map((i) => {
+ let feature = features.find((f) => f.id === i.feature_id);
+ let priceStr = formatTiers({
+ tiers: i.tiers!,
+ org,
+ });
- let featureStr =
- i.billing_units && i.billing_units > 1
- ? `${i.billing_units} ${feature?.name}`
- : feature?.name;
+ let featureStr =
+ i.billing_units && i.billing_units > 1
+ ? `${i.billing_units} ${feature?.name}`
+ : feature?.name;
- return `${priceStr} / ${featureStr}`;
- });
- return [...itemStrs, ...prepaidStrings];
+ return `${priceStr} / ${featureStr}`;
+ });
+ return [...itemStrs, ...prepaidStrings];
};
export const getItemDescription = ({
- item,
- features,
- product,
- org,
+ item,
+ features,
+ product,
+ org,
}: {
- item: ProductItem;
- features: Feature[];
- product: ProductV2;
- org: Organization;
+ item: ProductItem;
+ features: Feature[];
+ product: ProductV2;
+ org: Organization;
}) => {
- let prices = product.items.filter((i) => !isFeatureItem(i));
+ let prices = product.items.filter((i) => !isFeatureItem(i));
- let priceStr = getPriceText({
- item,
- org,
- });
+ let priceStr = getPriceText({
+ item,
+ org,
+ });
- if (isPriceItem(item)) {
- let baseName =
- prices.length == 1
- ? product.name
- : notNullish(item.interval)
- ? "Subscription"
- : "One-time";
+ if (isPriceItem(item)) {
+ let baseName =
+ prices.length == 1
+ ? product.name
+ : notNullish(item.interval)
+ ? "Subscription"
+ : "One-time";
- return baseName;
- } else {
- let feature = features.find((f) => f.id === item.feature_id);
- // let pricecnItem = featurePricetoPricecnItem({
- // feature,
- // item,
- // org,
- // });
+ return baseName;
+ } else {
+ let feature = features.find((f) => f.id === item.feature_id);
+ // let pricecnItem = featurePricetoPricecnItem({
+ // feature,
+ // item,
+ // org,
+ // });
- // // let combinedStr = pricecnItem.primaryText + " " + pricecnItem.secondaryText;
- // // combinedStr = `${feature?.name} - ${combinedStr}`;
- // // if (item.usage_model == "pay_per_use") {
- // // combinedStr = `${combinedStr}`;
- // // }
- return `${getFeatureNameWithCapital({ feature: feature! })}`;
- }
+ // // let combinedStr = pricecnItem.primaryText + " " + pricecnItem.secondaryText;
+ // // combinedStr = `${feature?.name} - ${combinedStr}`;
+ // // if (item.usage_model == "pay_per_use") {
+ // // combinedStr = `${combinedStr}`;
+ // // }
+ return `${getFeatureNameWithCapital({ feature: feature! })}`;
+ }
};
diff --git a/server/src/internal/customers/previews/previewUtils.ts b/server/src/internal/customers/previews/previewUtils.ts
index 24dc0c171..ccdd7a278 100644
--- a/server/src/internal/customers/previews/previewUtils.ts
+++ b/server/src/internal/customers/previews/previewUtils.ts
@@ -1,89 +1,89 @@
import { Organization, PriceTier } from "@autumn/shared";
export const formatCurrency = ({
- amount,
- defaultCurrency,
+ amount,
+ defaultCurrency,
}: {
- amount: number;
- defaultCurrency?: string;
+ amount: number;
+ defaultCurrency?: string;
}) => {
- const formatter = new Intl.NumberFormat("en-US", {
- style: "currency",
- currency: defaultCurrency || "usd",
- });
- return formatter.format(amount);
+ const formatter = new Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: defaultCurrency || "usd",
+ });
+ return formatter.format(amount);
};
export const formatTiers = ({
- tiers,
+ tiers,
- org,
+ org,
}: {
- tiers: PriceTier[];
+ tiers: PriceTier[];
- org: Organization;
+ org: Organization;
}) => {
- if (tiers.length == 1) {
- return formatCurrency({
- amount: tiers[0].amount,
- defaultCurrency: org.default_currency!,
- });
- }
+ if (tiers.length == 1) {
+ return formatCurrency({
+ amount: tiers[0].amount,
+ defaultCurrency: org.default_currency!,
+ });
+ }
- let tiersStart = formatCurrency({
- amount: tiers[0].amount,
- defaultCurrency: org.default_currency!,
- });
- let tiersEnd = formatCurrency({
- amount: tiers[tiers.length - 1].amount,
- defaultCurrency: org.default_currency!,
- });
+ let tiersStart = formatCurrency({
+ amount: tiers[0].amount,
+ defaultCurrency: org.default_currency!,
+ });
+ let tiersEnd = formatCurrency({
+ amount: tiers[tiers.length - 1].amount,
+ defaultCurrency: org.default_currency!,
+ });
- return `${tiersStart} - ${tiersEnd}`;
+ return `${tiersStart} - ${tiersEnd}`;
};
export const getItemsHtml = ({
- items,
- org,
+ items,
+ org,
}: {
- items: any[];
- org: Organization;
+ items: any[];
+ org: Organization;
}) => {
- let html = "";
- let pricedItems = items.filter((item) => item.amount != 0);
- let totalAmount = pricedItems.reduce((acc: number, item: any) => {
- return acc + item.amount;
- }, 0);
+ let html = "";
+ let pricedItems = items.filter((item) => item.amount != 0);
+ let totalAmount = pricedItems.reduce((acc: number, item: any) => {
+ return acc + item.amount;
+ }, 0);
- if (pricedItems.length == 1) {
- html += `
${formatCurrency({
- amount: totalAmount,
- defaultCurrency: org.default_currency!,
- })}
`;
- } else {
- html += `
${itemsToHtml({ items: pricedItems })}
`;
- html += `
Total: ${formatCurrency({
- amount: totalAmount,
- defaultCurrency: org.default_currency!,
- })}
`;
- }
+ if (pricedItems.length == 1) {
+ html += `
${formatCurrency({
+ amount: totalAmount,
+ defaultCurrency: org.default_currency!,
+ })}
`;
+ } else {
+ html += `
${itemsToHtml({ items: pricedItems })}
`;
+ html += `
Total: ${formatCurrency({
+ amount: totalAmount,
+ defaultCurrency: org.default_currency!,
+ })}
`;
+ }
- return html;
+ return html;
};
export const itemsToHtml = ({ items }: { items: any[] }) => {
- let html = "";
+ let html = "";
- for (let item of items) {
- if (item.amount == 0) {
- continue;
- }
+ for (let item of items) {
+ if (item.amount == 0) {
+ continue;
+ }
- html += `- ${item.name || item.description}: ${formatCurrency({
- amount: item.amount,
- defaultCurrency: item.currency,
- })}`;
- }
+ html += `- ${item.name || item.description}: ${formatCurrency({
+ amount: item.amount,
+ defaultCurrency: item.currency,
+ })}`;
+ }
- return html;
+ return html;
};
diff --git a/server/src/internal/dev/ApiKeyService.ts b/server/src/internal/dev/ApiKeyService.ts
index 105e3e7f4..b1d074ea6 100644
--- a/server/src/internal/dev/ApiKeyService.ts
+++ b/server/src/internal/dev/ApiKeyService.ts
@@ -4,116 +4,116 @@ import { CacheManager } from "@/external/caching/CacheManager.js";
import { getApiVersion } from "@/utils/versionUtils.js";
import {
- ApiKey,
- apiKeys,
- AppEnv,
- Feature,
- features,
- Organization,
- OrgConfigSchema,
+ ApiKey,
+ apiKeys,
+ AppEnv,
+ Feature,
+ features,
+ Organization,
+ OrgConfigSchema,
} from "@autumn/shared";
import { desc, and, eq } from "drizzle-orm";
export class ApiKeyService {
- static async verifyAndFetch({
- db,
- secretKey,
- hashedKey,
- env,
- }: {
- db: DrizzleCli;
- secretKey: string;
- hashedKey: string;
- env: AppEnv;
- }) {
- let data = await db.query.apiKeys.findFirst({
- where: eq(apiKeys.hashed_key, hashedKey),
- with: {
- org: {
- with: {
- features: {
- where: eq(features.env, env),
- },
- },
- },
- },
- });
+ static async verifyAndFetch({
+ db,
+ secretKey,
+ hashedKey,
+ env,
+ }: {
+ db: DrizzleCli;
+ secretKey: string;
+ hashedKey: string;
+ env: AppEnv;
+ }) {
+ let data = await db.query.apiKeys.findFirst({
+ where: eq(apiKeys.hashed_key, hashedKey),
+ with: {
+ org: {
+ with: {
+ features: {
+ where: eq(features.env, env),
+ },
+ },
+ },
+ },
+ });
- if (!data || !data.org) {
- console.warn(`verify secret key ${secretKey} returned null`);
- return null;
- }
+ if (!data || !data.org) {
+ console.warn(`verify secret key ${secretKey} returned null`);
+ return null;
+ }
- let org = structuredClone(data.org) as Organization & {
- features?: Feature[];
- };
+ let org = structuredClone(data.org) as Organization & {
+ features?: Feature[];
+ };
- delete org.features;
+ delete org.features;
- org.config = OrgConfigSchema.parse(org.config || {});
- org.api_version = getApiVersion({
- createdAt: org.created_at!,
- });
+ org.config = OrgConfigSchema.parse(org.config || {});
+ org.api_version = getApiVersion({
+ createdAt: org.created_at!,
+ });
- let result = {
- org,
- features: (data.org.features || []) as Feature[],
- env,
- userId: data.user_id,
- };
+ let result = {
+ org,
+ features: (data.org.features || []) as Feature[],
+ env,
+ userId: data.user_id,
+ };
- // console.log("result", result);
- return result;
- }
+ // console.log("result", result);
+ return result;
+ }
- static async getByOrg({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- return await db.query.apiKeys.findMany({
- where: and(eq(apiKeys.org_id, orgId), eq(apiKeys.env, env)),
- orderBy: [desc(apiKeys.id)],
- limit: 200,
- });
- }
+ static async getByOrg({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ return await db.query.apiKeys.findMany({
+ where: and(eq(apiKeys.org_id, orgId), eq(apiKeys.env, env)),
+ orderBy: [desc(apiKeys.id)],
+ limit: 200,
+ });
+ }
- static async insert({ db, apiKey }: { db: DrizzleCli; apiKey: ApiKey }) {
- await db.insert(apiKeys).values(apiKey);
- }
+ static async insert({ db, apiKey }: { db: DrizzleCli; apiKey: ApiKey }) {
+ await db.insert(apiKeys).values(apiKey);
+ }
- static async delete({
- db,
- id,
- orgId,
- }: {
- db: DrizzleCli;
- id: string;
- orgId: string;
- }) {
- return await db
- .delete(apiKeys)
- .where(and(eq(apiKeys.id, id), eq(apiKeys.org_id, orgId)))
- .returning();
- }
+ static async delete({
+ db,
+ id,
+ orgId,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ orgId: string;
+ }) {
+ return await db
+ .delete(apiKeys)
+ .where(and(eq(apiKeys.id, id), eq(apiKeys.org_id, orgId)))
+ .returning();
+ }
}
export class CachedKeyService {
- static async clearCache({ hashedKey }: { hashedKey: string }) {
- try {
- await CacheManager.invalidate({
- action: CacheType.SecretKey,
- value: hashedKey,
- });
- } catch (error) {
- console.error(
- `(warning) failed to clear cache for verify action: ${error}`
- );
- }
- }
+ static async clearCache({ hashedKey }: { hashedKey: string }) {
+ try {
+ await CacheManager.invalidate({
+ action: CacheType.SecretKey,
+ value: hashedKey,
+ });
+ } catch (error) {
+ console.error(
+ `(warning) failed to clear cache for verify action: ${error}`,
+ );
+ }
+ }
}
diff --git a/server/src/internal/dev/api-keys/apiKeyUtils.ts b/server/src/internal/dev/api-keys/apiKeyUtils.ts
index e7ef26a50..2ae0d0190 100644
--- a/server/src/internal/dev/api-keys/apiKeyUtils.ts
+++ b/server/src/internal/dev/api-keys/apiKeyUtils.ts
@@ -8,98 +8,98 @@ import { queryWithCache } from "@/external/caching/cacheUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
function generateApiKey(length = 32, prefix = "") {
- try {
- // Define allowed characters (alphanumeric only)
- const allowedChars =
- "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
- const array = new Uint8Array(length);
- crypto.getRandomValues(array);
+ try {
+ // Define allowed characters (alphanumeric only)
+ const allowedChars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const array = new Uint8Array(length);
+ crypto.getRandomValues(array);
- // Convert random bytes to alphanumeric string
- const key = Array.from(array)
- .map((byte) => allowedChars[byte % allowedChars.length])
- .join("");
+ // Convert random bytes to alphanumeric string
+ const key = Array.from(array)
+ .map((byte) => allowedChars[byte % allowedChars.length])
+ .join("");
- return prefix ? `${prefix}_${key}` : key;
- } catch (error) {
- console.error("Failed to generate API key:", error);
- throw new Error("Failed to generate secure API key");
- }
+ return prefix ? `${prefix}_${key}` : key;
+ } catch (error) {
+ console.error("Failed to generate API key:", error);
+ throw new Error("Failed to generate secure API key");
+ }
}
export const hashApiKey = (apiKey: string) => {
- return crypto.createHash("sha256").update(apiKey).digest("hex");
+ return crypto.createHash("sha256").update(apiKey).digest("hex");
};
export const createKey = async ({
- db,
- env,
- name,
- userId,
- orgId,
- prefix,
- meta,
+ db,
+ env,
+ name,
+ userId,
+ orgId,
+ prefix,
+ meta,
}: {
- db: DrizzleCli;
- env: AppEnv;
- name: string;
- orgId: string;
- prefix: string;
- meta: any;
- userId?: string;
+ db: DrizzleCli;
+ env: AppEnv;
+ name: string;
+ orgId: string;
+ prefix: string;
+ meta: any;
+ userId?: string;
}) => {
- const apiKey = generateApiKey(42, prefix);
- const hashedKey = hashApiKey(apiKey);
+ const apiKey = generateApiKey(42, prefix);
+ const hashedKey = hashApiKey(apiKey);
- const apiKeyData: ApiKey = {
- id: generateId("key"),
- org_id: orgId,
- user_id: userId || null,
- name,
- prefix: apiKey.substring(0, 14),
- created_at: Date.now(),
- env,
- hashed_key: hashedKey,
- meta,
- };
+ const apiKeyData: ApiKey = {
+ id: generateId("key"),
+ org_id: orgId,
+ user_id: userId || null,
+ name,
+ prefix: apiKey.substring(0, 14),
+ created_at: Date.now(),
+ env,
+ hashed_key: hashedKey,
+ meta,
+ };
- await ApiKeyService.insert({ db, apiKey: apiKeyData });
+ await ApiKeyService.insert({ db, apiKey: apiKeyData });
- return apiKey;
+ return apiKey;
};
export const verifyKey = async ({
- db,
- key,
+ db,
+ key,
}: {
- db: DrizzleCli;
- key: string;
+ db: DrizzleCli;
+ key: string;
}) => {
- const hashedKey = hashApiKey(key);
+ const hashedKey = hashApiKey(key);
- const env = key.startsWith("am_sk_test") ? AppEnv.Sandbox : AppEnv.Live;
+ const env = key.startsWith("am_sk_test") ? AppEnv.Sandbox : AppEnv.Live;
- const data = await queryWithCache({
- action: CacheType.SecretKey,
- key: hashedKey,
- fn: async () =>
- await ApiKeyService.verifyAndFetch({
- db,
- secretKey: key,
- hashedKey,
- env,
- }),
- });
+ const data = await queryWithCache({
+ action: CacheType.SecretKey,
+ key: hashedKey,
+ fn: async () =>
+ await ApiKeyService.verifyAndFetch({
+ db,
+ secretKey: key,
+ hashedKey,
+ env,
+ }),
+ });
- if (!data) {
- return {
- valid: false,
- data: null,
- };
- }
+ if (!data) {
+ return {
+ valid: false,
+ data: null,
+ };
+ }
- return {
- valid: true,
- data: data,
- };
+ return {
+ valid: true,
+ data: data,
+ };
};
diff --git a/server/src/internal/dev/api-keys/publicKeyUtils.ts b/server/src/internal/dev/api-keys/publicKeyUtils.ts
index f54424707..0cccb832e 100644
--- a/server/src/internal/dev/api-keys/publicKeyUtils.ts
+++ b/server/src/internal/dev/api-keys/publicKeyUtils.ts
@@ -5,34 +5,34 @@ import { OrgService } from "@/internal/orgs/OrgService.js";
import { AppEnv } from "@autumn/shared";
export const verifyPublicKey = async ({
- db,
- pkey,
- env,
+ db,
+ pkey,
+ env,
}: {
- db: DrizzleCli;
- pkey: string;
- env: AppEnv;
+ db: DrizzleCli;
+ pkey: string;
+ env: AppEnv;
}) => {
- let data = await queryWithCache({
- action: CacheType.PublicKey,
- key: pkey,
- fn: async () =>
- await OrgService.getFromPkeyWithFeatures({
- db,
- pkey,
- env,
- }),
- });
+ let data = await queryWithCache({
+ action: CacheType.PublicKey,
+ key: pkey,
+ fn: async () =>
+ await OrgService.getFromPkeyWithFeatures({
+ db,
+ pkey,
+ env,
+ }),
+ });
- if (!data) {
- return null;
- }
+ if (!data) {
+ return null;
+ }
- let org = structuredClone(data);
+ let org = structuredClone(data);
- delete org.features;
- return {
- org,
- features: data.features,
- };
+ delete org.features;
+ return {
+ org,
+ features: data.features,
+ };
};
diff --git a/server/src/internal/dev/devRouter.ts b/server/src/internal/dev/devRouter.ts
index 8e8663c89..b3c9b3daf 100644
--- a/server/src/internal/dev/devRouter.ts
+++ b/server/src/internal/dev/devRouter.ts
@@ -12,8 +12,8 @@ import { routeHandler } from "@/utils/routerUtils.js";
import { encryptData } from "@/utils/encryptUtils.js";
import Stripe from "stripe";
import {
- checkKeyValid,
- createWebhookEndpoint,
+ checkKeyValid,
+ createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js";
import * as crypto from "crypto";
@@ -22,342 +22,342 @@ import { isStripeConnected } from "../orgs/orgUtils.js";
export const devRouter: Router = Router();
devRouter.get("/data", withOrgAuth, async (req: any, res) => {
- try {
- const { db, env, orgId } = req;
- const apiKeys = await ApiKeyService.getByOrg({
- db,
- orgId,
- env,
- });
+ try {
+ const { db, env, orgId } = req;
+ const apiKeys = await ApiKeyService.getByOrg({
+ db,
+ orgId,
+ env,
+ });
- const org = await OrgService.getFromReq(req);
- const dashboardUrl = await getSvixDashboardUrl({
- env: req.env,
- org: org,
- });
+ const org = await OrgService.getFromReq(req);
+ const dashboardUrl = await getSvixDashboardUrl({
+ env: req.env,
+ org: org,
+ });
- res.status(200).json({
- api_keys: apiKeys,
- org,
- svix_dashboard_url: dashboardUrl,
- });
- } catch (error) {
- handleRequestError({ error, req, res, action: "Get /dev/data" });
- }
+ res.status(200).json({
+ api_keys: apiKeys,
+ org,
+ svix_dashboard_url: dashboardUrl,
+ });
+ } catch (error) {
+ handleRequestError({ error, req, res, action: "Get /dev/data" });
+ }
});
devRouter.post("/api_key", withOrgAuth, async (req: any, res) =>
- routeHandler({
- req,
- res,
- action: "Create API key",
- handler: async (req: any, res: any) => {
- const { db, env, orgId } = req;
- const { name } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "Create API key",
+ handler: async (req: any, res: any) => {
+ const { db, env, orgId } = req;
+ const { name } = req.body;
- // 1. Create API key
- let prefix = "am_sk_test";
- if (env === AppEnv.Live) {
- prefix = "am_sk_live";
- }
- const apiKey = await createKey({
- db,
- env,
- name,
- orgId,
- userId: req.user?.id,
- prefix,
- meta: {},
- });
+ // 1. Create API key
+ let prefix = "am_sk_test";
+ if (env === AppEnv.Live) {
+ prefix = "am_sk_live";
+ }
+ const apiKey = await createKey({
+ db,
+ env,
+ name,
+ orgId,
+ userId: req.user?.id,
+ prefix,
+ meta: {},
+ });
- res.status(200).json({
- api_key: apiKey,
- });
- },
- })
+ res.status(200).json({
+ api_key: apiKey,
+ });
+ },
+ }),
);
devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => {
- try {
- const { db, orgId } = req;
- const { id } = req.params;
+ try {
+ const { db, orgId } = req;
+ const { id } = req.params;
- let data = await ApiKeyService.delete({
- db,
- id,
- orgId,
- });
+ let data = await ApiKeyService.delete({
+ db,
+ id,
+ orgId,
+ });
- if (data.length === 0) {
- console.error("API key not found");
- res.status(404).json({ error: "API key not found" });
- return;
- }
+ if (data.length === 0) {
+ console.error("API key not found");
+ res.status(404).json({ error: "API key not found" });
+ return;
+ }
- let batchInvalidate = [];
- for (let apiKey of data) {
- batchInvalidate.push(
- CacheManager.invalidate({
- action: CacheType.SecretKey,
- value: apiKey.hashed_key!,
- })
- );
- }
- await Promise.all(batchInvalidate);
+ let batchInvalidate = [];
+ for (let apiKey of data) {
+ batchInvalidate.push(
+ CacheManager.invalidate({
+ action: CacheType.SecretKey,
+ value: apiKey.hashed_key!,
+ }),
+ );
+ }
+ await Promise.all(batchInvalidate);
- res
- .status(200)
- .json({ message: "API key deleted", code: "api_key_deleted" });
- } catch (error) {
- console.error("Failed to delete API key", error);
- res.status(500).json({ error: "Failed to delete API key" });
- return;
- }
+ res
+ .status(200)
+ .json({ message: "API key deleted", code: "api_key_deleted" });
+ } catch (error) {
+ console.error("Failed to delete API key", error);
+ res.status(500).json({ error: "Failed to delete API key" });
+ return;
+ }
});
const generateOtp = (): string => {
- // Use Web Crypto API if available for cryptographically-secure randomness
- const getRandomInt = (): number => {
- if (
- typeof crypto !== "undefined" &&
- typeof crypto.getRandomValues === "function"
- ) {
- const array = new Uint32Array(1);
- crypto.getRandomValues(array);
- return array[0];
- }
+ // Use Web Crypto API if available for cryptographically-secure randomness
+ const getRandomInt = (): number => {
+ if (
+ typeof crypto !== "undefined" &&
+ typeof crypto.getRandomValues === "function"
+ ) {
+ const array = new Uint32Array(1);
+ crypto.getRandomValues(array);
+ return array[0];
+ }
- // Node.js (SSR / tests) – use crypto module's webcrypto if available
- try {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- const { webcrypto } = require("crypto");
- if (webcrypto?.getRandomValues) {
- const arr = new Uint32Array(1);
- webcrypto.getRandomValues(arr);
- return arr[0];
- }
- } catch (_) {
- /* ignore */
- }
+ // Node.js (SSR / tests) – use crypto module's webcrypto if available
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
+ const { webcrypto } = require("crypto");
+ if (webcrypto?.getRandomValues) {
+ const arr = new Uint32Array(1);
+ webcrypto.getRandomValues(arr);
+ return arr[0];
+ }
+ } catch (_) {
+ /* ignore */
+ }
- // Fallback (non-cryptographic)
- return Math.floor(Math.random() * 0xffffffff);
- };
+ // Fallback (non-cryptographic)
+ return Math.floor(Math.random() * 0xffffffff);
+ };
- // Limit to range [100000, 999999]
- const randomSixDigits = (getRandomInt() % 900000) + 100000;
- return randomSixDigits.toString();
+ // Limit to range [100000, 999999]
+ const randomSixDigits = (getRandomInt() % 900000) + 100000;
+ return randomSixDigits.toString();
};
const OTP_TTL = 300;
export const handleCreateOtp = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Create OTP",
- handler: async () => {
- const { orgId, env, db } = req;
+ routeHandler({
+ req,
+ res,
+ action: "Create OTP",
+ handler: async () => {
+ const { orgId, env, db } = req;
- // Check if there's already an OTP to use
- const maybeCacheKey = `orgOTPExists:${orgId}`;
- const maybeCacheData = await CacheManager.getJson(maybeCacheKey);
- if (maybeCacheData) {
- res.status(200).json({
- otp: maybeCacheData,
- });
- return;
- }
+ // Check if there's already an OTP to use
+ const maybeCacheKey = `orgOTPExists:${orgId}`;
+ const maybeCacheData = await CacheManager.getJson(maybeCacheKey);
+ if (maybeCacheData) {
+ res.status(200).json({
+ otp: maybeCacheData,
+ });
+ return;
+ }
- // Generate OTP
- const otp = generateOtp();
+ // Generate OTP
+ const otp = generateOtp();
- const cacheData = {
- otp: otp,
- orgId: orgId,
- };
+ const cacheData = {
+ otp: otp,
+ orgId: orgId,
+ };
- const cacheKey = `otp:${otp}`;
- await CacheManager.setJson(cacheKey, cacheData, OTP_TTL);
+ const cacheKey = `otp:${otp}`;
+ await CacheManager.setJson(cacheKey, cacheData, OTP_TTL);
- const orgCacheKey = `orgOTPExists:${orgId}`;
- await CacheManager.setJson(orgCacheKey, otp, OTP_TTL);
+ const orgCacheKey = `orgOTPExists:${orgId}`;
+ await CacheManager.setJson(orgCacheKey, otp, OTP_TTL);
- res.status(200).json({
- otp,
- });
- },
- });
+ res.status(200).json({
+ otp,
+ });
+ },
+ });
devRouter.post("/otp", withOrgAuth, handleCreateOtp);
export const generateRandomKey = (lengthInBytes: number = 32): string => {
- if (lengthInBytes <= 0) {
- throw new Error("Key length must be a positive number.");
- }
- return crypto.randomBytes(lengthInBytes).toString("hex");
+ if (lengthInBytes <= 0) {
+ throw new Error("Key length must be a positive number.");
+ }
+ return crypto.randomBytes(lengthInBytes).toString("hex");
};
export const handleGetOtp = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Get OTP",
- handler: async () => {
- const { db, env } = req;
- const { otp } = req.params;
- const cacheKey = `otp:${otp}`;
- const cacheData = await CacheManager.getJson(cacheKey);
- if (!cacheData) {
- res.status(404).json({ error: "OTP not found" });
- return;
- }
+ routeHandler({
+ req,
+ res,
+ action: "Get OTP",
+ handler: async () => {
+ const { db, env } = req;
+ const { otp } = req.params;
+ const cacheKey = `otp:${otp}`;
+ const cacheData = await CacheManager.getJson(cacheKey);
+ if (!cacheData) {
+ res.status(404).json({ error: "OTP not found" });
+ return;
+ }
- // Generate API key for the OTP
- const sandboxKey = await createKey({
- db,
- env: AppEnv.Sandbox,
- name: `Autumn Key CLI`,
- orgId: cacheData.orgId,
- prefix: "am_sk_test",
- meta: {
- fromCli: true,
- generatedAt: new Date().toISOString(),
- },
- userId: req.user?.id,
- });
+ // Generate API key for the OTP
+ const sandboxKey = await createKey({
+ db,
+ env: AppEnv.Sandbox,
+ name: `Autumn Key CLI`,
+ orgId: cacheData.orgId,
+ prefix: "am_sk_test",
+ meta: {
+ fromCli: true,
+ generatedAt: new Date().toISOString(),
+ },
+ userId: req.user?.id,
+ });
- const prodKey = await createKey({
- db,
- env: AppEnv.Live,
- name: `Autumn Key CLI`,
- orgId: cacheData.orgId,
- prefix: "am_sk_live",
- meta: {
- fromCli: true,
- generatedAt: new Date().toISOString(),
- },
- userId: req.user?.id,
- });
+ const prodKey = await createKey({
+ db,
+ env: AppEnv.Live,
+ name: `Autumn Key CLI`,
+ orgId: cacheData.orgId,
+ prefix: "am_sk_live",
+ meta: {
+ fromCli: true,
+ generatedAt: new Date().toISOString(),
+ },
+ userId: req.user?.id,
+ });
- let org = await OrgService.get({
- db: req.db,
- orgId: cacheData.orgId,
- });
+ let org = await OrgService.get({
+ db: req.db,
+ orgId: cacheData.orgId,
+ });
- let stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox });
+ let stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox });
- let responseData = {
- ...cacheData,
- stripe_connected: stripeConnected,
- sandboxKey,
- prodKey,
- };
+ let responseData = {
+ ...cacheData,
+ stripe_connected: stripeConnected,
+ sandboxKey,
+ prodKey,
+ };
- await CacheManager.invalidate({
- action: "otp",
- value: otp,
- });
- await CacheManager.invalidate({
- action: "orgOTPExists",
- value: cacheData.orgId,
- });
+ await CacheManager.invalidate({
+ action: "otp",
+ value: otp,
+ });
+ await CacheManager.invalidate({
+ action: "orgOTPExists",
+ value: cacheData.orgId,
+ });
- if (!stripeConnected) {
- // we need to generate a key for the CLI to use.
- let key = generateRandomKey();
- responseData.stripeFlowAuthKey = key;
- let stripeCacheData = {
- orgId: cacheData.orgId,
- };
- await CacheManager.setJson(key, stripeCacheData, OTP_TTL);
- }
+ if (!stripeConnected) {
+ // we need to generate a key for the CLI to use.
+ let key = generateRandomKey();
+ responseData.stripeFlowAuthKey = key;
+ let stripeCacheData = {
+ orgId: cacheData.orgId,
+ };
+ await CacheManager.setJson(key, stripeCacheData, OTP_TTL);
+ }
- res.status(200).json(responseData);
- },
- });
+ res.status(200).json(responseData);
+ },
+ });
devRouter.post("/cli/stripe", async (req: any, res: any) => {
- routeHandler({
- req,
- res,
- action: "Get Stripe Flow Auth Key",
- handler: async () => {
- const { db, logtail: logger } = req;
- const key = req.headers["authorization"];
- if (!key) {
- res.status(401).json({ message: "Unauthorized" });
- return;
- }
+ routeHandler({
+ req,
+ res,
+ action: "Get Stripe Flow Auth Key",
+ handler: async () => {
+ const { db, logtail: logger } = req;
+ const key = req.headers["authorization"];
+ if (!key) {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
- const cacheData = await CacheManager.getJson(key);
- if (!cacheData) {
- res.status(404).json({ message: "Key not found" });
- return;
- }
+ const cacheData = await CacheManager.getJson(key);
+ if (!cacheData) {
+ res.status(404).json({ message: "Key not found" });
+ return;
+ }
- const { orgId } = cacheData;
- const { stripeTestKey, stripeLiveKey } = req.body;
+ const { orgId } = cacheData;
+ const { stripeTestKey, stripeLiveKey } = req.body;
- await clearOrgCache({
- db,
- orgId,
- logger,
- });
+ await clearOrgCache({
+ db,
+ orgId,
+ logger,
+ });
- await checkKeyValid(stripeTestKey);
- await checkKeyValid(stripeLiveKey);
+ await checkKeyValid(stripeTestKey);
+ await checkKeyValid(stripeLiveKey);
- let testWebhook: Stripe.WebhookEndpoint;
- let liveWebhook: Stripe.WebhookEndpoint;
+ let testWebhook: Stripe.WebhookEndpoint;
+ let liveWebhook: Stripe.WebhookEndpoint;
- try {
- testWebhook = await createWebhookEndpoint(
- stripeTestKey,
- AppEnv.Sandbox,
- orgId
- );
+ try {
+ testWebhook = await createWebhookEndpoint(
+ stripeTestKey,
+ AppEnv.Sandbox,
+ orgId,
+ );
- liveWebhook = await createWebhookEndpoint(
- stripeLiveKey,
- AppEnv.Live,
- orgId
- );
- } catch (error) {
- console.log(error);
- res.status(500).json({ message: "Error creating stripe webhook" });
- return;
- }
+ liveWebhook = await createWebhookEndpoint(
+ stripeLiveKey,
+ AppEnv.Live,
+ orgId,
+ );
+ } catch (error) {
+ console.log(error);
+ res.status(500).json({ message: "Error creating stripe webhook" });
+ return;
+ }
- await OrgService.update({
- db,
- orgId: orgId,
- updates: {
- stripe_connected: true,
- default_currency: "usd",
- stripe_config: {
- test_api_key: encryptData(stripeTestKey),
- live_api_key: encryptData(stripeLiveKey),
- test_webhook_secret: encryptData(testWebhook.secret as string),
- live_webhook_secret: encryptData(liveWebhook.secret as string),
- // success_url: "https://useautumn.com",
- },
- },
- });
+ await OrgService.update({
+ db,
+ orgId: orgId,
+ updates: {
+ stripe_connected: true,
+ default_currency: "usd",
+ stripe_config: {
+ test_api_key: encryptData(stripeTestKey),
+ live_api_key: encryptData(stripeLiveKey),
+ test_webhook_secret: encryptData(testWebhook.secret as string),
+ live_webhook_secret: encryptData(liveWebhook.secret as string),
+ // success_url: "https://useautumn.com",
+ },
+ },
+ });
- let redisClient = await CacheManager.getClient();
- if (!redisClient) {
- res.status(500).json({ message: "Cache client not initialized" });
- return;
- }
+ let redisClient = await CacheManager.getClient();
+ if (!redisClient) {
+ res.status(500).json({ message: "Cache client not initialized" });
+ return;
+ }
- await redisClient.del(key);
+ await redisClient.del(key);
- res.status(200).json({
- message: "Stripe keys updated",
- });
- },
- });
+ res.status(200).json({
+ message: "Stripe keys updated",
+ });
+ },
+ });
});
devRouter.get("/otp/:otp", handleGetOtp);
diff --git a/server/src/internal/emails/sendInvitationEmail.ts b/server/src/internal/emails/sendInvitationEmail.ts
index 2f56134f1..b1d1a1b56 100644
--- a/server/src/internal/emails/sendInvitationEmail.ts
+++ b/server/src/internal/emails/sendInvitationEmail.ts
@@ -4,7 +4,7 @@ import { safeResend } from "@/external/resend/safeResend.js";
import { FROM_AUTUMN } from "./constants.js";
const getInvitationEmailBody = ({ orgName }: { orgName: string }) => {
- return `Hey there! You've been invited to join ${orgName} on Autumn.
+ return `Hey there! You've been invited to join ${orgName} on Autumn.
Click the link below to create an account / sign in to Autumn and accept the invitation.
@@ -13,22 +13,22 @@ ${process.env.CLIENT_URL}/sign-in
};
export const sendInvitationEmail = safeResend({
- fn: async ({
- email,
- orgName,
- inviteLink,
- }: {
- email: string;
- orgName: string;
- inviteLink?: string;
- }) => {
- logger.info(`Sending invitation email to ${email}`);
- await sendTextEmail({
- from: FROM_AUTUMN,
- to: email,
- subject: `Join ${orgName} on Autumn`,
- body: getInvitationEmailBody({ orgName }),
- });
- },
- action: "send org invitation email",
+ fn: async ({
+ email,
+ orgName,
+ inviteLink,
+ }: {
+ email: string;
+ orgName: string;
+ inviteLink?: string;
+ }) => {
+ logger.info(`Sending invitation email to ${email}`);
+ await sendTextEmail({
+ from: FROM_AUTUMN,
+ to: email,
+ subject: `Join ${orgName} on Autumn`,
+ body: getInvitationEmailBody({ orgName }),
+ });
+ },
+ action: "send org invitation email",
});
diff --git a/server/src/internal/emails/sendMigrationEmail.ts b/server/src/internal/emails/sendMigrationEmail.ts
index 08a7b4d15..088d358b7 100644
--- a/server/src/internal/emails/sendMigrationEmail.ts
+++ b/server/src/internal/emails/sendMigrationEmail.ts
@@ -6,32 +6,32 @@ import { safeResend } from "@/external/resend/safeResend.js";
import { FROM_AUTUMN } from "./constants.js";
export const sendMigrationEmail = safeResend({
- fn: async ({
- db,
- migrationJobId,
- org,
- }: {
- db: DrizzleCli;
- migrationJobId: string;
- org: Organization;
- }) => {
- let migrationJob = await MigrationService.getJob({
- db,
- id: migrationJobId,
- });
+ fn: async ({
+ db,
+ migrationJobId,
+ org,
+ }: {
+ db: DrizzleCli;
+ migrationJobId: string;
+ org: Organization;
+ }) => {
+ let migrationJob = await MigrationService.getJob({
+ db,
+ id: migrationJobId,
+ });
- // Send email
- let getCustomersStep =
- migrationJob.step_details[MigrationJobStep.GetCustomers];
- let migrateStep =
- migrationJob.step_details[MigrationJobStep.MigrateCustomers];
+ // Send email
+ let getCustomersStep =
+ migrationJob.step_details[MigrationJobStep.GetCustomers];
+ let migrateStep =
+ migrationJob.step_details[MigrationJobStep.MigrateCustomers];
- console.log("Sending migration email");
- await sendTextEmail({
- from: FROM_AUTUMN,
- to: "johnyeocx@gmail.com",
- subject: `Migration Job Finished -- ${migrationJob.id}`,
- body: `
+ console.log("Sending migration email");
+ await sendTextEmail({
+ from: FROM_AUTUMN,
+ to: "johnyeocx@gmail.com",
+ subject: `Migration Job Finished -- ${migrationJob.id}`,
+ body: `
ORG: ${org.id}, ${org.slug}
Step: Get migration customers
@@ -45,7 +45,7 @@ export const sendMigrationEmail = safeResend({
2. Failed customers:
${migrateStep?.failed_customers}
`,
- });
- },
- action: "send migration email",
+ });
+ },
+ action: "send migration email",
});
diff --git a/server/src/internal/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts
index 1f4d2ad35..2a08c8d9f 100644
--- a/server/src/internal/emails/sendOTPEmail.ts
+++ b/server/src/internal/emails/sendOTPEmail.ts
@@ -4,24 +4,24 @@ import OTPEmail from "@emails/OTPEmail.js";
import { FROM_AUTUMN } from "./constants.js";
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {
- if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
- logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
- return;
- }
+ if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
+ logger.warn(`RESEND NOT SET UP, SIGN IN OTP: ${otp}`);
+ return;
+ }
- try {
- logger.info(`Sending OTP email to ${email}`);
- const resend = createResendCli();
- const { error } = await resend.emails.send({
- from: FROM_AUTUMN,
- to: email,
- subject: "Your verification code for Autumn",
- react: OTPEmail({ otpCode: otp }),
- });
- if (error) throw error;
- } catch (error: any) {
- logger.error(`Error sending OTP email: ${error.message}`);
- }
+ try {
+ logger.info(`Sending OTP email to ${email}`);
+ const resend = createResendCli();
+ const { error } = await resend.emails.send({
+ from: FROM_AUTUMN,
+ to: email,
+ subject: "Your verification code for Autumn",
+ react: OTPEmail({ otpCode: otp }),
+ });
+ if (error) throw error;
+ } catch (error: any) {
+ logger.error(`Error sending OTP email: ${error.message}`);
+ }
};
export default sendOTPEmail;
diff --git a/server/src/internal/emails/sendOnboardingEmail.ts b/server/src/internal/emails/sendOnboardingEmail.ts
index 22a6da83c..5a4c55892 100644
--- a/server/src/internal/emails/sendOnboardingEmail.ts
+++ b/server/src/internal/emails/sendOnboardingEmail.ts
@@ -3,7 +3,7 @@ import { safeResend } from "@/external/resend/safeResend.js";
import { FROM_AYUSH } from "./constants.js";
const getWelcomeEmailBody = (userFirstName: string) => {
- return `
+ return `
Hey ${userFirstName} :)
Just wanted to say thank you for signing up to Autumn!
@@ -20,16 +20,16 @@ Co-founder, Autumn
};
export const sendOnboardingEmail = safeResend({
- fn: async ({ name, email }: { name: string; email: string }) => {
- const firstName = name.split(" ")[0];
+ fn: async ({ name, email }: { name: string; email: string }) => {
+ const firstName = name.split(" ")[0];
- await sendHtmlEmail({
- from: FROM_AYUSH,
- to: email,
- subject: "Anything I can help with?",
- body: getWelcomeEmailBody(firstName),
- replyTo: "ayush@useautumn.com",
- });
- },
- action: "send onboarding email",
+ await sendHtmlEmail({
+ from: FROM_AYUSH,
+ to: email,
+ subject: "Anything I can help with?",
+ body: getWelcomeEmailBody(firstName),
+ replyTo: "ayush@useautumn.com",
+ });
+ },
+ action: "send onboarding email",
});
diff --git a/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts b/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts
index bade25a6a..7939ab470 100644
--- a/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts
+++ b/server/src/internal/entities/entityUtils/entityInvoiceUtils.ts
@@ -4,45 +4,45 @@ import { Feature, getFeatureName } from "@autumn/shared";
import { AppEnv, Entity } from "autumn-js";
export const getEntityInvoiceDescription = async ({
- db,
- internalEntityId,
- features,
- logger,
+ db,
+ internalEntityId,
+ features,
+ logger,
}: {
- db: DrizzleCli;
- internalEntityId: string;
- features: Feature[];
- logger: any;
+ db: DrizzleCli;
+ internalEntityId: string;
+ features: Feature[];
+ logger: any;
}) => {
- try {
- let entity = await EntityService.getByInternalId({
- db,
- internalId: internalEntityId,
- });
+ try {
+ let entity = await EntityService.getByInternalId({
+ db,
+ internalId: internalEntityId,
+ });
- let feature = features.find(
- (f) => f.internal_id == entity?.internal_feature_id,
- );
+ let feature = features.find(
+ (f) => f.internal_id == entity?.internal_feature_id,
+ );
- let entDetails = "";
- if (entity.name) {
- entDetails = `${entity.name}${entity.id ? ` (ID: ${entity.id})` : ""}`;
- } else if (entity.id) {
- entDetails = `${entity.id}`;
- }
+ let entDetails = "";
+ if (entity.name) {
+ entDetails = `${entity.name}${entity.id ? ` (ID: ${entity.id})` : ""}`;
+ } else if (entity.id) {
+ entDetails = `${entity.id}`;
+ }
- if (feature && entDetails) {
- let featureName = getFeatureName({
- feature,
- plural: false,
- capitalize: true,
- });
- return `${featureName}: ${entDetails}`;
- }
+ if (feature && entDetails) {
+ let featureName = getFeatureName({
+ feature,
+ plural: false,
+ capitalize: true,
+ });
+ return `${featureName}: ${entDetails}`;
+ }
- return "";
- } catch (error) {
- logger.error(`Failed to get entity invoice description`, { error });
- return "";
- }
+ return "";
+ } catch (error) {
+ logger.error(`Failed to get entity invoice description`, { error });
+ return "";
+ }
};
diff --git a/server/src/internal/entities/entityUtils/entityUtils.ts b/server/src/internal/entities/entityUtils/entityUtils.ts
index 166d54306..5d044e326 100644
--- a/server/src/internal/entities/entityUtils/entityUtils.ts
+++ b/server/src/internal/entities/entityUtils/entityUtils.ts
@@ -1,38 +1,38 @@
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
import {
- AppEnv,
- Entity,
- FeatureType,
- FullCusEntWithFullCusProduct,
+ AppEnv,
+ Entity,
+ FeatureType,
+ FullCusEntWithFullCusProduct,
} from "@autumn/shared";
export const constructEntity = ({
- inputEntity,
- feature,
- internalCustomerId,
- orgId,
- env,
- deleted = false,
+ inputEntity,
+ feature,
+ internalCustomerId,
+ orgId,
+ env,
+ deleted = false,
}: {
- inputEntity: any;
- feature: any;
- internalCustomerId: string;
- orgId: string;
- env: AppEnv;
- deleted?: boolean;
+ inputEntity: any;
+ feature: any;
+ internalCustomerId: string;
+ orgId: string;
+ env: AppEnv;
+ deleted?: boolean;
}) => {
- let entity: Entity = {
- internal_id: generateId("ety"),
- id: inputEntity.id,
- name: inputEntity.name,
- internal_customer_id: internalCustomerId,
- feature_id: feature.id,
- internal_feature_id: feature.internal_id,
- org_id: orgId,
- env,
- deleted,
- created_at: Date.now(),
- };
+ let entity: Entity = {
+ internal_id: generateId("ety"),
+ id: inputEntity.id,
+ name: inputEntity.name,
+ internal_customer_id: internalCustomerId,
+ feature_id: feature.id,
+ internal_feature_id: feature.internal_id,
+ org_id: orgId,
+ env,
+ deleted,
+ created_at: Date.now(),
+ };
- return entity;
+ return entity;
};
diff --git a/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts b/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts
index 3efcdf5b1..f79ac870e 100644
--- a/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts
+++ b/server/src/internal/entities/handlers/handleCreateEntity/autoCreateEntity.ts
@@ -7,97 +7,97 @@ import { EntityService } from "@/internal/api/entities/EntityService.js";
import { constructEntity } from "../../entityUtils/entityUtils.js";
export const autoCreateEntity = async ({
- req,
- logger,
- customer,
- entityId,
- entityData,
+ req,
+ logger,
+ customer,
+ entityId,
+ entityData,
}: {
- req: ExtendedRequest;
- logger: any;
- entityId: string;
- customer: FullCustomer;
- entityData?: CreateEntity;
+ req: ExtendedRequest;
+ logger: any;
+ entityId: string;
+ customer: FullCustomer;
+ entityData?: CreateEntity;
}) => {
- // Validate CreatEntity
- // Failed to auto-create entity, no `feature_id` provided. Please pass in `feature_id` into the `entity_data` field of the request body",
- if (!entityData || !entityData.feature_id) {
- throw new RecaseError({
- message: `Entity with id ${entityData?.id || "unknown"} not found. To automatically create this entity, please pass in 'feature_id' into the 'entity_data' field of the request body.`,
- code: ErrCode.InvalidInputs,
- });
- }
+ // Validate CreatEntity
+ // Failed to auto-create entity, no `feature_id` provided. Please pass in `feature_id` into the `entity_data` field of the request body",
+ if (!entityData || !entityData.feature_id) {
+ throw new RecaseError({
+ message: `Entity with id ${entityData?.id || "unknown"} not found. To automatically create this entity, please pass in 'feature_id' into the 'entity_data' field of the request body.`,
+ code: ErrCode.InvalidInputs,
+ });
+ }
- const { features, db } = req;
+ const { features, db } = req;
- const feature = features.find((f) => f.id === entityData.feature_id);
+ const feature = features.find((f) => f.id === entityData.feature_id);
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${entityData.feature_id} not found`,
- code: ErrCode.InvalidInputs,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${entityData.feature_id} not found`,
+ code: ErrCode.InvalidInputs,
+ });
+ }
- const inputEntity = {
- id: entityId,
- name: entityData.name,
- feature_id: entityData.feature_id,
- };
+ const inputEntity = {
+ id: entityId,
+ name: entityData.name,
+ feature_id: entityData.feature_id,
+ };
- for (const cusProduct of customer.customer_products) {
- await createEntityForCusProduct({
- req,
- customer,
- cusProduct,
- inputEntities: [inputEntity],
- fromAutoCreate: true,
- logger,
- });
- }
+ for (const cusProduct of customer.customer_products) {
+ await createEntityForCusProduct({
+ req,
+ customer,
+ cusProduct,
+ inputEntities: [inputEntity],
+ fromAutoCreate: true,
+ logger,
+ });
+ }
- let replaceEntity = await EntityService.getNull({
- db,
- orgId: customer.org_id,
- env: customer.env,
- internalCustomerId: customer.internal_id,
- internalFeatureId: feature.internal_id,
- });
+ let replaceEntity = await EntityService.getNull({
+ db,
+ orgId: customer.org_id,
+ env: customer.env,
+ internalCustomerId: customer.internal_id,
+ internalFeatureId: feature.internal_id,
+ });
- if (replaceEntity) {
- return await EntityService.update({
- db,
- internalId: replaceEntity.internal_id!,
- update: {
- id: entityId,
- name: entityData.name,
- },
- });
- } else {
- try {
- const result = await EntityService.insert({
- db,
- data: [
- constructEntity({
- inputEntity,
- feature,
- internalCustomerId: customer.internal_id,
- orgId: customer.org_id,
- env: customer.env,
- }),
- ],
- });
- } catch (error: any) {
- if (error.code == "23505") {
- return await EntityService.get({
- db,
- id: entityId,
- internalCustomerId: customer.internal_id,
- internalFeatureId: feature.internal_id,
- });
- } else {
- throw error;
- }
- }
- }
+ if (replaceEntity) {
+ return await EntityService.update({
+ db,
+ internalId: replaceEntity.internal_id!,
+ update: {
+ id: entityId,
+ name: entityData.name,
+ },
+ });
+ } else {
+ try {
+ const result = await EntityService.insert({
+ db,
+ data: [
+ constructEntity({
+ inputEntity,
+ feature,
+ internalCustomerId: customer.internal_id,
+ orgId: customer.org_id,
+ env: customer.env,
+ }),
+ ],
+ });
+ } catch (error: any) {
+ if (error.code == "23505") {
+ return await EntityService.get({
+ db,
+ id: entityId,
+ internalCustomerId: customer.internal_id,
+ internalFeatureId: feature.internal_id,
+ });
+ } else {
+ throw error;
+ }
+ }
+ }
};
diff --git a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts
index 784c02cba..2ab8c5844 100644
--- a/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts
+++ b/server/src/internal/entities/handlers/handleCreateEntity/createEntityForCusProduct.ts
@@ -1,12 +1,12 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import {
- getRelatedCusPrice,
- getResetBalance,
+ getRelatedCusPrice,
+ getResetBalance,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import {
- findLinkedCusEnts,
- findMainCusEntForFeature,
+ findLinkedCusEnts,
+ findMainCusEntForFeature,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
import { getReps } from "@/trigger/arrearProratedUsage/handleProratedUpgrade.js";
@@ -14,176 +14,176 @@ import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- CreateEntity,
- ErrCode,
- Feature,
- FullCusProduct,
- FullCustomer,
- FullCustomerEntitlement,
- Replaceable,
+ CreateEntity,
+ ErrCode,
+ Feature,
+ FullCusProduct,
+ FullCustomer,
+ FullCustomerEntitlement,
+ Replaceable,
} from "@autumn/shared";
export const updateLinkedCusEnt = async ({
- db,
- linkedCusEnt,
- inputEntities,
- entityToReplacement,
+ db,
+ linkedCusEnt,
+ inputEntities,
+ entityToReplacement,
}: {
- db: DrizzleCli;
- linkedCusEnt: FullCustomerEntitlement;
- inputEntities: CreateEntity[];
- entityToReplacement: Record;
+ db: DrizzleCli;
+ linkedCusEnt: FullCustomerEntitlement;
+ inputEntities: CreateEntity[];
+ entityToReplacement: Record;
}) => {
- let newEntities = structuredClone(linkedCusEnt.entities) || {};
- for (const entity of inputEntities) {
- let replaceableId = entityToReplacement[entity.id];
- let replaceableInEntities = replaceableId
- ? newEntities[replaceableId]
- : null;
+ let newEntities = structuredClone(linkedCusEnt.entities) || {};
+ for (const entity of inputEntities) {
+ let replaceableId = entityToReplacement[entity.id];
+ let replaceableInEntities = replaceableId
+ ? newEntities[replaceableId]
+ : null;
- if (replaceableInEntities) {
- newEntities[entity.id] = {
- ...replaceableInEntities,
- id: entity.id,
- };
- delete newEntities[replaceableId];
- } else {
- let balance = linkedCusEnt.entitlement.allowance!;
- newEntities[entity.id] = {
- id: entity.id,
- balance,
- adjustment: 0,
- };
- }
+ if (replaceableInEntities) {
+ newEntities[entity.id] = {
+ ...replaceableInEntities,
+ id: entity.id,
+ };
+ delete newEntities[replaceableId];
+ } else {
+ let balance = linkedCusEnt.entitlement.allowance!;
+ newEntities[entity.id] = {
+ id: entity.id,
+ balance,
+ adjustment: 0,
+ };
+ }
- await CusEntService.update({
- db,
- id: linkedCusEnt.id,
- updates: {
- entities: newEntities,
- },
- });
- }
+ await CusEntService.update({
+ db,
+ id: linkedCusEnt.id,
+ updates: {
+ entities: newEntities,
+ },
+ });
+ }
};
export const createEntityForCusProduct = async ({
- req,
- customer,
- cusProduct,
- inputEntities,
- logger,
- fromAutoCreate = false,
+ req,
+ customer,
+ cusProduct,
+ inputEntities,
+ logger,
+ fromAutoCreate = false,
}: {
- req: ExtendedRequest;
- customer: FullCustomer;
- cusProduct: FullCusProduct;
- inputEntities: CreateEntity[];
- logger: any;
- fromAutoCreate?: boolean;
+ req: ExtendedRequest;
+ customer: FullCustomer;
+ cusProduct: FullCusProduct;
+ inputEntities: CreateEntity[];
+ logger: any;
+ fromAutoCreate?: boolean;
}) => {
- const featureToEntities = inputEntities.reduce(
- (acc, entity) => {
- acc[entity.feature_id!] = [...(acc[entity.feature_id!] || []), entity];
- return acc;
- },
- {} as Record,
- );
+ const featureToEntities = inputEntities.reduce(
+ (acc, entity) => {
+ acc[entity.feature_id!] = [...(acc[entity.feature_id!] || []), entity];
+ return acc;
+ },
+ {} as Record,
+ );
- const { db, env, org, features } = req;
+ const { db, env, org, features } = req;
- const cusEnts = cusProduct.customer_entitlements;
- const cusPrices = cusProduct.customer_prices;
+ const cusEnts = cusProduct.customer_entitlements;
+ const cusPrices = cusProduct.customer_prices;
- for (const featureId in featureToEntities) {
- const inputEntities = featureToEntities[featureId]!;
- const feature = features.find((f: any) => f.id === featureId)!;
+ for (const featureId in featureToEntities) {
+ const inputEntities = featureToEntities[featureId]!;
+ const feature = features.find((f: any) => f.id === featureId)!;
- const mainCusEnt = findMainCusEntForFeature({
- cusEnts,
- feature,
- });
+ const mainCusEnt = findMainCusEntForFeature({
+ cusEnts,
+ feature,
+ });
- if (mainCusEnt) {
- const cusPrice = getRelatedCusPrice(mainCusEnt, cusPrices);
+ if (mainCusEnt) {
+ const cusPrice = getRelatedCusPrice(mainCusEnt, cusPrices);
- if (fromAutoCreate && cusPrice) {
- throw new RecaseError({
- message: `Failed to auto create entity for feature ${feature.name} because it is a paid feature.`,
- code: ErrCode.InvalidInputs,
- });
- }
- }
+ if (fromAutoCreate && cusPrice) {
+ throw new RecaseError({
+ message: `Failed to auto create entity for feature ${feature.name} because it is a paid feature.`,
+ code: ErrCode.InvalidInputs,
+ });
+ }
+ }
- // 1. If main cus ent:
- let deletedReplaceables: Replaceable[] = [];
- if (mainCusEnt) {
- const originalBalance = mainCusEnt.balance || 0;
- const newBalance = originalBalance - inputEntities.length;
+ // 1. If main cus ent:
+ let deletedReplaceables: Replaceable[] = [];
+ if (mainCusEnt) {
+ const originalBalance = mainCusEnt.balance || 0;
+ const newBalance = originalBalance - inputEntities.length;
- let repsLength = getReps({
- cusEnt: mainCusEnt as any,
- prevBalance: originalBalance,
- newBalance,
- }).length;
- const innerNewBalance = newBalance + repsLength;
+ let repsLength = getReps({
+ cusEnt: mainCusEnt as any,
+ prevBalance: originalBalance,
+ newBalance,
+ }).length;
+ const innerNewBalance = newBalance + repsLength;
- // Check if new balance would exceed usage limit
- if (
- notNullish(mainCusEnt.entitlement.usage_limit) &&
- innerNewBalance < -mainCusEnt.entitlement.usage_limit!
- ) {
- throw new RecaseError({
- message: `Cannot create ${inputEntities.length} entities for feature ${feature.name} as it would exceed the usage limit.`,
- code: ErrCode.FeatureLimitReached,
- });
- }
+ // Check if new balance would exceed usage limit
+ if (
+ notNullish(mainCusEnt.entitlement.usage_limit) &&
+ innerNewBalance < -mainCusEnt.entitlement.usage_limit!
+ ) {
+ throw new RecaseError({
+ message: `Cannot create ${inputEntities.length} entities for feature ${feature.name} as it would exceed the usage limit.`,
+ code: ErrCode.FeatureLimitReached,
+ });
+ }
- const { deletedReplaceables: deletedReplaceables_, invoice } =
- await adjustAllowance({
- db,
- env,
- org,
- cusPrices,
- customer,
- affectedFeature: feature,
- cusEnt: { ...mainCusEnt, customer_product: cusProduct },
- originalBalance,
- newBalance,
- logger,
- errorIfIncomplete: true,
- });
+ const { deletedReplaceables: deletedReplaceables_, invoice } =
+ await adjustAllowance({
+ db,
+ env,
+ org,
+ cusPrices,
+ customer,
+ affectedFeature: feature,
+ cusEnt: { ...mainCusEnt, customer_product: cusProduct },
+ originalBalance,
+ newBalance,
+ logger,
+ errorIfIncomplete: true,
+ });
- deletedReplaceables = deletedReplaceables_ || [];
+ deletedReplaceables = deletedReplaceables_ || [];
- await CusEntService.decrement({
- db,
- id: mainCusEnt.id,
- amount: inputEntities.length - deletedReplaceables.length,
- });
- }
+ await CusEntService.decrement({
+ db,
+ id: mainCusEnt.id,
+ amount: inputEntities.length - deletedReplaceables.length,
+ });
+ }
- const entityToReplacement: Record = {};
- for (let i = 0; i < deletedReplaceables.length; i++) {
- const replaceable = deletedReplaceables[i];
- entityToReplacement[inputEntities[i].id] = replaceable.id;
+ const entityToReplacement: Record = {};
+ for (let i = 0; i < deletedReplaceables.length; i++) {
+ const replaceable = deletedReplaceables[i];
+ entityToReplacement[inputEntities[i].id] = replaceable.id;
- if (i >= inputEntities.length) {
- break;
- }
- }
+ if (i >= inputEntities.length) {
+ break;
+ }
+ }
- const linkedCusEnts = findLinkedCusEnts({
- cusEnts,
- feature,
- });
+ const linkedCusEnts = findLinkedCusEnts({
+ cusEnts,
+ feature,
+ });
- for (const linkedCusEnt of linkedCusEnts) {
- await updateLinkedCusEnt({
- db,
- linkedCusEnt,
- inputEntities,
- entityToReplacement,
- });
- }
- }
+ for (const linkedCusEnt of linkedCusEnts) {
+ await updateLinkedCusEnt({
+ db,
+ linkedCusEnt,
+ inputEntities,
+ entityToReplacement,
+ });
+ }
+ }
};
diff --git a/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts b/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts
index a2a011a69..ba74b244f 100644
--- a/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts
+++ b/server/src/internal/entities/handlers/handleCreateEntity/entityToAction.ts
@@ -3,98 +3,98 @@ import { nullish } from "@/utils/genUtils.js";
import { Entity } from "@autumn/shared";
export const logEntityToAction = ({
- entityToAction,
- logger,
+ entityToAction,
+ logger,
}: {
- entityToAction: any;
- logger: any;
+ entityToAction: any;
+ logger: any;
}) => {
- for (const id in entityToAction) {
- logger.info(
- `${id} - ${entityToAction[id].action}${
- entityToAction[id].replace
- ? ` (replace ${
- entityToAction[id].replace.id ||
- entityToAction[id].replace.internal_id
- })`
- : ""
- }`,
- );
- }
+ for (const id in entityToAction) {
+ logger.info(
+ `${id} - ${entityToAction[id].action}${
+ entityToAction[id].replace
+ ? ` (replace ${
+ entityToAction[id].replace.id ||
+ entityToAction[id].replace.internal_id
+ })`
+ : ""
+ }`,
+ );
+ }
};
// Probably going to change
export const getEntityToAction = ({
- inputEntities,
- existingEntities,
- feature,
- logger,
+ inputEntities,
+ existingEntities,
+ feature,
+ logger,
}: {
- inputEntities: any[];
- existingEntities: Entity[];
- feature: any;
- logger: any;
+ inputEntities: any[];
+ existingEntities: Entity[];
+ feature: any;
+ logger: any;
}) => {
- let entityToAction: any = {};
- let createCount = 0;
- let replacedEntities: string[] = [];
- for (const inputEntity of inputEntities) {
- let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id);
+ let entityToAction: any = {};
+ let createCount = 0;
+ let replacedEntities: string[] = [];
+ for (const inputEntity of inputEntities) {
+ let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id);
- if (curEntity && curEntity.deleted) {
- entityToAction[inputEntity.id] = {
- action: "replace",
- replace: curEntity,
- entity: inputEntity,
- };
- replacedEntities.push(curEntity.id);
- continue;
- }
+ if (curEntity && curEntity.deleted) {
+ entityToAction[inputEntity.id] = {
+ action: "replace",
+ replace: curEntity,
+ entity: inputEntity,
+ };
+ replacedEntities.push(curEntity.id);
+ continue;
+ }
- let replaced = false;
+ let replaced = false;
- for (const entity of existingEntities) {
- if (entity.deleted && !replacedEntities.includes(entity.id)) {
- replaced = true;
- replacedEntities.push(entity.id);
+ for (const entity of existingEntities) {
+ if (entity.deleted && !replacedEntities.includes(entity.id)) {
+ replaced = true;
+ replacedEntities.push(entity.id);
- entityToAction[inputEntity.id] = {
- action: "replace",
- replace: entity,
- entity: inputEntity,
- };
- break;
- }
+ entityToAction[inputEntity.id] = {
+ action: "replace",
+ replace: entity,
+ entity: inputEntity,
+ };
+ break;
+ }
- // If there's an entity with null ID and cur input entity has an ID, fill it up!
- if (
- nullish(entity.id) &&
- notNullish(inputEntity.id) &&
- !replacedEntities.includes(entity.internal_id) &&
- entity.feature_id === feature.id
- ) {
- entityToAction[inputEntity.id] = {
- action: "replace",
- replace: entity,
- entity: inputEntity,
- };
+ // If there's an entity with null ID and cur input entity has an ID, fill it up!
+ if (
+ nullish(entity.id) &&
+ notNullish(inputEntity.id) &&
+ !replacedEntities.includes(entity.internal_id) &&
+ entity.feature_id === feature.id
+ ) {
+ entityToAction[inputEntity.id] = {
+ action: "replace",
+ replace: entity,
+ entity: inputEntity,
+ };
- replacedEntities.push(entity.internal_id);
- replaced = true;
- break;
- }
- }
+ replacedEntities.push(entity.internal_id);
+ replaced = true;
+ break;
+ }
+ }
- if (!replaced) {
- entityToAction[inputEntity.id] = {
- action: "create",
- entity: inputEntity,
- };
- createCount++;
- }
- }
+ if (!replaced) {
+ entityToAction[inputEntity.id] = {
+ action: "create",
+ entity: inputEntity,
+ };
+ createCount++;
+ }
+ }
- logEntityToAction({ entityToAction, logger });
+ logEntityToAction({ entityToAction, logger });
- return entityToAction;
+ return entityToAction;
};
diff --git a/server/src/internal/entities/handlers/handleCreateEntity/getInputEntities.ts b/server/src/internal/entities/handlers/handleCreateEntity/getInputEntities.ts
index 2535ffe19..4373dba74 100644
--- a/server/src/internal/entities/handlers/handleCreateEntity/getInputEntities.ts
+++ b/server/src/internal/entities/handlers/handleCreateEntity/getInputEntities.ts
@@ -5,87 +5,87 @@ import { CreateEntity, CustomerData, Entity, ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const validateAndGetInputEntities = async ({
- req,
- customerId,
- customerData,
- createEntityData,
- logger,
+ req,
+ customerId,
+ customerData,
+ createEntityData,
+ logger,
}: {
- req: ExtendedRequest;
- customerId: string;
- customerData?: CustomerData;
- createEntityData: CreateEntity[] | CreateEntity;
- logger: any;
+ req: ExtendedRequest;
+ customerId: string;
+ customerData?: CustomerData;
+ createEntityData: CreateEntity[] | CreateEntity;
+ logger: any;
}) => {
- const { features } = req;
+ const { features } = req;
- // 1. Get customer
- let customer = await getOrCreateCustomer({
- req,
- customerId,
- customerData,
- withEntities: true,
- });
+ // 1. Get customer
+ let customer = await getOrCreateCustomer({
+ req,
+ customerId,
+ customerData,
+ withEntities: true,
+ });
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${customerId} not found`,
- code: ErrCode.CustomerNotFound,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${customerId} not found`,
+ code: ErrCode.CustomerNotFound,
+ });
+ }
- // 2. Get input entities
- let inputEntities: any[] = [];
- if (Array.isArray(createEntityData)) {
- inputEntities = createEntityData;
- } else {
- inputEntities = [createEntityData];
- }
+ // 2. Get input entities
+ let inputEntities: any[] = [];
+ if (Array.isArray(createEntityData)) {
+ inputEntities = createEntityData;
+ } else {
+ inputEntities = [createEntityData];
+ }
- for (const entity of inputEntities) {
- let feature = features.find((f: any) => f.id === entity.feature_id);
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${entity.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- });
- }
- }
+ for (const entity of inputEntities) {
+ let feature = features.find((f: any) => f.id === entity.feature_id);
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${entity.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ });
+ }
+ }
- let cusProducts = customer.customer_products;
- let existingEntities = customer.entities;
+ let cusProducts = customer.customer_products;
+ let existingEntities = customer.entities;
- let noIdEntities = existingEntities.filter((e: Entity) => !e.id);
- let noIdNewEntities = inputEntities.filter((e: CreateEntity) => !e.id);
+ let noIdEntities = existingEntities.filter((e: Entity) => !e.id);
+ let noIdNewEntities = inputEntities.filter((e: CreateEntity) => !e.id);
- if (noIdEntities.length + noIdNewEntities.length > 1) {
- throw new RecaseError({
- message: "Can only have one entity with no ID",
- code: ErrCode.EntityIdRequired,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (noIdEntities.length + noIdNewEntities.length > 1) {
+ throw new RecaseError({
+ message: "Can only have one entity with no ID",
+ code: ErrCode.EntityIdRequired,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- for (const entity of existingEntities) {
- if (inputEntities.some((e: any) => e.id === entity.id) && !entity.deleted) {
- throw new RecaseError({
- message: `Entity ${entity.id} already exists`,
- code: "ENTITY_ALREADY_EXISTS",
- data: {
- entity,
- },
- statusCode: StatusCodes.CONFLICT,
- });
- }
- }
+ for (const entity of existingEntities) {
+ if (inputEntities.some((e: any) => e.id === entity.id) && !entity.deleted) {
+ throw new RecaseError({
+ message: `Entity ${entity.id} already exists`,
+ code: "ENTITY_ALREADY_EXISTS",
+ data: {
+ entity,
+ },
+ statusCode: StatusCodes.CONFLICT,
+ });
+ }
+ }
- return {
- customer,
- features,
- inputEntities,
- // feature_id,
- // feature,
- cusProducts,
- existingEntities,
- };
+ return {
+ customer,
+ features,
+ inputEntities,
+ // feature_id,
+ // feature,
+ cusProducts,
+ existingEntities,
+ };
};
diff --git a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts
index f3a2f92e2..64b895e45 100644
--- a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts
+++ b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts
@@ -9,140 +9,140 @@ import { validateAndGetInputEntities } from "./getInputEntities.js";
import { createEntityForCusProduct } from "./createEntityForCusProduct.js";
export const createEntities = async ({
- req,
- logger,
- customerId,
- customerData,
- createEntityData,
- withAutumnId = false,
- apiVersion,
- fromAutoCreate = false,
+ req,
+ logger,
+ customerId,
+ customerData,
+ createEntityData,
+ withAutumnId = false,
+ apiVersion,
+ fromAutoCreate = false,
}: {
- req: ExtendedRequest;
- customerData?: CustomerData;
- logger: any;
- customerId: string;
- createEntityData: CreateEntity[] | CreateEntity;
- withAutumnId?: boolean;
- apiVersion?: APIVersion;
- fromAutoCreate?: boolean;
+ req: ExtendedRequest;
+ customerData?: CustomerData;
+ logger: any;
+ customerId: string;
+ createEntityData: CreateEntity[] | CreateEntity;
+ withAutumnId?: boolean;
+ apiVersion?: APIVersion;
+ fromAutoCreate?: boolean;
}) => {
- const { db, org, env, features } = req;
+ const { db, org, env, features } = req;
- // 1. Get data
- const { customer, inputEntities, cusProducts, existingEntities } =
- await validateAndGetInputEntities({
- req,
- customerId,
- customerData,
- createEntityData,
- logger,
- });
+ // 1. Get data
+ const { customer, inputEntities, cusProducts, existingEntities } =
+ await validateAndGetInputEntities({
+ req,
+ customerId,
+ customerData,
+ createEntityData,
+ logger,
+ });
- for (const cusProduct of cusProducts) {
- await createEntityForCusProduct({
- req,
- customer,
- cusProduct,
- inputEntities,
- logger,
- });
- }
+ for (const cusProduct of cusProducts) {
+ await createEntityForCusProduct({
+ req,
+ customer,
+ cusProduct,
+ inputEntities,
+ logger,
+ });
+ }
- let data = inputEntities.map((e: any) =>
- constructEntity({
- inputEntity: e,
- feature: features.find((f: any) => f.id === e.feature_id)!,
- internalCustomerId: customer.internal_id,
- orgId: org.id,
- env,
- })
- );
+ let data = inputEntities.map((e: any) =>
+ constructEntity({
+ inputEntity: e,
+ feature: features.find((f: any) => f.id === e.feature_id)!,
+ internalCustomerId: customer.internal_id,
+ orgId: org.id,
+ env,
+ }),
+ );
- let newEntities: Entity[] = [];
- if (existingEntities.some((e: any) => e.id === null)) {
- let updatedEntity = await EntityService.update({
- db,
- internalId: existingEntities.find((e: any) => e.id === null)!.internal_id,
- update: {
- id: inputEntities[0].id,
- name: inputEntities[0].name,
- },
- });
+ let newEntities: Entity[] = [];
+ if (existingEntities.some((e: any) => e.id === null)) {
+ let updatedEntity = await EntityService.update({
+ db,
+ internalId: existingEntities.find((e: any) => e.id === null)!.internal_id,
+ update: {
+ id: inputEntities[0].id,
+ name: inputEntities[0].name,
+ },
+ });
- data = data.slice(1);
- newEntities.push(updatedEntity);
- }
+ data = data.slice(1);
+ newEntities.push(updatedEntity);
+ }
- let insertedEntities = await EntityService.insert({
- db,
- data,
- });
+ let insertedEntities = await EntityService.insert({
+ db,
+ data,
+ });
- newEntities.push(...insertedEntities);
+ newEntities.push(...insertedEntities);
- if (fromAutoCreate) {
- return newEntities;
- }
+ if (fromAutoCreate) {
+ return newEntities;
+ }
- let { entities } = await getEntityResponse({
- db,
- entityIds: newEntities.map((e: any) => e.id || e.internal_id),
- org,
- env,
- customerId: customer.id || customer.internal_id,
- withAutumnId,
- apiVersion: apiVersion!,
- features,
- logger,
- skipCache: true,
- });
+ let { entities } = await getEntityResponse({
+ db,
+ entityIds: newEntities.map((e: any) => e.id || e.internal_id),
+ org,
+ env,
+ customerId: customer.id || customer.internal_id,
+ withAutumnId,
+ apiVersion: apiVersion!,
+ features,
+ logger,
+ skipCache: true,
+ });
- return entities;
+ return entities;
};
export const handlePostEntityRequest = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "create entity",
- handler: async (req: any, res: any) => {
- const { logtail: logger, org } = req;
+ routeHandler({
+ req,
+ res,
+ action: "create entity",
+ handler: async (req: any, res: any) => {
+ const { logtail: logger, org } = req;
- let apiVersion = orgToVersion({
- org,
- reqApiVersion: req.apiVersion,
- });
+ let apiVersion = orgToVersion({
+ org,
+ reqApiVersion: req.apiVersion,
+ });
- let customerData =
- Array.isArray(req.body) && req.body.length > 0
- ? req.body[0].customer_data
- : req.body.customer_data;
+ let customerData =
+ Array.isArray(req.body) && req.body.length > 0
+ ? req.body[0].customer_data
+ : req.body.customer_data;
- const entities = await createEntities({
- req,
- logger,
- customerId: req.params.customer_id,
- createEntityData: req.body,
- customerData,
- withAutumnId: req.query.with_autumn_id === "true",
- apiVersion,
- });
+ const entities = await createEntities({
+ req,
+ logger,
+ customerId: req.params.customer_id,
+ createEntityData: req.body,
+ customerData,
+ withAutumnId: req.query.with_autumn_id === "true",
+ apiVersion,
+ });
- logger.info(` Created / replaced entities!`);
+ logger.info(` Created / replaced entities!`);
- if (apiVersion < APIVersion.v1_2) {
- res.status(200).json({
- success: true,
- });
- return;
- }
- if (Array.isArray(req.body)) {
- res.status(200).json({
- list: entities,
- });
- } else {
- res.status(200).json(entities[0]);
- }
- },
- });
+ if (apiVersion < APIVersion.v1_2) {
+ res.status(200).json({
+ success: true,
+ });
+ return;
+ }
+ if (Array.isArray(req.body)) {
+ res.status(200).json({
+ list: entities,
+ });
+ } else {
+ res.status(200).json(entities[0]);
+ }
+ },
+ });
diff --git a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts
index 06f071e59..f000afa20 100644
--- a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts
+++ b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts
@@ -6,41 +6,41 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { CusProductStatus, Entity, FullCusProduct } from "@autumn/shared";
export const cancelSubsForEntity = async ({
- req,
- cusProducts,
- entity,
+ req,
+ cusProducts,
+ entity,
}: {
- req: ExtendedRequest;
- cusProducts: FullCusProduct[];
- entity: Entity;
+ req: ExtendedRequest;
+ cusProducts: FullCusProduct[];
+ entity: Entity;
}) => {
- const { org, env, db, logtail: logger } = req;
- try {
- let stripeCli = createStripeCli({ org, env });
- let curSubs = await cusProductsToStripeSubs({
- cusProducts,
- stripeCli,
- });
+ const { org, env, db, logtail: logger } = req;
+ try {
+ let stripeCli = createStripeCli({ org, env });
+ let curSubs = await cusProductsToStripeSubs({
+ cusProducts,
+ stripeCli,
+ });
- for (const cusProduct of cusProducts) {
- if (cusProduct.internal_entity_id !== entity.internal_id) {
- continue;
- }
+ for (const cusProduct of cusProducts) {
+ if (cusProduct.internal_entity_id !== entity.internal_id) {
+ continue;
+ }
- if (cusProduct.status == CusProductStatus.Scheduled) {
- await CusProductService.delete({
- db,
- cusProductId: cusProduct.id,
- });
- } else {
- await cancelCurSubs({
- curCusProduct: cusProduct,
- curSubs,
- stripeCli,
- });
- }
- }
- } catch (error) {
- logger.error("Failed to cancel subs for deleted entity", { error });
- }
+ if (cusProduct.status == CusProductStatus.Scheduled) {
+ await CusProductService.delete({
+ db,
+ cusProductId: cusProduct.id,
+ });
+ } else {
+ await cancelCurSubs({
+ curCusProduct: cusProduct,
+ curSubs,
+ stripeCli,
+ });
+ }
+ }
+ } catch (error) {
+ logger.error("Failed to cancel subs for deleted entity", { error });
+ }
};
diff --git a/server/src/internal/features/FeatureService.ts b/server/src/internal/features/FeatureService.ts
index 99cc587d3..f6b5c83f4 100644
--- a/server/src/internal/features/FeatureService.ts
+++ b/server/src/internal/features/FeatureService.ts
@@ -7,206 +7,206 @@ import { and, eq } from "drizzle-orm";
import { notNullish } from "@/utils/genUtils.js";
export class FeatureService {
- static async list({
- db,
- orgId,
- env,
- archived,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- archived?: boolean;
- }) {
- const features = await db.query.features.findMany({
- where: (features, { eq, and }) =>
- and(
- eq(features.org_id, orgId),
- eq(features.env, env),
- notNullish(archived) ? eq(features.archived, archived!) : undefined
- ),
+ static async list({
+ db,
+ orgId,
+ env,
+ archived,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ archived?: boolean;
+ }) {
+ const features = await db.query.features.findMany({
+ where: (features, { eq, and }) =>
+ and(
+ eq(features.org_id, orgId),
+ eq(features.env, env),
+ notNullish(archived) ? eq(features.archived, archived!) : undefined,
+ ),
- orderBy: (features, { desc }) => [desc(features.internal_id)],
- });
+ orderBy: (features, { desc }) => [desc(features.internal_id)],
+ });
- return features as Feature[]; // TODO: DRIZZLE TYPE REFACTOR
- }
+ return features as Feature[]; // TODO: DRIZZLE TYPE REFACTOR
+ }
- static async getFromReq(req: any) {
- if (req.features) return req.features as Feature[];
- const features = await FeatureService.list({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- });
+ static async getFromReq(req: any) {
+ if (req.features) return req.features as Feature[];
+ const features = await FeatureService.list({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ });
- return features as Feature[];
- }
+ return features as Feature[];
+ }
- static async get({
- db,
- id,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- id: string;
- orgId: string;
- env: AppEnv;
- }) {
- const feature = await db.query.features.findFirst({
- where: (features, { eq, and }) =>
- and(
- eq(features.id, id),
- eq(features.org_id, orgId),
- eq(features.env, env)
- ),
- });
- return feature as Feature;
- }
+ static async get({
+ db,
+ id,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const feature = await db.query.features.findFirst({
+ where: (features, { eq, and }) =>
+ and(
+ eq(features.id, id),
+ eq(features.org_id, orgId),
+ eq(features.env, env),
+ ),
+ });
+ return feature as Feature;
+ }
- static async update({
- db,
- id,
- orgId,
- env,
- internalId,
- updates,
- }: {
- db: DrizzleCli;
- id?: string;
- orgId?: string;
- env?: AppEnv;
- internalId?: string;
- updates: any;
- }) {
- if (!id && !internalId) {
- throw new RecaseError({
- message: "id or internalId is required to update feature",
- code: ErrCode.InternalError,
- statusCode: 500,
- });
- }
+ static async update({
+ db,
+ id,
+ orgId,
+ env,
+ internalId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id?: string;
+ orgId?: string;
+ env?: AppEnv;
+ internalId?: string;
+ updates: any;
+ }) {
+ if (!id && !internalId) {
+ throw new RecaseError({
+ message: "id or internalId is required to update feature",
+ code: ErrCode.InternalError,
+ statusCode: 500,
+ });
+ }
- if (id && (!orgId || !env)) {
- throw new RecaseError({
- message: "orgId and env are required to update feature by id",
- code: ErrCode.InternalError,
- statusCode: 500,
- });
- }
+ if (id && (!orgId || !env)) {
+ throw new RecaseError({
+ message: "orgId and env are required to update feature by id",
+ code: ErrCode.InternalError,
+ statusCode: 500,
+ });
+ }
- let updatedFeatures;
- if (internalId) {
- updatedFeatures = await db
- .update(features)
- .set(updates)
- .where(eq(features.internal_id, internalId))
- .returning();
- } else {
- updatedFeatures = await db
- .update(features)
- .set(updates)
- .where(
- and(
- eq(features.id, id!),
- eq(features.org_id, orgId!),
- eq(features.env, env!)
- )
- )
- .returning();
- }
+ let updatedFeatures;
+ if (internalId) {
+ updatedFeatures = await db
+ .update(features)
+ .set(updates)
+ .where(eq(features.internal_id, internalId))
+ .returning();
+ } else {
+ updatedFeatures = await db
+ .update(features)
+ .set(updates)
+ .where(
+ and(
+ eq(features.id, id!),
+ eq(features.org_id, orgId!),
+ eq(features.env, env!),
+ ),
+ )
+ .returning();
+ }
- await clearOrgCache({
- db,
- orgId: updatedFeatures[0].org_id!,
- env: updatedFeatures[0].env as AppEnv,
- });
+ await clearOrgCache({
+ db,
+ orgId: updatedFeatures[0].org_id!,
+ env: updatedFeatures[0].env as AppEnv,
+ });
- return updatedFeatures.length > 0 ? (updatedFeatures[0] as Feature) : null;
- }
+ return updatedFeatures.length > 0 ? (updatedFeatures[0] as Feature) : null;
+ }
- static async insert({
- db,
- data,
- logger,
- }: {
- db: DrizzleCli;
- data: Feature[] | Feature;
- logger: any;
- }) {
- try {
- let insertedData = await db
- .insert(features)
- .values(data as any) // DRIZZLE TYPE REFACTOR
- .returning();
+ static async insert({
+ db,
+ data,
+ logger,
+ }: {
+ db: DrizzleCli;
+ data: Feature[] | Feature;
+ logger: any;
+ }) {
+ try {
+ let insertedData = await db
+ .insert(features)
+ .values(data as any) // DRIZZLE TYPE REFACTOR
+ .returning();
- if (insertedData && insertedData.length > 0) {
- let orgId = insertedData[0].org_id;
- await clearOrgCache({
- db,
- orgId: orgId!,
- logger,
- });
- }
- return insertedData as Feature[]; // DRIZZLE TYPE REFACTOR
- } catch (error: any) {
- if (error.code === "23505") {
- let id = Array.isArray(data) ? data.map((f) => f.id) : data.id;
- throw new RecaseError({
- message: `Feature ${id} already exists`,
- code: ErrCode.DuplicateFeatureId,
- statusCode: 400,
- });
- }
- }
- }
+ if (insertedData && insertedData.length > 0) {
+ let orgId = insertedData[0].org_id;
+ await clearOrgCache({
+ db,
+ orgId: orgId!,
+ logger,
+ });
+ }
+ return insertedData as Feature[]; // DRIZZLE TYPE REFACTOR
+ } catch (error: any) {
+ if (error.code === "23505") {
+ let id = Array.isArray(data) ? data.map((f) => f.id) : data.id;
+ throw new RecaseError({
+ message: `Feature ${id} already exists`,
+ code: ErrCode.DuplicateFeatureId,
+ statusCode: 400,
+ });
+ }
+ }
+ }
- static async delete({
- db,
- featureId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- featureId: string;
- orgId: string;
- env: AppEnv;
- }) {
- let deletedFeatures = await db
- .delete(features)
- .where(
- and(
- eq(features.id, featureId),
- eq(features.org_id, orgId),
- eq(features.env, env)
- )
- )
- .returning();
+ static async delete({
+ db,
+ featureId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ featureId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ let deletedFeatures = await db
+ .delete(features)
+ .where(
+ and(
+ eq(features.id, featureId),
+ eq(features.org_id, orgId),
+ eq(features.env, env),
+ ),
+ )
+ .returning();
- if (deletedFeatures.length === 0) {
- return null;
- }
+ if (deletedFeatures.length === 0) {
+ return null;
+ }
- await clearOrgCache({
- db,
- orgId,
- env,
- });
- return deletedFeatures[0] as Feature;
- }
+ await clearOrgCache({
+ db,
+ orgId,
+ env,
+ });
+ return deletedFeatures[0] as Feature;
+ }
- static async deleteByOrgId({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- await db
- .delete(features)
- .where(and(eq(features.org_id, orgId), eq(features.env, env)));
- }
+ static async deleteByOrgId({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ await db
+ .delete(features)
+ .where(and(eq(features.org_id, orgId), eq(features.env, env)));
+ }
}
diff --git a/server/src/internal/features/creditSystemUtils.ts b/server/src/internal/features/creditSystemUtils.ts
index ddb8607d3..aa8f6e00a 100644
--- a/server/src/internal/features/creditSystemUtils.ts
+++ b/server/src/internal/features/creditSystemUtils.ts
@@ -4,63 +4,63 @@ import { Feature } from "@autumn/shared";
import { Decimal } from "decimal.js";
export const creditSystemContainsFeature = ({
- creditSystem,
- meteredFeatureId,
+ creditSystem,
+ meteredFeatureId,
}: {
- creditSystem: Feature;
- meteredFeatureId: string;
+ creditSystem: Feature;
+ meteredFeatureId: string;
}) => {
- const schema: CreditSchemaItem[] = creditSystem.config.schema;
+ const schema: CreditSchemaItem[] = creditSystem.config.schema;
- for (const schemaItem of schema) {
- if (schemaItem.metered_feature_id === meteredFeatureId) {
- return true;
- }
- }
+ for (const schemaItem of schema) {
+ if (schemaItem.metered_feature_id === meteredFeatureId) {
+ return true;
+ }
+ }
- return false;
+ return false;
};
export const getCreditSystemsFromFeature = ({
- featureId,
- features,
+ featureId,
+ features,
}: {
- featureId: string;
- features: Feature[];
+ featureId: string;
+ features: Feature[];
}) => {
- return features.filter(
- (f) =>
- f.type == FeatureType.CreditSystem &&
- f.id != featureId &&
- creditSystemContainsFeature({
- creditSystem: f,
- meteredFeatureId: featureId,
- }),
- );
+ return features.filter(
+ (f) =>
+ f.type == FeatureType.CreditSystem &&
+ f.id != featureId &&
+ creditSystemContainsFeature({
+ creditSystem: f,
+ meteredFeatureId: featureId,
+ }),
+ );
};
export const featureToCreditSystem = ({
- featureId,
- creditSystem,
- amount,
+ featureId,
+ creditSystem,
+ amount,
}: {
- featureId: string;
- creditSystem: Feature;
- amount: number;
+ featureId: string;
+ creditSystem: Feature;
+ amount: number;
}) => {
- const schema: CreditSchemaItem[] = creditSystem.config.schema;
+ const schema: CreditSchemaItem[] = creditSystem.config.schema;
- for (const schemaItem of schema) {
- if (schemaItem.metered_feature_id === featureId) {
- let creditAmount = schemaItem.credit_amount;
- let featureAmount = schemaItem.feature_amount;
+ for (const schemaItem of schema) {
+ if (schemaItem.metered_feature_id === featureId) {
+ let creditAmount = schemaItem.credit_amount;
+ let featureAmount = schemaItem.feature_amount;
- return new Decimal(creditAmount)
- .div(featureAmount)
- .mul(amount)
- .toNumber();
- }
- }
+ return new Decimal(creditAmount)
+ .div(featureAmount)
+ .mul(amount)
+ .toNumber();
+ }
+ }
- return amount;
+ return amount;
};
diff --git a/server/src/internal/features/featureRouter.ts b/server/src/internal/features/featureRouter.ts
index b10794b44..5e1498697 100644
--- a/server/src/internal/features/featureRouter.ts
+++ b/server/src/internal/features/featureRouter.ts
@@ -2,12 +2,12 @@ import express, { Router } from "express";
import { FeatureService } from "./FeatureService.js";
import { fromAPIFeature, toAPIFeature } from "./utils/mapFeatureUtils.js";
import {
- APIFeatureSchema,
- APIFeatureType,
- ErrCode,
- Feature,
- FeatureType,
- UpdateAPIFeatureSchema,
+ APIFeatureSchema,
+ APIFeatureType,
+ ErrCode,
+ Feature,
+ FeatureType,
+ UpdateAPIFeatureSchema,
} from "@autumn/shared";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import { JobName } from "@/queue/JobName.js";
@@ -23,157 +23,157 @@ export const featureRouter: Router = express.Router();
// 1. Get features...
featureRouter.get("", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "list features",
- handler: async () => {
- const includeArchived = req.query.include_archived === "true";
- let features = await FeatureService.list({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- archived: includeArchived ? undefined : false,
- // showOnlyArchived: includeArchived ? undefined : false,
- });
+ routeHandler({
+ req,
+ res,
+ action: "list features",
+ handler: async () => {
+ const includeArchived = req.query.include_archived === "true";
+ let features = await FeatureService.list({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ archived: includeArchived ? undefined : false,
+ // showOnlyArchived: includeArchived ? undefined : false,
+ });
- res
- .status(200)
- .json({ list: features.map((feature) => toAPIFeature({ feature })) });
- },
- })
+ res
+ .status(200)
+ .json({ list: features.map((feature) => toAPIFeature({ feature })) });
+ },
+ }),
);
featureRouter.get("/:featureId", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Get feature",
- handler: async () => {
- const feature = req.features.find(
- (f: Feature) => f.id == req.params.featureId
- );
+ routeHandler({
+ req,
+ res,
+ action: "Get feature",
+ handler: async () => {
+ const feature = req.features.find(
+ (f: Feature) => f.id == req.params.featureId,
+ );
- if (!feature) {
- throw new RecaseError({
- message: `Feature with id ${req.params.featureId} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature with id ${req.params.featureId} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- res.status(200).json(toAPIFeature({ feature }));
- },
- })
+ res.status(200).json(toAPIFeature({ feature }));
+ },
+ }),
);
featureRouter.post("", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Create feature",
- handler: async () => {
- let apiFeature = APIFeatureSchema.parse(req.body);
- if (!apiFeature.name) {
- apiFeature.name = keyToTitle(apiFeature.id);
- }
+ routeHandler({
+ req,
+ res,
+ action: "Create feature",
+ handler: async () => {
+ let apiFeature = APIFeatureSchema.parse(req.body);
+ if (!apiFeature.name) {
+ apiFeature.name = keyToTitle(apiFeature.id);
+ }
- validateFeatureId(apiFeature.id);
+ validateFeatureId(apiFeature.id);
- let feature = fromAPIFeature({
- apiFeature,
- orgId: req.orgId,
- env: req.env,
- });
+ let feature = fromAPIFeature({
+ apiFeature,
+ orgId: req.orgId,
+ env: req.env,
+ });
- const { db, logger, features: curFeatures } = req;
+ const { db, logger, features: curFeatures } = req;
- let curFeature = curFeatures.find((f: Feature) => f.id == feature.id);
+ let curFeature = curFeatures.find((f: Feature) => f.id == feature.id);
- if (curFeature) {
- throw new RecaseError({
- message: `Feature with id ${feature.id} already exists`,
- code: ErrCode.DuplicateFeatureId,
- statusCode: 400,
- });
- }
+ if (curFeature) {
+ throw new RecaseError({
+ message: `Feature with id ${feature.id} already exists`,
+ code: ErrCode.DuplicateFeatureId,
+ statusCode: 400,
+ });
+ }
- await FeatureService.insert({ db, data: [feature], logger });
+ await FeatureService.insert({ db, data: [feature], logger });
- await addTaskToQueue({
- jobName: JobName.GenerateFeatureDisplay,
- payload: { feature },
- });
+ await addTaskToQueue({
+ jobName: JobName.GenerateFeatureDisplay,
+ payload: { feature },
+ });
- res.status(200).json(apiFeature);
- },
- })
+ res.status(200).json(apiFeature);
+ },
+ }),
);
featureRouter.post("/:feature_id", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Update feature",
- handler: async (req: any, res: any) => {
- let { feature_id: featureId } = req.params;
- let { features: curFeatures } = req;
- let apiFeature = UpdateAPIFeatureSchema.parse(req.body);
+ routeHandler({
+ req,
+ res,
+ action: "Update feature",
+ handler: async (req: any, res: any) => {
+ let { feature_id: featureId } = req.params;
+ let { features: curFeatures } = req;
+ let apiFeature = UpdateAPIFeatureSchema.parse(req.body);
- let originalFeature = curFeatures.find((f: Feature) => f.id == featureId);
- if (!originalFeature) {
- throw new RecaseError({
- message: `Feature with id ${featureId} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ let originalFeature = curFeatures.find((f: Feature) => f.id == featureId);
+ if (!originalFeature) {
+ throw new RecaseError({
+ message: `Feature with id ${featureId} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- // Replace body...
- let featureType = apiFeature.type as unknown as FeatureType;
- let usageType = undefined;
- if (
- apiFeature.type == APIFeatureType.SingleUsage ||
- apiFeature.type == APIFeatureType.ContinuousUse
- ) {
- featureType = FeatureType.Metered;
- usageType = apiFeature.type;
- }
+ // Replace body...
+ let featureType = apiFeature.type as unknown as FeatureType;
+ let usageType = undefined;
+ if (
+ apiFeature.type == APIFeatureType.SingleUsage ||
+ apiFeature.type == APIFeatureType.ContinuousUse
+ ) {
+ featureType = FeatureType.Metered;
+ usageType = apiFeature.type;
+ }
- let newConfig = originalFeature.config;
- if (usageType) {
- newConfig.usage_type = usageType;
- }
+ let newConfig = originalFeature.config;
+ if (usageType) {
+ newConfig.usage_type = usageType;
+ }
- if (apiFeature.credit_schema) {
- newConfig.schema = apiFeature.credit_schema.map((credit) => ({
- metered_feature_id: credit.metered_feature_id,
- credit_amount: credit.credit_cost,
- }));
- }
+ if (apiFeature.credit_schema) {
+ newConfig.schema = apiFeature.credit_schema.map((credit) => ({
+ metered_feature_id: credit.metered_feature_id,
+ credit_amount: credit.credit_cost,
+ }));
+ }
- let newBody = {
- id: req.body.id || undefined,
- name: req.body.name || undefined,
- type: featureType,
- config: newConfig,
- archived: req.body.archived ?? undefined,
- };
+ let newBody = {
+ id: req.body.id || undefined,
+ name: req.body.name || undefined,
+ type: featureType,
+ config: newConfig,
+ archived: req.body.archived ?? undefined,
+ };
- req.body = newBody;
+ req.body = newBody;
- await handleUpdateFeature(req, res, true);
+ await handleUpdateFeature(req, res, true);
- // let newFeature = await FeatureService.get({
- // db: req.db,
- // id: featureId,
- // orgId: req.orgId,
- // env: req.env,
- // });
+ // let newFeature = await FeatureService.get({
+ // db: req.db,
+ // id: featureId,
+ // orgId: req.orgId,
+ // env: req.env,
+ // });
- // res.status(200).json(toAPIFeature({ feature: newFeature }));
- },
- })
+ // res.status(200).json(toAPIFeature({ feature: newFeature }));
+ },
+ }),
);
featureRouter.delete("/:featureId", handleDeleteFeature);
diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts
index 97481b1e4..2d3f6a43a 100644
--- a/server/src/internal/features/featureUtils.ts
+++ b/server/src/internal/features/featureUtils.ts
@@ -1,16 +1,16 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- MeteredConfig,
- ErrCode,
- AggregateType,
- CreditSystemConfig,
- Feature,
- FeatureUsageType,
- Organization,
- ProductItemFeatureType,
- FeatureType,
- FullCustomer,
- UsagePriceConfig,
+ MeteredConfig,
+ ErrCode,
+ AggregateType,
+ CreditSystemConfig,
+ Feature,
+ FeatureUsageType,
+ Organization,
+ ProductItemFeatureType,
+ FeatureType,
+ FullCustomer,
+ UsagePriceConfig,
} from "@autumn/shared";
import { FeatureService } from "./FeatureService.js";
import { StatusCodes } from "http-status-codes";
@@ -23,217 +23,217 @@ import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js";
import { ACTIVE_STATUSES } from "../customers/cusProducts/CusProductService.js";
export const validateFeatureId = (featureId: string) => {
- if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) {
- throw new RecaseError({
- message:
- "Feature ID can only contain alphanumeric characters, underscores, and hyphens",
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
- return;
+ if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) {
+ throw new RecaseError({
+ message:
+ "Feature ID can only contain alphanumeric characters, underscores, and hyphens",
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
+ return;
};
export const validateMeteredConfig = (config: MeteredConfig) => {
- let newConfig = { ...config };
+ let newConfig = { ...config };
- if (!config.usage_type) {
- throw new RecaseError({
- message: `Usage type (single or continuous) is required for metered feature`,
- code: ErrCode.InvalidFeature,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!config.usage_type) {
+ throw new RecaseError({
+ message: `Usage type (single or continuous) is required for metered feature`,
+ code: ErrCode.InvalidFeature,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (config.aggregate?.type == AggregateType.Count) {
- newConfig.aggregate = {
- type: AggregateType.Count,
- property: null,
- }; // to continue testing support for count...
- } else {
- newConfig.aggregate = {
- type: AggregateType.Sum,
- property: "value",
- };
- }
+ if (config.aggregate?.type == AggregateType.Count) {
+ newConfig.aggregate = {
+ type: AggregateType.Count,
+ property: null,
+ }; // to continue testing support for count...
+ } else {
+ newConfig.aggregate = {
+ type: AggregateType.Sum,
+ property: "value",
+ };
+ }
- if (newConfig.filters.length == 0) {
- newConfig.filters = [
- {
- property: "",
- operator: "",
- value: [],
- },
- ];
- }
+ if (newConfig.filters.length == 0) {
+ newConfig.filters = [
+ {
+ property: "",
+ operator: "",
+ value: [],
+ },
+ ];
+ }
- return newConfig as MeteredConfig;
+ return newConfig as MeteredConfig;
};
export const validateCreditSystem = (config: CreditSystemConfig) => {
- let schema = config.schema;
- if (!schema || schema.length == 0) {
- throw new RecaseError({
- message: `At least one metered feature is required for credit system`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ let schema = config.schema;
+ if (!schema || schema.length == 0) {
+ throw new RecaseError({
+ message: `At least one metered feature is required for credit system`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- // Check if multiple of the same feature
- const meteredFeatureIds = schema.map(
- (schemaItem) => schemaItem.metered_feature_id
- );
- // console.log("Metered feature ids:", meteredFeatureIds);
- const uniqueMeteredFeatureIds = Array.from(new Set(meteredFeatureIds));
- if (meteredFeatureIds.length !== uniqueMeteredFeatureIds.length) {
- throw new RecaseError({
- message: `Credit system contains multiple of the same metered_feature_id`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ // Check if multiple of the same feature
+ const meteredFeatureIds = schema.map(
+ (schemaItem) => schemaItem.metered_feature_id,
+ );
+ // console.log("Metered feature ids:", meteredFeatureIds);
+ const uniqueMeteredFeatureIds = Array.from(new Set(meteredFeatureIds));
+ if (meteredFeatureIds.length !== uniqueMeteredFeatureIds.length) {
+ throw new RecaseError({
+ message: `Credit system contains multiple of the same metered_feature_id`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- let newConfig = { ...config, usage_type: FeatureUsageType.Single };
- for (let i = 0; i < newConfig.schema.length; i++) {
- newConfig.schema[i].feature_amount = 1;
+ let newConfig = { ...config, usage_type: FeatureUsageType.Single };
+ for (let i = 0; i < newConfig.schema.length; i++) {
+ newConfig.schema[i].feature_amount = 1;
- let creditAmount = parseFloat(newConfig.schema[i].credit_amount.toString());
- if (isNaN(creditAmount)) {
- throw new RecaseError({
- message: `Credit amount should be a number`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ let creditAmount = parseFloat(newConfig.schema[i].credit_amount.toString());
+ if (isNaN(creditAmount)) {
+ throw new RecaseError({
+ message: `Credit amount should be a number`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- newConfig.schema[i].credit_amount = creditAmount;
- }
+ newConfig.schema[i].credit_amount = creditAmount;
+ }
- return newConfig;
+ return newConfig;
};
export const getObjectsUsingFeature = async ({
- db,
- orgId,
- env,
- allFeatures,
- feature,
+ db,
+ orgId,
+ env,
+ allFeatures,
+ feature,
}: {
- db: DrizzleCli;
- orgId: string;
- env: any;
- allFeatures: Feature[];
- feature: Feature;
+ db: DrizzleCli;
+ orgId: string;
+ env: any;
+ allFeatures: Feature[];
+ feature: Feature;
}) => {
- let products = await ProductService.listFull({
- db,
- orgId,
- env,
- });
+ let products = await ProductService.listFull({
+ db,
+ orgId,
+ env,
+ });
- let allPrices = products.flatMap((p) => p.prices);
- let allEnts = products.flatMap((p) => p.entitlements);
- let creditSystems = getCreditSystemsFromFeature({
- featureId: feature.id,
- features: allFeatures,
- });
+ let allPrices = products.flatMap((p) => p.prices);
+ let allEnts = products.flatMap((p) => p.entitlements);
+ let creditSystems = getCreditSystemsFromFeature({
+ featureId: feature.id,
+ features: allFeatures,
+ });
- let entitlements = allEnts.filter(
- (entitlement) => entitlement.internal_feature_id == feature.internal_id
- );
- let linkedEntitlements = allEnts.filter(
- (entitlement) => entitlement.entity_feature_id == feature.id
- );
+ let entitlements = allEnts.filter(
+ (entitlement) => entitlement.internal_feature_id == feature.internal_id,
+ );
+ let linkedEntitlements = allEnts.filter(
+ (entitlement) => entitlement.entity_feature_id == feature.id,
+ );
- let prices = allPrices.filter(
- (price) => (price.config as any).internal_feature_id == feature.internal_id
- );
+ let prices = allPrices.filter(
+ (price) => (price.config as any).internal_feature_id == feature.internal_id,
+ );
- return { entitlements, prices, creditSystems, linkedEntitlements };
+ return { entitlements, prices, creditSystems, linkedEntitlements };
};
export const runSaveFeatureDisplayTask = async ({
- db,
- feature,
+ db,
+ feature,
- logger,
+ logger,
}: {
- db: DrizzleCli;
- feature: Feature;
+ db: DrizzleCli;
+ feature: Feature;
- logger: any;
+ logger: any;
}) => {
- let display;
- try {
- if (!process.env.ANTHROPIC_API_KEY) {
- logger.warn(
- "ANTHROPIC_API_KEY is not set, skipping feature display generation"
- );
- return;
- }
+ let display;
+ try {
+ if (!process.env.ANTHROPIC_API_KEY) {
+ logger.warn(
+ "ANTHROPIC_API_KEY is not set, skipping feature display generation",
+ );
+ return;
+ }
- logger.info(`Generating feature display for ${feature.id}`);
- display = await generateFeatureDisplay(feature);
- logger.info(`Result: ${JSON.stringify(display)}`);
+ logger.info(`Generating feature display for ${feature.id}`);
+ display = await generateFeatureDisplay(feature);
+ logger.info(`Result: ${JSON.stringify(display)}`);
- await FeatureService.update({
- db,
- internalId: feature.internal_id!,
- updates: {
- display,
- },
- });
- } catch (error) {
- logger.error("failed to generate feature display", {
- error,
- feature,
- });
- }
+ await FeatureService.update({
+ db,
+ internalId: feature.internal_id!,
+ updates: {
+ display,
+ },
+ });
+ } catch (error) {
+ logger.error("failed to generate feature display", {
+ error,
+ feature,
+ });
+ }
};
export const getCusFeatureType = ({ feature }: { feature: Feature }) => {
- if (feature.type == FeatureType.Boolean) {
- return ProductItemFeatureType.Static;
- } else if (feature.type == FeatureType.Metered) {
- if (feature.config.usage_type == FeatureUsageType.Single) {
- return ProductItemFeatureType.SingleUse;
- } else {
- return ProductItemFeatureType.ContinuousUse;
- }
- } else {
- return ProductItemFeatureType.SingleUse;
- }
+ if (feature.type == FeatureType.Boolean) {
+ return ProductItemFeatureType.Static;
+ } else if (feature.type == FeatureType.Metered) {
+ if (feature.config.usage_type == FeatureUsageType.Single) {
+ return ProductItemFeatureType.SingleUse;
+ } else {
+ return ProductItemFeatureType.ContinuousUse;
+ }
+ } else {
+ return ProductItemFeatureType.SingleUse;
+ }
};
export const isCreditSystem = ({ feature }: { feature: Feature }) => {
- return feature.type == FeatureType.CreditSystem;
+ return feature.type == FeatureType.CreditSystem;
};
export const isPaidContinuousUse = ({
- feature,
- fullCus,
+ feature,
+ fullCus,
}: {
- feature: Feature;
- fullCus: FullCustomer;
+ feature: Feature;
+ fullCus: FullCustomer;
}) => {
- let isContinuous = feature.config?.usage_type == FeatureUsageType.Continuous;
+ let isContinuous = feature.config?.usage_type == FeatureUsageType.Continuous;
- if (!isContinuous) {
- return false;
- }
+ if (!isContinuous) {
+ return false;
+ }
- let cusPrices = cusProductsToCusPrices({
- cusProducts: fullCus.customer_products,
- inStatuses: ACTIVE_STATUSES,
- });
+ let cusPrices = cusProductsToCusPrices({
+ cusProducts: fullCus.customer_products,
+ inStatuses: ACTIVE_STATUSES,
+ });
- let hasPaid = cusPrices.some((cp) => {
- let config = cp.price.config as UsagePriceConfig;
- if (config.internal_feature_id == feature.internal_id) {
- return true;
- }
- });
+ let hasPaid = cusPrices.some((cp) => {
+ let config = cp.price.config as UsagePriceConfig;
+ if (config.internal_feature_id == feature.internal_id) {
+ return true;
+ }
+ });
- return hasPaid;
+ return hasPaid;
};
diff --git a/server/src/internal/features/handlers/handleCreateFeature.ts b/server/src/internal/features/handlers/handleCreateFeature.ts
index 1e6344b80..6578359d1 100644
--- a/server/src/internal/features/handlers/handleCreateFeature.ts
+++ b/server/src/internal/features/handlers/handleCreateFeature.ts
@@ -8,40 +8,40 @@ import { addTaskToQueue } from "@/queue/queueUtils.js";
import { handleFrontendReqError } from "@/utils/errorUtils.js";
export const handleCreateFeature = async (req: any, res: any) => {
- try {
- console.log("Trying to create feature");
- const data = req.body;
- let { db, orgId, env, logtail: logger } = req;
- let parsedFeature = validateFeature(data);
+ try {
+ console.log("Trying to create feature");
+ const data = req.body;
+ let { db, orgId, env, logtail: logger } = req;
+ let parsedFeature = validateFeature(data);
- const feature: Feature = {
- archived: false,
- internal_id: generateId("fe"),
- org_id: orgId,
- created_at: Date.now(),
- env: env,
- ...parsedFeature,
- };
+ const feature: Feature = {
+ archived: false,
+ internal_id: generateId("fe"),
+ org_id: orgId,
+ created_at: Date.now(),
+ env: env,
+ ...parsedFeature,
+ };
- let org = await OrgService.getFromReq(req);
- let insertedData = await FeatureService.insert({
- db,
- data: feature,
- logger,
- });
+ let org = await OrgService.getFromReq(req);
+ let insertedData = await FeatureService.insert({
+ db,
+ data: feature,
+ logger,
+ });
- await addTaskToQueue({
- jobName: JobName.GenerateFeatureDisplay,
- payload: {
- feature,
- org: org,
- },
- });
+ await addTaskToQueue({
+ jobName: JobName.GenerateFeatureDisplay,
+ payload: {
+ feature,
+ org: org,
+ },
+ });
- let insertedFeature =
- insertedData && insertedData.length > 0 ? insertedData[0] : null;
- res.status(200).json(insertedFeature);
- } catch (error) {
- handleFrontendReqError({ req, error, res, action: "Create feature" });
- }
+ let insertedFeature =
+ insertedData && insertedData.length > 0 ? insertedData[0] : null;
+ res.status(200).json(insertedFeature);
+ } catch (error) {
+ handleFrontendReqError({ req, error, res, action: "Create feature" });
+ }
};
diff --git a/server/src/internal/features/handlers/handleDeleteFeature.ts b/server/src/internal/features/handlers/handleDeleteFeature.ts
index 5880bbb12..1c241e733 100644
--- a/server/src/internal/features/handlers/handleDeleteFeature.ts
+++ b/server/src/internal/features/handlers/handleDeleteFeature.ts
@@ -3,62 +3,65 @@ import { getCreditSystemsFromFeature } from "@/internal/features/creditSystemUti
import { FeatureService } from "@/internal/features/FeatureService.js";
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
import RecaseError from "@/utils/errorUtils.js";
-import type { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
+import type {
+ ExtendedRequest,
+ ExtendedResponse,
+} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
export const handleDeleteFeature = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Delete feature",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { db, orgId } = req;
+ routeHandler({
+ req,
+ res,
+ action: "Delete feature",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { db, orgId } = req;
- let { featureId } = req.params;
- let features = await FeatureService.getFromReq(req);
- let feature = features.find((f) => f.id === featureId);
- let creditSystems = getCreditSystemsFromFeature({
- featureId,
- features,
- });
+ let { featureId } = req.params;
+ let features = await FeatureService.getFromReq(req);
+ let feature = features.find((f) => f.id === featureId);
+ let creditSystems = getCreditSystemsFromFeature({
+ featureId,
+ features,
+ });
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${featureId} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${featureId} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- if (creditSystems.length > 0) {
- throw new RecaseError({
- message: `Feature ${featureId} is used by credit system ${creditSystems[0].id}`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ if (creditSystems.length > 0) {
+ throw new RecaseError({
+ message: `Feature ${featureId} is used by credit system ${creditSystems[0].id}`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- // Get prices that use this feature
- const ent = await EntitlementService.getByFeature({
- db,
- internalFeatureId: feature.internal_id!,
- });
+ // Get prices that use this feature
+ const ent = await EntitlementService.getByFeature({
+ db,
+ internalFeatureId: feature.internal_id!,
+ });
- if (ent) {
- throw new RecaseError({
- message: `Feature ${featureId} is used in a product. You must delete the product first, or archive it instead.`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ if (ent) {
+ throw new RecaseError({
+ message: `Feature ${featureId} is used in a product. You must delete the product first, or archive it instead.`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- await FeatureService.delete({
- db: req.db,
- orgId,
- featureId,
- env: req.env,
- });
+ await FeatureService.delete({
+ db: req.db,
+ orgId,
+ featureId,
+ env: req.env,
+ });
- res.status(200).json({ message: "Feature deleted" });
- },
- });
+ res.status(200).json({ message: "Feature deleted" });
+ },
+ });
diff --git a/server/src/internal/features/handlers/handleGetFeatureDeletionInfo.ts b/server/src/internal/features/handlers/handleGetFeatureDeletionInfo.ts
index 1400e0da0..1ec58fd6a 100644
--- a/server/src/internal/features/handlers/handleGetFeatureDeletionInfo.ts
+++ b/server/src/internal/features/handlers/handleGetFeatureDeletionInfo.ts
@@ -5,60 +5,60 @@ import { entitlements, products } from "@autumn/shared";
import { routeHandler } from "@/utils/routerUtils.js";
export const handleGetFeatureDeletionInfo = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Get feature deletion info",
- handler: async (req: any, res: any) => {
- let { db } = req;
- let { feature_id } = req.params;
+ routeHandler({
+ req,
+ res,
+ action: "Get feature deletion info",
+ handler: async (req: any, res: any) => {
+ let { db } = req;
+ let { feature_id } = req.params;
- let feature = await FeatureService.get({
- db,
- id: feature_id,
- orgId: req.orgId,
- env: req.env,
- });
+ let feature = await FeatureService.get({
+ db,
+ id: feature_id,
+ orgId: req.orgId,
+ env: req.env,
+ });
- if (!feature) {
- return res.status(404).json({ error: "Feature not found" });
- }
+ if (!feature) {
+ return res.status(404).json({ error: "Feature not found" });
+ }
- // Use Drizzle query similar to ProductService.getDeletionText
- let res_data = await db
- .select({
- productName: sql`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`,
- totalCount: sql`COUNT(*) OVER ()`,
- })
- .from(products)
- .innerJoin(
- entitlements,
- eq(products.internal_id, entitlements.internal_product_id)
- )
- .where(
- and(
- eq(entitlements.internal_feature_id, feature.internal_id!),
- eq(products.env, req.env),
- eq(products.org_id, req.orgId)
- )
- )
- .limit(1);
+ // Use Drizzle query similar to ProductService.getDeletionText
+ let res_data = await db
+ .select({
+ productName: sql`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`,
+ totalCount: sql`COUNT(*) OVER ()`,
+ })
+ .from(products)
+ .innerJoin(
+ entitlements,
+ eq(products.internal_id, entitlements.internal_product_id),
+ )
+ .where(
+ and(
+ eq(entitlements.internal_feature_id, feature.internal_id!),
+ eq(products.env, req.env),
+ eq(products.org_id, req.orgId),
+ ),
+ )
+ .limit(1);
- console.log(
- `Feature ${feature_id} has ${res_data.length} products. First product name: ${res_data[0]?.productName}`
- );
+ console.log(
+ `Feature ${feature_id} has ${res_data.length} products. First product name: ${res_data[0]?.productName}`,
+ );
- // If no products found, return explicit zero count
- if (!res_data || res_data.length === 0) {
- res.status(200).json({
- productName: null,
- totalCount: 0,
- });
- } else {
- res.status(200).json({
- productName: res_data[0]?.productName || null,
- totalCount: Number(res_data[0]?.totalCount) || 0,
- });
- }
- },
- });
+ // If no products found, return explicit zero count
+ if (!res_data || res_data.length === 0) {
+ res.status(200).json({
+ productName: null,
+ totalCount: 0,
+ });
+ } else {
+ res.status(200).json({
+ productName: res_data[0]?.productName || null,
+ totalCount: Number(res_data[0]?.totalCount) || 0,
+ });
+ }
+ },
+ });
diff --git a/server/src/internal/features/handlers/handleUpdateFeature.ts b/server/src/internal/features/handlers/handleUpdateFeature.ts
index 77ca1e896..f7474b3a0 100644
--- a/server/src/internal/features/handlers/handleUpdateFeature.ts
+++ b/server/src/internal/features/handlers/handleUpdateFeature.ts
@@ -4,8 +4,8 @@ import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntit
import { FeatureService } from "@/internal/features/FeatureService.js";
import {
- getObjectsUsingFeature,
- validateCreditSystem,
+ getObjectsUsingFeature,
+ validateCreditSystem,
} from "@/internal/features/featureUtils.js";
import { validateMeteredConfig } from "@/internal/features/featureUtils.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
@@ -16,394 +16,396 @@ import RecaseError from "@/utils/errorUtils.js";
import { keyToTitle } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import {
- Entitlement,
- Feature,
- FeatureType,
- Price,
- UsagePriceConfig,
- AppEnv,
- EntitlementWithFeature,
- EntInterval,
- FeatureUsageType,
- notNullish,
+ Entitlement,
+ Feature,
+ FeatureType,
+ Price,
+ UsagePriceConfig,
+ AppEnv,
+ EntitlementWithFeature,
+ EntInterval,
+ FeatureUsageType,
+ notNullish,
} from "@autumn/shared";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import { toAPIFeature } from "../utils/mapFeatureUtils.js";
const handleFeatureIdChanged = async ({
- db,
- orgId,
- env,
- feature,
- linkedEntitlements,
- entitlements,
- prices,
- creditSystems,
- newId,
- logger,
+ db,
+ orgId,
+ env,
+ feature,
+ linkedEntitlements,
+ entitlements,
+ prices,
+ creditSystems,
+ newId,
+ logger,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- feature: Feature;
- linkedEntitlements: Entitlement[];
- entitlements: Entitlement[];
- prices: Price[];
- creditSystems: Feature[];
- newId: string;
- logger: any;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ feature: Feature;
+ linkedEntitlements: Entitlement[];
+ entitlements: Entitlement[];
+ prices: Price[];
+ creditSystems: Feature[];
+ newId: string;
+ logger: any;
}) => {
- // 1. Check if any customer entitlement linked to this feature
- let cusEnts = await CusEntService.getByFeature({
- db,
- internalFeatureId: feature.internal_id!,
- });
+ // 1. Check if any customer entitlement linked to this feature
+ let cusEnts = await CusEntService.getByFeature({
+ db,
+ internalFeatureId: feature.internal_id!,
+ });
- if (cusEnts.length > 0) {
- throw new RecaseError({
- message: `Cannot change id of feature ${feature.id} because a customer is using it`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ if (cusEnts.length > 0) {
+ throw new RecaseError({
+ message: `Cannot change id of feature ${feature.id} because a customer is using it`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- // 2. Update all linked objects
- let batchUpdate = [];
+ // 2. Update all linked objects
+ let batchUpdate = [];
- for (let entitlement of linkedEntitlements) {
- batchUpdate.push(
- EntitlementService.update({
- db,
- id: entitlement.id!,
- updates: {
- entity_feature_id: newId,
- },
- })
- );
- }
+ for (let entitlement of linkedEntitlements) {
+ batchUpdate.push(
+ EntitlementService.update({
+ db,
+ id: entitlement.id!,
+ updates: {
+ entity_feature_id: newId,
+ },
+ }),
+ );
+ }
- await Promise.all(batchUpdate);
+ await Promise.all(batchUpdate);
- // 3. Update all linked prices
- let priceUpdate = [];
- for (let price of prices) {
- priceUpdate.push(
- PriceService.update({
- db,
- id: price.id!,
- update: {
- config: {
- ...price.config,
- feature_id: newId,
- } as UsagePriceConfig,
- },
- })
- );
- }
+ // 3. Update all linked prices
+ let priceUpdate = [];
+ for (let price of prices) {
+ priceUpdate.push(
+ PriceService.update({
+ db,
+ id: price.id!,
+ update: {
+ config: {
+ ...price.config,
+ feature_id: newId,
+ } as UsagePriceConfig,
+ },
+ }),
+ );
+ }
- await Promise.all(priceUpdate);
+ await Promise.all(priceUpdate);
- // 4. Update all linked credit systems
- let creditSystemUpdate = [];
- for (let creditSystem of creditSystems) {
- let newSchema = structuredClone(creditSystem.config.schema);
- for (let i = 0; i < newSchema.length; i++) {
- if (newSchema[i].metered_feature_id === feature.id) {
- newSchema[i].metered_feature_id = newId;
- }
- }
- creditSystemUpdate.push(
- FeatureService.update({
- db,
- id: creditSystem.id!,
- orgId,
- env,
- updates: {
- config: {
- ...creditSystem.config,
- schema: newSchema,
- },
- },
- })
- );
- }
+ // 4. Update all linked credit systems
+ let creditSystemUpdate = [];
+ for (let creditSystem of creditSystems) {
+ let newSchema = structuredClone(creditSystem.config.schema);
+ for (let i = 0; i < newSchema.length; i++) {
+ if (newSchema[i].metered_feature_id === feature.id) {
+ newSchema[i].metered_feature_id = newId;
+ }
+ }
+ creditSystemUpdate.push(
+ FeatureService.update({
+ db,
+ id: creditSystem.id!,
+ orgId,
+ env,
+ updates: {
+ config: {
+ ...creditSystem.config,
+ schema: newSchema,
+ },
+ },
+ }),
+ );
+ }
- await Promise.all(creditSystemUpdate);
+ await Promise.all(creditSystemUpdate);
- // 5. Update all linked entitlements
- let entitlementUpdate = [];
+ // 5. Update all linked entitlements
+ let entitlementUpdate = [];
- for (let entitlement of entitlements) {
- entitlementUpdate.push(
- EntitlementService.update({
- db,
- id: entitlement.id!,
- updates: {
- feature_id: newId,
- },
- })
- );
- }
+ for (let entitlement of entitlements) {
+ entitlementUpdate.push(
+ EntitlementService.update({
+ db,
+ id: entitlement.id!,
+ updates: {
+ feature_id: newId,
+ },
+ }),
+ );
+ }
- await Promise.all(entitlementUpdate);
+ await Promise.all(entitlementUpdate);
};
const handleFeatureUsageTypeChanged = async ({
- db,
- feature,
- newUsageType,
- linkedEntitlements,
- entitlements,
- prices,
- creditSystems,
+ db,
+ feature,
+ newUsageType,
+ linkedEntitlements,
+ entitlements,
+ prices,
+ creditSystems,
}: {
- db: DrizzleCli;
- feature: Feature;
- newUsageType: FeatureUsageType;
- linkedEntitlements: EntitlementWithFeature[];
- entitlements: EntitlementWithFeature[];
- prices: Price[];
- creditSystems: Feature[];
+ db: DrizzleCli;
+ feature: Feature;
+ newUsageType: FeatureUsageType;
+ linkedEntitlements: EntitlementWithFeature[];
+ entitlements: EntitlementWithFeature[];
+ prices: Price[];
+ creditSystems: Feature[];
}) => {
- let usageTypeTitle = keyToTitle(newUsageType).toLowerCase();
- if (creditSystems.length > 0) {
- throw new RecaseError({
- message: `Cannot set to ${usageTypeTitle} because it is used in credit system ${creditSystems[0].id}`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ let usageTypeTitle = keyToTitle(newUsageType).toLowerCase();
+ if (creditSystems.length > 0) {
+ throw new RecaseError({
+ message: `Cannot set to ${usageTypeTitle} because it is used in credit system ${creditSystems[0].id}`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- if (linkedEntitlements.length > 0) {
- throw new RecaseError({
- message: `Cannot set to ${usageTypeTitle} because it is used as an entity by ${linkedEntitlements[0].feature.name}`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ if (linkedEntitlements.length > 0) {
+ throw new RecaseError({
+ message: `Cannot set to ${usageTypeTitle} because it is used as an entity by ${linkedEntitlements[0].feature.name}`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- // Get cus product using feature...
- let cusEnts = await CusEntService.getByFeature({
- db,
- internalFeatureId: feature.internal_id!,
- });
+ // Get cus product using feature...
+ let cusEnts = await CusEntService.getByFeature({
+ db,
+ internalFeatureId: feature.internal_id!,
+ });
- if (cusEnts && cusEnts.length > 0) {
- throw new RecaseError({
- message: `Cannot set to ${usageTypeTitle} because it is / was used by customers`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ if (cusEnts && cusEnts.length > 0) {
+ throw new RecaseError({
+ message: `Cannot set to ${usageTypeTitle} because it is / was used by customers`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- if (entitlements.length > 0) {
- console.log(
- `Feature usage type changed to ${newUsageType}, updating entitlements and prices`
- );
- if (newUsageType == FeatureUsageType.Continuous) {
- let batchEntUpdate = [];
- for (let entitlement of entitlements) {
- batchEntUpdate.push(
- EntitlementService.update({
- db,
- id: entitlement.id!,
- updates: {
- interval: EntInterval.Lifetime,
- },
- })
- );
- }
+ if (entitlements.length > 0) {
+ console.log(
+ `Feature usage type changed to ${newUsageType}, updating entitlements and prices`,
+ );
+ if (newUsageType == FeatureUsageType.Continuous) {
+ let batchEntUpdate = [];
+ for (let entitlement of entitlements) {
+ batchEntUpdate.push(
+ EntitlementService.update({
+ db,
+ id: entitlement.id!,
+ updates: {
+ interval: EntInterval.Lifetime,
+ },
+ }),
+ );
+ }
- await Promise.all(batchEntUpdate);
- console.log(`Updated ${entitlements.length} entitlements`);
- }
- }
+ await Promise.all(batchEntUpdate);
+ console.log(`Updated ${entitlements.length} entitlements`);
+ }
+ }
- if (prices.length > 0) {
- let batchPriceUpdate = [];
- for (let price of prices) {
- let priceConfig = price.config as UsagePriceConfig;
+ if (prices.length > 0) {
+ let batchPriceUpdate = [];
+ for (let price of prices) {
+ let priceConfig = price.config as UsagePriceConfig;
- batchPriceUpdate.push(
- PriceService.update({
- db,
- id: price.id!,
- update: {
- config: {
- ...priceConfig,
- should_prorate:
- newUsageType == FeatureUsageType.Continuous ? false : true, // if continuous, don't prorate -> get usage_in_arrear type...
- stripe_price_id: null,
- },
- },
- })
- );
- }
+ batchPriceUpdate.push(
+ PriceService.update({
+ db,
+ id: price.id!,
+ update: {
+ config: {
+ ...priceConfig,
+ should_prorate:
+ newUsageType == FeatureUsageType.Continuous ? false : true, // if continuous, don't prorate -> get usage_in_arrear type...
+ stripe_price_id: null,
+ },
+ },
+ }),
+ );
+ }
- await Promise.all(batchPriceUpdate);
- console.log(`Updated ${prices.length} prices`);
- }
+ await Promise.all(batchPriceUpdate);
+ console.log(`Updated ${prices.length} prices`);
+ }
- // // Allow update for entitlement / price?
- // if (entitlements.length > 0) {
- // }
+ // // Allow update for entitlement / price?
+ // if (entitlements.length > 0) {
+ // }
};
export const handleUpdateFeature = async (
- req: any,
- res: any,
- fromApi: boolean = false
+ req: any,
+ res: any,
+ fromApi: boolean = false,
) =>
- routeHandler({
- req,
- res,
- action: "Update feature",
- handler: async (req: any, res: any) => {
- let featureId = req.params.feature_id;
- let data = req.body;
- let { db, orgId, env, logtail: logger } = req;
+ routeHandler({
+ req,
+ res,
+ action: "Update feature",
+ handler: async (req: any, res: any) => {
+ let featureId = req.params.feature_id;
+ let data = req.body;
+ let { db, orgId, env, logtail: logger } = req;
- // 1. Get feature by ID
- let features = await FeatureService.getFromReq(req);
- let feature = features.find((f) => f.id == featureId);
+ // 1. Get feature by ID
+ let features = await FeatureService.getFromReq(req);
+ let feature = features.find((f) => f.id == featureId);
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${featureId} not found`,
- code: ErrCode.InvalidFeature,
- statusCode: 404,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${featureId} not found`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 404,
+ });
+ }
- // If only archiving, skip other checks and just update
- if (data.archived !== undefined && Object.keys(data).length === 1) {
- console.log("Updating feature archived to: ", data.archived);
- let updatedFeature = await FeatureService.update({
- db: req.db,
- id: featureId,
- orgId: req.orgId,
- env: req.env,
- updates: {
- archived: data.archived,
- },
- });
+ // If only archiving, skip other checks and just update
+ if (data.archived !== undefined && Object.keys(data).length === 1) {
+ console.log("Updating feature archived to: ", data.archived);
+ let updatedFeature = await FeatureService.update({
+ db: req.db,
+ id: featureId,
+ orgId: req.orgId,
+ env: req.env,
+ updates: {
+ archived: data.archived,
+ },
+ });
- if (res) {
- res
- .status(200)
- .json(
- updatedFeature
- ? toAPIFeature({ feature: updatedFeature })
- : undefined
- );
- }
- return;
- }
+ if (res) {
+ res
+ .status(200)
+ .json(
+ updatedFeature
+ ? toAPIFeature({ feature: updatedFeature })
+ : undefined,
+ );
+ }
+ return;
+ }
- // 1. Check if changing type...
- let isChangingType = notNullish(data.type) && feature.type !== data.type;
+ // 1. Check if changing type...
+ let isChangingType = notNullish(data.type) && feature.type !== data.type;
- let isChangingId = notNullish(data.id) && feature.id !== data.id;
+ let isChangingId = notNullish(data.id) && feature.id !== data.id;
- let isChangingUsageType =
- feature.type != FeatureType.Boolean &&
- data.type != FeatureType.Boolean &&
- feature.config?.usage_type != data.config?.usage_type;
+ let isChangingUsageType =
+ feature.type != FeatureType.Boolean &&
+ data.type != FeatureType.Boolean &&
+ feature.config?.usage_type != data.config?.usage_type;
- let isChangingName = feature.name !== data.name;
+ let isChangingName = feature.name !== data.name;
- if (isChangingType || isChangingId || isChangingUsageType) {
- let { entitlements, prices, creditSystems, linkedEntitlements } =
- await getObjectsUsingFeature({
- db,
- orgId: req.orgId,
- env: req.env,
- allFeatures: features,
- feature,
- });
+ if (isChangingType || isChangingId || isChangingUsageType) {
+ let { entitlements, prices, creditSystems, linkedEntitlements } =
+ await getObjectsUsingFeature({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ allFeatures: features,
+ feature,
+ });
- // 1. Can't change type if any objects are linked to it
- if (
- isChangingType &&
- (linkedEntitlements.length > 0 ||
- prices.length > 0 ||
- creditSystems.length > 0 ||
- entitlements.length > 0)
- ) {
- throw new RecaseError({
- message: `Cannot change type of feature ${featureId} because it is used in an entitlement or credit system`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ // 1. Can't change type if any objects are linked to it
+ if (
+ isChangingType &&
+ (linkedEntitlements.length > 0 ||
+ prices.length > 0 ||
+ creditSystems.length > 0 ||
+ entitlements.length > 0)
+ ) {
+ throw new RecaseError({
+ message: `Cannot change type of feature ${featureId} because it is used in an entitlement or credit system`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
- if (isChangingId) {
- await handleFeatureIdChanged({
- db,
- orgId,
- env,
- feature,
- linkedEntitlements,
- entitlements,
- prices,
- creditSystems,
- newId: data.id,
- logger,
- });
- }
+ if (isChangingId) {
+ await handleFeatureIdChanged({
+ db,
+ orgId,
+ env,
+ feature,
+ linkedEntitlements,
+ entitlements,
+ prices,
+ creditSystems,
+ newId: data.id,
+ logger,
+ });
+ }
- if (isChangingUsageType && data.config?.usage_type) {
- await handleFeatureUsageTypeChanged({
- db,
- feature,
- linkedEntitlements,
- entitlements,
- prices,
- creditSystems,
- newUsageType: data.config.usage_type,
- });
- }
- }
+ if (isChangingUsageType && data.config?.usage_type) {
+ await handleFeatureUsageTypeChanged({
+ db,
+ feature,
+ linkedEntitlements,
+ entitlements,
+ prices,
+ creditSystems,
+ newUsageType: data.config.usage_type,
+ });
+ }
+ }
- const newConfig =
- data.config !== undefined
- ? feature.type == FeatureType.CreditSystem
- ? validateCreditSystem(data.config)
- : feature.type == FeatureType.Metered
- ? validateMeteredConfig(data.config)
- : data.config
- : feature.config;
+ const newConfig =
+ data.config !== undefined
+ ? feature.type == FeatureType.CreditSystem
+ ? validateCreditSystem(data.config)
+ : feature.type == FeatureType.Metered
+ ? validateMeteredConfig(data.config)
+ : data.config
+ : feature.config;
- let updatedFeature = await FeatureService.update({
- db: req.db,
- id: featureId,
- orgId: req.orgId,
- env: req.env,
- updates: {
- id: data.id !== undefined ? data.id : feature.id,
- name: data.name !== undefined ? data.name : feature.name,
- type: data.type !== undefined ? data.type : feature.type,
- archived:
- data.archived !== undefined ? data.archived : feature.archived,
+ let updatedFeature = await FeatureService.update({
+ db: req.db,
+ id: featureId,
+ orgId: req.orgId,
+ env: req.env,
+ updates: {
+ id: data.id !== undefined ? data.id : feature.id,
+ name: data.name !== undefined ? data.name : feature.name,
+ type: data.type !== undefined ? data.type : feature.type,
+ archived:
+ data.archived !== undefined ? data.archived : feature.archived,
- config: newConfig,
- },
- });
+ config: newConfig,
+ },
+ });
- if (isChangingName) {
- await addTaskToQueue({
- jobName: JobName.GenerateFeatureDisplay,
- payload: {
- feature: updatedFeature,
- org: req.org,
- },
- });
- }
+ if (isChangingName) {
+ await addTaskToQueue({
+ jobName: JobName.GenerateFeatureDisplay,
+ payload: {
+ feature: updatedFeature,
+ org: req.org,
+ },
+ });
+ }
- res
- .status(200)
- .json(
- updatedFeature ? toAPIFeature({ feature: updatedFeature }) : undefined
- );
- },
- });
+ res
+ .status(200)
+ .json(
+ updatedFeature
+ ? toAPIFeature({ feature: updatedFeature })
+ : undefined,
+ );
+ },
+ });
diff --git a/server/src/internal/features/internalFeatureRouter.ts b/server/src/internal/features/internalFeatureRouter.ts
index e0eb31be5..24c81f401 100644
--- a/server/src/internal/features/internalFeatureRouter.ts
+++ b/server/src/internal/features/internalFeatureRouter.ts
@@ -16,76 +16,76 @@ import { handleGetFeatureDeletionInfo } from "./handlers/handleGetFeatureDeletio
export const internalFeatureRouter: Router = express.Router();
internalFeatureRouter.get("", async (req: any, res: any) => {
- try {
- let { showArchived } = req.query;
+ try {
+ let { showArchived } = req.query;
- if (showArchived !== undefined) {
- // If showArchived is specified, use FeatureService.list with the parameter
- let features = await FeatureService.list({
- db: req.db,
- orgId: req.orgId,
- env: req.env,
- archived: showArchived === "true" ? true : false,
- });
- res.status(200).json({ features });
- } else {
- // If no showArchived parameter, use the original getFromReq method
- let features = await FeatureService.getFromReq(req);
- res.status(200).json({ features });
- }
- } catch (error: any) {
- console.log("Error fetching features:", error);
- res.status(500).json({ error: error.message });
- }
+ if (showArchived !== undefined) {
+ // If showArchived is specified, use FeatureService.list with the parameter
+ let features = await FeatureService.list({
+ db: req.db,
+ orgId: req.orgId,
+ env: req.env,
+ archived: showArchived === "true" ? true : false,
+ });
+ res.status(200).json({ features });
+ } else {
+ // If no showArchived parameter, use the original getFromReq method
+ let features = await FeatureService.getFromReq(req);
+ res.status(200).json({ features });
+ }
+ } catch (error: any) {
+ console.log("Error fetching features:", error);
+ res.status(500).json({ error: error.message });
+ }
});
export const validateFeature = (data: any) => {
- let featureType = data.type;
+ let featureType = data.type;
- validateFeatureId(data.id);
+ validateFeatureId(data.id);
- let config = data.config;
- if (featureType == FeatureType.Metered) {
- config = validateMeteredConfig(config);
- } else if (featureType == FeatureType.CreditSystem) {
- config = validateCreditSystem(config);
- }
+ let config = data.config;
+ if (featureType == FeatureType.Metered) {
+ config = validateMeteredConfig(config);
+ } else if (featureType == FeatureType.CreditSystem) {
+ config = validateCreditSystem(config);
+ }
- try {
- const parsedFeature = CreateFeatureSchema.parse({ ...data, config });
- return parsedFeature;
- } catch (error: any) {
- throw new RecaseError({
- message: `Invalid feature: ${formatZodError(error)}`,
- code: ErrCode.InvalidFeature,
- statusCode: 400,
- });
- }
+ try {
+ const parsedFeature = CreateFeatureSchema.parse({ ...data, config });
+ return parsedFeature;
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Invalid feature: ${formatZodError(error)}`,
+ code: ErrCode.InvalidFeature,
+ statusCode: 400,
+ });
+ }
};
export const initNewFeature = ({
- data,
- orgId,
- env,
+ data,
+ orgId,
+ env,
}: {
- data: any;
- orgId: string;
- env: any;
+ data: any;
+ orgId: string;
+ env: any;
}) => {
- return {
- ...data,
- org_id: orgId,
- env,
- created_at: Date.now(),
- internal_id: generateId("fe"),
- };
+ return {
+ ...data,
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
+ internal_id: generateId("fe"),
+ };
};
internalFeatureRouter.post("", handleCreateFeature);
internalFeatureRouter.get(
- "/data/deletion_text/:feature_id",
- handleGetFeatureDeletionInfo
+ "/data/deletion_text/:feature_id",
+ handleGetFeatureDeletionInfo,
);
internalFeatureRouter.post("/:feature_id", handleUpdateFeature as any);
diff --git a/server/src/internal/features/utils/constructFeatureUtils.ts b/server/src/internal/features/utils/constructFeatureUtils.ts
index 87437b962..cc4a4c4a3 100644
--- a/server/src/internal/features/utils/constructFeatureUtils.ts
+++ b/server/src/internal/features/utils/constructFeatureUtils.ts
@@ -6,144 +6,144 @@ import { generateId, keyToTitle } from "@/utils/genUtils.js";
import { Feature } from "@autumn/shared";
export const constructFeature = ({
- id,
- name,
- orgId,
- type,
- env,
- config,
- display,
+ id,
+ name,
+ orgId,
+ type,
+ env,
+ config,
+ display,
}: {
- id: string;
- name: string;
- orgId: string;
- type: FeatureType;
- env: AppEnv;
- config: any;
- display: any;
+ id: string;
+ name: string;
+ orgId: string;
+ type: FeatureType;
+ env: AppEnv;
+ config: any;
+ display: any;
}) => {
- let newFeature: Feature = {
- internal_id: generateId("fe"),
- id,
- name,
- org_id: orgId,
- env,
- created_at: Date.now(),
- type,
- config,
- display,
- archived: false,
- };
+ let newFeature: Feature = {
+ internal_id: generateId("fe"),
+ id,
+ name,
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
+ type,
+ config,
+ display,
+ archived: false,
+ };
- return newFeature;
+ return newFeature;
};
export const constructBooleanFeature = ({
- featureId,
- orgId,
- env,
- name,
+ featureId,
+ orgId,
+ env,
+ name,
}: {
- featureId: string;
- orgId: string;
- env: AppEnv;
- name?: string;
+ featureId: string;
+ orgId: string;
+ env: AppEnv;
+ name?: string;
}) => {
- let newFeature: Feature = {
- internal_id: generateId("fe"),
- org_id: orgId,
- env,
- created_at: Date.now(),
+ let newFeature: Feature = {
+ internal_id: generateId("fe"),
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
- id: featureId,
- name: name || keyToTitle(featureId),
- type: FeatureType.Boolean,
- config: null,
- archived: false,
- };
+ id: featureId,
+ name: name || keyToTitle(featureId),
+ type: FeatureType.Boolean,
+ config: null,
+ archived: false,
+ };
- return newFeature;
+ return newFeature;
};
export const constructMeteredFeature = ({
- featureId,
- name,
- orgId,
- env,
- usageType,
+ featureId,
+ name,
+ orgId,
+ env,
+ usageType,
}: {
- featureId: string;
- name?: string;
- orgId: string;
- env: AppEnv;
- usageType: FeatureUsageType;
+ featureId: string;
+ name?: string;
+ orgId: string;
+ env: AppEnv;
+ usageType: FeatureUsageType;
}) => {
- let newFeature: Feature = {
- internal_id: generateId("fe"),
- org_id: orgId,
- env,
- created_at: Date.now(),
+ let newFeature: Feature = {
+ internal_id: generateId("fe"),
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
- id: featureId,
- name: name || keyToTitle(featureId),
- type: FeatureType.Metered,
- config: {
- filters: [
- {
- property: "event_name",
- operator: "eq",
- value: [],
- },
- ],
- aggregate: {
- type: AggregateType.Sum,
- property: "value",
- },
- usage_type: usageType,
- },
- archived: false,
- };
+ id: featureId,
+ name: name || keyToTitle(featureId),
+ type: FeatureType.Metered,
+ config: {
+ filters: [
+ {
+ property: "event_name",
+ operator: "eq",
+ value: [],
+ },
+ ],
+ aggregate: {
+ type: AggregateType.Sum,
+ property: "value",
+ },
+ usage_type: usageType,
+ },
+ archived: false,
+ };
- return newFeature;
+ return newFeature;
};
export const constructCreditSystem = ({
- featureId,
- name,
- orgId,
- env,
- schema,
+ featureId,
+ name,
+ orgId,
+ env,
+ schema,
}: {
- featureId: string;
- name?: string;
- orgId: string;
- env: AppEnv;
- schema: {
- metered_feature_id: string;
- credit_cost: number;
- }[];
+ featureId: string;
+ name?: string;
+ orgId: string;
+ env: AppEnv;
+ schema: {
+ metered_feature_id: string;
+ credit_cost: number;
+ }[];
}) => {
- const config = {
- schema: schema.map((item) => ({
- feature_amount: 1,
- metered_feature_id: item.metered_feature_id,
- credit_amount: item.credit_cost,
- })),
- usage_type: FeatureUsageType.Single,
- };
+ const config = {
+ schema: schema.map((item) => ({
+ feature_amount: 1,
+ metered_feature_id: item.metered_feature_id,
+ credit_amount: item.credit_cost,
+ })),
+ usage_type: FeatureUsageType.Single,
+ };
- let newFeature: Feature = {
- internal_id: generateId("fe"),
- org_id: orgId,
- env,
- created_at: Date.now(),
+ let newFeature: Feature = {
+ internal_id: generateId("fe"),
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
- id: featureId,
- name: name || keyToTitle(featureId),
- type: FeatureType.CreditSystem,
- config,
- archived: false,
- };
+ id: featureId,
+ name: name || keyToTitle(featureId),
+ type: FeatureType.CreditSystem,
+ config,
+ archived: false,
+ };
- return newFeature;
+ return newFeature;
};
diff --git a/server/src/internal/features/utils/displayUtils.ts b/server/src/internal/features/utils/displayUtils.ts
index 7eac9cc11..08aace632 100644
--- a/server/src/internal/features/utils/displayUtils.ts
+++ b/server/src/internal/features/utils/displayUtils.ts
@@ -3,39 +3,39 @@ import { Feature } from "@autumn/shared";
import { Organization } from "@autumn/shared";
export const getFeatureName = ({
- feature,
- plural,
- capitalize = false,
+ feature,
+ plural,
+ capitalize = false,
}: {
- feature: Feature;
- plural: boolean;
- capitalize?: boolean;
+ feature: Feature;
+ plural: boolean;
+ capitalize?: boolean;
}) => {
- let featureName = feature.name;
+ let featureName = feature.name;
- if (feature.display) {
- if (plural) {
- featureName = feature.display.plural || featureName;
- } else {
- featureName = feature.display.singular || featureName;
- }
- }
+ if (feature.display) {
+ if (plural) {
+ featureName = feature.display.plural || featureName;
+ } else {
+ featureName = feature.display.singular || featureName;
+ }
+ }
- if (capitalize) {
- featureName = featureName.charAt(0).toUpperCase() + featureName.slice(1);
- }
+ if (capitalize) {
+ featureName = featureName.charAt(0).toUpperCase() + featureName.slice(1);
+ }
- return featureName;
+ return featureName;
};
export const getFeatureNameWithCapital = ({
- feature,
+ feature,
}: {
- feature: Feature;
+ feature: Feature;
}) => {
- if (feature.name.length > 0) {
- return `${feature.name.charAt(0).toUpperCase()}${feature.name.slice(1)}`;
- }
+ if (feature.name.length > 0) {
+ return `${feature.name.charAt(0).toUpperCase()}${feature.name.slice(1)}`;
+ }
- return feature.name;
+ return feature.name;
};
diff --git a/server/src/internal/features/utils/mapFeatureUtils.ts b/server/src/internal/features/utils/mapFeatureUtils.ts
index eb14cf3f5..8bc19d38e 100644
--- a/server/src/internal/features/utils/mapFeatureUtils.ts
+++ b/server/src/internal/features/utils/mapFeatureUtils.ts
@@ -1,98 +1,98 @@
import {
- APIFeature,
- APIFeatureType,
- AppEnv,
- CreditSchemaItem,
- Feature,
- FeatureType,
- FeatureUsageType,
+ APIFeature,
+ APIFeatureType,
+ AppEnv,
+ CreditSchemaItem,
+ Feature,
+ FeatureType,
+ FeatureUsageType,
} from "@autumn/shared";
import { APIFeatureSchema } from "@autumn/shared";
import {
- constructBooleanFeature,
- constructCreditSystem,
- constructMeteredFeature,
+ constructBooleanFeature,
+ constructCreditSystem,
+ constructMeteredFeature,
} from "./constructFeatureUtils.js";
import RecaseError from "@/utils/errorUtils.js";
export const toAPIFeature = ({ feature }: { feature: Feature }) => {
- // return FeatureResponseSchema.parse(feature);
- // 1. Get feature type
- let featureType = feature.type;
- if (feature.type == FeatureType.Metered) {
- featureType = feature.config.usage_type;
- }
+ // return FeatureResponseSchema.parse(feature);
+ // 1. Get feature type
+ let featureType = feature.type;
+ if (feature.type == FeatureType.Metered) {
+ featureType = feature.config.usage_type;
+ }
- let creditSchema = undefined;
- if (feature.type == FeatureType.CreditSystem) {
- creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
- metered_feature_id: s.metered_feature_id,
- credit_cost: s.credit_amount,
- }));
- }
+ let creditSchema = undefined;
+ if (feature.type == FeatureType.CreditSystem) {
+ creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
+ metered_feature_id: s.metered_feature_id,
+ credit_cost: s.credit_amount,
+ }));
+ }
- return APIFeatureSchema.parse({
- id: feature.id,
- name: feature.name,
- type: featureType,
- display: {
- singular: feature.display?.singular || feature.name,
- plural: feature.display?.plural || feature.name,
- },
- credit_schema: creditSchema,
- archived: feature.archived,
- });
+ return APIFeatureSchema.parse({
+ id: feature.id,
+ name: feature.name,
+ type: featureType,
+ display: {
+ singular: feature.display?.singular || feature.name,
+ plural: feature.display?.plural || feature.name,
+ },
+ credit_schema: creditSchema,
+ archived: feature.archived,
+ });
};
export const fromAPIFeature = ({
- apiFeature,
- orgId,
- env,
+ apiFeature,
+ orgId,
+ env,
}: {
- apiFeature: APIFeature;
- orgId: string;
- env: AppEnv;
+ apiFeature: APIFeature;
+ orgId: string;
+ env: AppEnv;
}) => {
- let isMetered =
- apiFeature.type == APIFeatureType.SingleUsage ||
- apiFeature.type == APIFeatureType.ContinuousUse;
+ let isMetered =
+ apiFeature.type == APIFeatureType.SingleUsage ||
+ apiFeature.type == APIFeatureType.ContinuousUse;
- let featureType: FeatureType = isMetered
- ? FeatureType.Metered
- : (apiFeature.type as unknown as FeatureType);
+ let featureType: FeatureType = isMetered
+ ? FeatureType.Metered
+ : (apiFeature.type as unknown as FeatureType);
- if (isMetered) {
- return constructMeteredFeature({
- featureId: apiFeature.id,
- name: apiFeature.name || "",
- usageType: apiFeature.type as unknown as FeatureUsageType,
- orgId,
- env,
- });
- }
+ if (isMetered) {
+ return constructMeteredFeature({
+ featureId: apiFeature.id,
+ name: apiFeature.name || "",
+ usageType: apiFeature.type as unknown as FeatureUsageType,
+ orgId,
+ env,
+ });
+ }
- if (featureType == FeatureType.CreditSystem) {
- if (!apiFeature.credit_schema || apiFeature.credit_schema.length == 0) {
- throw new RecaseError({
- message: "Credit system schema is required",
- code: "CREDIT_SYSTEM_SCHEMA_REQUIRED",
- statusCode: 400,
- });
- }
+ if (featureType == FeatureType.CreditSystem) {
+ if (!apiFeature.credit_schema || apiFeature.credit_schema.length == 0) {
+ throw new RecaseError({
+ message: "Credit system schema is required",
+ code: "CREDIT_SYSTEM_SCHEMA_REQUIRED",
+ statusCode: 400,
+ });
+ }
- return constructCreditSystem({
- featureId: apiFeature.id,
- name: apiFeature.name || "",
- orgId,
- env,
- schema: apiFeature.credit_schema!,
- });
- }
+ return constructCreditSystem({
+ featureId: apiFeature.id,
+ name: apiFeature.name || "",
+ orgId,
+ env,
+ schema: apiFeature.credit_schema!,
+ });
+ }
- return constructBooleanFeature({
- featureId: apiFeature.id,
- name: apiFeature.name || "",
- orgId,
- env,
- });
+ return constructBooleanFeature({
+ featureId: apiFeature.id,
+ name: apiFeature.name || "",
+ orgId,
+ env,
+ });
};
diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts
index 3357edeff..5153c47c2 100644
--- a/server/src/internal/invoices/InvoiceService.ts
+++ b/server/src/internal/invoices/InvoiceService.ts
@@ -1,13 +1,13 @@
import {
- Customer,
- Feature,
- Invoice,
- InvoiceItem,
- InvoiceItemResponseSchema,
- InvoiceResponse,
- InvoiceStatus,
- LoggerAction,
- Organization,
+ Customer,
+ Feature,
+ Invoice,
+ InvoiceItem,
+ InvoiceItemResponseSchema,
+ InvoiceResponse,
+ InvoiceStatus,
+ LoggerAction,
+ Organization,
} from "@autumn/shared";
import Stripe from "stripe";
import { generateId } from "@/utils/genUtils.js";
@@ -19,201 +19,201 @@ import { invoices } from "@autumn/shared";
import { and, desc, eq } from "drizzle-orm";
export const processInvoice = ({
- invoice,
- withItems = false,
- features,
+ invoice,
+ withItems = false,
+ features,
}: {
- invoice: Invoice;
- withItems?: boolean;
- features?: Feature[];
+ invoice: Invoice;
+ withItems?: boolean;
+ features?: Feature[];
}) => {
- return {
- product_ids: invoice.product_ids,
- stripe_id: invoice.stripe_id,
- status: invoice.status,
- total: invoice.total,
- currency: invoice.currency,
- created_at: invoice.created_at,
- // hosted_invoice_url: invoice.hosted_invoice_url,
- hosted_invoice_url: `${process.env.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`,
- items: withItems
- ? (invoice.items || []).map((i) => {
- let feature = features?.find(
- (f) => f.internal_id === i.internal_feature_id
- );
+ return {
+ product_ids: invoice.product_ids,
+ stripe_id: invoice.stripe_id,
+ status: invoice.status,
+ total: invoice.total,
+ currency: invoice.currency,
+ created_at: invoice.created_at,
+ // hosted_invoice_url: invoice.hosted_invoice_url,
+ hosted_invoice_url: `${process.env.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`,
+ items: withItems
+ ? (invoice.items || []).map((i) => {
+ let feature = features?.find(
+ (f) => f.internal_id === i.internal_feature_id,
+ );
- return InvoiceItemResponseSchema.parse({
- description: i.description,
- period_start: i.period_start,
- period_end: i.period_end,
- feature_id: feature?.id,
- feature_name: feature?.name,
- });
- })
- : undefined,
- } as InvoiceResponse;
+ return InvoiceItemResponseSchema.parse({
+ description: i.description,
+ period_start: i.period_start,
+ period_end: i.period_end,
+ feature_id: feature?.id,
+ feature_name: feature?.name,
+ });
+ })
+ : undefined,
+ } as InvoiceResponse;
};
export class InvoiceService {
- static async get({ db, id }: { db: DrizzleCli; id: string }) {
- return (await db.query.invoices.findFirst({
- where: eq(invoices.id, id),
- with: {
- customer: {
- with: {
- org: true,
- },
- },
- },
- })) as Invoice & { customer: Customer & { org: Organization } };
- }
+ static async get({ db, id }: { db: DrizzleCli; id: string }) {
+ return (await db.query.invoices.findFirst({
+ where: eq(invoices.id, id),
+ with: {
+ customer: {
+ with: {
+ org: true,
+ },
+ },
+ },
+ })) as Invoice & { customer: Customer & { org: Organization } };
+ }
- static async list({
- db,
- internalCustomerId,
- internalEntityId,
- limit = 100,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- internalEntityId?: string;
- limit?: number;
- }) {
- return (await db.query.invoices.findMany({
- where: and(
- eq(invoices.internal_customer_id, internalCustomerId),
- internalEntityId
- ? eq(invoices.internal_entity_id, internalEntityId)
- : undefined
- ),
- orderBy: [desc(invoices.created_at)],
- limit,
- })) as Invoice[];
- }
+ static async list({
+ db,
+ internalCustomerId,
+ internalEntityId,
+ limit = 100,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ internalEntityId?: string;
+ limit?: number;
+ }) {
+ return (await db.query.invoices.findMany({
+ where: and(
+ eq(invoices.internal_customer_id, internalCustomerId),
+ internalEntityId
+ ? eq(invoices.internal_entity_id, internalEntityId)
+ : undefined,
+ ),
+ orderBy: [desc(invoices.created_at)],
+ limit,
+ })) as Invoice[];
+ }
- static async getByStripeId({
- db,
- stripeId,
- }: {
- db: DrizzleCli;
- stripeId: string;
- }) {
- const invoice = await db.query.invoices.findFirst({
- where: eq(invoices.stripe_id, stripeId),
- });
+ static async getByStripeId({
+ db,
+ stripeId,
+ }: {
+ db: DrizzleCli;
+ stripeId: string;
+ }) {
+ const invoice = await db.query.invoices.findFirst({
+ where: eq(invoices.stripe_id, stripeId),
+ });
- if (!invoice) {
- return null;
- }
+ if (!invoice) {
+ return null;
+ }
- return invoice as Invoice;
- }
+ return invoice as Invoice;
+ }
- static async createInvoiceFromStripe({
- db,
- stripeInvoice,
- internalCustomerId,
- internalEntityId,
- productIds,
- internalProductIds,
- status,
- org,
- sendRevenueEvent = true,
- items = [],
- }: {
- db: DrizzleCli;
- stripeInvoice: Stripe.Invoice;
- internalCustomerId: string;
- internalEntityId?: string | null;
- productIds: string[];
- internalProductIds: string[];
- status?: InvoiceStatus | null;
- org: Organization;
- sendRevenueEvent?: boolean;
- items?: InvoiceItem[];
- }) {
- // Convert product ids to unique product ids
- const uniqueProductIds = [...new Set(productIds)];
- const uniqueInternalProductIds = [...new Set(internalProductIds)];
- let total = stripeInvoice.total / 100;
+ static async createInvoiceFromStripe({
+ db,
+ stripeInvoice,
+ internalCustomerId,
+ internalEntityId,
+ productIds,
+ internalProductIds,
+ status,
+ org,
+ sendRevenueEvent = true,
+ items = [],
+ }: {
+ db: DrizzleCli;
+ stripeInvoice: Stripe.Invoice;
+ internalCustomerId: string;
+ internalEntityId?: string | null;
+ productIds: string[];
+ internalProductIds: string[];
+ status?: InvoiceStatus | null;
+ org: Organization;
+ sendRevenueEvent?: boolean;
+ items?: InvoiceItem[];
+ }) {
+ // Convert product ids to unique product ids
+ const uniqueProductIds = [...new Set(productIds)];
+ const uniqueInternalProductIds = [...new Set(internalProductIds)];
+ let total = stripeInvoice.total / 100;
- if (stripeInvoice.currency.toLowerCase() == "clp") {
- total = stripeInvoice.total;
- }
+ if (stripeInvoice.currency.toLowerCase() == "clp") {
+ total = stripeInvoice.total;
+ }
- const invoice: Invoice = {
- id: generateId("inv"),
- internal_customer_id: internalCustomerId,
- product_ids: uniqueProductIds,
- created_at: stripeInvoice.created * 1000,
- stripe_id: stripeInvoice.id!,
- hosted_invoice_url: stripeInvoice.hosted_invoice_url || null,
- status: status || (stripeInvoice.status as InvoiceStatus | null),
- internal_product_ids: uniqueInternalProductIds,
- internal_entity_id: internalEntityId || null,
+ const invoice: Invoice = {
+ id: generateId("inv"),
+ internal_customer_id: internalCustomerId,
+ product_ids: uniqueProductIds,
+ created_at: stripeInvoice.created * 1000,
+ stripe_id: stripeInvoice.id!,
+ hosted_invoice_url: stripeInvoice.hosted_invoice_url || null,
+ status: status || (stripeInvoice.status as InvoiceStatus | null),
+ internal_product_ids: uniqueInternalProductIds,
+ internal_entity_id: internalEntityId || null,
- // Stripe stuff
- total,
- currency: stripeInvoice.currency,
- discounts: getInvoiceDiscounts({
- expandedInvoice: stripeInvoice,
- }),
+ // Stripe stuff
+ total,
+ currency: stripeInvoice.currency,
+ discounts: getInvoiceDiscounts({
+ expandedInvoice: stripeInvoice,
+ }),
- items: items,
- };
+ items: items,
+ };
- try {
- await db.insert(invoices).values(invoice as any);
- } catch (error: any) {
- if (error.code == "23505") {
- console.log(" 🧐 Invoice already exists");
- return;
- } else {
- console.error(" ❌ Error inserting Stripe invoice: ", error);
- throw error;
- }
- }
+ try {
+ await db.insert(invoices).values(invoice as any);
+ } catch (error: any) {
+ if (error.code == "23505") {
+ console.log(" 🧐 Invoice already exists");
+ return;
+ } else {
+ console.error(" ❌ Error inserting Stripe invoice: ", error);
+ throw error;
+ }
+ }
- // Send monthly_revenue event
- try {
- if (!stripeInvoice.livemode || !sendRevenueEvent) {
- return;
- }
+ // Send monthly_revenue event
+ try {
+ if (!stripeInvoice.livemode || !sendRevenueEvent) {
+ return;
+ }
- const autumn = new Autumn();
- await autumn.track({
- customer_id: org.id,
- event_name: "revenue",
- value: Math.round(stripeInvoice.total / 100),
- customer_data: {
- name: org.slug,
- },
- });
- console.log(" ✅ Sent revenue event");
- } catch (error) {
- console.log("Failed to send revenue event", error);
- }
- }
+ const autumn = new Autumn();
+ await autumn.track({
+ customer_id: org.id,
+ event_name: "revenue",
+ value: Math.round(stripeInvoice.total / 100),
+ customer_data: {
+ name: org.slug,
+ },
+ });
+ console.log(" ✅ Sent revenue event");
+ } catch (error) {
+ console.log("Failed to send revenue event", error);
+ }
+ }
- static async updateByStripeId({
- db,
- stripeId,
- updates,
- }: {
- db: DrizzleCli;
- stripeId: string;
- updates: Partial;
- }) {
- const results = await db
- .update(invoices)
- .set(updates as any)
- .where(eq(invoices.stripe_id, stripeId))
- .returning();
+ static async updateByStripeId({
+ db,
+ stripeId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ stripeId: string;
+ updates: Partial;
+ }) {
+ const results = await db
+ .update(invoices)
+ .set(updates as any)
+ .where(eq(invoices.stripe_id, stripeId))
+ .returning();
- if (results.length === 0) {
- return null;
- }
+ if (results.length === 0) {
+ return null;
+ }
- return results[0] as Invoice;
- }
+ return results[0] as Invoice;
+ }
}
diff --git a/server/src/internal/invoices/invoiceFormatUtils.ts b/server/src/internal/invoices/invoiceFormatUtils.ts
index 05a4cf051..236aec227 100644
--- a/server/src/internal/invoices/invoiceFormatUtils.ts
+++ b/server/src/internal/invoices/invoiceFormatUtils.ts
@@ -1,22 +1,22 @@
import {
- BillingInterval,
- BillingType,
- EntitlementWithFeature,
- Feature,
- FixedPriceConfig,
- FullCusProduct,
- FullCustomerPrice,
- FullProduct,
- getFeatureName,
- getFeatureNameWithCapital,
- Organization,
- Price,
- ProductItemInterval,
- UsagePriceConfig,
+ BillingInterval,
+ BillingType,
+ EntitlementWithFeature,
+ Feature,
+ FixedPriceConfig,
+ FullCusProduct,
+ FullCustomerPrice,
+ FullProduct,
+ getFeatureName,
+ getFeatureNameWithCapital,
+ Organization,
+ Price,
+ ProductItemInterval,
+ UsagePriceConfig,
} from "@autumn/shared";
import {
- getBillingType,
- getPriceEntitlement,
+ getBillingType,
+ getPriceEntitlement,
} from "../products/prices/priceUtils.js";
import { cusProductToEnts } from "@autumn/shared";
@@ -26,65 +26,65 @@ import { getIntervalString } from "../products/productUtils/productResponseUtils
import { billingToItemInterval } from "../products/product-items/itemIntervalUtils.js";
const getSingularAndPlural = (feature: Feature) => {
- const singular = getFeatureName({
- feature,
- plural: false,
- });
- const plural = getFeatureName({
- feature,
- plural: true,
- });
- return { singular, plural };
+ const singular = getFeatureName({
+ feature,
+ plural: false,
+ });
+ const plural = getFeatureName({
+ feature,
+ plural: true,
+ });
+ return { singular, plural };
};
export const formatPrepaidPrice = ({
- quantity,
- ents,
- price,
+ quantity,
+ ents,
+ price,
}: {
- quantity: number;
- ents: EntitlementWithFeature[];
- price: Price;
+ quantity: number;
+ ents: EntitlementWithFeature[];
+ price: Price;
}) => {
- const ent = getPriceEntitlement(price, ents);
+ const ent = getPriceEntitlement(price, ents);
- const { singular, plural } = getSingularAndPlural(ent.feature);
+ const { singular, plural } = getSingularAndPlural(ent.feature);
- const config = price.config as UsagePriceConfig;
- const billingUnits = config.billing_units || 1;
+ const config = price.config as UsagePriceConfig;
+ const billingUnits = config.billing_units || 1;
- if (billingUnits == 1) {
- if (quantity == 1)
- return `${quantity} ${singular}`; // eg. 1 credit
- else return `${quantity} ${plural}`; // eg. 4 credits
- } else {
- return `${quantity} x ${billingUnits} ${plural}`; // eg. 4 x 100 credits
- }
+ if (billingUnits == 1) {
+ if (quantity == 1)
+ return `${quantity} ${singular}`; // eg. 1 credit
+ else return `${quantity} ${plural}`; // eg. 4 credits
+ } else {
+ return `${quantity} x ${billingUnits} ${plural}`; // eg. 4 x 100 credits
+ }
};
export const formatFixedPrice = ({
- org,
- price,
- quantity,
+ org,
+ price,
+ quantity,
}: {
- org: Organization;
- price: Price;
- quantity?: number;
+ org: Organization;
+ price: Price;
+ quantity?: number;
}) => {
- const config = price.config as FixedPriceConfig;
- const amount = formatAmount({ org, amount: config.amount });
+ const config = price.config as FixedPriceConfig;
+ const amount = formatAmount({ org, amount: config.amount });
- const intervalStr = getIntervalString({
- interval: billingToItemInterval(config.interval) as ProductItemInterval,
- intervalCount: config.interval_count || 1,
- prefix: "",
- });
+ const intervalStr = getIntervalString({
+ interval: billingToItemInterval(config.interval) as ProductItemInterval,
+ intervalCount: config.interval_count || 1,
+ prefix: "",
+ });
- if (config.interval == BillingInterval.OneOff) {
- return `${amount}`;
- } else {
- return `${amount} / ${intervalStr}`;
- }
+ if (config.interval == BillingInterval.OneOff) {
+ return `${amount}`;
+ } else {
+ return `${amount} / ${intervalStr}`;
+ }
};
// export const formatUsageInArrear = ({
@@ -122,112 +122,112 @@ export const formatFixedPrice = ({
// };
export const formatInArrearProrated = ({
- price,
- ents,
- quantity,
+ price,
+ ents,
+ quantity,
}: {
- price: Price;
- ents: EntitlementWithFeature[];
- quantity?: number;
+ price: Price;
+ ents: EntitlementWithFeature[];
+ quantity?: number;
}) => {
- const ent = getPriceEntitlement(price, ents);
+ const ent = getPriceEntitlement(price, ents);
- const { singular, plural } = getSingularAndPlural(ent.feature);
+ const { singular, plural } = getSingularAndPlural(ent.feature);
- if (quantity == 1) {
- return `${quantity} x ${singular}`;
- } else {
- return `${quantity} x ${plural}`;
- }
+ if (quantity == 1) {
+ return `${quantity} x ${singular}`;
+ } else {
+ return `${quantity} x ${plural}`;
+ }
};
export const priceToInvoiceDescription = ({
- org,
- price,
- cusProduct,
- quantity,
- logger,
+ org,
+ price,
+ cusProduct,
+ quantity,
+ logger,
}: {
- price: Price;
- cusProduct: FullCusProduct;
- org?: Organization;
- quantity?: number;
- logger: any;
+ price: Price;
+ cusProduct: FullCusProduct;
+ org?: Organization;
+ quantity?: number;
+ logger: any;
}) => {
- let billingType = getBillingType(price.config);
- let productName = cusProduct.product.name;
- const ents = cusProductToEnts({ cusProduct });
+ let billingType = getBillingType(price.config);
+ let productName = cusProduct.product.name;
+ const ents = cusProductToEnts({ cusProduct });
- let description = "";
- if (billingType == BillingType.UsageInAdvance) {
- const ent = getPriceEntitlement(price, ents);
- const quantity = getFeatureQuantity({
- cusProduct,
- internalFeatureId: ent.feature.internal_id,
- });
+ let description = "";
+ if (billingType == BillingType.UsageInAdvance) {
+ const ent = getPriceEntitlement(price, ents);
+ const quantity = getFeatureQuantity({
+ cusProduct,
+ internalFeatureId: ent.feature.internal_id,
+ });
- description = formatPrepaidPrice({ price, ents, quantity });
- }
+ description = formatPrepaidPrice({ price, ents, quantity });
+ }
- if (
- billingType == BillingType.FixedCycle ||
- billingType == BillingType.OneOff
- ) {
- description = formatFixedPrice({
- org: org!,
- price,
- quantity: cusProduct.quantity,
- });
- }
+ if (
+ billingType == BillingType.FixedCycle ||
+ billingType == BillingType.OneOff
+ ) {
+ description = formatFixedPrice({
+ org: org!,
+ price,
+ quantity: cusProduct.quantity,
+ });
+ }
- if (billingType == BillingType.InArrearProrated) {
- description = formatInArrearProrated({ price, ents, quantity });
- }
+ if (billingType == BillingType.InArrearProrated) {
+ description = formatInArrearProrated({ price, ents, quantity });
+ }
- return `${productName} - ${description}`;
+ return `${productName} - ${description}`;
};
export const newPriceToInvoiceDescription = ({
- org,
- price,
- product,
- quantity,
- withProductPrefix = true,
- ents,
+ org,
+ price,
+ product,
+ quantity,
+ withProductPrefix = true,
+ ents,
}: {
- org: Organization;
- price: Price;
- product: FullProduct;
- quantity?: number;
- withProductPrefix?: boolean;
- ents?: EntitlementWithFeature[];
+ org: Organization;
+ price: Price;
+ product: FullProduct;
+ quantity?: number;
+ withProductPrefix?: boolean;
+ ents?: EntitlementWithFeature[];
}) => {
- const billingType = getBillingType(price.config);
+ const billingType = getBillingType(price.config);
- if (!ents) {
- ents = product.entitlements;
- }
+ if (!ents) {
+ ents = product.entitlements;
+ }
- let description = "";
- if (
- billingType == BillingType.FixedCycle ||
- billingType == BillingType.OneOff
- ) {
- description = formatFixedPrice({ org, price });
- }
+ let description = "";
+ if (
+ billingType == BillingType.FixedCycle ||
+ billingType == BillingType.OneOff
+ ) {
+ description = formatFixedPrice({ org, price });
+ }
- if (billingType == BillingType.InArrearProrated) {
- description = formatInArrearProrated({ price, ents, quantity });
- }
+ if (billingType == BillingType.InArrearProrated) {
+ description = formatInArrearProrated({ price, ents, quantity });
+ }
- if (billingType == BillingType.UsageInArrear) {
- const ent = getPriceEntitlement(price, ents);
- description = getFeatureNameWithCapital({ feature: ent.feature });
- }
+ if (billingType == BillingType.UsageInArrear) {
+ const ent = getPriceEntitlement(price, ents);
+ description = getFeatureNameWithCapital({ feature: ent.feature });
+ }
- if (billingType == BillingType.UsageInAdvance) {
- description = formatPrepaidPrice({ price, ents, quantity: quantity! });
- }
+ if (billingType == BillingType.UsageInAdvance) {
+ description = formatPrepaidPrice({ price, ents, quantity: quantity! });
+ }
- return `${withProductPrefix ? `${product.name} - ` : ""}${description}`;
+ return `${withProductPrefix ? `${product.name} - ` : ""}${description}`;
};
diff --git a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts
index 3c0c85f2b..e28e61241 100644
--- a/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts
+++ b/server/src/internal/invoices/invoiceItemUtils/invoiceItemUtils.ts
@@ -3,57 +3,57 @@ import { Decimal } from "decimal.js";
import Stripe from "stripe";
export const constructStripeInvoiceItem = ({
- product,
- amount,
- org,
- price,
- description,
- stripeSubId,
- stripeCustomerId,
- periodStart,
- periodEnd,
+ product,
+ amount,
+ org,
+ price,
+ description,
+ stripeSubId,
+ stripeCustomerId,
+ periodStart,
+ periodEnd,
}: {
- product: Product;
- amount: number;
- org: Organization;
- price: Price;
- description: string;
- stripeSubId: string;
- stripeCustomerId: string;
- periodStart: number;
- periodEnd: number;
+ product: Product;
+ amount: number;
+ org: Organization;
+ price: Price;
+ description: string;
+ stripeSubId: string;
+ stripeCustomerId: string;
+ periodStart: number;
+ periodEnd: number;
}) => {
- const config = price.config as UsagePriceConfig;
+ const config = price.config as UsagePriceConfig;
- const amountInCents = Math.floor(
- new Decimal(amount).mul(100).round().toNumber(),
- );
+ const amountInCents = Math.floor(
+ new Decimal(amount).mul(100).round().toNumber(),
+ );
- let priceData =
- amountInCents > 0
- ? {
- price_data: {
- unit_amount: amountInCents,
- currency: org.default_currency || "usd",
- product: config.stripe_product_id || product.processor?.id!,
- },
- }
- : {
- amount: amountInCents,
- currency: org.default_currency || "usd",
- };
+ let priceData =
+ amountInCents > 0
+ ? {
+ price_data: {
+ unit_amount: amountInCents,
+ currency: org.default_currency || "usd",
+ product: config.stripe_product_id || product.processor?.id!,
+ },
+ }
+ : {
+ amount: amountInCents,
+ currency: org.default_currency || "usd",
+ };
- let invoiceItem: Stripe.InvoiceItemCreateParams = {
- subscription: stripeSubId,
- ...priceData,
+ let invoiceItem: Stripe.InvoiceItemCreateParams = {
+ subscription: stripeSubId,
+ ...priceData,
- description,
- customer: stripeCustomerId,
- period: {
- start: periodStart,
- end: periodEnd,
- },
- };
+ description,
+ customer: stripeCustomerId,
+ period: {
+ start: periodStart,
+ end: periodEnd,
+ },
+ };
- return invoiceItem;
+ return invoiceItem;
};
diff --git a/server/src/internal/invoices/invoiceMemoUtils.ts b/server/src/internal/invoices/invoiceMemoUtils.ts
index 111066cb4..ce92eb449 100644
--- a/server/src/internal/invoices/invoiceMemoUtils.ts
+++ b/server/src/internal/invoices/invoiceMemoUtils.ts
@@ -1,7 +1,9 @@
import {
- type EntitlementWithFeature,
- type Feature, type FullProduct, type Organization,
- type Price
+ type EntitlementWithFeature,
+ type Feature,
+ type FullProduct,
+ type Organization,
+ type Price,
} from "@autumn/shared";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { isFeatureItem } from "../products/product-items/productItemUtils/getItemType.js";
diff --git a/server/src/internal/invoices/invoiceUtils.ts b/server/src/internal/invoices/invoiceUtils.ts
index b5c53f5a7..7529e73dd 100644
--- a/server/src/internal/invoices/invoiceUtils.ts
+++ b/server/src/internal/invoices/invoiceUtils.ts
@@ -8,164 +8,164 @@ import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeS
// Purpose of this function is to insert an invoice from attach params when sub is updated -> Correct product ID is set...
export const insertInvoiceFromAttach = async ({
- db,
- attachParams,
- invoiceId,
- stripeInvoice,
- logger,
+ db,
+ attachParams,
+ invoiceId,
+ stripeInvoice,
+ logger,
}: {
- db: DrizzleCli;
- attachParams: AttachParams;
- invoiceId?: string;
- stripeInvoice?: Stripe.Invoice;
- logger: any;
+ db: DrizzleCli;
+ attachParams: AttachParams;
+ invoiceId?: string;
+ stripeInvoice?: Stripe.Invoice;
+ logger: any;
}) => {
- try {
- if (!stripeInvoice) {
- stripeInvoice = await getStripeExpandedInvoice({
- stripeCli: attachParams.stripeCli,
- stripeInvoiceId: invoiceId!,
- });
- }
+ try {
+ if (!stripeInvoice) {
+ stripeInvoice = await getStripeExpandedInvoice({
+ stripeCli: attachParams.stripeCli,
+ stripeInvoiceId: invoiceId!,
+ });
+ }
- // Create or update
- let invoice = await InvoiceService.getByStripeId({
- db,
- stripeId: stripeInvoice.id!,
- });
+ // Create or update
+ let invoice = await InvoiceService.getByStripeId({
+ db,
+ stripeId: stripeInvoice.id!,
+ });
- let autumnInvoiceItems = await getInvoiceItems({
- stripeInvoice,
- prices: attachParams.prices,
- logger,
- });
+ let autumnInvoiceItems = await getInvoiceItems({
+ stripeInvoice,
+ prices: attachParams.prices,
+ logger,
+ });
- if (invoice) {
- // console.log("UPDATING INVOICE FROM ATTACH:");
- // console.log(
- // "Product IDs:",
- // attachParams.products.map((p) => p.id)
- // );
- // console.log(
- // "Internal Product IDs:",
- // attachParams.products.map((p) => p.internal_id)
- // );
+ if (invoice) {
+ // console.log("UPDATING INVOICE FROM ATTACH:");
+ // console.log(
+ // "Product IDs:",
+ // attachParams.products.map((p) => p.id)
+ // );
+ // console.log(
+ // "Internal Product IDs:",
+ // attachParams.products.map((p) => p.internal_id)
+ // );
- await InvoiceService.updateByStripeId({
- db,
- stripeId: stripeInvoice.id!,
- updates: {
- product_ids: attachParams.products.map((p) => p.id),
- internal_product_ids: attachParams.products.map((p) => p.internal_id),
- },
- });
- } else {
- // console.log("INSERTING INVOICE FROM ATTACH:");
- // console.log(
- // "Product IDs:",
- // attachParams.products.map((p) => p.id)
- // );
- // console.log(
- // "Internal Product IDs:",
- // attachParams.products.map((p) => p.internal_id)
- // );
+ await InvoiceService.updateByStripeId({
+ db,
+ stripeId: stripeInvoice.id!,
+ updates: {
+ product_ids: attachParams.products.map((p) => p.id),
+ internal_product_ids: attachParams.products.map((p) => p.internal_id),
+ },
+ });
+ } else {
+ // console.log("INSERTING INVOICE FROM ATTACH:");
+ // console.log(
+ // "Product IDs:",
+ // attachParams.products.map((p) => p.id)
+ // );
+ // console.log(
+ // "Internal Product IDs:",
+ // attachParams.products.map((p) => p.internal_id)
+ // );
- await InvoiceService.createInvoiceFromStripe({
- db,
- stripeInvoice,
- internalCustomerId: attachParams.customer.internal_id,
- internalEntityId: attachParams.internalEntityId,
- org: attachParams.org,
- productIds: attachParams.products.map((p) => p.id),
- internalProductIds: attachParams.products.map((p) => p.internal_id),
- items: autumnInvoiceItems,
- });
- }
- return stripeInvoice;
- } catch (error) {
- logger.warn("Failed to insert invoice from attach params");
- logger.warn(error);
- }
+ await InvoiceService.createInvoiceFromStripe({
+ db,
+ stripeInvoice,
+ internalCustomerId: attachParams.customer.internal_id,
+ internalEntityId: attachParams.internalEntityId,
+ org: attachParams.org,
+ productIds: attachParams.products.map((p) => p.id),
+ internalProductIds: attachParams.products.map((p) => p.internal_id),
+ items: autumnInvoiceItems,
+ });
+ }
+ return stripeInvoice;
+ } catch (error) {
+ logger.warn("Failed to insert invoice from attach params");
+ logger.warn(error);
+ }
};
export const invoicesToResponse = ({
- invoices,
- logger,
+ invoices,
+ logger,
}: {
- invoices: Invoice[];
- logger: any;
+ invoices: Invoice[];
+ logger: any;
}) => {
- return invoices.map((i) =>
- processInvoice({
- invoice: i,
- withItems: false,
- features: [],
- })
- );
+ return invoices.map((i) =>
+ processInvoice({
+ invoice: i,
+ withItems: false,
+ features: [],
+ }),
+ );
};
export const getInvoiceItems = async ({
- stripeInvoice,
- prices,
- logger,
+ stripeInvoice,
+ prices,
+ logger,
}: {
- stripeInvoice: Stripe.Invoice;
- prices: Price[];
- logger: any;
+ stripeInvoice: Stripe.Invoice;
+ prices: Price[];
+ logger: any;
}) => {
- let invoiceItems: InvoiceItem[] = [];
+ let invoiceItems: InvoiceItem[] = [];
- try {
- for (const line of stripeInvoice.lines.data) {
- let price = findPriceInStripeItems({
- prices,
- lineItem: line,
- });
+ try {
+ for (const line of stripeInvoice.lines.data) {
+ let price = findPriceInStripeItems({
+ prices,
+ lineItem: line,
+ });
- if (!price) {
- continue;
- }
+ if (!price) {
+ continue;
+ }
- let usageConfig = price.config as UsagePriceConfig;
- invoiceItems.push({
- price_id: price.id!,
- stripe_id: line.id,
- internal_feature_id: usageConfig.internal_feature_id || null,
- description: line.description || "",
- period_start: line.period.start * 1000,
- period_end: line.period.end * 1000,
- });
- }
- } catch (error) {
- logger.error(
- `Failed to get invoice items for invoice ${stripeInvoice.id}`,
- error
- );
- return [];
- }
+ let usageConfig = price.config as UsagePriceConfig;
+ invoiceItems.push({
+ price_id: price.id!,
+ stripe_id: line.id,
+ internal_feature_id: usageConfig.internal_feature_id || null,
+ description: line.description || "",
+ period_start: line.period.start * 1000,
+ period_end: line.period.end * 1000,
+ });
+ }
+ } catch (error) {
+ logger.error(
+ `Failed to get invoice items for invoice ${stripeInvoice.id}`,
+ error,
+ );
+ return [];
+ }
- return invoiceItems;
+ return invoiceItems;
};
export const attachToInvoiceResponse = ({
- invoice,
+ invoice,
}: {
- invoice?: Stripe.Invoice | null;
+ invoice?: Stripe.Invoice | null;
}) => {
- if (!invoice) {
- return undefined;
- }
+ if (!invoice) {
+ return undefined;
+ }
- return {
- status: invoice.status,
- stripe_id: invoice.id,
- hosted_invoice_url: invoice.hosted_invoice_url,
- total: invoice.total,
- currency: invoice.currency,
- // id: invoice.id,
- // stripe_id: invoice.stripe_id,
- // status: invoice.status,
- // created_at: invoice.created_at,
- // updated_at: invoice.updated_at,
- };
+ return {
+ status: invoice.status,
+ stripe_id: invoice.id,
+ hosted_invoice_url: invoice.hosted_invoice_url,
+ total: invoice.total,
+ currency: invoice.currency,
+ // id: invoice.id,
+ // stripe_id: invoice.stripe_id,
+ // status: invoice.status,
+ // created_at: invoice.created_at,
+ // updated_at: invoice.updated_at,
+ };
};
diff --git a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts
index 743333f77..9d7c01768 100644
--- a/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts
+++ b/server/src/internal/invoices/invoiceUtils/createAndFinalizeInvoice.ts
@@ -2,62 +2,62 @@ import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import Stripe from "stripe";
export const createAndFinalizeInvoice = async ({
- stripeCli,
- paymentMethod,
- stripeCusId,
- stripeSubId,
- invoiceItems,
- errorOnPaymentFail = true,
- voidIfFailed = true,
- logger,
+ stripeCli,
+ paymentMethod,
+ stripeCusId,
+ stripeSubId,
+ invoiceItems,
+ errorOnPaymentFail = true,
+ voidIfFailed = true,
+ logger,
}: {
- stripeCli: Stripe;
- paymentMethod: Stripe.PaymentMethod | null;
- stripeCusId: string;
- stripeSubId: string;
- invoiceItems?: Stripe.InvoiceItemCreateParams[];
- errorOnPaymentFail?: boolean;
- voidIfFailed?: boolean;
- logger?: any;
+ stripeCli: Stripe;
+ paymentMethod: Stripe.PaymentMethod | null;
+ stripeCusId: string;
+ stripeSubId: string;
+ invoiceItems?: Stripe.InvoiceItemCreateParams[];
+ errorOnPaymentFail?: boolean;
+ voidIfFailed?: boolean;
+ logger?: any;
}) => {
- const invoice = await stripeCli.invoices.create({
- customer: stripeCusId,
- auto_advance: false,
- subscription: stripeSubId,
- });
+ const invoice = await stripeCli.invoices.create({
+ customer: stripeCusId,
+ auto_advance: false,
+ subscription: stripeSubId,
+ });
- if (invoiceItems) {
- for (const item of invoiceItems) {
- await stripeCli.invoiceItems.create({
- ...item,
- invoice: invoice.id!,
- customer: stripeCusId,
- });
- }
- }
+ if (invoiceItems) {
+ for (const item of invoiceItems) {
+ await stripeCli.invoiceItems.create({
+ ...item,
+ invoice: invoice.id!,
+ customer: stripeCusId,
+ });
+ }
+ }
- let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, {
- auto_advance: false,
- });
+ let finalInvoice = await stripeCli.invoices.finalizeInvoice(invoice.id!, {
+ auto_advance: false,
+ });
- if (finalInvoice.status == "open") {
- const {
- paid,
- error,
- invoice: paidInvoice,
- } = await payForInvoice({
- stripeCli,
- invoiceId: finalInvoice.id!,
- paymentMethod,
- logger,
- errorOnFail: errorOnPaymentFail,
- voidIfFailed,
- });
+ if (finalInvoice.status == "open") {
+ const {
+ paid,
+ error,
+ invoice: paidInvoice,
+ } = await payForInvoice({
+ stripeCli,
+ invoiceId: finalInvoice.id!,
+ paymentMethod,
+ logger,
+ errorOnFail: errorOnPaymentFail,
+ voidIfFailed,
+ });
- if (paid) {
- finalInvoice = paidInvoice!;
- }
- }
+ if (paid) {
+ finalInvoice = paidInvoice!;
+ }
+ }
- return { invoice: finalInvoice };
+ return { invoice: finalInvoice };
};
diff --git a/server/src/internal/invoices/previewItemUtils/constructPreviewItem.ts b/server/src/internal/invoices/previewItemUtils/constructPreviewItem.ts
index 1326453e1..e680e7390 100644
--- a/server/src/internal/invoices/previewItemUtils/constructPreviewItem.ts
+++ b/server/src/internal/invoices/previewItemUtils/constructPreviewItem.ts
@@ -3,32 +3,32 @@ import { formatAmount } from "@/utils/formatUtils.js";
import { Organization, Price } from "@autumn/shared";
export const constructPreviewItem = ({
- price,
- description,
- priceStr,
- org,
- amount,
+ price,
+ description,
+ priceStr,
+ org,
+ amount,
}: {
- price: Price;
- description: string;
- priceStr?: string;
- org?: Organization;
- amount?: number;
+ price: Price;
+ description: string;
+ priceStr?: string;
+ org?: Organization;
+ amount?: number;
}) => {
- if (amount) {
- priceStr = formatAmount({
- org,
- amount,
- });
- } else {
- priceStr = priceStr;
- }
+ if (amount) {
+ priceStr = formatAmount({
+ org,
+ amount,
+ });
+ } else {
+ priceStr = priceStr;
+ }
- return {
- price: priceStr!,
- description,
- amount,
- usage_model: priceToUsageModel(price),
- price_id: price.id!,
- };
+ return {
+ price: priceStr!,
+ description,
+ amount,
+ usage_model: priceToUsageModel(price),
+ price_id: price.id!,
+ };
};
diff --git a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts
index 5bcadb3a0..4c60323cd 100644
--- a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts
+++ b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts
@@ -3,10 +3,10 @@ import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeS
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
- BillingType,
- getFeatureInvoiceDescription,
- PreviewLineItem,
- UsagePriceConfig,
+ BillingType,
+ getFeatureInvoiceDescription,
+ PreviewLineItem,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import Stripe from "stripe";
@@ -15,8 +15,8 @@ import { calculateProrationAmount } from "../prorationUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import {
- priceToFeature,
- priceToUsageModel,
+ priceToFeature,
+ priceToUsageModel,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import { cusProductToEnts, cusProductToPrices } from "@autumn/shared";
@@ -24,87 +24,87 @@ import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProduct
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
export const getCurContUseItems = async ({
- sub,
- attachParams,
+ sub,
+ attachParams,
}: {
- sub: Stripe.Subscription;
- attachParams: AttachParams;
+ sub: Stripe.Subscription;
+ attachParams: AttachParams;
}) => {
- const { features } = attachParams;
- const { curMainProduct, curSameProduct } = attachParamToCusProducts({
- attachParams,
- });
- const curCusProduct = curSameProduct || curMainProduct!;
- const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
- const curEnts = cusProductToEnts({ cusProduct: curCusProduct });
+ const { features } = attachParams;
+ const { curMainProduct, curSameProduct } = attachParamToCusProducts({
+ attachParams,
+ });
+ const curCusProduct = curSameProduct || curMainProduct!;
+ const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
+ const curEnts = cusProductToEnts({ cusProduct: curCusProduct });
- let items: PreviewLineItem[] = [];
- let now = attachParams.now || Date.now();
+ let items: PreviewLineItem[] = [];
+ let now = attachParams.now || Date.now();
- for (const item of sub.items.data) {
- const price = findPriceInStripeItems({
- prices: curPrices,
- subItem: item,
- billingType: BillingType.InArrearProrated,
- });
+ for (const item of sub.items.data) {
+ const price = findPriceInStripeItems({
+ prices: curPrices,
+ subItem: item,
+ billingType: BillingType.InArrearProrated,
+ });
- if (!price) continue;
+ if (!price) continue;
- const periodEnd = item.current_period_end * 1000;
- const totalAmountCents = getSubItemAmount({ subItem: item });
- const totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
- const ent = getPriceEntitlement(price, curEnts);
+ const periodEnd = item.current_period_end * 1000;
+ const totalAmountCents = getSubItemAmount({ subItem: item });
+ const totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
+ const ent = getPriceEntitlement(price, curEnts);
- if (now < periodEnd) {
- const finalProration = getProration({
- now,
- intervalConfig: {
- interval: price.config.interval!,
- intervalCount: price.config.interval_count || 1,
- },
- proration: {
- end: periodEnd,
- },
- })!;
+ if (now < periodEnd) {
+ const finalProration = getProration({
+ now,
+ intervalConfig: {
+ interval: price.config.interval!,
+ intervalCount: price.config.interval_count || 1,
+ },
+ proration: {
+ end: periodEnd,
+ },
+ })!;
- const proratedAmount = -calculateProrationAmount({
- periodEnd: finalProration?.end,
- periodStart: finalProration?.start,
- now,
- amount: totalAmount,
- });
+ const proratedAmount = -calculateProrationAmount({
+ periodEnd: finalProration?.end,
+ periodStart: finalProration?.start,
+ now,
+ amount: totalAmount,
+ });
- const existingUsage = getExistingUsageFromCusProducts({
- entitlement: ent!,
- cusProducts: [curCusProduct],
- entities: attachParams.entities,
- carryExistingUsages: true,
- internalEntityId: attachParams.internalEntityId,
- });
+ const existingUsage = getExistingUsageFromCusProducts({
+ entitlement: ent!,
+ cusProducts: [curCusProduct],
+ entities: attachParams.entities,
+ carryExistingUsages: true,
+ internalEntityId: attachParams.internalEntityId,
+ });
- const feature = priceToFeature({ price, features });
+ const feature = priceToFeature({ price, features });
- let description = getFeatureInvoiceDescription({
- feature: feature!,
- usage: existingUsage,
- billingUnits: (price.config as UsagePriceConfig).billing_units,
- prodName: curMainProduct?.product.name,
- });
+ let description = getFeatureInvoiceDescription({
+ feature: feature!,
+ usage: existingUsage,
+ billingUnits: (price.config as UsagePriceConfig).billing_units,
+ prodName: curMainProduct?.product.name,
+ });
- description = `Unused ${description} (from ${formatUnixToDate(now)})`;
+ description = `Unused ${description} (from ${formatUnixToDate(now)})`;
- items.push({
- price: formatAmount({
- org: attachParams.org,
- amount: proratedAmount,
- }),
- description,
- amount: proratedAmount,
- usage_model: priceToUsageModel(price),
- price_id: price.id!,
- });
- }
- }
+ items.push({
+ price: formatAmount({
+ org: attachParams.org,
+ amount: proratedAmount,
+ }),
+ description,
+ amount: proratedAmount,
+ usage_model: priceToUsageModel(price),
+ price_id: price.id!,
+ });
+ }
+ }
- return items;
+ return items;
};
diff --git a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts
index 869567bd9..4fb168344 100644
--- a/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts
+++ b/server/src/internal/invoices/previewItemUtils/getItemsForCurProduct.ts
@@ -6,10 +6,10 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import {
- AttachBranch,
- AttachConfig,
- BillingType,
- PreviewLineItem,
+ AttachBranch,
+ AttachConfig,
+ BillingType,
+ PreviewLineItem,
} from "@autumn/shared";
import { formatAmount } from "@/utils/formatUtils.js";
@@ -19,91 +19,91 @@ import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convert
import { getContUseInvoiceItems } from "@/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.js";
import {
- isArrearPrice,
- isContUsePrice,
+ isArrearPrice,
+ isContUsePrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { priceToUnusedPreviewItem } from "@/internal/customers/attach/attachPreviewUtils/priceToUnusedPreviewItem.js";
export const getItemsForCurProduct = async ({
- sub,
- attachParams,
- branch,
- config,
- now,
- logger,
+ sub,
+ attachParams,
+ branch,
+ config,
+ now,
+ logger,
}: {
- sub?: Stripe.Subscription;
- attachParams: AttachParams;
- branch: AttachBranch;
- config: AttachConfig;
- now: number;
- logger: any;
+ sub?: Stripe.Subscription;
+ attachParams: AttachParams;
+ branch: AttachBranch;
+ config: AttachConfig;
+ now: number;
+ logger: any;
}) => {
- const { curMainProduct, curSameProduct } = attachParamToCusProducts({
- attachParams,
- });
+ const { curMainProduct, curSameProduct } = attachParamToCusProducts({
+ attachParams,
+ });
- const curCusProduct = curSameProduct || curMainProduct!;
+ const curCusProduct = curSameProduct || curMainProduct!;
- let items: PreviewLineItem[] = [];
- const subItems = sub?.items.data || [];
- const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
+ let items: PreviewLineItem[] = [];
+ const subItems = sub?.items.data || [];
+ const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
- for (const price of curPrices) {
- if (isArrearPrice({ price }) || isContUsePrice({ price })) {
- continue;
- }
+ for (const price of curPrices) {
+ if (isArrearPrice({ price }) || isContUsePrice({ price })) {
+ continue;
+ }
- const previewLineItem = priceToUnusedPreviewItem({
- price,
- stripeItems: subItems,
- cusProduct: curCusProduct,
- org: attachParams.org,
- now,
- latestInvoice: sub?.latest_invoice as Stripe.Invoice,
- subDiscounts: sub?.discounts as Stripe.Discount[],
- });
+ const previewLineItem = priceToUnusedPreviewItem({
+ price,
+ stripeItems: subItems,
+ cusProduct: curCusProduct,
+ org: attachParams.org,
+ now,
+ latestInvoice: sub?.latest_invoice as Stripe.Invoice,
+ subDiscounts: sub?.discounts as Stripe.Discount[],
+ });
- if (!previewLineItem) continue;
+ if (!previewLineItem) continue;
- items.push(previewLineItem);
- }
+ items.push(previewLineItem);
+ }
- // console.log("items: ", items);
+ // console.log("items: ", items);
- let { oldItems } = await getContUseInvoiceItems({
- sub,
- attachParams,
- logger,
- cusProduct: curCusProduct,
- });
+ let { oldItems } = await getContUseInvoiceItems({
+ sub,
+ attachParams,
+ logger,
+ cusProduct: curCusProduct,
+ });
- items = [...items, ...oldItems];
+ items = [...items, ...oldItems];
- for (const price of curPrices) {
- let billingType = getBillingType(price.config);
+ for (const price of curPrices) {
+ let billingType = getBillingType(price.config);
- if (billingType == BillingType.UsageInArrear) {
- const { amount, description } = getCusPriceUsage({
- price,
- cusProduct: curCusProduct,
- logger,
- });
+ if (billingType == BillingType.UsageInArrear) {
+ const { amount, description } = getCusPriceUsage({
+ price,
+ cusProduct: curCusProduct,
+ logger,
+ });
- if (!amount || amount <= 0) continue;
+ if (!amount || amount <= 0) continue;
- items.push({
- price: formatAmount({
- org: attachParams.org,
- amount,
- }),
- description,
- amount,
- price_id: price.id!,
- usage_model: priceToUsageModel(price),
- });
- }
- }
+ items.push({
+ price: formatAmount({
+ org: attachParams.org,
+ amount,
+ }),
+ description,
+ amount,
+ price_id: price.id!,
+ usage_model: priceToUsageModel(price),
+ });
+ }
+ }
- return items;
+ return items;
};
diff --git a/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts b/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts
index 7e023a2b1..204390027 100644
--- a/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts
+++ b/server/src/internal/invoices/previewItemUtils/getItemsForNewProduct.ts
@@ -1,34 +1,34 @@
import {
- EntitlementWithFeature,
- FullProduct,
- Organization,
- Price,
- Feature,
- BillingInterval,
- FreeTrial,
- PreviewLineItem,
- BillingType,
- getFeatureInvoiceDescription,
- UsagePriceConfig,
- UsageModel,
- AttachConfig,
- AttachBranch,
- ProrationBehavior,
- IntervalConfig,
+ EntitlementWithFeature,
+ FullProduct,
+ Organization,
+ Price,
+ Feature,
+ BillingInterval,
+ FreeTrial,
+ PreviewLineItem,
+ BillingType,
+ getFeatureInvoiceDescription,
+ UsagePriceConfig,
+ UsageModel,
+ AttachConfig,
+ AttachBranch,
+ ProrationBehavior,
+ IntervalConfig,
} from "@autumn/shared";
import { AttachParams } from "../../customers/cusProducts/AttachParams.js";
import {
- formatPrice,
- getBillingType,
- getPriceForOverage,
- getPriceOptions,
+ formatPrice,
+ getBillingType,
+ getPriceForOverage,
+ getPriceOptions,
} from "../../products/prices/priceUtils.js";
import { getPriceEntitlement } from "../../products/prices/priceUtils.js";
import {
- isFixedPrice,
- isOneOffPrice,
- isPrepaidPrice,
- isUsagePrice,
+ isFixedPrice,
+ isOneOffPrice,
+ isPrepaidPrice,
+ isUsagePrice,
} from "../../products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { newPriceToInvoiceDescription } from "../invoiceFormatUtils.js";
@@ -39,8 +39,8 @@ import { formatAmount } from "@/utils/formatUtils.js";
import { formatUnixToDate, notNullish } from "@/utils/genUtils.js";
import { subtractIntervalForProration } from "../../products/prices/billingIntervalUtils.js";
import {
- priceToFeature,
- priceToUsageModel,
+ priceToFeature,
+ priceToUsageModel,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { getContUseInvoiceItems } from "@/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.js";
import Stripe from "stripe";
@@ -51,256 +51,256 @@ import { Decimal } from "decimal.js";
import { getAlignedUnix } from "@/internal/products/prices/billingIntervalUtils2.js";
export const getDefaultPriceStr = ({
- org,
- price,
- ent,
- features,
+ org,
+ price,
+ ent,
+ features,
}: {
- org: Organization;
- price: Price;
- ent: EntitlementWithFeature;
- features: Feature[];
+ org: Organization;
+ price: Price;
+ ent: EntitlementWithFeature;
+ features: Feature[];
}) => {
- const item = toProductItem({
- ent: ent!,
- price,
- });
+ const item = toProductItem({
+ ent: ent!,
+ price,
+ });
- const priceText = getPricecnPrice({
- org,
- items: [item],
- features,
- isMainPrice: true,
- });
+ const priceText = getPricecnPrice({
+ org,
+ items: [item],
+ features,
+ isMainPrice: true,
+ });
- return `${priceText.primaryText} ${priceText.secondaryText}`;
+ return `${priceText.primaryText} ${priceText.secondaryText}`;
};
export const getProration = ({
- proration,
- anchor,
- intervalConfig,
- now,
+ proration,
+ anchor,
+ intervalConfig,
+ now,
}: {
- proration?: Partial<{
- start: number;
- end: number;
- }>;
- anchor?: number; // used to indicate a future date to anchor the next period end to...
- intervalConfig: IntervalConfig;
- now?: number;
+ proration?: Partial<{
+ start: number;
+ end: number;
+ }>;
+ anchor?: number; // used to indicate a future date to anchor the next period end to...
+ intervalConfig: IntervalConfig;
+ now?: number;
}) => {
- let { interval, intervalCount } = intervalConfig;
- intervalCount = intervalCount ?? 1;
- now = now || Date.now();
+ let { interval, intervalCount } = intervalConfig;
+ intervalCount = intervalCount ?? 1;
+ now = now || Date.now();
- if (interval == BillingInterval.OneOff) return undefined;
+ if (interval == BillingInterval.OneOff) return undefined;
- let end = proration?.end;
- if (!end && anchor) {
- end = getAlignedUnix({
- anchor: anchor!,
- intervalConfig,
- now,
- });
- }
+ let end = proration?.end;
+ if (!end && anchor) {
+ end = getAlignedUnix({
+ anchor: anchor!,
+ intervalConfig,
+ now,
+ });
+ }
- let start = proration?.start;
- if (!start && end) {
- start = subtractIntervalForProration({
- unixTimestamp: end!,
- interval,
- intervalCount,
- });
- }
+ let start = proration?.start;
+ if (!start && end) {
+ start = subtractIntervalForProration({
+ unixTimestamp: end!,
+ interval,
+ intervalCount,
+ });
+ }
- if (!start || !end) return undefined;
+ if (!start || !end) return undefined;
- return {
- start,
- end,
- };
+ return {
+ start,
+ end,
+ };
};
export const getItemsForNewProduct = async ({
- newProduct,
- attachParams,
- now,
- proration,
- anchor,
- freeTrial,
- sub,
- logger,
- withPrepaid = false,
- skipOneOff = false,
+ newProduct,
+ attachParams,
+ now,
+ proration,
+ anchor,
+ freeTrial,
+ sub,
+ logger,
+ withPrepaid = false,
+ skipOneOff = false,
}: {
- newProduct: FullProduct;
- attachParams: AttachParams;
- now?: number;
- proration?: {
- start: number;
- end: number;
- };
- anchor?: number;
- freeTrial?: FreeTrial | null;
- sub?: Stripe.Subscription;
- logger: any;
- withPrepaid?: boolean;
- skipOneOff?: boolean;
+ newProduct: FullProduct;
+ attachParams: AttachParams;
+ now?: number;
+ proration?: {
+ start: number;
+ end: number;
+ };
+ anchor?: number;
+ freeTrial?: FreeTrial | null;
+ sub?: Stripe.Subscription;
+ logger: any;
+ withPrepaid?: boolean;
+ skipOneOff?: boolean;
}) => {
- const { org, features } = attachParams;
- now = now || Date.now();
+ const { org, features } = attachParams;
+ now = now || Date.now();
- const items: PreviewLineItem[] = [];
+ const items: PreviewLineItem[] = [];
- sortPricesByType(newProduct.prices);
+ sortPricesByType(newProduct.prices);
- const printLogs = false;
+ const printLogs = false;
- for (const price of newProduct.prices) {
- if (skipOneOff && isOneOffPrice({ price })) continue;
+ for (const price of newProduct.prices) {
+ if (skipOneOff && isOneOffPrice({ price })) continue;
- const ent = getPriceEntitlement(price, newProduct.entitlements);
- const billingType = getBillingType(price.config);
+ const ent = getPriceEntitlement(price, newProduct.entitlements);
+ const billingType = getBillingType(price.config);
- if (printLogs) {
- console.log("price", formatPrice({ price }));
- console.log("now:", formatUnixToDate(now));
- }
+ if (printLogs) {
+ console.log("price", formatPrice({ price }));
+ console.log("now:", formatUnixToDate(now));
+ }
- const finalProration = getProration({
- proration,
- anchor,
- now,
- intervalConfig: {
- interval: price.config.interval!,
- intervalCount: price.config.interval_count || 1,
- },
- });
+ const finalProration = getProration({
+ proration,
+ anchor,
+ now,
+ intervalConfig: {
+ interval: price.config.interval!,
+ intervalCount: price.config.interval_count || 1,
+ },
+ });
- if (printLogs && finalProration) {
- console.log(
- `PRORATION: ${formatUnixToDate(finalProration.start)} to ${formatUnixToDate(finalProration.end)}`
- );
- }
- if (printLogs) console.log("--------------------------------");
+ if (printLogs && finalProration) {
+ console.log(
+ `PRORATION: ${formatUnixToDate(finalProration.start)} to ${formatUnixToDate(finalProration.end)}`,
+ );
+ }
+ if (printLogs) console.log("--------------------------------");
- if (isFixedPrice({ price })) {
- let amount = finalProration
- ? calculateProrationAmount({
- periodEnd: finalProration.end,
- periodStart: finalProration.start,
- now,
- amount: getPriceForOverage(price),
- })
- : getPriceForOverage(price, 0);
+ if (isFixedPrice({ price })) {
+ let amount = finalProration
+ ? calculateProrationAmount({
+ periodEnd: finalProration.end,
+ periodStart: finalProration.start,
+ now,
+ amount: getPriceForOverage(price),
+ })
+ : getPriceForOverage(price, 0);
- if (freeTrial) {
- amount = 0;
- }
+ if (freeTrial) {
+ amount = 0;
+ }
- let description = newPriceToInvoiceDescription({
- org,
- price,
- product: newProduct,
- });
+ let description = newPriceToInvoiceDescription({
+ org,
+ price,
+ product: newProduct,
+ });
- if (finalProration) {
- description = `${description} (from ${formatUnixToDate(now)})`;
- }
+ if (finalProration) {
+ description = `${description} (from ${formatUnixToDate(now)})`;
+ }
- items.push({
- price_id: price.id,
- price: formatAmount({ org, amount }),
- description,
- amount,
- usage_model: priceToUsageModel(price),
- feature_id: ent?.feature_id,
- });
- continue;
- }
+ items.push({
+ price_id: price.id,
+ price: formatAmount({ org, amount }),
+ description,
+ amount,
+ usage_model: priceToUsageModel(price),
+ feature_id: ent?.feature_id,
+ });
+ continue;
+ }
- if (billingType == BillingType.UsageInArrear) {
- items.push({
- price: getDefaultPriceStr({ org, price, ent: ent!, features }),
- description: newPriceToInvoiceDescription({
- org,
- price,
- product: newProduct,
- }),
- usage_model: priceToUsageModel(price),
- price_id: price.id,
- feature_id: ent?.feature_id,
- });
- continue;
- }
+ if (billingType == BillingType.UsageInArrear) {
+ items.push({
+ price: getDefaultPriceStr({ org, price, ent: ent!, features }),
+ description: newPriceToInvoiceDescription({
+ org,
+ price,
+ product: newProduct,
+ }),
+ usage_model: priceToUsageModel(price),
+ price_id: price.id,
+ feature_id: ent?.feature_id,
+ });
+ continue;
+ }
- if (withPrepaid && isPrepaidPrice({ price })) {
- let options = getPriceOptions(price, attachParams.optionsList);
- let quantity = notNullish(options?.quantity) ? options?.quantity! : 1;
+ if (withPrepaid && isPrepaidPrice({ price })) {
+ let options = getPriceOptions(price, attachParams.optionsList);
+ let quantity = notNullish(options?.quantity) ? options?.quantity! : 1;
- const quantityWithBillingUnits = new Decimal(quantity).mul(
- (price.config as UsagePriceConfig).billing_units || 1
- );
+ const quantityWithBillingUnits = new Decimal(quantity).mul(
+ (price.config as UsagePriceConfig).billing_units || 1,
+ );
- // console.log("price", price);
- // console.log("Quantity", quantity);
- let amount = priceToInvoiceAmount({
- price,
- quantity: quantityWithBillingUnits.toNumber(),
- proration: finalProration,
- now,
- });
- // console.log("Amount", amount);
- let feature = priceToFeature({
- price,
- features,
- })!;
+ // console.log("price", price);
+ // console.log("Quantity", quantity);
+ let amount = priceToInvoiceAmount({
+ price,
+ quantity: quantityWithBillingUnits.toNumber(),
+ proration: finalProration,
+ now,
+ });
+ // console.log("Amount", amount);
+ let feature = priceToFeature({
+ price,
+ features,
+ })!;
- items.push({
- price_id: price.id,
- price: formatAmount({ org, amount: 0 }),
- description: getFeatureInvoiceDescription({
- feature,
- usage: quantity,
- billingUnits: (price.config as UsagePriceConfig).billing_units,
- prodName: newProduct.name,
- isPrepaid: true,
- fromUnix: now,
- }),
- amount,
- usage_model: UsageModel.Prepaid,
- feature_id: ent?.feature_id,
- });
- }
+ items.push({
+ price_id: price.id,
+ price: formatAmount({ org, amount: 0 }),
+ description: getFeatureInvoiceDescription({
+ feature,
+ usage: quantity,
+ billingUnits: (price.config as UsagePriceConfig).billing_units,
+ prodName: newProduct.name,
+ isPrepaid: true,
+ fromUnix: now,
+ }),
+ amount,
+ usage_model: UsageModel.Prepaid,
+ feature_id: ent?.feature_id,
+ });
+ }
- if (isUsagePrice({ price })) continue;
- }
+ if (isUsagePrice({ price })) continue;
+ }
- const cusProduct = attachParamsToCurCusProduct({
- attachParams,
- });
+ const cusProduct = attachParamsToCurCusProduct({
+ attachParams,
+ });
- let { newItems } = await getContUseInvoiceItems({
- cusProduct,
- sub,
- attachParams,
- logger,
- });
+ let { newItems } = await getContUseInvoiceItems({
+ cusProduct,
+ sub,
+ attachParams,
+ logger,
+ });
- items.push(...newItems);
+ items.push(...newItems);
- for (const item of items) {
- if (item.amount && freeTrial) {
- item.amount = 0;
- }
- if (item.amount && item.amount < 0) {
- item.amount = 0;
- }
- if (notNullish(item.amount)) {
- item.price = formatAmount({ org, amount: item.amount! });
- }
- }
+ for (const item of items) {
+ if (item.amount && freeTrial) {
+ item.amount = 0;
+ }
+ if (item.amount && item.amount < 0) {
+ item.amount = 0;
+ }
+ if (notNullish(item.amount)) {
+ item.price = formatAmount({ org, amount: item.amount! });
+ }
+ }
- return items;
+ return items;
};
diff --git a/server/src/internal/invoices/prorationUtils.ts b/server/src/internal/invoices/prorationUtils.ts
index d80de758b..4c6a2ad78 100644
--- a/server/src/internal/invoices/prorationUtils.ts
+++ b/server/src/internal/invoices/prorationUtils.ts
@@ -2,31 +2,31 @@ import { formatUnixToDate } from "@/utils/genUtils.js";
import { Decimal } from "decimal.js";
export type Proration = {
- start: number;
- end: number;
+ start: number;
+ end: number;
};
export const calculateProrationAmount = ({
- periodEnd,
- periodStart,
- now,
- amount,
- allowNegative = false,
+ periodEnd,
+ periodStart,
+ now,
+ amount,
+ allowNegative = false,
}: {
- periodEnd: number;
- periodStart: number;
- now: number;
- amount: number;
- allowNegative?: boolean;
+ periodEnd: number;
+ periodStart: number;
+ now: number;
+ amount: number;
+ allowNegative?: boolean;
}) => {
- const num = new Decimal(periodEnd).minus(now);
- const denom = new Decimal(periodEnd).minus(periodStart);
+ const num = new Decimal(periodEnd).minus(now);
+ const denom = new Decimal(periodEnd).minus(periodStart);
- const proratedAmount = num.div(denom).mul(amount);
+ const proratedAmount = num.div(denom).mul(amount);
- if (proratedAmount.lte(0) && !allowNegative) {
- return 0;
- }
+ if (proratedAmount.lte(0) && !allowNegative) {
+ return 0;
+ }
- return proratedAmount.toNumber();
+ return proratedAmount.toNumber();
};
diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts
index 5319c42cb..76cccaf5f 100644
--- a/server/src/internal/mainRouter.ts
+++ b/server/src/internal/mainRouter.ts
@@ -25,7 +25,7 @@ import { viewsRouter } from "./saved-views/savedViewsRouter.js";
const mainRouter: Router = Router();
mainRouter.get("", async (req: any, res) => {
- res.status(200).json({ message: "Hello World" });
+ res.status(200).json({ message: "Hello World" });
});
mainRouter.post("/organization", withAuth, handlePostOrg);
@@ -43,91 +43,91 @@ mainRouter.use("/saved_views", withOrgAuth, viewsRouter);
mainRouter.use("/trmnl", trmnlRouter);
const limiter = rateLimit({
- windowMs: 60 * 1000, // 15 minutes
- limit: 10, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
- standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
- legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
+ windowMs: 60 * 1000, // 15 minutes
+ limit: 10, // Limit each IP to 100 requests per `window` (here, per 15 minutes).
+ standardHeaders: "draft-8", // draft-6: `RateLimit-*` headers; draft-7 & draft-8: combined `RateLimit` header
+ legacyHeaders: false, // Disable the `X-RateLimit-*` headers.
});
mainRouter.use(
- "/invoices/hosted_invoice_url/:invoiceId",
- limiter,
- async (req: any, res: any) => {
- let invoiceId = req.params.invoiceId;
- let invoice = await InvoiceService.get({
- db: req.db,
- id: invoiceId,
- });
+ "/invoices/hosted_invoice_url/:invoiceId",
+ limiter,
+ async (req: any, res: any) => {
+ let invoiceId = req.params.invoiceId;
+ let invoice = await InvoiceService.get({
+ db: req.db,
+ id: invoiceId,
+ });
- if (!invoice) return res.status(404).json({ error: "Invoice not found" });
+ if (!invoice) return res.status(404).json({ error: "Invoice not found" });
- try {
- let org = invoice.customer.org;
- let env = invoice.customer.env;
- let stripeCli = createStripeCli({
- org,
- env,
- });
- let stripeInvoice = await stripeCli.invoices.retrieve(invoice.stripe_id);
+ try {
+ let org = invoice.customer.org;
+ let env = invoice.customer.env;
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ });
+ let stripeInvoice = await stripeCli.invoices.retrieve(invoice.stripe_id);
- if (stripeInvoice.status == "draft") {
- return res
- .status(404)
- .json({ error: "This invoice is in draft status and has no URL" });
- }
+ if (stripeInvoice.status == "draft") {
+ return res
+ .status(404)
+ .json({ error: "This invoice is in draft status and has no URL" });
+ }
- res.redirect(stripeInvoice.hosted_invoice_url);
- } catch (e) {
- console.error(e);
- return res.status(500).json({ error: "Error retrieving invoice" });
- }
- }
+ res.redirect(stripeInvoice.hosted_invoice_url);
+ } catch (e) {
+ console.error(e);
+ return res.status(500).json({ error: "Error retrieving invoice" });
+ }
+ },
);
// Optional...
if (process.env.AUTUMN_SECRET_KEY) {
- mainRouter.use(
- "/api/autumn",
- withOrgAuth,
- autumnHandler({
- identify: async (req: any) => {
- return {
- customerId: req.org?.id,
- customerData: {
- name: req.org?.slug,
- email: req.user?.email,
- },
- };
- },
- })
- );
+ mainRouter.use(
+ "/api/autumn",
+ withOrgAuth,
+ autumnHandler({
+ identify: async (req: any) => {
+ return {
+ customerId: req.org?.id,
+ customerData: {
+ name: req.org?.slug,
+ email: req.user?.email,
+ },
+ };
+ },
+ }),
+ );
}
mainRouter.use(
- "/demo/api/autumn",
- withOrgAuth,
- autumnHandler({
- autumn: (req: any) => {
- let client = new Autumn({
- url: "http://localhost:8080/v1",
- headers: {
- cookie: req.headers.cookie,
- "Content-Type": "application/json",
- origin: req.get("origin"),
- },
- });
- return client as any;
- },
- identify: async (req: any) => {
- return {
- customerId: "onboarding_demo_user",
- customerData: {
- name: "Demo User",
- email: "demo@useautumn.com",
- },
- };
- },
- })
+ "/demo/api/autumn",
+ withOrgAuth,
+ autumnHandler({
+ autumn: (req: any) => {
+ let client = new Autumn({
+ url: "http://localhost:8080/v1",
+ headers: {
+ cookie: req.headers.cookie,
+ "Content-Type": "application/json",
+ origin: req.get("origin"),
+ },
+ });
+ return client as any;
+ },
+ identify: async (req: any) => {
+ return {
+ customerId: "onboarding_demo_user",
+ customerData: {
+ name: "Demo User",
+ email: "demo@useautumn.com",
+ },
+ };
+ },
+ }),
);
export default mainRouter;
diff --git a/server/src/internal/metadata/MetadataService.ts b/server/src/internal/metadata/MetadataService.ts
index a55f0fb7d..c40b8f6b6 100644
--- a/server/src/internal/metadata/MetadataService.ts
+++ b/server/src/internal/metadata/MetadataService.ts
@@ -3,21 +3,21 @@ import { AutumnMetadata, metadata } from "@autumn/shared";
import { eq } from "drizzle-orm";
export class MetadataService {
- static async insert({ db, data }: { db: DrizzleCli; data: AutumnMetadata }) {
- await db.insert(metadata).values(data);
- }
+ static async insert({ db, data }: { db: DrizzleCli; data: AutumnMetadata }) {
+ await db.insert(metadata).values(data);
+ }
- static async get({ db, id }: { db: DrizzleCli; id: string }) {
- const data = await db
- .select()
- .from(metadata)
- .where(eq(metadata.id, id))
- .limit(1);
+ static async get({ db, id }: { db: DrizzleCli; id: string }) {
+ const data = await db
+ .select()
+ .from(metadata)
+ .where(eq(metadata.id, id))
+ .limit(1);
- if (data.length === 0) {
- return null;
- }
+ if (data.length === 0) {
+ return null;
+ }
- return data[0] as AutumnMetadata;
- }
+ return data[0] as AutumnMetadata;
+ }
}
diff --git a/server/src/internal/metadata/metadataUtils.ts b/server/src/internal/metadata/metadataUtils.ts
index 6d900624b..c4619ebd3 100644
--- a/server/src/internal/metadata/metadataUtils.ts
+++ b/server/src/internal/metadata/metadataUtils.ts
@@ -7,51 +7,51 @@ import { AttachParams } from "../customers/cusProducts/AttachParams.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export const createCheckoutMetadata = async ({
- db,
- attachParams,
+ db,
+ attachParams,
}: {
- db: DrizzleCli;
- attachParams: AttachParams;
+ db: DrizzleCli;
+ attachParams: AttachParams;
}) => {
- const metaId = generateId("meta");
+ const metaId = generateId("meta");
- let { req, checkoutSessionParams, stripeCli, paymentMethod, ...rest } =
- attachParams;
+ let { req, checkoutSessionParams, stripeCli, paymentMethod, ...rest } =
+ attachParams;
- let attachClone = structuredClone(rest);
+ let attachClone = structuredClone(rest);
- const metadata: AutumnMetadata = {
- id: metaId,
- created_at: Date.now(),
- expires_at: addDays(Date.now(), 10).getTime(), // 10 days
- data: {
- ...attachClone,
- },
- };
+ const metadata: AutumnMetadata = {
+ id: metaId,
+ created_at: Date.now(),
+ expires_at: addDays(Date.now(), 10).getTime(), // 10 days
+ data: {
+ ...attachClone,
+ },
+ };
- await MetadataService.insert({ db, data: metadata });
+ await MetadataService.insert({ db, data: metadata });
- return metaId;
+ return metaId;
};
export const getMetadataFromCheckoutSession = async (
- checkoutSession: Stripe.Checkout.Session,
- db: DrizzleCli
+ checkoutSession: Stripe.Checkout.Session,
+ db: DrizzleCli,
) => {
- const metadataId = checkoutSession.metadata?.autumn_metadata_id;
+ const metadataId = checkoutSession.metadata?.autumn_metadata_id;
- if (!metadataId) {
- return null;
- }
+ if (!metadataId) {
+ return null;
+ }
- const metadata = await MetadataService.get({
- db,
- id: metadataId,
- });
+ const metadata = await MetadataService.get({
+ db,
+ id: metadataId,
+ });
- if (!metadata) {
- return null;
- }
+ if (!metadata) {
+ return null;
+ }
- return metadata;
+ return metadata;
};
diff --git a/server/src/internal/migrations/MigrationService.ts b/server/src/internal/migrations/MigrationService.ts
index ebf7b212b..758694b9b 100644
--- a/server/src/internal/migrations/MigrationService.ts
+++ b/server/src/internal/migrations/MigrationService.ts
@@ -1,8 +1,8 @@
import {
- AppEnv,
- migrationErrors,
- MigrationJob,
- MigrationJobStep,
+ AppEnv,
+ migrationErrors,
+ MigrationJob,
+ MigrationJobStep,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -12,107 +12,107 @@ import { ErrCode } from "@autumn/shared";
import { and, eq, ne } from "drizzle-orm";
export class MigrationService {
- static async createJob({ db, data }: { db: DrizzleCli; data: MigrationJob }) {
- let result = await db.insert(migrationJobs).values(data).returning();
+ static async createJob({ db, data }: { db: DrizzleCli; data: MigrationJob }) {
+ let result = await db.insert(migrationJobs).values(data).returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Failed to create migration job",
- code: ErrCode.InsertMigrationJobFailed,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Failed to create migration job",
+ code: ErrCode.InsertMigrationJobFailed,
+ });
+ }
- return result[0];
- }
+ return result[0];
+ }
- static async updateJob({
- db,
- migrationJobId,
- updates,
- }: {
- db: DrizzleCli;
- migrationJobId: string;
- updates: any;
- }) {
- let results = await db
- .update(migrationJobs)
- .set({
- ...updates,
- updated_at: Date.now(),
- })
- .where(eq(migrationJobs.id, migrationJobId))
- .returning();
+ static async updateJob({
+ db,
+ migrationJobId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ migrationJobId: string;
+ updates: any;
+ }) {
+ let results = await db
+ .update(migrationJobs)
+ .set({
+ ...updates,
+ updated_at: Date.now(),
+ })
+ .where(eq(migrationJobs.id, migrationJobId))
+ .returning();
- if (results.length === 0) {
- return null;
- }
+ if (results.length === 0) {
+ return null;
+ }
- return results[0];
- }
+ return results[0];
+ }
- static async getJob({ db, id }: { db: DrizzleCli; id: string }) {
- let job = await db.query.migrationJobs.findFirst({
- where: eq(migrationJobs.id, id),
- });
+ static async getJob({ db, id }: { db: DrizzleCli; id: string }) {
+ let job = await db.query.migrationJobs.findFirst({
+ where: eq(migrationJobs.id, id),
+ });
- if (!job) {
- throw new RecaseError({
- message: `Migration job ${id} not found`,
- code: ErrCode.MigrationJobNotFound,
- });
- }
+ if (!job) {
+ throw new RecaseError({
+ message: `Migration job ${id} not found`,
+ code: ErrCode.MigrationJobNotFound,
+ });
+ }
- return job as MigrationJob;
- }
+ return job as MigrationJob;
+ }
- static async getExistingJobs({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- let jobs = await db.query.migrationJobs.findMany({
- where: and(
- eq(migrationJobs.org_id, orgId),
- eq(migrationJobs.env, env),
- ne(migrationJobs.current_step, MigrationJobStep.Failed),
- ne(migrationJobs.current_step, MigrationJobStep.Finished),
- ),
- });
+ static async getExistingJobs({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ let jobs = await db.query.migrationJobs.findMany({
+ where: and(
+ eq(migrationJobs.org_id, orgId),
+ eq(migrationJobs.env, env),
+ ne(migrationJobs.current_step, MigrationJobStep.Failed),
+ ne(migrationJobs.current_step, MigrationJobStep.Finished),
+ ),
+ });
- return jobs as MigrationJob[];
- }
+ return jobs as MigrationJob[];
+ }
- static async insertError({ db, data }: { db: DrizzleCli; data: any }) {
- let result = await db.insert(migrationErrors).values(data).returning();
+ static async insertError({ db, data }: { db: DrizzleCli; data: any }) {
+ let result = await db.insert(migrationErrors).values(data).returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Failed to insert migration error",
- code: ErrCode.InsertMigrationErrorFailed,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Failed to insert migration error",
+ code: ErrCode.InsertMigrationErrorFailed,
+ });
+ }
- return result[0];
- }
+ return result[0];
+ }
- static async getErrors({
- db,
- migrationJobId,
- }: {
- db: DrizzleCli;
- migrationJobId: string;
- }) {
- let errors = await db.query.migrationErrors.findMany({
- where: eq(migrationErrors.migration_job_id, migrationJobId),
- with: {
- customer: true,
- },
- });
+ static async getErrors({
+ db,
+ migrationJobId,
+ }: {
+ db: DrizzleCli;
+ migrationJobId: string;
+ }) {
+ let errors = await db.query.migrationErrors.findMany({
+ where: eq(migrationErrors.migration_job_id, migrationJobId),
+ with: {
+ customer: true,
+ },
+ });
- return errors;
- }
+ return errors;
+ }
}
diff --git a/server/src/internal/migrations/migrationRouter.ts b/server/src/internal/migrations/migrationRouter.ts
index c1d01b25b..08dd0bbd8 100644
--- a/server/src/internal/migrations/migrationRouter.ts
+++ b/server/src/internal/migrations/migrationRouter.ts
@@ -1,10 +1,10 @@
import { ProductService } from "@/internal/products/ProductService.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- BillingType,
- CusProductStatus,
- ErrCode,
- UsagePriceConfig,
+ BillingType,
+ CusProductStatus,
+ ErrCode,
+ UsagePriceConfig,
} from "@autumn/shared";
import { routeHandler } from "@/utils/routerUtils.js";
import express, { Router } from "express";
@@ -13,8 +13,8 @@ import { MigrationService } from "@/internal/migrations/MigrationService.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import {
- getBillingType,
- pricesOnlyOneOff,
+ getBillingType,
+ pricesOnlyOneOff,
} from "@/internal/products/prices/priceUtils.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
@@ -23,141 +23,141 @@ import { findPrepaidPrice } from "../products/prices/priceUtils/findPriceUtils.j
export const migrationRouter: Router = express.Router();
migrationRouter.post("", async (req: any, res: any) => {
- return routeHandler({
- req,
- res,
- action: "migrate",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { orgId, env, db, features } = req;
+ return routeHandler({
+ req,
+ res,
+ action: "migrate",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { orgId, env, db, features } = req;
- const { from_product_id, from_version, to_product_id, to_version } =
- req.body;
+ const { from_product_id, from_version, to_product_id, to_version } =
+ req.body;
- let fromProduct = await ProductService.getFull({
- db,
- env,
- orgId,
- idOrInternalId: from_product_id,
- version: from_version,
- });
+ let fromProduct = await ProductService.getFull({
+ db,
+ env,
+ orgId,
+ idOrInternalId: from_product_id,
+ version: from_version,
+ });
- let toProduct = await ProductService.getFull({
- db,
- env,
- orgId,
- idOrInternalId: to_product_id,
- version: to_version,
- });
+ let toProduct = await ProductService.getFull({
+ db,
+ env,
+ orgId,
+ idOrInternalId: to_product_id,
+ version: to_version,
+ });
- if (
- isFreeProduct(fromProduct.prices) &&
- !isFreeProduct(toProduct.prices)
- ) {
- throw new RecaseError({
- message: `Cannot migrate customers from free product to paid product`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (
+ isFreeProduct(fromProduct.prices) &&
+ !isFreeProduct(toProduct.prices)
+ ) {
+ throw new RecaseError({
+ message: `Cannot migrate customers from free product to paid product`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- // Check if from product is one off, or to product is one off
- if (
- pricesOnlyOneOff(fromProduct.prices) ||
- pricesOnlyOneOff(toProduct.prices)
- ) {
- let fromIsOneOff = pricesOnlyOneOff(fromProduct.prices);
- let msg = fromIsOneOff
- ? `${fromProduct.name} is a one off product, cannot migrate customers on it`
- : `${toProduct.name} is a one off product, cannot migrate customers to this product`;
+ // Check if from product is one off, or to product is one off
+ if (
+ pricesOnlyOneOff(fromProduct.prices) ||
+ pricesOnlyOneOff(toProduct.prices)
+ ) {
+ let fromIsOneOff = pricesOnlyOneOff(fromProduct.prices);
+ let msg = fromIsOneOff
+ ? `${fromProduct.name} is a one off product, cannot migrate customers on it`
+ : `${toProduct.name} is a one off product, cannot migrate customers to this product`;
- throw new RecaseError({
- message: msg,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ throw new RecaseError({
+ message: msg,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (fromProduct.is_add_on || toProduct.is_add_on) {
- throw new RecaseError({
- message: `Cannot migrate customers for add on products`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (fromProduct.is_add_on || toProduct.is_add_on) {
+ throw new RecaseError({
+ message: `Cannot migrate customers for add on products`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- for (const price of toProduct.prices) {
- let billingType = getBillingType(price.config);
- if (billingType != BillingType.UsageInAdvance) continue;
+ for (const price of toProduct.prices) {
+ let billingType = getBillingType(price.config);
+ if (billingType != BillingType.UsageInAdvance) continue;
- let config = price.config as UsagePriceConfig;
- let internalFeatureId = config.internal_feature_id;
- let feature = features.find((f) => f.internal_id == internalFeatureId)!;
+ let config = price.config as UsagePriceConfig;
+ let internalFeatureId = config.internal_feature_id;
+ let feature = features.find((f) => f.internal_id == internalFeatureId)!;
- for (const price of fromProduct.prices) {
- let prepaidPrice = findPrepaidPrice({
- prices: fromProduct.prices,
- internalFeatureId,
- });
+ for (const price of fromProduct.prices) {
+ let prepaidPrice = findPrepaidPrice({
+ prices: fromProduct.prices,
+ internalFeatureId,
+ });
- if (!prepaidPrice) {
- throw new RecaseError({
- message: `New product has prepaid price for feature ${feature.name}, but old product does not, can't perform migration`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- }
- }
+ if (!prepaidPrice) {
+ throw new RecaseError({
+ message: `New product has prepaid price for feature ${feature.name}, but old product does not, can't perform migration`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ }
+ }
- if (
- !isFreeProduct(fromProduct.prices) &&
- isFreeProduct(toProduct.prices)
- ) {
- throw new RecaseError({
- message: `Cannot migrate customers from paid product to free product`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (
+ !isFreeProduct(fromProduct.prices) &&
+ isFreeProduct(toProduct.prices)
+ ) {
+ throw new RecaseError({
+ message: `Cannot migrate customers from paid product to free product`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (
- isFreeProduct(fromProduct.prices) &&
- !isFreeProduct(toProduct.prices)
- ) {
- throw new RecaseError({
- message: `Cannot migrate customers from free product to paid product`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (
+ isFreeProduct(fromProduct.prices) &&
+ !isFreeProduct(toProduct.prices)
+ ) {
+ throw new RecaseError({
+ message: `Cannot migrate customers from free product to paid product`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- // 1. Create migration JOB
- let migrationJob = constructMigrationJob({
- fromProduct,
- toProduct,
- });
+ // 1. Create migration JOB
+ let migrationJob = constructMigrationJob({
+ fromProduct,
+ toProduct,
+ });
- await MigrationService.createJob({
- db,
- data: migrationJob,
- });
+ await MigrationService.createJob({
+ db,
+ data: migrationJob,
+ });
- if (!fromProduct || !toProduct) {
- throw new RecaseError({
- message: `Product ${from_product_id} version ${from_version} or ${to_product_id} version ${to_version} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: 404,
- });
- }
+ if (!fromProduct || !toProduct) {
+ throw new RecaseError({
+ message: `Product ${from_product_id} version ${from_version} or ${to_product_id} version ${to_version} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: 404,
+ });
+ }
- await addTaskToQueue({
- jobName: JobName.Migration,
- payload: {
- migrationJobId: migrationJob.id,
- },
- });
+ await addTaskToQueue({
+ jobName: JobName.Migration,
+ payload: {
+ migrationJobId: migrationJob.id,
+ },
+ });
- res.status(200).json(migrationJob);
- },
- });
+ res.status(200).json(migrationJob);
+ },
+ });
});
diff --git a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts
index adc50cc72..4e2c3a6ff 100644
--- a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts
+++ b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts
@@ -1,8 +1,8 @@
import {
- CusProductStatus,
- ErrCode,
- MigrationJobStep,
- Product,
+ CusProductStatus,
+ ErrCode,
+ MigrationJobStep,
+ Product,
} from "@autumn/shared";
import { MigrationService } from "../MigrationService.js";
import RecaseError from "@/utils/errorUtils.js";
@@ -11,118 +11,118 @@ import { customerProducts } from "@autumn/shared";
import { and, asc, eq, gt, inArray } from "drizzle-orm";
const getAllCustomersOnProduct = async ({
- db,
- internalProductId,
+ db,
+ internalProductId,
}: {
- db: DrizzleCli;
- internalProductId: string;
+ db: DrizzleCli;
+ internalProductId: string;
}) => {
- let allData: any[] = [];
- const PAGE_SIZE = 1000;
- let lastId: string | null = null;
+ let allData: any[] = [];
+ const PAGE_SIZE = 1000;
+ let lastId: string | null = null;
- while (true) {
- let data;
- try {
- data = await db.query.customerProducts.findMany({
- where: and(
- eq(customerProducts.internal_product_id, internalProductId),
- inArray(customerProducts.status, [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- ]),
- lastId ? gt(customerProducts.id, lastId) : undefined,
- ),
- with: {
- customer: true,
- },
- orderBy: [asc(customerProducts.id)],
- limit: PAGE_SIZE,
- });
- } catch (error) {
- throw new RecaseError({
- message: "Error getting customers on product",
- code: ErrCode.GetCusProductsFailed,
- data: error,
- });
- }
+ while (true) {
+ let data;
+ try {
+ data = await db.query.customerProducts.findMany({
+ where: and(
+ eq(customerProducts.internal_product_id, internalProductId),
+ inArray(customerProducts.status, [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ ]),
+ lastId ? gt(customerProducts.id, lastId) : undefined,
+ ),
+ with: {
+ customer: true,
+ },
+ orderBy: [asc(customerProducts.id)],
+ limit: PAGE_SIZE,
+ });
+ } catch (error) {
+ throw new RecaseError({
+ message: "Error getting customers on product",
+ code: ErrCode.GetCusProductsFailed,
+ data: error,
+ });
+ }
- if (!data || data.length === 0) break;
+ if (!data || data.length === 0) break;
- let filtered = data.reduce((acc: any[], curr: any) => {
- const existingIndex = acc.findIndex(
- (item) => item.customer.id === curr.customer.id,
- );
- if (existingIndex === -1) {
- acc.push(curr);
- } else if (
- new Date(curr.created_at) > new Date(acc[existingIndex].created_at)
- ) {
- acc[existingIndex] = curr;
- }
- return acc;
- }, []);
+ let filtered = data.reduce((acc: any[], curr: any) => {
+ const existingIndex = acc.findIndex(
+ (item) => item.customer.id === curr.customer.id,
+ );
+ if (existingIndex === -1) {
+ acc.push(curr);
+ } else if (
+ new Date(curr.created_at) > new Date(acc[existingIndex].created_at)
+ ) {
+ acc[existingIndex] = curr;
+ }
+ return acc;
+ }, []);
- allData = [...allData, ...filtered];
- lastId = data[data.length - 1].id;
+ allData = [...allData, ...filtered];
+ lastId = data[data.length - 1].id;
- if (data.length < PAGE_SIZE) break;
- }
+ if (data.length < PAGE_SIZE) break;
+ }
- return { cusProducts: allData, error: null };
+ return { cusProducts: allData, error: null };
};
export const getMigrationCustomers = async ({
- db,
- migrationJobId,
- fromProduct,
- logger,
+ db,
+ migrationJobId,
+ fromProduct,
+ logger,
}: {
- db: DrizzleCli;
- migrationJobId: string;
- fromProduct: Product;
- logger: any;
+ db: DrizzleCli;
+ migrationJobId: string;
+ fromProduct: Product;
+ logger: any;
}) => {
- await MigrationService.updateJob({
- db,
- migrationJobId,
- updates: {
- current_step: MigrationJobStep.GetCustomers,
- },
- });
+ await MigrationService.updateJob({
+ db,
+ migrationJobId,
+ updates: {
+ current_step: MigrationJobStep.GetCustomers,
+ },
+ });
- let { cusProducts } = await getAllCustomersOnProduct({
- db,
- internalProductId: fromProduct.internal_id,
- });
+ let { cusProducts } = await getAllCustomersOnProduct({
+ db,
+ internalProductId: fromProduct.internal_id,
+ });
- let totalCount = cusProducts.length;
- let canceledCount = cusProducts.filter(
- (cusProd) => cusProd.canceled_at !== null,
- ).length;
+ let totalCount = cusProducts.length;
+ let canceledCount = cusProducts.filter(
+ (cusProd) => cusProd.canceled_at !== null,
+ ).length;
- let customCount = cusProducts.filter((cusProd) => cusProd.is_custom).length;
+ let customCount = cusProducts.filter((cusProd) => cusProd.is_custom).length;
- let filteredCusProducts = cusProducts.filter(
- (cusProd) => cusProd.canceled_at === null && !cusProd.is_custom,
- );
+ let filteredCusProducts = cusProducts.filter(
+ (cusProd) => cusProd.canceled_at === null && !cusProd.is_custom,
+ );
- let customers = filteredCusProducts.map((cusProd) => cusProd.customer);
+ let customers = filteredCusProducts.map((cusProd) => cusProd.customer);
- await MigrationService.updateJob({
- db,
- migrationJobId,
- updates: {
- step_details: {
- [MigrationJobStep.GetCustomers]: {
- total_customers: totalCount,
- canceled_customers: canceledCount,
- custom_customers: customCount,
- migration_customers: filteredCusProducts.length,
- },
- },
- },
- });
+ await MigrationService.updateJob({
+ db,
+ migrationJobId,
+ updates: {
+ step_details: {
+ [MigrationJobStep.GetCustomers]: {
+ total_customers: totalCount,
+ canceled_customers: canceledCount,
+ custom_customers: customCount,
+ migration_customers: filteredCusProducts.length,
+ },
+ },
+ },
+ });
- return customers;
+ return customers;
};
diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts
index 4a255598a..2cf8937d1 100644
--- a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts
+++ b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts
@@ -1,11 +1,11 @@
import {
- MigrationJob,
- Customer,
- Organization,
- AppEnv,
- FullProduct,
- FullCusProduct,
- Feature,
+ MigrationJob,
+ Customer,
+ Organization,
+ AppEnv,
+ FullProduct,
+ FullCusProduct,
+ Feature,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -18,84 +18,84 @@ import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
export const migrateCustomer = async ({
- db,
- customerId,
- org,
- logger,
- env,
- orgId,
- fromProduct,
- toProduct,
- features,
- migrationJob,
+ db,
+ customerId,
+ org,
+ logger,
+ env,
+ orgId,
+ fromProduct,
+ toProduct,
+ features,
+ migrationJob,
}: {
- db: DrizzleCli;
- customerId: string;
- org: Organization;
- env: AppEnv;
- orgId: string;
- fromProduct: FullProduct;
- toProduct: FullProduct;
- logger: any;
- features: Feature[];
- migrationJob?: MigrationJob;
+ db: DrizzleCli;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
+ orgId: string;
+ fromProduct: FullProduct;
+ toProduct: FullProduct;
+ logger: any;
+ features: Feature[];
+ migrationJob?: MigrationJob;
}) => {
- try {
- const stripeCli = createStripeCli({ org, env });
- let fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId,
- env,
- withEntities: true,
- });
+ try {
+ const stripeCli = createStripeCli({ org, env });
+ let fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ withEntities: true,
+ });
- // 1. Build req object
- let req = {
- db,
- orgId,
- env,
- org,
- features,
- logtail: logger,
- timestamp: Date.now(),
- } as ExtendedRequest;
+ // 1. Build req object
+ let req = {
+ db,
+ orgId,
+ env,
+ org,
+ features,
+ logtail: logger,
+ timestamp: Date.now(),
+ } as ExtendedRequest;
- const cusProducts = fullCus.customer_products;
- const filteredCusProducts = cusProducts.filter(
- (cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id
- );
+ const cusProducts = fullCus.customer_products;
+ const filteredCusProducts = cusProducts.filter(
+ (cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id,
+ );
- for (const cusProduct of filteredCusProducts) {
- const attachParams = await migrationToAttachParams({
- req,
- stripeCli,
- customer: fullCus,
- cusProduct,
- newProduct: toProduct,
- });
+ for (const cusProduct of filteredCusProducts) {
+ const attachParams = await migrationToAttachParams({
+ req,
+ stripeCli,
+ customer: fullCus,
+ cusProduct,
+ newProduct: toProduct,
+ });
- await runMigrationAttach({
- req,
- attachParams,
- fromProduct,
- });
+ await runMigrationAttach({
+ req,
+ attachParams,
+ fromProduct,
+ });
- await deleteCusCache({
- db,
- customerId,
- org,
- env,
- });
- }
+ await deleteCusCache({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ }
- return true;
- } catch (error: any) {
- logger.error(
- `Migration failed for customer ${customerId}, job id: ${migrationJob?.id}`
- );
- logger.error(error);
+ return true;
+ } catch (error: any) {
+ logger.error(
+ `Migration failed for customer ${customerId}, job id: ${migrationJob?.id}`,
+ );
+ logger.error(error);
- return false;
- }
+ return false;
+ }
};
diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
index 0660ed320..13680238f 100644
--- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
+++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts
@@ -1,11 +1,11 @@
import dotenv from "dotenv";
dotenv.config();
import {
- Customer,
- Feature,
- FullProduct,
- MigrationJob,
- MigrationJobStep,
+ Customer,
+ Feature,
+ FullProduct,
+ MigrationJob,
+ MigrationJobStep,
} from "@autumn/shared";
import { MigrationService } from "../MigrationService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -16,162 +16,162 @@ import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice
import { DrizzleCli } from "@/db/initDrizzle.js";
export const migrateCustomers = async ({
- db,
- migrationJob,
- fromProduct,
- toProduct,
- logger,
- customers,
- features,
+ db,
+ migrationJob,
+ fromProduct,
+ toProduct,
+ logger,
+ customers,
+ features,
}: {
- db: DrizzleCli;
- migrationJob: MigrationJob;
- fromProduct: FullProduct;
- toProduct: FullProduct;
- logger: any;
- customers: Customer[];
- features: Feature[];
+ db: DrizzleCli;
+ migrationJob: MigrationJob;
+ fromProduct: FullProduct;
+ toProduct: FullProduct;
+ logger: any;
+ customers: Customer[];
+ features: Feature[];
}) => {
- await MigrationService.updateJob({
- db,
- migrationJobId: migrationJob.id,
- updates: {
- current_step: MigrationJobStep.MigrateCustomers,
- },
- });
+ await MigrationService.updateJob({
+ db,
+ migrationJobId: migrationJob.id,
+ updates: {
+ current_step: MigrationJobStep.MigrateCustomers,
+ },
+ });
- let batchCount = 0;
- let { org_id: orgId, env } = migrationJob;
+ let batchCount = 0;
+ let { org_id: orgId, env } = migrationJob;
- let org = await OrgService.get({
- db,
- orgId,
- });
+ let org = await OrgService.get({
+ db,
+ orgId,
+ });
- // Create stripe prices if they don't exist
- let stripeCli = createStripeCli({ org, env });
- let batchCreate = [];
- for (let price of toProduct.prices) {
- batchCreate.push(
- createStripePriceIFNotExist({
- db,
- stripeCli,
- price,
- entitlements: toProduct.entitlements,
- product: toProduct,
- org,
- logger,
- }),
- );
- }
+ // Create stripe prices if they don't exist
+ let stripeCli = createStripeCli({ org, env });
+ let batchCreate = [];
+ for (let price of toProduct.prices) {
+ batchCreate.push(
+ createStripePriceIFNotExist({
+ db,
+ stripeCli,
+ price,
+ entitlements: toProduct.entitlements,
+ product: toProduct,
+ org,
+ logger,
+ }),
+ );
+ }
- await Promise.all(batchCreate);
+ await Promise.all(batchCreate);
- let batchSize = 5;
+ let batchSize = 5;
- for (let i = 0; i < customers.length; i += batchSize) {
- let batchCustomers = customers.slice(i, i + batchSize);
- let batchPromises = [];
- for (let customer of batchCustomers) {
- if (!customer.id) continue;
- batchPromises.push(
- migrateCustomer({
- db,
- migrationJob,
- customerId: customer.id!,
- org,
- logger,
- env,
- orgId,
- fromProduct,
- toProduct,
- features,
- }),
- );
- }
+ for (let i = 0; i < customers.length; i += batchSize) {
+ let batchCustomers = customers.slice(i, i + batchSize);
+ let batchPromises = [];
+ for (let customer of batchCustomers) {
+ if (!customer.id) continue;
+ batchPromises.push(
+ migrateCustomer({
+ db,
+ migrationJob,
+ customerId: customer.id!,
+ org,
+ logger,
+ env,
+ orgId,
+ fromProduct,
+ toProduct,
+ features,
+ }),
+ );
+ }
- let results = await Promise.all(batchPromises);
- let numPassed = results.filter((r) => r).length;
- let numFailed = results.filter((r) => !r).length;
- logger.info(
- `Job: ${migrationJob.id} - Migrated ${i + batchCustomers.length}/${
- customers.length
- } customers, ${numPassed} passed, ${numFailed} failed`,
- );
+ let results = await Promise.all(batchPromises);
+ let numPassed = results.filter((r) => r).length;
+ let numFailed = results.filter((r) => !r).length;
+ logger.info(
+ `Job: ${migrationJob.id} - Migrated ${i + batchCustomers.length}/${
+ customers.length
+ } customers, ${numPassed} passed, ${numFailed} failed`,
+ );
- // Get current number of customers migrated
- let curMigrationJob = await MigrationService.getJob({
- db,
- id: migrationJob.id,
- });
- let curSucceeded =
- curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]
- ?.succeeded || 0;
- let curFailed =
- curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]?.failed ||
- 0;
+ // Get current number of customers migrated
+ let curMigrationJob = await MigrationService.getJob({
+ db,
+ id: migrationJob.id,
+ });
+ let curSucceeded =
+ curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]
+ ?.succeeded || 0;
+ let curFailed =
+ curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]?.failed ||
+ 0;
- await MigrationService.updateJob({
- db,
- migrationJobId: migrationJob.id,
- updates: {
- step_details: {
- ...curMigrationJob.step_details,
- [MigrationJobStep.MigrateCustomers]: {
- ...(curMigrationJob.step_details[
- MigrationJobStep.MigrateCustomers
- ] || {}),
+ await MigrationService.updateJob({
+ db,
+ migrationJobId: migrationJob.id,
+ updates: {
+ step_details: {
+ ...curMigrationJob.step_details,
+ [MigrationJobStep.MigrateCustomers]: {
+ ...(curMigrationJob.step_details[
+ MigrationJobStep.MigrateCustomers
+ ] || {}),
- succeeded: curSucceeded + numPassed,
+ succeeded: curSucceeded + numPassed,
- failed: curFailed + numFailed,
- },
- },
- },
- });
+ failed: curFailed + numFailed,
+ },
+ },
+ },
+ });
- batchCount++;
- }
+ batchCount++;
+ }
- // Get number of errors
- let migrationDetails: any = {};
- // try {
- // let errors = await MigrationService.getErrors({
- // db,
- // migrationJobId: migrationJob.id,
- // });
+ // Get number of errors
+ let migrationDetails: any = {};
+ // try {
+ // let errors = await MigrationService.getErrors({
+ // db,
+ // migrationJobId: migrationJob.id,
+ // });
- // migrationDetails.num_errors = errors!.length;
- // migrationDetails.failed_customers = errors!.map(
- // (e: any) => `${e.customer.id} - ${e.customer.name}`,
- // );
- // } catch (error) {
- // migrationDetails.failed_to_get_errors = true;
- // migrationDetails.error = error;
- // logger.error("Failed to get migration errors");
- // logger.error(error);
- // }
+ // migrationDetails.num_errors = errors!.length;
+ // migrationDetails.failed_customers = errors!.map(
+ // (e: any) => `${e.customer.id} - ${e.customer.name}`,
+ // );
+ // } catch (error) {
+ // migrationDetails.failed_to_get_errors = true;
+ // migrationDetails.error = error;
+ // logger.error("Failed to get migration errors");
+ // logger.error(error);
+ // }
- let curMigrationJob = await MigrationService.getJob({
- db,
- id: migrationJob.id,
- });
+ let curMigrationJob = await MigrationService.getJob({
+ db,
+ id: migrationJob.id,
+ });
- await MigrationService.updateJob({
- db,
- migrationJobId: migrationJob.id,
- updates: {
- current_step: MigrationJobStep.Finished,
- step_details: {
- ...curMigrationJob.step_details,
- [MigrationJobStep.MigrateCustomers]: migrationDetails,
- },
- },
- });
+ await MigrationService.updateJob({
+ db,
+ migrationJobId: migrationJob.id,
+ updates: {
+ current_step: MigrationJobStep.Finished,
+ step_details: {
+ ...curMigrationJob.step_details,
+ [MigrationJobStep.MigrateCustomers]: migrationDetails,
+ },
+ },
+ });
- // await sendMigrationEmail({
- // db,
- // migrationJobId: migrationJob.id,
- // org,
- // });
+ // await sendMigrationEmail({
+ // db,
+ // migrationJobId: migrationJob.id,
+ // org,
+ // });
};
diff --git a/server/src/internal/migrations/migrationUtils.ts b/server/src/internal/migrations/migrationUtils.ts
index bcf63e216..9c77e5b28 100644
--- a/server/src/internal/migrations/migrationUtils.ts
+++ b/server/src/internal/migrations/migrationUtils.ts
@@ -1,58 +1,58 @@
import { generateId } from "@/utils/genUtils.js";
import {
- AppEnv,
- MigrationError,
- MigrationJob,
- MigrationJobStep,
- Product,
+ AppEnv,
+ MigrationError,
+ MigrationJob,
+ MigrationJobStep,
+ Product,
} from "@autumn/shared";
export const constructMigrationError = ({
- migrationJobId,
- internalCustomerId,
- data,
- code,
- message,
+ migrationJobId,
+ internalCustomerId,
+ data,
+ code,
+ message,
}: {
- migrationJobId: string;
- internalCustomerId: string;
- data: any;
- code: string;
- message: string;
+ migrationJobId: string;
+ internalCustomerId: string;
+ data: any;
+ code: string;
+ message: string;
}) => {
- let migrationError: MigrationError = {
- migration_job_id: migrationJobId,
- internal_customer_id: internalCustomerId,
- data,
- code,
- message,
- created_at: Date.now(),
- updated_at: Date.now(),
- };
+ let migrationError: MigrationError = {
+ migration_job_id: migrationJobId,
+ internal_customer_id: internalCustomerId,
+ data,
+ code,
+ message,
+ created_at: Date.now(),
+ updated_at: Date.now(),
+ };
- return migrationError;
+ return migrationError;
};
export const constructMigrationJob = ({
- fromProduct,
- toProduct,
+ fromProduct,
+ toProduct,
}: {
- fromProduct: Product;
- toProduct: Product;
+ fromProduct: Product;
+ toProduct: Product;
}) => {
- let migrationJob: MigrationJob = {
- id: generateId("mig_job"),
- created_at: Date.now(),
- updated_at: Date.now(),
+ let migrationJob: MigrationJob = {
+ id: generateId("mig_job"),
+ created_at: Date.now(),
+ updated_at: Date.now(),
- from_internal_product_id: fromProduct.internal_id,
- to_internal_product_id: toProduct.internal_id,
- current_step: MigrationJobStep.Queued,
- step_details: {},
+ from_internal_product_id: fromProduct.internal_id,
+ to_internal_product_id: toProduct.internal_id,
+ current_step: MigrationJobStep.Queued,
+ step_details: {},
- org_id: fromProduct.org_id,
- env: fromProduct.env as AppEnv,
- };
+ org_id: fromProduct.org_id,
+ env: fromProduct.env as AppEnv,
+ };
- return migrationJob;
+ return migrationJob;
};
diff --git a/server/src/internal/migrations/migrationUtils/migrationToAttachParams.ts b/server/src/internal/migrations/migrationUtils/migrationToAttachParams.ts
index f03ffd724..67e2bad3f 100644
--- a/server/src/internal/migrations/migrationUtils/migrationToAttachParams.ts
+++ b/server/src/internal/migrations/migrationUtils/migrationToAttachParams.ts
@@ -2,65 +2,65 @@ import { getStripeCusData } from "@/internal/customers/attach/attachUtils/attach
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- APIVersion,
- FullCusProduct,
- FullCustomer,
- FullProduct,
+ APIVersion,
+ FullCusProduct,
+ FullCustomer,
+ FullProduct,
} from "@autumn/shared";
import Stripe from "stripe";
export const migrationToAttachParams = async ({
- req,
- stripeCli,
- customer,
- cusProduct,
- newProduct,
+ req,
+ stripeCli,
+ customer,
+ cusProduct,
+ newProduct,
}: {
- req: ExtendedRequest;
- stripeCli: Stripe;
- customer: FullCustomer;
- cusProduct: FullCusProduct;
- newProduct: FullProduct;
+ req: ExtendedRequest;
+ stripeCli: Stripe;
+ customer: FullCustomer;
+ cusProduct: FullCusProduct;
+ newProduct: FullProduct;
}): Promise => {
- const { org } = req;
+ const { org } = req;
- const apiVersion = org.config.api_version || APIVersion.v1;
- const internalEntityId = cusProduct.internal_entity_id || undefined;
+ const apiVersion = org.config.api_version || APIVersion.v1;
+ const internalEntityId = cusProduct.internal_entity_id || undefined;
- const { stripeCus, paymentMethod, now } = await getStripeCusData({
- stripeCli,
- db: req.db,
- org: req.org,
- env: req.env,
- customer,
- logger: req.logtail,
- allowNoStripe: true,
- });
+ const { stripeCus, paymentMethod, now } = await getStripeCusData({
+ stripeCli,
+ db: req.db,
+ org: req.org,
+ env: req.env,
+ customer,
+ logger: req.logtail,
+ allowNoStripe: true,
+ });
- const attachParams: AttachParams = {
- stripeCli,
- stripeCus,
- now,
- paymentMethod,
+ const attachParams: AttachParams = {
+ stripeCli,
+ stripeCus,
+ now,
+ paymentMethod,
- customer,
- products: [newProduct],
- optionsList: cusProduct.options,
- prices: newProduct.prices,
- entitlements: newProduct.entitlements,
- freeTrial: newProduct.free_trial || null,
- replaceables: [],
+ customer,
+ products: [newProduct],
+ optionsList: cusProduct.options,
+ prices: newProduct.prices,
+ entitlements: newProduct.entitlements,
+ freeTrial: newProduct.free_trial || null,
+ replaceables: [],
- req,
- org: req.org,
- entities: customer.entities,
- features: req.features,
- internalEntityId,
- cusProducts: customer.customer_products,
+ req,
+ org: req.org,
+ entities: customer.entities,
+ features: req.features,
+ internalEntityId,
+ cusProducts: customer.customer_products,
- // Others
- apiVersion,
- };
+ // Others
+ apiVersion,
+ };
- return attachParams;
+ return attachParams;
};
diff --git a/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts b/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts
index d5f8c90fc..bef648f6a 100644
--- a/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts
+++ b/server/src/internal/migrations/migrationUtils/runMigrationAttach.ts
@@ -6,99 +6,99 @@ import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
- AttachBranch,
- AttachConfig,
- AttachFunction,
- FullProduct,
- ProrationBehavior,
+ AttachBranch,
+ AttachConfig,
+ AttachFunction,
+ FullProduct,
+ ProrationBehavior,
} from "@autumn/shared";
const getAttachFunction = async ({
- attachParams,
+ attachParams,
}: {
- attachParams: AttachParams;
+ attachParams: AttachParams;
}) => {
- if (isFreeProduct(attachParams.prices)) {
- return AttachFunction.AddProduct;
- }
+ if (isFreeProduct(attachParams.prices)) {
+ return AttachFunction.AddProduct;
+ }
- const sameIntervals = intervalsAreSame({ attachParams });
+ const sameIntervals = intervalsAreSame({ attachParams });
- if (sameIntervals) {
- return AttachFunction.UpgradeSameInterval;
- }
+ if (sameIntervals) {
+ return AttachFunction.UpgradeSameInterval;
+ }
- return AttachFunction.UpgradeDiffInterval;
+ return AttachFunction.UpgradeDiffInterval;
};
export const runMigrationAttach = async ({
- req,
- attachParams,
- fromProduct,
+ req,
+ attachParams,
+ fromProduct,
}: {
- req: ExtendedRequest;
- attachParams: AttachParams;
- fromProduct: FullProduct;
+ req: ExtendedRequest;
+ attachParams: AttachParams;
+ fromProduct: FullProduct;
}) => {
- const { logtail: logger } = req;
- const sameIntervals = intervalsAreSame({ attachParams });
- const branch = AttachBranch.NewVersion;
+ const { logtail: logger } = req;
+ const sameIntervals = intervalsAreSame({ attachParams });
+ const branch = AttachBranch.NewVersion;
- // Set config
- let config: AttachConfig = {
- onlyCheckout: false,
- carryUsage: true,
- branch,
- proration: ProrationBehavior.None,
- disableTrial: true,
- invoiceOnly: false,
- disableMerge: false,
- sameIntervals,
- carryTrial: true,
- invoiceCheckout: false,
- finalizeInvoice: true,
- };
+ // Set config
+ let config: AttachConfig = {
+ onlyCheckout: false,
+ carryUsage: true,
+ branch,
+ proration: ProrationBehavior.None,
+ disableTrial: true,
+ invoiceOnly: false,
+ disableMerge: false,
+ sameIntervals,
+ carryTrial: true,
+ invoiceCheckout: false,
+ finalizeInvoice: true,
+ };
- // Check if branch is update custom ents...
+ // Check if branch is update custom ents...
- let attachFunction = await getAttachFunction({ attachParams });
+ let attachFunction = await getAttachFunction({ attachParams });
- let customer = attachParams.customer;
- logger.info(`--------------------------------`);
- logger.info(
- `Running migration for ${customer.id}, function: ${attachFunction}`
- );
+ let customer = attachParams.customer;
+ logger.info(`--------------------------------`);
+ logger.info(
+ `Running migration for ${customer.id}, function: ${attachFunction}`,
+ );
- let sameCustomBranch: AttachBranch | undefined;
- try {
- const curSameProduct = attachParams.customer.customer_products.find(
- (cp) => cp.product.internal_id == fromProduct.internal_id
- );
- sameCustomBranch = curSameProduct
- ? await checkSameCustom({
- attachParams,
- curSameProduct,
- })
- : undefined;
- } catch (error) {
- console.log("Error:", error);
- }
+ let sameCustomBranch: AttachBranch | undefined;
+ try {
+ const curSameProduct = attachParams.customer.customer_products.find(
+ (cp) => cp.product.internal_id == fromProduct.internal_id,
+ );
+ sameCustomBranch = curSameProduct
+ ? await checkSameCustom({
+ attachParams,
+ curSameProduct,
+ })
+ : undefined;
+ } catch (error) {
+ console.log("Error:", error);
+ }
- if (attachFunction == AttachFunction.AddProduct) {
- return await handleAddProduct({
- req,
- attachParams,
- config,
- });
- } else if (attachFunction == AttachFunction.UpgradeSameInterval) {
- await handleUpgradeFlow({
- req,
- attachParams,
- config,
- branch:
- sameCustomBranch == AttachBranch.SameCustomEnts
- ? AttachBranch.SameCustomEnts
- : branch,
- });
- }
+ if (attachFunction == AttachFunction.AddProduct) {
+ return await handleAddProduct({
+ req,
+ attachParams,
+ config,
+ });
+ } else if (attachFunction == AttachFunction.UpgradeSameInterval) {
+ await handleUpgradeFlow({
+ req,
+ attachParams,
+ config,
+ branch:
+ sameCustomBranch == AttachBranch.SameCustomEnts
+ ? AttachBranch.SameCustomEnts
+ : branch,
+ });
+ }
};
diff --git a/server/src/internal/migrations/runMigrationTask.ts b/server/src/internal/migrations/runMigrationTask.ts
index a76bcfd75..600d87ef1 100644
--- a/server/src/internal/migrations/runMigrationTask.ts
+++ b/server/src/internal/migrations/runMigrationTask.ts
@@ -8,83 +8,83 @@ import { FeatureService } from "../features/FeatureService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const runMigrationTask = async ({
- db,
- payload,
- logger,
+ db,
+ payload,
+ logger,
}: {
- db: DrizzleCli;
- payload: any;
- logger: any;
+ db: DrizzleCli;
+ payload: any;
+ logger: any;
}) => {
- const { migrationJobId } = payload;
+ const { migrationJobId } = payload;
- try {
- logger.info(`Running migration task, ID: ${migrationJobId}`);
+ try {
+ logger.info(`Running migration task, ID: ${migrationJobId}`);
- const migrationJob = await MigrationService.getJob({
- db,
- id: migrationJobId,
- });
+ const migrationJob = await MigrationService.getJob({
+ db,
+ id: migrationJobId,
+ });
- let { org_id: orgId, env } = migrationJob;
+ let { org_id: orgId, env } = migrationJob;
- // Get from and to products
- let [fromProduct, toProduct] = await Promise.all([
- ProductService.getFull({
- db,
- idOrInternalId: migrationJob.from_internal_product_id,
- orgId,
- env,
- }),
- ProductService.getFull({
- db,
- idOrInternalId: migrationJob.to_internal_product_id,
- orgId,
- env,
- }),
- ]);
+ // Get from and to products
+ let [fromProduct, toProduct] = await Promise.all([
+ ProductService.getFull({
+ db,
+ idOrInternalId: migrationJob.from_internal_product_id,
+ orgId,
+ env,
+ }),
+ ProductService.getFull({
+ db,
+ idOrInternalId: migrationJob.to_internal_product_id,
+ orgId,
+ env,
+ }),
+ ]);
- // STEP 1: GET ALL CUSTOMERS AND INSERT INTO MIGRATIONS...
- let customers = await getMigrationCustomers({
- db,
- migrationJobId,
- fromProduct,
- logger,
- });
+ // STEP 1: GET ALL CUSTOMERS AND INSERT INTO MIGRATIONS...
+ let customers = await getMigrationCustomers({
+ db,
+ migrationJobId,
+ fromProduct,
+ logger,
+ });
- let features = await FeatureService.list({
- db,
- orgId,
- env,
- });
+ let features = await FeatureService.list({
+ db,
+ orgId,
+ env,
+ });
- logger.info(`Job ${migrationJobId} | Found ${customers?.length} customers`);
+ logger.info(`Job ${migrationJobId} | Found ${customers?.length} customers`);
- // STEP 2: MIGRATE CUSTOMERS..
- await migrateCustomers({
- db,
- migrationJob,
- fromProduct,
- toProduct,
- customers,
- logger,
- features,
- });
+ // STEP 2: MIGRATE CUSTOMERS..
+ await migrateCustomers({
+ db,
+ migrationJob,
+ fromProduct,
+ toProduct,
+ customers,
+ logger,
+ features,
+ });
- // await new Promise((resolve) => setTimeout(resolve, 10000));
- } catch (error) {
- logger.error(`Migration failed: ${migrationJobId}`);
- logger.error(error);
- await MigrationService.updateJob({
- db,
- migrationJobId,
- updates: {
- current_step: MigrationJobStep.Failed,
- step_details: {
- error: error,
- },
- },
- });
- throw error;
- }
+ // await new Promise((resolve) => setTimeout(resolve, 10000));
+ } catch (error) {
+ logger.error(`Migration failed: ${migrationJobId}`);
+ logger.error(error);
+ await MigrationService.updateJob({
+ db,
+ migrationJobId,
+ updates: {
+ current_step: MigrationJobStep.Failed,
+ step_details: {
+ error: error,
+ },
+ },
+ });
+ throw error;
+ }
};
diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts
index d57c8fed5..0ce29e31f 100644
--- a/server/src/internal/migrations/runRewardMigrationTask.ts
+++ b/server/src/internal/migrations/runRewardMigrationTask.ts
@@ -1,15 +1,15 @@
import {
- type AppEnv,
- type FixedPriceConfig,
- type FullProduct,
- type Price,
- type UsagePriceConfig,
- DiscountConfig,
- PriceType,
- RewardType,
- getBillingType,
- isFixedPrice,
- isUsagePrice,
+ type AppEnv,
+ type FixedPriceConfig,
+ type FullProduct,
+ type Price,
+ type UsagePriceConfig,
+ DiscountConfig,
+ PriceType,
+ RewardType,
+ getBillingType,
+ isFixedPrice,
+ isUsagePrice,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
@@ -26,243 +26,243 @@ import { ProductService } from "../products/ProductService.js";
// Helper function to check if tier structures match
const tiersMatch = (oldTiers: any[], newTiers: any[]): boolean => {
- if (oldTiers.length !== newTiers.length) return false;
+ if (oldTiers.length !== newTiers.length) return false;
- return oldTiers.every((oldTier, index) => {
- const newTier = newTiers[index];
- return oldTier.to === newTier.to && oldTier.amount === newTier.amount;
- });
+ return oldTiers.every((oldTier, index) => {
+ const newTier = newTiers[index];
+ return oldTier.to === newTier.to && oldTier.amount === newTier.amount;
+ });
};
// Match fixed prices by amount
const findMatchingFixedPrice = (
- oldPrice: Price,
- candidates: Price[]
+ oldPrice: Price,
+ candidates: Price[],
): Price | null => {
- const oldConfig = oldPrice.config as FixedPriceConfig;
+ const oldConfig = oldPrice.config as FixedPriceConfig;
- const possibleCandidate = candidates.find((candidate) => {
- const newConfig = candidate.config as FixedPriceConfig;
- return newConfig.amount === oldConfig.amount;
- });
+ const possibleCandidate = candidates.find((candidate) => {
+ const newConfig = candidate.config as FixedPriceConfig;
+ return newConfig.amount === oldConfig.amount;
+ });
- return possibleCandidate || candidates?.[0];
+ return possibleCandidate || candidates?.[0];
};
// Match usage prices by feature and billing characteristics
const findMatchingUsagePrice = (
- oldPrice: Price,
- candidates: Price[]
+ oldPrice: Price,
+ candidates: Price[],
): Price | null => {
- const oldConfig = oldPrice.config as UsagePriceConfig;
+ const oldConfig = oldPrice.config as UsagePriceConfig;
- return (
- candidates.find((candidate) => {
- const newConfig = candidate.config as UsagePriceConfig;
+ return (
+ candidates.find((candidate) => {
+ const newConfig = candidate.config as UsagePriceConfig;
- // Match by feature
- if (newConfig.internal_feature_id !== oldConfig.internal_feature_id)
- return false;
+ // Match by feature
+ if (newConfig.internal_feature_id !== oldConfig.internal_feature_id)
+ return false;
- // Match by billing behavior
- let newBillingType = getBillingType(newConfig);
- let oldBillingType = getBillingType(oldConfig);
- if (newBillingType !== oldBillingType) return false;
+ // Match by billing behavior
+ let newBillingType = getBillingType(newConfig);
+ let oldBillingType = getBillingType(oldConfig);
+ if (newBillingType !== oldBillingType) return false;
- // Optionally match by tier structure
- // if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers))
- if (!tiersAreSame(oldConfig.usage_tiers, newConfig.usage_tiers))
- return false;
+ // Optionally match by tier structure
+ // if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers))
+ if (!tiersAreSame(oldConfig.usage_tiers, newConfig.usage_tiers))
+ return false;
- return true;
- }) || null
- );
+ return true;
+ }) || null
+ );
};
// Main matching function with type-specific logic
const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => {
- // First, filter by basic characteristics
+ // First, filter by basic characteristics
- const candidates = newPrices.filter((newPrice) => {
- if (newPrice.id === oldPrice.id) return true;
+ const candidates = newPrices.filter((newPrice) => {
+ if (newPrice.id === oldPrice.id) return true;
- const oldConfig = oldPrice.config as UsagePriceConfig;
- const newConfig = newPrice.config as UsagePriceConfig;
+ const oldConfig = oldPrice.config as UsagePriceConfig;
+ const newConfig = newPrice.config as UsagePriceConfig;
- return (
- getBillingType(newPrice.config) === getBillingType(oldPrice.config) &&
- newPrice.config.interval === oldPrice.config.interval &&
- newPrice.config.interval_count === oldPrice.config.interval_count &&
- (oldConfig.type == PriceType.Usage
- ? oldConfig.internal_feature_id === newConfig.internal_feature_id
- : true)
- );
- });
+ return (
+ getBillingType(newPrice.config) === getBillingType(oldPrice.config) &&
+ newPrice.config.interval === oldPrice.config.interval &&
+ newPrice.config.interval_count === oldPrice.config.interval_count &&
+ (oldConfig.type == PriceType.Usage
+ ? oldConfig.internal_feature_id === newConfig.internal_feature_id
+ : true)
+ );
+ });
- if (candidates.length === 0) return null;
- if (candidates.length === 1) return candidates[0];
+ if (candidates.length === 0) return null;
+ if (candidates.length === 1) return candidates[0];
- // If multiple candidates, use type-specific matching
- if (isFixedPrice({ price: oldPrice })) {
- return findMatchingFixedPrice(oldPrice, candidates);
- } else if (isUsagePrice({ price: oldPrice })) {
- return findMatchingUsagePrice(oldPrice, candidates);
- }
+ // If multiple candidates, use type-specific matching
+ if (isFixedPrice({ price: oldPrice })) {
+ return findMatchingFixedPrice(oldPrice, candidates);
+ } else if (isUsagePrice({ price: oldPrice })) {
+ return findMatchingUsagePrice(oldPrice, candidates);
+ }
- // Fallback to first candidate
- return candidates[0];
+ // Fallback to first candidate
+ return candidates[0];
};
export async function runRewardMigrationTask({
- db,
- payload,
- logger,
+ db,
+ payload,
+ logger,
}: {
- db: DrizzleCli;
- payload: Payloads[JobName.RewardMigration];
- logger: ReturnType;
+ db: DrizzleCli;
+ payload: Payloads[JobName.RewardMigration];
+ logger: ReturnType;
}) {
- try {
- const {
- oldPrices,
- productId,
- // newPrices,
- orgId,
- env,
- }: {
- oldPrices: Price[];
- // newPrices: Price[];
- productId: string;
- orgId: string;
- env: AppEnv;
- } = payload;
+ try {
+ const {
+ oldPrices,
+ productId,
+ // newPrices,
+ orgId,
+ env,
+ }: {
+ oldPrices: Price[];
+ // newPrices: Price[];
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ } = payload;
- const fullProduct = await ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- });
+ const fullProduct = await ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ });
- const newPrices = fullProduct.prices;
+ const newPrices = fullProduct.prices;
- // Get organization for Stripe operations
- const org = await OrgService.get({
- db,
- orgId,
- });
+ // Get organization for Stripe operations
+ const org = await OrgService.get({
+ db,
+ orgId,
+ });
- const rewards = await RewardService.list({
- db,
- orgId,
- env,
- inTypes: [
- RewardType.PercentageDiscount,
- RewardType.FixedDiscount,
- RewardType.InvoiceCredits,
- ],
- });
+ const rewards = await RewardService.list({
+ db,
+ orgId,
+ env,
+ inTypes: [
+ RewardType.PercentageDiscount,
+ RewardType.FixedDiscount,
+ RewardType.InvoiceCredits,
+ ],
+ });
- const filteredRewards = rewards.filter(
- (x) =>
- x.org_id === orgId &&
- x.env === env &&
- x.type !== RewardType.FreeProduct &&
- x.discount_config &&
- x.discount_config.price_ids?.some((p) =>
- oldPrices.map((p) => p.id).includes(p)
- )
- );
+ const filteredRewards = rewards.filter(
+ (x) =>
+ x.org_id === orgId &&
+ x.env === env &&
+ x.type !== RewardType.FreeProduct &&
+ x.discount_config &&
+ x.discount_config.price_ids?.some((p) =>
+ oldPrices.map((p) => p.id).includes(p),
+ ),
+ );
- let shouldUpdateReward = false;
+ let shouldUpdateReward = false;
- for (const reward of filteredRewards) {
- const newPriceIds: string[] = [];
- const unmatchedPrices: string[] = [];
+ for (const reward of filteredRewards) {
+ const newPriceIds: string[] = [];
+ const unmatchedPrices: string[] = [];
- if (reward.discount_config?.price_ids) {
- for (const priceId of reward.discount_config.price_ids) {
- const oldPrice = oldPrices.find((p) => p.id === priceId);
+ if (reward.discount_config?.price_ids) {
+ for (const priceId of reward.discount_config.price_ids) {
+ const oldPrice = oldPrices.find((p) => p.id === priceId);
- // From other product
- if (!oldPrice) {
- newPriceIds.push(priceId);
- continue;
- }
+ // From other product
+ if (!oldPrice) {
+ newPriceIds.push(priceId);
+ continue;
+ }
- const matchingNewPrice = findBestMatch(oldPrice, newPrices);
+ const matchingNewPrice = findBestMatch(oldPrice, newPrices);
- if (matchingNewPrice) {
- newPriceIds.push(matchingNewPrice.id);
- const shouldUpdate =
- matchingNewPrice.config.stripe_price_id !==
- oldPrice.config.stripe_price_id ||
- matchingNewPrice.config.stripe_product_id !==
- oldPrice.config.stripe_product_id;
+ if (matchingNewPrice) {
+ newPriceIds.push(matchingNewPrice.id);
+ const shouldUpdate =
+ matchingNewPrice.config.stripe_price_id !==
+ oldPrice.config.stripe_price_id ||
+ matchingNewPrice.config.stripe_product_id !==
+ oldPrice.config.stripe_product_id;
- if (shouldUpdate) {
- shouldUpdateReward = true;
- }
- } else {
- unmatchedPrices.push(oldPrice.id);
- }
- }
- }
+ if (shouldUpdate) {
+ shouldUpdateReward = true;
+ }
+ } else {
+ unmatchedPrices.push(oldPrice.id);
+ }
+ }
+ }
- // Update the reward with new price IDs
- if (shouldUpdateReward) {
- try {
- // Update Stripe coupon and reward if price IDs have changed
- console.log(
- `Updating ${reward.id}, updating reward and Stripe coupon...`
- );
+ // Update the reward with new price IDs
+ if (shouldUpdateReward) {
+ try {
+ // Update Stripe coupon and reward if price IDs have changed
+ console.log(
+ `Updating ${reward.id}, updating reward and Stripe coupon...`,
+ );
- // Update the reward in the database
- const updatedReward = await RewardService.update({
- db,
- internalId: reward.internal_id!,
- env,
- orgId,
- update: {
- discount_config: {
- ...(reward.discount_config as DiscountConfig),
- price_ids: newPriceIds,
- },
- },
- });
+ // Update the reward in the database
+ const updatedReward = await RewardService.update({
+ db,
+ internalId: reward.internal_id!,
+ env,
+ orgId,
+ update: {
+ discount_config: {
+ ...(reward.discount_config as DiscountConfig),
+ price_ids: newPriceIds,
+ },
+ },
+ });
- // Get the price objects for the new price IDs
- const prices = await PriceService.getInIds({
- db,
- ids: newPriceIds,
- });
+ // Get the price objects for the new price IDs
+ const prices = await PriceService.getInIds({
+ db,
+ ids: newPriceIds,
+ });
- // Recreate the Stripe coupon with new product restrictions
- await createStripeCoupon({
- reward: updatedReward,
- org,
- env,
- prices,
- logger,
- });
+ // Recreate the Stripe coupon with new product restrictions
+ await createStripeCoupon({
+ reward: updatedReward,
+ org,
+ env,
+ prices,
+ logger,
+ });
- console.log(
- `Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions`
- );
- } catch (error) {
- console.error(`Failed to update reward ${reward.id}:`, error);
- }
- }
+ console.log(
+ `Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions`,
+ );
+ } catch (error) {
+ console.error(`Failed to update reward ${reward.id}:`, error);
+ }
+ }
- if (unmatchedPrices.length > 0) {
- console.warn(
- `Unmatched prices for reward ${reward.id}:`,
- unmatchedPrices
- );
- }
- }
- } catch (error) {
- console.error("Error running reward migration task", { error });
- throw error;
- }
+ if (unmatchedPrices.length > 0) {
+ console.warn(
+ `Unmatched prices for reward ${reward.id}:`,
+ unmatchedPrices,
+ );
+ }
+ }
+ } catch (error) {
+ console.error("Error running reward migration task", { error });
+ throw error;
+ }
}
diff --git a/server/src/internal/orgs/OrgService.ts b/server/src/internal/orgs/OrgService.ts
index f88d44858..1729301a6 100644
--- a/server/src/internal/orgs/OrgService.ts
+++ b/server/src/internal/orgs/OrgService.ts
@@ -1,15 +1,15 @@
import RecaseError from "@/utils/errorUtils.js";
import { and, eq, sql, inArray } from "drizzle-orm";
import {
- AppEnv,
- ErrCode,
- Feature,
- features,
- invitation,
- member,
- Organization,
- OrgConfigSchema,
- user,
+ AppEnv,
+ ErrCode,
+ Feature,
+ features,
+ invitation,
+ member,
+ Organization,
+ OrgConfigSchema,
+ user,
} from "@autumn/shared";
import { getApiVersion } from "@/utils/versionUtils.js";
@@ -18,281 +18,281 @@ import { organizations, apiKeys } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
export class OrgService {
- static async getFromReq(req: any) {
- if (req.org) {
- let org = structuredClone(req.org);
- let config = org.config || {};
- let apiVersion = getApiVersion({
- createdAt: org.created_at,
- });
- return {
- ...org,
- config: OrgConfigSchema.parse(config),
- api_version: apiVersion,
- };
- }
+ static async getFromReq(req: any) {
+ if (req.org) {
+ let org = structuredClone(req.org);
+ let config = org.config || {};
+ let apiVersion = getApiVersion({
+ createdAt: org.created_at,
+ });
+ return {
+ ...org,
+ config: OrgConfigSchema.parse(config),
+ api_version: apiVersion,
+ };
+ }
- return await this.get({ db: req.db, orgId: req.orgId });
- }
+ return await this.get({ db: req.db, orgId: req.orgId });
+ }
- static async getMembers({ db, orgId }: { db: DrizzleCli; orgId: string }) {
- const results = await db
- .select()
- .from(member)
- .where(eq(member.organizationId, orgId))
- .innerJoin(user, eq(member.userId, user.id));
+ static async getMembers({ db, orgId }: { db: DrizzleCli; orgId: string }) {
+ const results = await db
+ .select()
+ .from(member)
+ .where(eq(member.organizationId, orgId))
+ .innerJoin(user, eq(member.userId, user.id));
- return results;
+ return results;
- // // Try to get members with user data
- // let results;
- // try {
- // results = await db.query.member.findMany({
- // where: eq(member.organizationId, orgId),
- // with: {
- // user: true,
- // },
- // });
- // } catch (error) {
- // // Fallback: get members and users separately
- // const members = await db.query.member.findMany({
- // where: eq(member.organizationId, orgId),
- // });
+ // // Try to get members with user data
+ // let results;
+ // try {
+ // results = await db.query.member.findMany({
+ // where: eq(member.organizationId, orgId),
+ // with: {
+ // user: true,
+ // },
+ // });
+ // } catch (error) {
+ // // Fallback: get members and users separately
+ // const members = await db.query.member.findMany({
+ // where: eq(member.organizationId, orgId),
+ // });
- // const userIds = members.map((m) => m.userId);
- // const users = await db.query.user.findMany({
- // where: inArray(user.id, userIds),
- // });
+ // const userIds = members.map((m) => m.userId);
+ // const users = await db.query.user.findMany({
+ // where: inArray(user.id, userIds),
+ // });
- // // Combine the data
- // results = members.map((member) => ({
- // ...member,
- // user: users.find((u) => u.id === member.userId),
- // }));
- // }
+ // // Combine the data
+ // results = members.map((member) => ({
+ // ...member,
+ // user: users.find((u) => u.id === member.userId),
+ // }));
+ // }
- // // Transform to the expected format
- // const transformed = results
- // .map((result) => {
- // // Check if user data exists
- // if (!result.user) {
- // console.error("Missing user data for member:", result);
- // return null;
- // }
+ // // Transform to the expected format
+ // const transformed = results
+ // .map((result) => {
+ // // Check if user data exists
+ // if (!result.user) {
+ // console.error("Missing user data for member:", result);
+ // return null;
+ // }
- // return {
- // member: {
- // id: result.id,
- // organizationId: result.organizationId,
- // userId: result.userId,
- // role: result.role,
- // createdAt: result.createdAt,
- // },
- // user: {
- // id: result.user.id,
- // name: result.user.name,
- // email: result.user.email,
- // emailVerified: result.user.emailVerified,
- // image: result.user.image,
- // createdAt: result.user.createdAt,
- // updatedAt: result.user.updatedAt,
- // role: result.user.role,
- // banned: result.user.banned,
- // banReason: result.user.banReason,
- // banExpires: result.user.banExpires,
- // createdBy: result.user.createdBy,
- // },
- // };
- // })
- // .filter(Boolean); // Remove null entries
+ // return {
+ // member: {
+ // id: result.id,
+ // organizationId: result.organizationId,
+ // userId: result.userId,
+ // role: result.role,
+ // createdAt: result.createdAt,
+ // },
+ // user: {
+ // id: result.user.id,
+ // name: result.user.name,
+ // email: result.user.email,
+ // emailVerified: result.user.emailVerified,
+ // image: result.user.image,
+ // createdAt: result.user.createdAt,
+ // updatedAt: result.user.updatedAt,
+ // role: result.user.role,
+ // banned: result.user.banned,
+ // banReason: result.user.banReason,
+ // banExpires: result.user.banExpires,
+ // createdBy: result.user.createdBy,
+ // },
+ // };
+ // })
+ // .filter(Boolean); // Remove null entries
- // return transformed;
- }
+ // return transformed;
+ }
- static async getInvites({ db, orgId }: { db: DrizzleCli; orgId: string }) {
- const results = await db.query.invitation.findMany({
- where: and(
- eq(invitation.organizationId, orgId),
- eq(invitation.status, "pending")
- ),
- });
+ static async getInvites({ db, orgId }: { db: DrizzleCli; orgId: string }) {
+ const results = await db.query.invitation.findMany({
+ where: and(
+ eq(invitation.organizationId, orgId),
+ eq(invitation.status, "pending"),
+ ),
+ });
- return results;
- }
+ return results;
+ }
- // Drizzle get
- static async get({ db, orgId }: { db: DrizzleCli; orgId: string }) {
- const result = await db.query.organizations.findFirst({
- where: eq(organizations.id, orgId),
- });
+ // Drizzle get
+ static async get({ db, orgId }: { db: DrizzleCli; orgId: string }) {
+ const result = await db.query.organizations.findFirst({
+ where: eq(organizations.id, orgId),
+ });
- if (!result) {
- throw new RecaseError({
- message: "Organization not found",
- code: ErrCode.OrgNotFound,
- statusCode: 404,
- });
- }
+ if (!result) {
+ throw new RecaseError({
+ message: "Organization not found",
+ code: ErrCode.OrgNotFound,
+ statusCode: 404,
+ });
+ }
- return {
- ...result,
- config: OrgConfigSchema.parse(result.config || {}),
- api_version: getApiVersion({
- createdAt: result.created_at!,
- }),
- };
- }
+ return {
+ ...result,
+ config: OrgConfigSchema.parse(result.config || {}),
+ api_version: getApiVersion({
+ createdAt: result.created_at!,
+ }),
+ };
+ }
- static async getWithKeys({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env?: AppEnv;
- }) {
- const result = await db.query.organizations.findFirst({
- where: eq(organizations.id, orgId),
- with: {
- api_keys: env ? { where: eq(apiKeys.env, env) } : true,
- },
- });
+ static async getWithKeys({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env?: AppEnv;
+ }) {
+ const result = await db.query.organizations.findFirst({
+ where: eq(organizations.id, orgId),
+ with: {
+ api_keys: env ? { where: eq(apiKeys.env, env) } : true,
+ },
+ });
- if (!result) {
- return null;
- }
+ if (!result) {
+ return null;
+ }
- return result;
- }
+ return result;
+ }
- static async getWithFeatures({
- db,
- orgId,
- env,
- allowNotFound = false,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- allowNotFound?: boolean;
- }) {
- const result = (await db.query.organizations.findFirst({
- where: eq(organizations.id, orgId),
- with: {
- features: {
- where: eq(features.env, env),
- },
- },
- })) as Organization & {
- features: Feature[];
- };
+ static async getWithFeatures({
+ db,
+ orgId,
+ env,
+ allowNotFound = false,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ allowNotFound?: boolean;
+ }) {
+ const result = (await db.query.organizations.findFirst({
+ where: eq(organizations.id, orgId),
+ with: {
+ features: {
+ where: eq(features.env, env),
+ },
+ },
+ })) as Organization & {
+ features: Feature[];
+ };
- if (!result) {
- if (allowNotFound) {
- return null;
- }
+ if (!result) {
+ if (allowNotFound) {
+ return null;
+ }
- throw new RecaseError({
- message: `Organization ${orgId} not found`,
- code: ErrCode.OrgNotFound,
- statusCode: 404,
- });
- }
+ throw new RecaseError({
+ message: `Organization ${orgId} not found`,
+ code: ErrCode.OrgNotFound,
+ statusCode: 404,
+ });
+ }
- let org = structuredClone(result);
- delete (org as any).features;
- return {
- org: {
- ...org,
- api_version: getApiVersion({
- createdAt: org.created_at!,
- }),
- config: OrgConfigSchema.parse(org.config || {}),
- },
- features: result.features || [],
- };
- }
+ let org = structuredClone(result);
+ delete (org as any).features;
+ return {
+ org: {
+ ...org,
+ api_version: getApiVersion({
+ createdAt: org.created_at!,
+ }),
+ config: OrgConfigSchema.parse(org.config || {}),
+ },
+ features: result.features || [],
+ };
+ }
- static async getFromPkeyWithFeatures({
- db,
- pkey,
- env,
- }: {
- db: DrizzleCli;
- pkey: string;
- env: AppEnv;
- }) {
- let org = await db.query.organizations.findFirst({
- where:
- env === AppEnv.Sandbox
- ? eq(organizations.test_pkey, pkey)
- : eq(organizations.live_pkey, pkey),
- with: {
- features: {
- where: eq(features.env, env),
- },
- },
- });
+ static async getFromPkeyWithFeatures({
+ db,
+ pkey,
+ env,
+ }: {
+ db: DrizzleCli;
+ pkey: string;
+ env: AppEnv;
+ }) {
+ let org = await db.query.organizations.findFirst({
+ where:
+ env === AppEnv.Sandbox
+ ? eq(organizations.test_pkey, pkey)
+ : eq(organizations.live_pkey, pkey),
+ with: {
+ features: {
+ where: eq(features.env, env),
+ },
+ },
+ });
- return org as Organization & {
- features: Feature[];
- };
- }
+ return org as Organization & {
+ features: Feature[];
+ };
+ }
- static async getBySlug({ db, slug }: { db: DrizzleCli; slug: string }) {
- const result = await db.query.organizations.findFirst({
- where: eq(organizations.slug, slug),
- });
+ static async getBySlug({ db, slug }: { db: DrizzleCli; slug: string }) {
+ const result = await db.query.organizations.findFirst({
+ where: eq(organizations.slug, slug),
+ });
- if (!result) {
- return null;
- }
+ if (!result) {
+ return null;
+ }
- return result as Organization;
- }
+ return result as Organization;
+ }
- static async insert({ db, org }: { db: DrizzleCli; org: any }) {
- await db.insert(organizations).values(org);
- }
+ static async insert({ db, org }: { db: DrizzleCli; org: any }) {
+ await db.insert(organizations).values(org);
+ }
- static async delete({ db, orgId }: { db: DrizzleCli; orgId: string }) {
- await db.delete(organizations).where(eq(organizations.id, orgId));
- }
+ static async delete({ db, orgId }: { db: DrizzleCli; orgId: string }) {
+ await db.delete(organizations).where(eq(organizations.id, orgId));
+ }
- static async update({
- db,
- orgId,
- updates,
- }: {
- db: DrizzleCli;
- orgId: string;
- updates: any;
- }) {
- try {
- let result = await db
- .update(organizations)
- .set(updates)
- .where(eq(organizations.id, orgId))
- .returning();
+ static async update({
+ db,
+ orgId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ updates: any;
+ }) {
+ try {
+ let result = await db
+ .update(organizations)
+ .set(updates)
+ .where(eq(organizations.id, orgId))
+ .returning();
- await clearOrgCache({
- db,
- orgId,
- });
+ await clearOrgCache({
+ db,
+ orgId,
+ });
- return result.length > 0 ? (result[0] as Organization) : null;
- } catch (error) {
- console.error(error);
- throw error;
- }
- }
+ return result.length > 0 ? (result[0] as Organization) : null;
+ } catch (error) {
+ console.error(error);
+ throw error;
+ }
+ }
- static async getCacheEnabledOrgs({ db }: { db: DrizzleCli }) {
- const result = await db.query.organizations.findMany({
- where: sql`${organizations.config}->>'cache_customer' = 'true'`,
- });
+ static async getCacheEnabledOrgs({ db }: { db: DrizzleCli }) {
+ const result = await db.query.organizations.findMany({
+ where: sql`${organizations.config}->>'cache_customer' = 'true'`,
+ });
- return result;
- }
+ return result;
+ }
}
diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe.ts
index 56fd939cc..f5da09fba 100644
--- a/server/src/internal/orgs/handlers/handleConnectStripe.ts
+++ b/server/src/internal/orgs/handlers/handleConnectStripe.ts
@@ -6,8 +6,8 @@ import { encryptData } from "@/utils/encryptUtils.js";
import { ErrCode } from "@/errors/errCodes.js";
import {
- checkKeyValid,
- createWebhookEndpoint,
+ checkKeyValid,
+ createWebhookEndpoint,
} from "@/external/stripe/stripeOnboardingUtils.js";
import { OrgService } from "../OrgService.js";
@@ -17,280 +17,280 @@ import { clearOrgCache } from "../orgUtils/clearOrgCache.js";
import { z } from "zod";
import { isStripeConnected } from "../orgUtils.js";
import {
- ensureStripeProducts,
- ensureStripeProductsWithEnv,
+ ensureStripeProducts,
+ ensureStripeProductsWithEnv,
} from "@/external/stripe/stripeEnsureUtils.js";
import { toSuccessUrl } from "../orgUtils/convertOrgUtils.js";
export const connectStripe = async ({
- orgId,
- apiKey,
- env,
+ orgId,
+ apiKey,
+ env,
}: {
- orgId: string;
- apiKey: string;
- env: AppEnv;
+ orgId: string;
+ apiKey: string;
+ env: AppEnv;
}) => {
- // 1. Check if key is valid
- await checkKeyValid(apiKey);
+ // 1. Check if key is valid
+ await checkKeyValid(apiKey);
- let stripe = new Stripe(apiKey);
+ let stripe = new Stripe(apiKey);
- let account = await stripe.accounts.retrieve();
+ let account = await stripe.accounts.retrieve();
- // 2. Disconnect existing webhook endpoints
- const curWebhooks = await stripe.webhookEndpoints.list();
- for (const webhook of curWebhooks.data) {
- if (webhook.url.includes(orgId) && webhook.url.includes(env)) {
- await stripe.webhookEndpoints.del(webhook.id);
- }
- }
+ // 2. Disconnect existing webhook endpoints
+ const curWebhooks = await stripe.webhookEndpoints.list();
+ for (const webhook of curWebhooks.data) {
+ if (webhook.url.includes(orgId) && webhook.url.includes(env)) {
+ await stripe.webhookEndpoints.del(webhook.id);
+ }
+ }
- // 3. Create new webhook endpoint
- let webhook = await createWebhookEndpoint(apiKey, env, orgId);
+ // 3. Create new webhook endpoint
+ let webhook = await createWebhookEndpoint(apiKey, env, orgId);
- // 3. Return encrypted
- if (env === AppEnv.Sandbox) {
- return {
- test_api_key: encryptData(apiKey),
- test_webhook_secret: encryptData(webhook.secret as string),
- env,
- defaultCurrency: account.default_currency,
- };
- } else {
- return {
- live_api_key: encryptData(apiKey),
- live_webhook_secret: encryptData(webhook.secret as string),
- env,
- defaultCurrency: account.default_currency,
- };
- }
+ // 3. Return encrypted
+ if (env === AppEnv.Sandbox) {
+ return {
+ test_api_key: encryptData(apiKey),
+ test_webhook_secret: encryptData(webhook.secret as string),
+ env,
+ defaultCurrency: account.default_currency,
+ };
+ } else {
+ return {
+ live_api_key: encryptData(apiKey),
+ live_webhook_secret: encryptData(webhook.secret as string),
+ env,
+ defaultCurrency: account.default_currency,
+ };
+ }
};
export const connectAllStripe = async ({
- db,
- orgId,
- logger,
- testApiKey,
- liveApiKey,
- defaultCurrency,
- successUrl,
+ db,
+ orgId,
+ logger,
+ testApiKey,
+ liveApiKey,
+ defaultCurrency,
+ successUrl,
}: {
- db: any;
- orgId: string;
- logger: any;
- testApiKey: string;
- liveApiKey: string;
- defaultCurrency?: string;
- successUrl: string;
+ db: any;
+ orgId: string;
+ logger: any;
+ testApiKey: string;
+ liveApiKey: string;
+ defaultCurrency?: string;
+ successUrl: string;
}) => {
- // 1. Check if API keys are valid
- try {
- await clearOrgCache({
- db,
- orgId,
- logger,
- });
+ // 1. Check if API keys are valid
+ try {
+ await clearOrgCache({
+ db,
+ orgId,
+ logger,
+ });
- await checkKeyValid(testApiKey);
- await checkKeyValid(liveApiKey);
+ await checkKeyValid(testApiKey);
+ await checkKeyValid(liveApiKey);
- // Get default currency from Stripe
- let stripe = new Stripe(testApiKey);
+ // Get default currency from Stripe
+ let stripe = new Stripe(testApiKey);
- let account = await stripe.accounts.retrieve();
+ let account = await stripe.accounts.retrieve();
- if (nullish(defaultCurrency) && nullish(account.default_currency)) {
- throw new RecaseError({
- message: "Default currency not set",
- code: ErrCode.StripeKeyInvalid,
- statusCode: 500,
- });
- } else if (nullish(defaultCurrency)) {
- defaultCurrency = account.default_currency;
- }
- } catch (error: any) {
- // console.error("Error checking stripe keys", error);
- throw new RecaseError({
- message: error.message || "Invalid Stripe API keys",
- code: ErrCode.StripeKeyInvalid,
- statusCode: 500,
- data: error,
- });
- }
- // 2. Create webhook endpoint
- let testWebhook: Stripe.WebhookEndpoint;
- let liveWebhook: Stripe.WebhookEndpoint;
- try {
- testWebhook = await createWebhookEndpoint(
- testApiKey,
- AppEnv.Sandbox,
- orgId
- );
- liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId);
- } catch (error) {
- throw new RecaseError({
- message: "Error creating stripe webhook",
- code: ErrCode.StripeKeyInvalid,
- statusCode: 500,
- data: error,
- });
- }
+ if (nullish(defaultCurrency) && nullish(account.default_currency)) {
+ throw new RecaseError({
+ message: "Default currency not set",
+ code: ErrCode.StripeKeyInvalid,
+ statusCode: 500,
+ });
+ } else if (nullish(defaultCurrency)) {
+ defaultCurrency = account.default_currency;
+ }
+ } catch (error: any) {
+ // console.error("Error checking stripe keys", error);
+ throw new RecaseError({
+ message: error.message || "Invalid Stripe API keys",
+ code: ErrCode.StripeKeyInvalid,
+ statusCode: 500,
+ data: error,
+ });
+ }
+ // 2. Create webhook endpoint
+ let testWebhook: Stripe.WebhookEndpoint;
+ let liveWebhook: Stripe.WebhookEndpoint;
+ try {
+ testWebhook = await createWebhookEndpoint(
+ testApiKey,
+ AppEnv.Sandbox,
+ orgId,
+ );
+ liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId);
+ } catch (error) {
+ throw new RecaseError({
+ message: "Error creating stripe webhook",
+ code: ErrCode.StripeKeyInvalid,
+ statusCode: 500,
+ data: error,
+ });
+ }
- return {
- defaultCurrency,
- stripeConfig: {
- test_api_key: encryptData(testApiKey),
- live_api_key: encryptData(liveApiKey),
- test_webhook_secret: encryptData(testWebhook.secret as string),
- live_webhook_secret: encryptData(liveWebhook.secret as string),
- success_url: successUrl,
- },
- };
+ return {
+ defaultCurrency,
+ stripeConfig: {
+ test_api_key: encryptData(testApiKey),
+ live_api_key: encryptData(liveApiKey),
+ test_webhook_secret: encryptData(testWebhook.secret as string),
+ live_webhook_secret: encryptData(liveWebhook.secret as string),
+ success_url: successUrl,
+ },
+ };
};
const connectStripeBody = z.object({
- secret_key: z.string().optional(),
- success_url: z.string().optional(),
- default_currency: z.string().optional(),
+ secret_key: z.string().optional(),
+ success_url: z.string().optional(),
+ default_currency: z.string().optional(),
});
export const handleConnectStripe = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "connect stripe",
+ routeHandler({
+ req,
+ res,
+ action: "connect stripe",
- handler: async (req: any, res: any) => {
- // 1. Get body
- const { secret_key, success_url, default_currency } =
- connectStripeBody.parse(req.body);
+ handler: async (req: any, res: any) => {
+ // 1. Get body
+ const { secret_key, success_url, default_currency } =
+ connectStripeBody.parse(req.body);
- if (!secret_key && !success_url && !default_currency) {
- throw new RecaseError({
- message: "Missing required fields",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!secret_key && !success_url && !default_currency) {
+ throw new RecaseError({
+ message: "Missing required fields",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- // 2. If secret_key present, but stripe not disconnected, throw an error
- if (secret_key && isStripeConnected({ org: req.org, env: req.env })) {
- throw new RecaseError({
- message:
- "Please disconnect Stripe before connecting a new secret key",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ // 2. If secret_key present, but stripe not disconnected, throw an error
+ if (secret_key && isStripeConnected({ org: req.org, env: req.env })) {
+ throw new RecaseError({
+ message:
+ "Please disconnect Stripe before connecting a new secret key",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (success_url) {
- if (
- !success_url.startsWith("http://") &&
- !success_url.startsWith("https://")
- ) {
- throw new RecaseError({
- message: `Success URL should start with http:// or https://, instead got ${success_url}`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- }
+ if (success_url) {
+ if (
+ !success_url.startsWith("http://") &&
+ !success_url.startsWith("https://")
+ ) {
+ throw new RecaseError({
+ message: `Success URL should start with http:// or https://, instead got ${success_url}`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ }
- const { logger } = req;
+ const { logger } = req;
- logger.info(`Connecting stripe for org ${req.org.slug}, ENV: ${req.env}`);
+ logger.info(`Connecting stripe for org ${req.org.slug}, ENV: ${req.env}`);
- if (!isStripeConnected({ org: req.org, env: req.env }) && !secret_key) {
- throw new RecaseError({
- message: "Please provide your stripe secret key",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!isStripeConnected({ org: req.org, env: req.env }) && !secret_key) {
+ throw new RecaseError({
+ message: "Please provide your stripe secret key",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- const curOrg = structuredClone(req.org);
- const isSandbox = req.env === AppEnv.Sandbox;
- const curDefaultCurrency = curOrg.default_currency;
+ const curOrg = structuredClone(req.org);
+ const isSandbox = req.env === AppEnv.Sandbox;
+ const curDefaultCurrency = curOrg.default_currency;
- // 1. Reconnect stripe
- let updates: any = {};
- if (secret_key) {
- const result = await connectStripe({
- orgId: req.orgId,
- apiKey: secret_key!,
- env: req.env,
- });
+ // 1. Reconnect stripe
+ let updates: any = {};
+ if (secret_key) {
+ const result = await connectStripe({
+ orgId: req.orgId,
+ apiKey: secret_key!,
+ env: req.env,
+ });
- logger.info(`Created new stripe connection`);
+ logger.info(`Created new stripe connection`);
- updates = {
- stripe_config: {
- ...curOrg.stripe_config,
- },
- default_currency: nullish(curDefaultCurrency)
- ? result.defaultCurrency
- : undefined,
- };
+ updates = {
+ stripe_config: {
+ ...curOrg.stripe_config,
+ },
+ default_currency: nullish(curDefaultCurrency)
+ ? result.defaultCurrency
+ : undefined,
+ };
- if (isSandbox) {
- updates.stripe_config.test_api_key = result.test_api_key;
- updates.stripe_config.test_webhook_secret =
- result.test_webhook_secret;
- } else {
- updates.stripe_config.live_api_key = result.live_api_key;
- updates.stripe_config.live_webhook_secret =
- result.live_webhook_secret;
- }
- }
+ if (isSandbox) {
+ updates.stripe_config.test_api_key = result.test_api_key;
+ updates.stripe_config.test_webhook_secret =
+ result.test_webhook_secret;
+ } else {
+ updates.stripe_config.live_api_key = result.live_api_key;
+ updates.stripe_config.live_webhook_secret =
+ result.live_webhook_secret;
+ }
+ }
- // 2. If success url present, add it to the updates
+ // 2. If success url present, add it to the updates
- if (success_url !== undefined && success_url !== curOrg.success_url) {
- updates = {
- ...updates,
- stripe_config: {
- ...curOrg.stripe_config,
- ...(updates?.stripe_config || {}),
- },
- };
+ if (success_url !== undefined && success_url !== curOrg.success_url) {
+ updates = {
+ ...updates,
+ stripe_config: {
+ ...curOrg.stripe_config,
+ ...(updates?.stripe_config || {}),
+ },
+ };
- if (isSandbox) {
- updates.stripe_config.sandbox_success_url = success_url;
- } else {
- updates.stripe_config.success_url = success_url;
- }
+ if (isSandbox) {
+ updates.stripe_config.sandbox_success_url = success_url;
+ } else {
+ updates.stripe_config.success_url = success_url;
+ }
- logger.info(`Updated success URL to ${success_url}`);
- }
+ logger.info(`Updated success URL to ${success_url}`);
+ }
- // 3. Default currency
- if (default_currency && default_currency !== curOrg.default_currency) {
- updates = {
- ...updates,
- default_currency: default_currency,
- };
+ // 3. Default currency
+ if (default_currency && default_currency !== curOrg.default_currency) {
+ updates = {
+ ...updates,
+ default_currency: default_currency,
+ };
- logger.info(`Updated default currency to ${default_currency}`);
- }
+ logger.info(`Updated default currency to ${default_currency}`);
+ }
- const newOrg = await OrgService.update({
- db: req.db,
- orgId: req.orgId,
- updates: updates,
- });
+ const newOrg = await OrgService.update({
+ db: req.db,
+ orgId: req.orgId,
+ updates: updates,
+ });
- // Initialize stripe prices...
- await ensureStripeProductsWithEnv({
- db: req.db,
- logger: req.logger,
- req,
- org: newOrg!,
- env: req.env,
- });
+ // Initialize stripe prices...
+ await ensureStripeProductsWithEnv({
+ db: req.db,
+ logger: req.logger,
+ req,
+ org: newOrg!,
+ env: req.env,
+ });
- res.status(200).json({
- message: "Stripe connected",
- });
- },
- });
+ res.status(200).json({
+ message: "Stripe connected",
+ });
+ },
+ });
diff --git a/server/src/internal/orgs/handlers/handleDeleteOrg.ts b/server/src/internal/orgs/handlers/handleDeleteOrg.ts
index 6726d68c0..1fc973337 100644
--- a/server/src/internal/orgs/handlers/handleDeleteOrg.ts
+++ b/server/src/internal/orgs/handlers/handleDeleteOrg.ts
@@ -7,101 +7,101 @@ import { Response } from "express";
import { deleteStripeWebhook } from "../orgUtils.js";
const deleteSvixWebhooks = async ({
- org,
- logger,
+ org,
+ logger,
}: {
- org: Organization;
- logger: any;
+ org: Organization;
+ logger: any;
}) => {
- const batch = [];
- if (org.svix_config?.sandbox_app_id) {
- batch.push(
- deleteSvixApp({
- appId: org.svix_config.sandbox_app_id,
- })
- );
- }
+ const batch = [];
+ if (org.svix_config?.sandbox_app_id) {
+ batch.push(
+ deleteSvixApp({
+ appId: org.svix_config.sandbox_app_id,
+ }),
+ );
+ }
- if (org.svix_config?.live_app_id) {
- batch.push(
- deleteSvixApp({
- appId: org.svix_config.live_app_id,
- })
- );
- }
+ if (org.svix_config?.live_app_id) {
+ batch.push(
+ deleteSvixApp({
+ appId: org.svix_config.live_app_id,
+ }),
+ );
+ }
- try {
- await Promise.all(batch);
- } catch (error) {
- logger.error(`Failed to delete svix webhooks for ${org.id}, ${org.slug}`);
- }
+ try {
+ await Promise.all(batch);
+ } catch (error) {
+ logger.error(`Failed to delete svix webhooks for ${org.id}, ${org.slug}`);
+ }
};
const deleteStripeWebhooks = async ({
- org,
- logger,
+ org,
+ logger,
}: {
- org: Organization;
- logger: any;
+ org: Organization;
+ logger: any;
}) => {
- try {
- await deleteStripeWebhook({
- org: org,
- env: AppEnv.Sandbox,
- });
+ try {
+ await deleteStripeWebhook({
+ org: org,
+ env: AppEnv.Sandbox,
+ });
- await deleteStripeWebhook({
- org: org,
- env: AppEnv.Live,
- });
- } catch (error: any) {
- logger.error(
- `Failed to delete stripe webhooks for ${org.id}, ${org.slug}. ${error.message})`
- );
- }
+ await deleteStripeWebhook({
+ org: org,
+ env: AppEnv.Live,
+ });
+ } catch (error: any) {
+ logger.error(
+ `Failed to delete stripe webhooks for ${org.id}, ${org.slug}. ${error.message})`,
+ );
+ }
};
export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => {
- try {
- const { org, db, logtail: logger } = req;
+ try {
+ const { org, db, logtail: logger } = req;
- // 1. Check if any customers
- let hasCustomers = await db.query.customers.findFirst({
- where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)),
- });
+ // 1. Check if any customers
+ let hasCustomers = await db.query.customers.findFirst({
+ where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)),
+ });
- if (hasCustomers)
- throw new RecaseError({
- message: "Cannot delete org with production mode customers",
- code: ErrCode.OrgHasCustomers,
- statusCode: 400,
- });
+ if (hasCustomers)
+ throw new RecaseError({
+ message: "Cannot delete org with production mode customers",
+ code: ErrCode.OrgHasCustomers,
+ statusCode: 400,
+ });
- // 2. Delete svix webhooks
- logger.info("1. Deleting svix webhooks");
- await deleteSvixWebhooks({ org, logger });
+ // 2. Delete svix webhooks
+ logger.info("1. Deleting svix webhooks");
+ await deleteSvixWebhooks({ org, logger });
- // 3. Delete stripe webhooks
- logger.info("2. Deleting stripe webhooks");
- await deleteStripeWebhooks({ org, logger });
+ // 3. Delete stripe webhooks
+ logger.info("2. Deleting stripe webhooks");
+ await deleteStripeWebhooks({ org, logger });
- // 4. Delete all sandbox customers
- logger.info("3. Deleting sandbox customers");
- await db
- .delete(customers)
- .where(
- and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox))
- );
+ // 4. Delete all sandbox customers
+ logger.info("3. Deleting sandbox customers");
+ await db
+ .delete(customers)
+ .where(
+ and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
+ );
- res.status(200).json({
- message: "Org deleted",
- });
- } catch (error) {
- handleFrontendReqError({
- res,
- error,
- req,
- action: "delete-org",
- });
- }
+ res.status(200).json({
+ message: "Org deleted",
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ res,
+ error,
+ req,
+ action: "delete-org",
+ });
+ }
};
diff --git a/server/src/internal/orgs/handlers/handleDeleteStripe.ts b/server/src/internal/orgs/handlers/handleDeleteStripe.ts
index f495dc9ed..f14781d95 100644
--- a/server/src/internal/orgs/handlers/handleDeleteStripe.ts
+++ b/server/src/internal/orgs/handlers/handleDeleteStripe.ts
@@ -6,64 +6,64 @@ import { AppEnv, Organization } from "@autumn/shared";
import { isStripeConnected } from "../orgUtils.js";
export const disconnectStripe = async ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- if (isStripeConnected({ org, env })) {
- const stripeCli = createStripeCli({ org, env });
- const webhooks = await stripeCli.webhookEndpoints.list();
- for (const webhook of webhooks.data) {
- if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
- await stripeCli.webhookEndpoints.del(webhook.id);
- }
- }
- }
+ if (isStripeConnected({ org, env })) {
+ const stripeCli = createStripeCli({ org, env });
+ const webhooks = await stripeCli.webhookEndpoints.list();
+ for (const webhook of webhooks.data) {
+ if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
+ await stripeCli.webhookEndpoints.del(webhook.id);
+ }
+ }
+ }
};
export const handleDeleteStripe = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "delete stripe",
- handler: async (req: any, res: any) => {
- const org = await OrgService.getFromReq(req);
+ routeHandler({
+ req,
+ res,
+ action: "delete stripe",
+ handler: async (req: any, res: any) => {
+ const org = await OrgService.getFromReq(req);
- let { db, orgId, logtail: logger } = req;
- await clearOrgCache({
- db,
- orgId,
- logger,
- });
+ let { db, orgId, logtail: logger } = req;
+ await clearOrgCache({
+ db,
+ orgId,
+ logger,
+ });
- try {
- await disconnectStripe({ org, env: req.env });
- } catch (error) {
- logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, {
- error,
- });
- }
+ try {
+ await disconnectStripe({ org, env: req.env });
+ } catch (error) {
+ logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, {
+ error,
+ });
+ }
- // Update stripe config:
- const newStripeConfig = structuredClone(req.org.stripe_config);
- if (req.env === AppEnv.Sandbox) {
- newStripeConfig.test_api_key = null;
- } else {
- newStripeConfig.live_api_key = null;
- }
+ // Update stripe config:
+ const newStripeConfig = structuredClone(req.org.stripe_config);
+ if (req.env === AppEnv.Sandbox) {
+ newStripeConfig.test_api_key = null;
+ } else {
+ newStripeConfig.live_api_key = null;
+ }
- await OrgService.update({
- db,
- orgId: req.orgId,
- updates: {
- stripe_config: newStripeConfig,
- },
- });
+ await OrgService.update({
+ db,
+ orgId: req.orgId,
+ updates: {
+ stripe_config: newStripeConfig,
+ },
+ });
- res.status(200).json({
- message: "Stripe disconnected",
- });
- },
- });
+ res.status(200).json({
+ message: "Stripe disconnected",
+ });
+ },
+ });
diff --git a/server/src/internal/orgs/handlers/handleGetInvites.ts b/server/src/internal/orgs/handlers/handleGetInvites.ts
index 5a89b2f86..bdf9c5c4c 100644
--- a/server/src/internal/orgs/handlers/handleGetInvites.ts
+++ b/server/src/internal/orgs/handlers/handleGetInvites.ts
@@ -3,56 +3,56 @@ import { invitation, user as userTable } from "@autumn/shared";
import { and, eq, gt } from "drizzle-orm";
export const handleGetInvites = async (
- req: ExtendedRequest,
- res: ExtendedResponse
+ req: ExtendedRequest,
+ res: ExtendedResponse,
) => {
- try {
- const { userId, db } = req;
+ try {
+ const { userId, db } = req;
- const user = await db.query.user.findFirst({
- where: eq(userTable.id, userId ?? ""),
- });
+ const user = await db.query.user.findFirst({
+ where: eq(userTable.id, userId ?? ""),
+ });
- const invites = await db.query.invitation.findMany({
- where: and(
- eq(invitation.status, "pending"),
- eq(invitation.email, user?.email ?? ""),
- gt(invitation.expiresAt, new Date())
- ),
- with: {
- inviter: true,
- organization: true,
- },
- });
+ const invites = await db.query.invitation.findMany({
+ where: and(
+ eq(invitation.status, "pending"),
+ eq(invitation.email, user?.email ?? ""),
+ gt(invitation.expiresAt, new Date()),
+ ),
+ with: {
+ inviter: true,
+ organization: true,
+ },
+ });
- // const joinRequests = await db
- // .select({
- // id: orgJoinRequests.id,
- // organizationId: orgJoinRequests.organizationId,
- // organizationName: organizations.name,
- // role: orgJoinRequests.role,
- // status: orgJoinRequests.status,
- // createdAt: orgJoinRequests.createdAt,
- // inviterName: user.name,
- // inviterEmail: user.email,
- // })
- // .from(orgJoinRequests)
- // .innerJoin(
- // organizations,
- // eq(orgJoinRequests.organizationId, organizations.id)
- // )
- // .innerJoin(user, eq(orgJoinRequests.inviterId, user.id))
- // .where(
- // and(
- // eq(orgJoinRequests.userId, userId),
- // eq(orgJoinRequests.status, "pending")
- // )
- // )
- // .orderBy(orgJoinRequests.createdAt);
+ // const joinRequests = await db
+ // .select({
+ // id: orgJoinRequests.id,
+ // organizationId: orgJoinRequests.organizationId,
+ // organizationName: organizations.name,
+ // role: orgJoinRequests.role,
+ // status: orgJoinRequests.status,
+ // createdAt: orgJoinRequests.createdAt,
+ // inviterName: user.name,
+ // inviterEmail: user.email,
+ // })
+ // .from(orgJoinRequests)
+ // .innerJoin(
+ // organizations,
+ // eq(orgJoinRequests.organizationId, organizations.id)
+ // )
+ // .innerJoin(user, eq(orgJoinRequests.inviterId, user.id))
+ // .where(
+ // and(
+ // eq(orgJoinRequests.userId, userId),
+ // eq(orgJoinRequests.status, "pending")
+ // )
+ // )
+ // .orderBy(orgJoinRequests.createdAt);
- res.status(200).json({ invites });
- } catch (error) {
- console.error("Error fetching join requests:", error);
- res.status(500).json({ message: "Internal server error" });
- }
+ res.status(200).json({ invites });
+ } catch (error) {
+ console.error("Error fetching join requests:", error);
+ res.status(500).json({ message: "Internal server error" });
+ }
};
diff --git a/server/src/internal/orgs/handlers/handleGetOrg.ts b/server/src/internal/orgs/handlers/handleGetOrg.ts
index 23c6b81c6..c2746bf3a 100644
--- a/server/src/internal/orgs/handlers/handleGetOrg.ts
+++ b/server/src/internal/orgs/handlers/handleGetOrg.ts
@@ -4,20 +4,20 @@ import { createOrgResponse } from "../orgUtils.js";
import { OrgService } from "../OrgService.js";
export const handleGetOrg = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "get org",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- if (!req.orgId) {
- res.status(400).json({
- message: "Missing orgId",
- });
- return;
- }
+ routeHandler({
+ req,
+ res,
+ action: "get org",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ if (!req.orgId) {
+ res.status(400).json({
+ message: "Missing orgId",
+ });
+ return;
+ }
- const org = await OrgService.getFromReq(req);
+ const org = await OrgService.getFromReq(req);
- res.status(200).json(createOrgResponse({ org, env: req.env }));
- },
- });
+ res.status(200).json(createOrgResponse({ org, env: req.env }));
+ },
+ });
diff --git a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts b/server/src/internal/orgs/handlers/handleGetOrgMembers.ts
index 18f552d6e..b7c3c720b 100644
--- a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts
+++ b/server/src/internal/orgs/handlers/handleGetOrgMembers.ts
@@ -5,95 +5,95 @@ import { eq, and } from "drizzle-orm";
import { member, session as authSession } from "@autumn/shared";
export const handleGetOrgMembers = async (req: any, res: any) => {
- try {
- const { org, db } = req;
- const orgId = org.id;
+ try {
+ const { org, db } = req;
+ const orgId = org.id;
- const memberships = await OrgService.getMembers({ db, orgId });
- const invites = await OrgService.getInvites({ db, orgId });
+ const memberships = await OrgService.getMembers({ db, orgId });
+ const invites = await OrgService.getInvites({ db, orgId });
- res.status(200).json({
- memberships,
- invites,
- });
- } catch (error) {
- console.error("Error getting org members:", error);
- handleFrontendReqError({
- req,
- error,
- res,
- action: "get org members",
- });
- }
+ res.status(200).json({
+ memberships,
+ invites,
+ });
+ } catch (error) {
+ console.error("Error getting org members:", error);
+ handleFrontendReqError({
+ req,
+ error,
+ res,
+ action: "get org members",
+ });
+ }
};
export const handleRemoveMember = async (req: any, res: any) => {
- try {
- const { org, db } = req;
- const { memberId, userId } = req.body;
- const orgId = org.id;
+ try {
+ const { org, db } = req;
+ const { memberId, userId } = req.body;
+ const orgId = org.id;
- // First check if the member exists by memberId
- let existingMember = await db.query.member.findFirst({
- where: and(eq(member.id, memberId), eq(member.organizationId, orgId)),
- });
+ // First check if the member exists by memberId
+ let existingMember = await db.query.member.findFirst({
+ where: and(eq(member.id, memberId), eq(member.organizationId, orgId)),
+ });
- if (!existingMember) {
- // Try to find by userId as fallback
- const memberByUserId = await db.query.member.findFirst({
- where: and(eq(member.userId, userId), eq(member.organizationId, orgId)),
- });
+ if (!existingMember) {
+ // Try to find by userId as fallback
+ const memberByUserId = await db.query.member.findFirst({
+ where: and(eq(member.userId, userId), eq(member.organizationId, orgId)),
+ });
- if (!memberByUserId) {
- console.warn(
- `Member not found by userId either: userId=${userId}, orgId=${orgId}`
- );
- return res.status(404).json({
- message: "Member not found in this organization",
- code: "MEMBER_NOT_FOUND",
- });
- }
+ if (!memberByUserId) {
+ console.warn(
+ `Member not found by userId either: userId=${userId}, orgId=${orgId}`,
+ );
+ return res.status(404).json({
+ message: "Member not found in this organization",
+ code: "MEMBER_NOT_FOUND",
+ });
+ }
- // Use the member found by userId
- existingMember = memberByUserId;
- }
+ // Use the member found by userId
+ existingMember = memberByUserId;
+ }
- // Remove member from database using the found member
- await db
- .delete(member)
- .where(
- and(eq(member.id, existingMember.id), eq(member.organizationId, orgId))
- );
+ // Remove member from database using the found member
+ await db
+ .delete(member)
+ .where(
+ and(eq(member.id, existingMember.id), eq(member.organizationId, orgId)),
+ );
- // Revoke all sessions for this user in this organization
- try {
- await db
- .delete(authSession)
- .where(
- and(
- eq(authSession.userId, existingMember.userId),
- eq(authSession.activeOrganizationId, orgId)
- )
- );
- } catch (error) {
- // Log but don't fail the request if session revocation fails
- req.logtail?.warn(
- `Failed to revoke sessions for user ${existingMember.userId} in org ${orgId}:`,
- error
- );
- console.warn("Session revocation failed:", error);
- }
+ // Revoke all sessions for this user in this organization
+ try {
+ await db
+ .delete(authSession)
+ .where(
+ and(
+ eq(authSession.userId, existingMember.userId),
+ eq(authSession.activeOrganizationId, orgId),
+ ),
+ );
+ } catch (error) {
+ // Log but don't fail the request if session revocation fails
+ req.logtail?.warn(
+ `Failed to revoke sessions for user ${existingMember.userId} in org ${orgId}:`,
+ error,
+ );
+ console.warn("Session revocation failed:", error);
+ }
- res.status(200).json({
- message: "Member removed successfully",
- });
- } catch (error) {
- console.error("Error in handleRemoveMember:", error);
- handleFrontendReqError({
- req,
- error,
- res,
- action: "remove member",
- });
- }
+ res.status(200).json({
+ message: "Member removed successfully",
+ });
+ } catch (error) {
+ console.error("Error in handleRemoveMember:", error);
+ handleFrontendReqError({
+ req,
+ error,
+ res,
+ action: "remove member",
+ });
+ }
};
diff --git a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
index ad24855ef..73683f28c 100644
--- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
+++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts
@@ -4,29 +4,29 @@ import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
import { ErrCode } from "@autumn/shared";
export const handleGetUploadUrl = async (req: any, res: any) => {
- try {
- const { org } = req;
+ try {
+ const { org } = req;
- let path = `logo/${org.id}`;
+ let path = `logo/${org.id}`;
- if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
- logger.warn("Supabase storage not set up");
- res.status(400).json({
- message: "Supabase storage not set up",
- code: ErrCode.SupabaseNotFound,
- });
- return;
- }
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
+ logger.warn("Supabase storage not set up");
+ res.status(400).json({
+ message: "Supabase storage not set up",
+ code: ErrCode.SupabaseNotFound,
+ });
+ return;
+ }
- const data = await getUploadUrl({ path });
+ const data = await getUploadUrl({ path });
- res.status(200).json(data);
- } catch (error) {
- handleFrontendReqError({
- req,
- error,
- res,
- action: "get upload url",
- });
- }
+ res.status(200).json(data);
+ } catch (error) {
+ handleFrontendReqError({
+ req,
+ error,
+ res,
+ action: "get upload url",
+ });
+ }
};
diff --git a/server/src/internal/orgs/handlers/handlePostOrg.ts b/server/src/internal/orgs/handlers/handlePostOrg.ts
index f54e43fc8..4518092ac 100644
--- a/server/src/internal/orgs/handlers/handlePostOrg.ts
+++ b/server/src/internal/orgs/handlers/handlePostOrg.ts
@@ -2,58 +2,58 @@ import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
export const handlePostOrg = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "create org",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { userId, db } = req;
+ routeHandler({
+ req,
+ res,
+ action: "create org",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { userId, db } = req;
- console.log("userId", userId);
+ console.log("userId", userId);
- // const userMemberships = await auth.api.({
- // userId: userId!,
- // });
+ // const userMemberships = await auth.api.({
+ // userId: userId!,
+ // });
- // const clerk = createClerkCli();
- // const user = await clerk.users.getUser(userId!);
+ // const clerk = createClerkCli();
+ // const user = await clerk.users.getUser(userId!);
- // let userMemberships = await clerk.users.getOrganizationMembershipList({
- // userId: userId!,
- // });
+ // let userMemberships = await clerk.users.getOrganizationMembershipList({
+ // userId: userId!,
+ // });
- // let org;
+ // let org;
- // if (userMemberships.data.length === 0) {
- // org = await clerk.organizations.createOrganization({
- // name: `${user.firstName}'s Org`,
- // });
+ // if (userMemberships.data.length === 0) {
+ // org = await clerk.organizations.createOrganization({
+ // name: `${user.firstName}'s Org`,
+ // });
- // // 2. Create org membership for user
- // await clerk.organizations.createOrganizationMembership({
- // organizationId: org.id,
- // userId: userId!,
- // role: "org:admin",
- // });
+ // // 2. Create org membership for user
+ // await clerk.organizations.createOrganizationMembership({
+ // organizationId: org.id,
+ // userId: userId!,
+ // role: "org:admin",
+ // });
- // await saveOrgToDB({
- // db,
- // id: org.id,
- // slug: org.slug,
- // });
+ // await saveOrgToDB({
+ // db,
+ // id: org.id,
+ // slug: org.slug,
+ // });
- // console.log(`Created new org: ${org.id} (${org.slug})`);
- // } else {
- // org = userMemberships.data[0].organization;
- // }
+ // console.log(`Created new org: ${org.id} (${org.slug})`);
+ // } else {
+ // org = userMemberships.data[0].organization;
+ // }
- res.status(200).json({
- id: "123",
- slug: "123",
- });
- // res.status(200).json({
- // id: org.id,
- // slug: org.slug,
- // });
- },
- });
+ res.status(200).json({
+ id: "123",
+ slug: "123",
+ });
+ // res.status(200).json({
+ // id: org.id,
+ // slug: org.slug,
+ // });
+ },
+ });
diff --git a/server/src/internal/orgs/onboarding/createOnboardingProducts.ts b/server/src/internal/orgs/onboarding/createOnboardingProducts.ts
index 6807a8f0f..4a7a3be12 100644
--- a/server/src/internal/orgs/onboarding/createOnboardingProducts.ts
+++ b/server/src/internal/orgs/onboarding/createOnboardingProducts.ts
@@ -1,161 +1,160 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { generateId, keyToTitle } from "@/utils/genUtils.js";
import {
- FeatureType,
- AggregateType,
- FeatureUsageType,
- EntInterval,
- AllowanceType,
- PriceType,
- BillingInterval,
-
- // DB Models
- entitlements,
- prices,
- features,
- products,
+ FeatureType,
+ AggregateType,
+ FeatureUsageType,
+ EntInterval,
+ AllowanceType,
+ PriceType,
+ BillingInterval,
+ // DB Models
+ entitlements,
+ prices,
+ features,
+ products,
} from "@autumn/shared";
import { AppEnv } from "autumn-js";
const defaultFeatures = [
- {
- internal_id: "",
- id: "pro_analytics",
- type: FeatureType.Boolean,
- display: {
- singular: "pro analytics",
- plural: "pro analytics",
- },
- },
- {
- internal_id: "",
- id: "chat_messages",
- type: FeatureType.Metered,
- config: {
- filters: [
- {
- value: ["chat_messages"],
- property: "",
- operator: "",
- },
- ],
- aggregate: {
- type: AggregateType.Count,
- },
- usage_type: FeatureUsageType.Single,
- display: {
- singular: "chat message",
- plural: "chat messages",
- },
- },
- },
+ {
+ internal_id: "",
+ id: "pro_analytics",
+ type: FeatureType.Boolean,
+ display: {
+ singular: "pro analytics",
+ plural: "pro analytics",
+ },
+ },
+ {
+ internal_id: "",
+ id: "chat_messages",
+ type: FeatureType.Metered,
+ config: {
+ filters: [
+ {
+ value: ["chat_messages"],
+ property: "",
+ operator: "",
+ },
+ ],
+ aggregate: {
+ type: AggregateType.Count,
+ },
+ usage_type: FeatureUsageType.Single,
+ display: {
+ singular: "chat message",
+ plural: "chat messages",
+ },
+ },
+ },
];
export const createOnboardingProducts = async ({
- db,
- orgId,
+ db,
+ orgId,
}: {
- db: DrizzleCli;
- orgId: string;
+ db: DrizzleCli;
+ orgId: string;
}) => {
- const env = AppEnv.Sandbox;
- const insertedFeatures = defaultFeatures.map((f) => ({
- ...f,
- org_id: orgId,
- env,
- internal_id: generateId("fe"),
- name: keyToTitle(f.id),
- created_at: Date.now(),
- }));
+ const env = AppEnv.Sandbox;
+ const insertedFeatures = defaultFeatures.map((f) => ({
+ ...f,
+ org_id: orgId,
+ env,
+ internal_id: generateId("fe"),
+ name: keyToTitle(f.id),
+ created_at: Date.now(),
+ }));
- await db.insert(features).values(insertedFeatures as any);
+ await db.insert(features).values(insertedFeatures as any);
- const defaultProducts = [
- {
- id: "free_example",
- name: "Free (Example)",
- env: AppEnv.Sandbox,
- is_default: true,
- entitlements: [
- {
- internal_feature_id: insertedFeatures[1].internal_id,
- feature_id: insertedFeatures[1].id,
- allowance: 10,
- interval: EntInterval.Month,
- allowance_type: AllowanceType.Fixed,
- },
- ],
- prices: [],
- },
- {
- id: "pro_example",
- name: "Pro (Example)",
- env: AppEnv.Sandbox,
- is_default: false,
- entitlements: [
- {
- internal_feature_id: insertedFeatures[0].internal_id,
- feature_id: insertedFeatures[0].id,
- },
- {
- internal_feature_id: insertedFeatures[1].internal_id,
- feature_id: insertedFeatures[1].id,
- allowance_type: AllowanceType.Unlimited,
- },
- ],
- prices: [
- {
- name: "Monthly",
- config: {
- type: PriceType.Fixed,
- amount: 20.5,
- interval: BillingInterval.Month,
- },
- },
- ],
- },
- ];
+ const defaultProducts = [
+ {
+ id: "free_example",
+ name: "Free (Example)",
+ env: AppEnv.Sandbox,
+ is_default: true,
+ entitlements: [
+ {
+ internal_feature_id: insertedFeatures[1].internal_id,
+ feature_id: insertedFeatures[1].id,
+ allowance: 10,
+ interval: EntInterval.Month,
+ allowance_type: AllowanceType.Fixed,
+ },
+ ],
+ prices: [],
+ },
+ {
+ id: "pro_example",
+ name: "Pro (Example)",
+ env: AppEnv.Sandbox,
+ is_default: false,
+ entitlements: [
+ {
+ internal_feature_id: insertedFeatures[0].internal_id,
+ feature_id: insertedFeatures[0].id,
+ },
+ {
+ internal_feature_id: insertedFeatures[1].internal_id,
+ feature_id: insertedFeatures[1].id,
+ allowance_type: AllowanceType.Unlimited,
+ },
+ ],
+ prices: [
+ {
+ name: "Monthly",
+ config: {
+ type: PriceType.Fixed,
+ amount: 20.5,
+ interval: BillingInterval.Month,
+ },
+ },
+ ],
+ },
+ ];
- const batchInsert = [];
- for (const product of defaultProducts) {
- const insertProduct = async (product: any) => {
- let internalProductId = generateId("pr");
+ const batchInsert = [];
+ for (const product of defaultProducts) {
+ const insertProduct = async (product: any) => {
+ let internalProductId = generateId("pr");
- await db.insert(products).values({
- ...product,
- internal_id: internalProductId,
- org_id: orgId,
- env,
- group: "",
- is_add_on: false,
- created_at: Date.now(),
- version: 1,
- });
+ await db.insert(products).values({
+ ...product,
+ internal_id: internalProductId,
+ org_id: orgId,
+ env,
+ group: "",
+ is_add_on: false,
+ created_at: Date.now(),
+ version: 1,
+ });
- for (const entitlement of product.entitlements) {
- await db.insert(entitlements).values({
- ...entitlement,
- id: generateId("en"),
- org_id: orgId,
- env,
- created_at: Date.now(),
- });
- }
+ for (const entitlement of product.entitlements) {
+ await db.insert(entitlements).values({
+ ...entitlement,
+ id: generateId("en"),
+ org_id: orgId,
+ env,
+ created_at: Date.now(),
+ });
+ }
- for (const price of product.prices) {
- await db.insert(prices).values({
- ...price,
- id: generateId("pr"),
- internal_product_id: internalProductId,
- created_at: Date.now(),
- org_id: orgId,
- });
- }
- };
+ for (const price of product.prices) {
+ await db.insert(prices).values({
+ ...price,
+ id: generateId("pr"),
+ internal_product_id: internalProductId,
+ created_at: Date.now(),
+ org_id: orgId,
+ });
+ }
+ };
- batchInsert.push(insertProduct(product));
- }
+ batchInsert.push(insertProduct(product));
+ }
- await Promise.all(batchInsert);
+ await Promise.all(batchInsert);
};
diff --git a/server/src/internal/orgs/onboarding/onboardingRouter.ts b/server/src/internal/orgs/onboarding/onboardingRouter.ts
index 10323a758..931350a80 100644
--- a/server/src/internal/orgs/onboarding/onboardingRouter.ts
+++ b/server/src/internal/orgs/onboarding/onboardingRouter.ts
@@ -16,97 +16,97 @@ import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
export const onboardingRouter: Router = Router();
onboardingRouter.post("", async (req: Request, res: any) =>
- routeHandler({
- req,
- res,
- action: "onboarding",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { db, logtail: logger, org } = req;
- const { token } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "onboarding",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { db, logtail: logger, org } = req;
+ const { token } = req.body;
- if (!token) {
- throw new RecaseError({
- message: "No token provided",
- code: "no_token_provided",
- statusCode: 400,
- });
- }
+ if (!token) {
+ throw new RecaseError({
+ message: "No token provided",
+ code: "no_token_provided",
+ statusCode: 400,
+ });
+ }
- let chatResult = await db.query.chatResults.findFirst({
- where: eq(chatResults.id, token),
- });
+ let chatResult = await db.query.chatResults.findFirst({
+ where: eq(chatResults.id, token),
+ });
- if (!chatResult) {
- throw new RecaseError({
- message: `Chat result from token ${token} not found`,
- code: "chat_result_not_found",
- statusCode: 404,
- });
- }
+ if (!chatResult) {
+ throw new RecaseError({
+ message: `Chat result from token ${token} not found`,
+ code: "chat_result_not_found",
+ statusCode: 404,
+ });
+ }
- let curProducts = await ProductService.listFull({
- db,
- orgId: org.id,
- env: AppEnv.Sandbox,
- });
+ let curProducts = await ProductService.listFull({
+ db,
+ orgId: org.id,
+ env: AppEnv.Sandbox,
+ });
- let curFeatures = await FeatureService.list({
- db,
- orgId: org.id,
- env: AppEnv.Sandbox,
- });
+ let curFeatures = await FeatureService.list({
+ db,
+ orgId: org.id,
+ env: AppEnv.Sandbox,
+ });
- let newProducts = chatResult.data.products.filter((product) => {
- return !curProducts.some((p) => p.id === product.id);
- });
+ let newProducts = chatResult.data.products.filter((product) => {
+ return !curProducts.some((p) => p.id === product.id);
+ });
- let newFeatures = chatResult.data.features.filter((feature) => {
- return !curFeatures.some((f) => f.id === feature.id);
- });
+ let newFeatures = chatResult.data.features.filter((feature) => {
+ return !curFeatures.some((f) => f.id === feature.id);
+ });
- if (newFeatures.length > 0 || newProducts.length > 0) {
- let backendFeatures = parseChatResultFeatures({
- features: newFeatures,
- orgId: org.id,
- });
+ if (newFeatures.length > 0 || newProducts.length > 0) {
+ let backendFeatures = parseChatResultFeatures({
+ features: newFeatures,
+ orgId: org.id,
+ });
- let { products, prices, ents } = await parseChatProducts({
- db,
- logger,
- orgId: org.id,
- features: [...curFeatures, ...backendFeatures],
- chatProducts: newProducts,
- });
+ let { products, prices, ents } = await parseChatProducts({
+ db,
+ logger,
+ orgId: org.id,
+ features: [...curFeatures, ...backendFeatures],
+ chatProducts: newProducts,
+ });
- await Promise.all([
- FeatureService.insert({
- db,
- data: backendFeatures,
- logger,
- }),
- (async () => {
- for (const product of products) {
- await ProductService.insert({ db, product });
- }
- })(),
- ]);
+ await Promise.all([
+ FeatureService.insert({
+ db,
+ data: backendFeatures,
+ logger,
+ }),
+ (async () => {
+ for (const product of products) {
+ await ProductService.insert({ db, product });
+ }
+ })(),
+ ]);
- await EntitlementService.insert({
- db,
- data: ents,
- });
+ await EntitlementService.insert({
+ db,
+ data: ents,
+ });
- await PriceService.insert({
- db,
- data: prices,
- });
- }
+ await PriceService.insert({
+ db,
+ data: prices,
+ });
+ }
- res.status(200).json({
- org_id: org.id,
- feature_ids: chatResult.data.features.map((f) => f.id),
- product_ids: chatResult.data.products.map((p) => p.id),
- });
- },
- }),
+ res.status(200).json({
+ org_id: org.id,
+ feature_ids: chatResult.data.features.map((f) => f.id),
+ product_ids: chatResult.data.products.map((p) => p.id),
+ });
+ },
+ }),
);
diff --git a/server/src/internal/orgs/onboarding/parseChatFeatures.ts b/server/src/internal/orgs/onboarding/parseChatFeatures.ts
index fd79f5707..6a0259055 100644
--- a/server/src/internal/orgs/onboarding/parseChatFeatures.ts
+++ b/server/src/internal/orgs/onboarding/parseChatFeatures.ts
@@ -3,102 +3,102 @@ import { constructFeature } from "@/internal/features/utils/constructFeatureUtil
import RecaseError from "@/utils/errorUtils.js";
import { keyToTitle } from "@/utils/genUtils.js";
import {
- AggregateType,
- AppEnv,
- ChatFeatureCreditSchema,
- ChatResultFeature,
- FeatureType,
- FeatureUsageType,
- MeteredConfig,
+ AggregateType,
+ AppEnv,
+ ChatFeatureCreditSchema,
+ ChatResultFeature,
+ FeatureType,
+ FeatureUsageType,
+ MeteredConfig,
} from "@autumn/shared";
import { CreditSystemConfig } from "@autumn/shared";
const validateFeatures = (features: ChatResultFeature[]) => {
- features.forEach((feature) => {
- if (feature.type == "credit_system") {
- if (!feature.credit_schema) {
- throw new RecaseError({
- message: "Credit schema is required for credit system",
- code: "invalid_chat_feature",
- statusCode: 400,
- });
- } else {
- feature.credit_schema.forEach((item) => {
- if (!ChatFeatureCreditSchema.safeParse(item).success) {
- throw new RecaseError({
- message: "Invalid credit schema",
- code: "invalid_chat_feature",
- statusCode: 400,
- });
- }
+ features.forEach((feature) => {
+ if (feature.type == "credit_system") {
+ if (!feature.credit_schema) {
+ throw new RecaseError({
+ message: "Credit schema is required for credit system",
+ code: "invalid_chat_feature",
+ statusCode: 400,
+ });
+ } else {
+ feature.credit_schema.forEach((item) => {
+ if (!ChatFeatureCreditSchema.safeParse(item).success) {
+ throw new RecaseError({
+ message: "Invalid credit schema",
+ code: "invalid_chat_feature",
+ statusCode: 400,
+ });
+ }
- let meteredFeature = features.some(
- (m) => m.id == item.metered_feature_id && m.id != feature.id,
- );
- if (!meteredFeature) {
- throw new RecaseError({
- message: `Metered feature ${item.metered_feature_id} not found`,
- code: "invalid_chat_feature",
- statusCode: 400,
- });
- }
- });
- }
- }
- });
+ let meteredFeature = features.some(
+ (m) => m.id == item.metered_feature_id && m.id != feature.id,
+ );
+ if (!meteredFeature) {
+ throw new RecaseError({
+ message: `Metered feature ${item.metered_feature_id} not found`,
+ code: "invalid_chat_feature",
+ statusCode: 400,
+ });
+ }
+ });
+ }
+ }
+ });
};
export const parseChatResultFeatures = ({
- features,
- orgId,
+ features,
+ orgId,
}: {
- features: ChatResultFeature[];
- orgId: string;
+ features: ChatResultFeature[];
+ orgId: string;
}) => {
- validateFeatures(features);
+ validateFeatures(features);
- return features.map((feature) => {
- let type =
- feature.type == "boolean"
- ? FeatureType.Boolean
- : feature.type == "credit_system"
- ? FeatureType.CreditSystem
- : FeatureType.Metered;
+ return features.map((feature) => {
+ let type =
+ feature.type == "boolean"
+ ? FeatureType.Boolean
+ : feature.type == "credit_system"
+ ? FeatureType.CreditSystem
+ : FeatureType.Metered;
- let config: CreditSystemConfig | MeteredConfig | undefined = undefined;
- if (type == FeatureType.CreditSystem) {
- config = {
- schema: feature.credit_schema!.map((item) => ({
- feature_amount: 1,
- metered_feature_id: item.metered_feature_id,
- credit_amount: item.credit_cost,
- })),
- usage_type: FeatureUsageType.Single,
- };
- } else if (type == FeatureType.Metered) {
- config = validateMeteredConfig({
- usage_type: feature.type as FeatureUsageType,
- filters: [
- {
- property: "",
- operator: "",
- value: [],
- },
- ],
- aggregate: { type: AggregateType.Sum, property: "value" },
- });
- }
+ let config: CreditSystemConfig | MeteredConfig | undefined = undefined;
+ if (type == FeatureType.CreditSystem) {
+ config = {
+ schema: feature.credit_schema!.map((item) => ({
+ feature_amount: 1,
+ metered_feature_id: item.metered_feature_id,
+ credit_amount: item.credit_cost,
+ })),
+ usage_type: FeatureUsageType.Single,
+ };
+ } else if (type == FeatureType.Metered) {
+ config = validateMeteredConfig({
+ usage_type: feature.type as FeatureUsageType,
+ filters: [
+ {
+ property: "",
+ operator: "",
+ value: [],
+ },
+ ],
+ aggregate: { type: AggregateType.Sum, property: "value" },
+ });
+ }
- let backendFeat = constructFeature({
- id: feature.id,
- name: keyToTitle(feature.id),
- type,
- env: AppEnv.Sandbox,
- config,
- orgId: orgId,
- display: feature.display,
- });
+ let backendFeat = constructFeature({
+ id: feature.id,
+ name: keyToTitle(feature.id),
+ type,
+ env: AppEnv.Sandbox,
+ config,
+ orgId: orgId,
+ display: feature.display,
+ });
- return backendFeat;
- });
+ return backendFeat;
+ });
};
diff --git a/server/src/internal/orgs/onboarding/parseChatProducts.ts b/server/src/internal/orgs/onboarding/parseChatProducts.ts
index 9fa4cb79b..f75408bfd 100644
--- a/server/src/internal/orgs/onboarding/parseChatProducts.ts
+++ b/server/src/internal/orgs/onboarding/parseChatProducts.ts
@@ -2,64 +2,64 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { constructProduct } from "@/internal/products/productUtils.js";
import {
- AppEnv,
- CreateProductSchema,
- EntInsertSchema,
- Entitlement,
- Feature,
- Price,
- Product,
- ProductV2,
+ AppEnv,
+ CreateProductSchema,
+ EntInsertSchema,
+ Entitlement,
+ Feature,
+ Price,
+ Product,
+ ProductV2,
} from "@autumn/shared";
export const parseChatProducts = async ({
- db,
- logger,
- features,
- orgId,
- chatProducts,
+ db,
+ logger,
+ features,
+ orgId,
+ chatProducts,
}: {
- db: DrizzleCli;
- logger: any;
- features: Feature[];
- orgId: string;
- chatProducts: ProductV2[];
+ db: DrizzleCli;
+ logger: any;
+ features: Feature[];
+ orgId: string;
+ chatProducts: ProductV2[];
}) => {
- let products: Product[] = [];
+ let products: Product[] = [];
- let allPrices: Price[] = [];
- let allEnts: Entitlement[] = [];
+ let allPrices: Price[] = [];
+ let allEnts: Entitlement[] = [];
- for (const product of chatProducts) {
- let backendProduct: Product = constructProduct({
- productData: CreateProductSchema.parse({
- ...product,
- }),
- orgId,
- env: AppEnv.Sandbox,
- });
+ for (const product of chatProducts) {
+ let backendProduct: Product = constructProduct({
+ productData: CreateProductSchema.parse({
+ ...product,
+ }),
+ orgId,
+ env: AppEnv.Sandbox,
+ });
- let { prices, entitlements } = await handleNewProductItems({
- db,
- curPrices: [],
- curEnts: [],
- newItems: product.items,
- product: backendProduct,
- features,
- saveToDb: false,
- isCustom: false,
- logger,
- });
+ let { prices, entitlements } = await handleNewProductItems({
+ db,
+ curPrices: [],
+ curEnts: [],
+ newItems: product.items,
+ product: backendProduct,
+ features,
+ saveToDb: false,
+ isCustom: false,
+ logger,
+ });
- products.push(backendProduct);
- allPrices.push(...prices);
+ products.push(backendProduct);
+ allPrices.push(...prices);
- allEnts.push(
- ...entitlements.map((ent) => {
- return EntInsertSchema.parse(ent) as unknown as Entitlement;
- }),
- );
- }
+ allEnts.push(
+ ...entitlements.map((ent) => {
+ return EntInsertSchema.parse(ent) as unknown as Entitlement;
+ }),
+ );
+ }
- return { products, prices: allPrices, ents: allEnts };
+ return { products, prices: allPrices, ents: allEnts };
};
diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts
index 305991d3d..d662f7573 100644
--- a/server/src/internal/orgs/orgRouter.ts
+++ b/server/src/internal/orgs/orgRouter.ts
@@ -1,7 +1,7 @@
import express, { Router } from "express";
import {
- handleGetOrgMembers,
- handleRemoveMember,
+ handleGetOrgMembers,
+ handleRemoveMember,
} from "./handlers/handleGetOrgMembers.js";
import { OrgService } from "./OrgService.js";
@@ -22,9 +22,9 @@ orgRouter.get("/invites", handleGetInvites as any);
orgRouter.delete("", handleDeleteOrg as any);
orgRouter.delete("/delete-user", async (req: any, res) => {
- res.status(200).json({
- message: "User deleted",
- });
+ res.status(200).json({
+ message: "User deleted",
+ });
});
orgRouter.get("", handleGetOrg);
diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts
index 96f23aa14..797b830ba 100644
--- a/server/src/internal/orgs/orgUtils.ts
+++ b/server/src/internal/orgs/orgUtils.ts
@@ -1,12 +1,12 @@
import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
- AppEnv,
- ErrCode,
- FrontendOrg,
- Organization,
- organizations,
- OrgConfig,
+ AppEnv,
+ ErrCode,
+ FrontendOrg,
+ Organization,
+ organizations,
+ OrgConfig,
} from "@autumn/shared";
import { createStripeCli } from "@/external/stripe/utils.js";
import { OrgService } from "./OrgService.js";
@@ -20,222 +20,222 @@ import { eq } from "drizzle-orm";
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
export const shouldReconnectStripe = async ({
- org,
- env,
- logger,
- stripeKey,
+ org,
+ env,
+ logger,
+ stripeKey,
}: {
- org: Organization;
- env: AppEnv;
- logger: any;
- stripeKey: string;
+ org: Organization;
+ env: AppEnv;
+ logger: any;
+ stripeKey: string;
}) => {
- if (!isStripeConnected({ org, env })) return true;
+ if (!isStripeConnected({ org, env })) return true;
- try {
- const stripeCli = createStripeCli({ org, env: env! });
- const newKey = new Stripe(stripeKey);
+ try {
+ const stripeCli = createStripeCli({ org, env: env! });
+ const newKey = new Stripe(stripeKey);
- const oldAccount = await stripeCli.accounts.retrieve();
- const newAccount = await newKey.accounts.retrieve();
+ const oldAccount = await stripeCli.accounts.retrieve();
+ const newAccount = await newKey.accounts.retrieve();
- return oldAccount.id !== newAccount.id;
- } catch (error) {
- logger.error("Error checking if stripe should be reconnected", { error });
- return true;
- }
+ return oldAccount.id !== newAccount.id;
+ } catch (error) {
+ logger.error("Error checking if stripe should be reconnected", { error });
+ return true;
+ }
};
export const isStripeConnected = ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env?: AppEnv;
+ org: Organization;
+ env?: AppEnv;
}) => {
- if (env === AppEnv.Sandbox) {
- return notNullish(org.stripe_config?.test_api_key);
- } else if (env === AppEnv.Live) {
- return notNullish(org.stripe_config?.live_api_key);
- } else {
- return (
- notNullish(org.stripe_config?.test_api_key) &&
- notNullish(org.stripe_config?.live_api_key)
- );
- }
+ if (env === AppEnv.Sandbox) {
+ return notNullish(org.stripe_config?.test_api_key);
+ } else if (env === AppEnv.Live) {
+ return notNullish(org.stripe_config?.live_api_key);
+ } else {
+ return (
+ notNullish(org.stripe_config?.test_api_key) &&
+ notNullish(org.stripe_config?.live_api_key)
+ );
+ }
};
export const constructOrg = ({ id, slug }: { id: string; slug: string }) => {
- return {
- id,
- slug,
- created_at: Date.now(),
- default_currency: "usd",
- stripe_connected: false,
- stripe_config: null,
- test_pkey: generatePublishableKey(AppEnv.Sandbox),
- live_pkey: generatePublishableKey(AppEnv.Live),
- svix_config: {
- sandbox_app_id: "",
- live_app_id: "",
- },
- config: {} as any,
- };
+ return {
+ id,
+ slug,
+ created_at: Date.now(),
+ default_currency: "usd",
+ stripe_connected: false,
+ stripe_config: null,
+ test_pkey: generatePublishableKey(AppEnv.Sandbox),
+ live_pkey: generatePublishableKey(AppEnv.Live),
+ svix_config: {
+ sandbox_app_id: "",
+ live_app_id: "",
+ },
+ config: {} as any,
+ };
};
export const deleteStripeWebhook = async ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- if (!isStripeConnected({ org, env })) return;
+ if (!isStripeConnected({ org, env })) return;
- const stripeCli = createStripeCli({ org, env });
- const webhookEndpoints = await stripeCli.webhookEndpoints.list({
- limit: 100,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const webhookEndpoints = await stripeCli.webhookEndpoints.list({
+ limit: 100,
+ });
- for (const webhook of webhookEndpoints.data) {
- if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
- try {
- await stripeCli.webhookEndpoints.del(webhook.id);
- console.log(`Deleted stripe webhook (${env}) ${webhook.url}`);
- } catch (error: any) {
- console.log(`Failed to delete stripe webhook (${env}) ${webhook.url}`);
- console.log(error.message);
- }
- }
- }
+ for (const webhook of webhookEndpoints.data) {
+ if (webhook.url.includes(org.id) && webhook.url.includes(env)) {
+ try {
+ await stripeCli.webhookEndpoints.del(webhook.id);
+ console.log(`Deleted stripe webhook (${env}) ${webhook.url}`);
+ } catch (error: any) {
+ console.log(`Failed to delete stripe webhook (${env}) ${webhook.url}`);
+ console.log(error.message);
+ }
+ }
+ }
};
export const getStripeWebhookSecret = (org: Organization, env: AppEnv) => {
- const webhookSecret =
- env === AppEnv.Sandbox
- ? org.stripe_config?.test_webhook_secret
- : org.stripe_config?.live_webhook_secret;
+ const webhookSecret =
+ env === AppEnv.Sandbox
+ ? org.stripe_config?.test_webhook_secret
+ : org.stripe_config?.live_webhook_secret;
- if (!webhookSecret) {
- throw new RecaseError({
- code: ErrCode.StripeConfigNotFound,
- message: `Stripe webhook secret not found for org ${org.id}`,
- statusCode: 400,
- });
- }
+ if (!webhookSecret) {
+ throw new RecaseError({
+ code: ErrCode.StripeConfigNotFound,
+ message: `Stripe webhook secret not found for org ${org.id}`,
+ statusCode: 400,
+ });
+ }
- return decryptData(webhookSecret);
+ return decryptData(webhookSecret);
};
export const initDefaultConfig = () => {
- return {
- free_trial_paid_to_paid: false,
+ return {
+ free_trial_paid_to_paid: false,
- // 1. Upgrade prorates immediately
- bill_upgrade_immediately: true,
+ // 1. Upgrade prorates immediately
+ bill_upgrade_immediately: true,
- // 2. Convert invoice to charge automatically
- convert_to_charge_automatically: false,
- };
+ // 2. Convert invoice to charge automatically
+ convert_to_charge_automatically: false,
+ };
};
export const createOrgResponse = ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}): FrontendOrg => {
- return {
- id: org.id,
- name: org.name,
- logo: org.logo,
- slug: org.slug,
- // sandbox_config: {
- // stripe_connected: isStripeConnected({ org, env: AppEnv.Sandbox }),
- // default_currency: org.default_currency || "USD",
- // return_url: org.sandbox_config?.return_url || "",
- // },
- // production_config: {
- // stripe_connected: isStripeConnected({ org, env: AppEnv.Live }),
- // default_currency: org.default_currency || "USD",
- // return_url: org.production_config?.return_url || "",
- // },
+ return {
+ id: org.id,
+ name: org.name,
+ logo: org.logo,
+ slug: org.slug,
+ // sandbox_config: {
+ // stripe_connected: isStripeConnected({ org, env: AppEnv.Sandbox }),
+ // default_currency: org.default_currency || "USD",
+ // return_url: org.sandbox_config?.return_url || "",
+ // },
+ // production_config: {
+ // stripe_connected: isStripeConnected({ org, env: AppEnv.Live }),
+ // default_currency: org.default_currency || "USD",
+ // return_url: org.production_config?.return_url || "",
+ // },
- success_url: toSuccessUrl({ org, env }) || "",
- default_currency: org.default_currency || "usd",
- stripe_connected: isStripeConnected({ org, env }),
- created_at: new Date(org.createdAt).getTime(),
- test_pkey: org.test_pkey,
- live_pkey: org.live_pkey,
- };
+ success_url: toSuccessUrl({ org, env }) || "",
+ default_currency: org.default_currency || "usd",
+ stripe_connected: isStripeConnected({ org, env }),
+ created_at: new Date(org.createdAt).getTime(),
+ test_pkey: org.test_pkey,
+ live_pkey: org.live_pkey,
+ };
};
export const getOrgAndFeatures = async ({ req }: { req: any }) => {
- let { orgId, env } = req;
+ let { orgId, env } = req;
- let [org, features] = await Promise.all([
- OrgService.getFromReq(req),
- FeatureService.getFromReq(req),
- ]);
+ let [org, features] = await Promise.all([
+ OrgService.getFromReq(req),
+ FeatureService.getFromReq(req),
+ ]);
- return { org, features };
+ return { org, features };
};
export const updateOrgConfig = async ({
- db,
- org,
- config,
- disconnectCache = true,
+ db,
+ org,
+ config,
+ disconnectCache = true,
}: {
- db: DrizzleCli;
- org: Organization;
- config: Partial;
- disconnectCache?: boolean;
+ db: DrizzleCli;
+ org: Organization;
+ config: Partial;
+ disconnectCache?: boolean;
}) => {
- await db
- .update(organizations)
- .set({
- config: {
- ...org.config,
- ...config,
- },
- })
- .where(eq(organizations.id, org.id));
+ await db
+ .update(organizations)
+ .set({
+ config: {
+ ...org.config,
+ ...config,
+ },
+ })
+ .where(eq(organizations.id, org.id));
- await clearOrgCache({
- db,
- orgId: org.id,
- });
+ await clearOrgCache({
+ db,
+ orgId: org.id,
+ });
- if (disconnectCache) {
- await CacheManager.disconnect();
- }
+ if (disconnectCache) {
+ await CacheManager.disconnect();
+ }
};
export const unsetOrgStripeKeys = async ({
- org,
- env,
- db,
+ org,
+ env,
+ db,
}: {
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
}) => {
- const newStripeConfig: any = structuredClone(org.stripe_config) || {};
- if (env === AppEnv.Sandbox) {
- newStripeConfig.test_api_key = null;
- newStripeConfig.test_webhook_secret = null;
- } else {
- newStripeConfig.live_api_key = null;
- newStripeConfig.live_webhook_secret = null;
- }
+ const newStripeConfig: any = structuredClone(org.stripe_config) || {};
+ if (env === AppEnv.Sandbox) {
+ newStripeConfig.test_api_key = null;
+ newStripeConfig.test_webhook_secret = null;
+ } else {
+ newStripeConfig.live_api_key = null;
+ newStripeConfig.live_webhook_secret = null;
+ }
- await OrgService.update({
- db,
- orgId: org.id,
- updates: {
- stripe_config: newStripeConfig,
- },
- });
+ await OrgService.update({
+ db,
+ orgId: org.id,
+ updates: {
+ stripe_config: newStripeConfig,
+ },
+ });
};
diff --git a/server/src/internal/orgs/orgUtils/clearOrgCache.ts b/server/src/internal/orgs/orgUtils/clearOrgCache.ts
index 66bb70c17..aa8eb6491 100644
--- a/server/src/internal/orgs/orgUtils/clearOrgCache.ts
+++ b/server/src/internal/orgs/orgUtils/clearOrgCache.ts
@@ -5,54 +5,54 @@ import { CacheType } from "@/external/caching/cacheActions.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const clearOrgCache = async ({
- db,
- orgId,
- env,
- logger = console,
+ db,
+ orgId,
+ env,
+ logger = console,
}: {
- db: DrizzleCli;
- orgId: string;
- env?: AppEnv;
- logger?: any;
+ db: DrizzleCli;
+ orgId: string;
+ env?: AppEnv;
+ logger?: any;
}) => {
- // 1. Get all hashed secret key and public key for org
- try {
- let org = await OrgService.getWithKeys({
- db,
- orgId,
- env,
- });
+ // 1. Get all hashed secret key and public key for org
+ try {
+ let org = await OrgService.getWithKeys({
+ db,
+ orgId,
+ env,
+ });
- if (!org) {
- return;
- }
+ if (!org) {
+ return;
+ }
- let secretKeys = org.api_keys.map((key: any) => key.hashed_key);
- let publicKeys = [org.test_pkey, org.live_pkey];
+ let secretKeys = org.api_keys.map((key: any) => key.hashed_key);
+ let publicKeys = [org.test_pkey, org.live_pkey];
- let batchDelete = [];
- for (let key of secretKeys) {
- batchDelete.push(
- CacheManager.invalidate({
- action: CacheType.SecretKey,
- value: key!,
- }),
- );
- }
+ let batchDelete = [];
+ for (let key of secretKeys) {
+ batchDelete.push(
+ CacheManager.invalidate({
+ action: CacheType.SecretKey,
+ value: key!,
+ }),
+ );
+ }
- for (let key of publicKeys) {
- batchDelete.push(
- CacheManager.invalidate({
- action: CacheType.PublicKey,
- value: key!,
- }),
- );
- }
+ for (let key of publicKeys) {
+ batchDelete.push(
+ CacheManager.invalidate({
+ action: CacheType.PublicKey,
+ value: key!,
+ }),
+ );
+ }
- await Promise.all(batchDelete);
- logger.info(`Cleared cache for org ${org.slug} (${orgId})`);
- } catch (error) {
- logger.error(`Failed to clear cache for org ${orgId}`);
- logger.error(error);
- }
+ await Promise.all(batchDelete);
+ logger.info(`Cleared cache for org ${org.slug} (${orgId})`);
+ } catch (error) {
+ logger.error(`Failed to clear cache for org ${orgId}`);
+ logger.error(error);
+ }
};
diff --git a/server/src/internal/orgs/orgUtils/convertOrgUtils.ts b/server/src/internal/orgs/orgUtils/convertOrgUtils.ts
index f994fd550..025a5cfe8 100644
--- a/server/src/internal/orgs/orgUtils/convertOrgUtils.ts
+++ b/server/src/internal/orgs/orgUtils/convertOrgUtils.ts
@@ -1,15 +1,15 @@
import { AppEnv, Organization } from "@autumn/shared";
export const toSuccessUrl = ({
- org,
- env,
+ org,
+ env,
}: {
- org: Organization;
- env: AppEnv;
+ org: Organization;
+ env: AppEnv;
}) => {
- if (env === AppEnv.Sandbox) {
- return org.stripe_config?.sandbox_success_url || "https://useautumn.com";
- } else {
- return org.stripe_config?.success_url || "https://useautumn.com";
- }
+ if (env === AppEnv.Sandbox) {
+ return org.stripe_config?.sandbox_success_url || "https://useautumn.com";
+ } else {
+ return org.stripe_config?.success_url || "https://useautumn.com";
+ }
};
diff --git a/server/src/internal/platform/platformRouter.ts b/server/src/internal/platform/platformRouter.ts
index a5ab46c04..6dd9c96c6 100644
--- a/server/src/internal/platform/platformRouter.ts
+++ b/server/src/internal/platform/platformRouter.ts
@@ -2,12 +2,12 @@ import { generateId } from "better-auth";
import { NextFunction, Router } from "express";
import {
- AppEnv,
- member,
- Organization,
- organizations,
- StripeConfig,
- user as userTable,
+ AppEnv,
+ member,
+ Organization,
+ organizations,
+ StripeConfig,
+ user as userTable,
} from "@autumn/shared";
import { ExtendedRequest } from "@/utils/models/Request.js";
@@ -25,300 +25,300 @@ import { shouldReconnectStripe } from "../orgs/orgUtils.js";
const platformRouter = Router();
const platformAuthMiddleware = async (
- req: any,
- res: any,
- next: NextFunction
+ req: any,
+ res: any,
+ next: NextFunction,
) => {
- if (!process.env.AUTUMN_SECRET_KEY) next();
+ if (!process.env.AUTUMN_SECRET_KEY) next();
- try {
- let autumn = new Autumn();
- const { data, error } = await autumn.check({
- customer_id: req.org.id,
- feature_id: "platform",
- });
+ try {
+ let autumn = new Autumn();
+ const { data, error } = await autumn.check({
+ customer_id: req.org.id,
+ feature_id: "platform",
+ });
- if (error) {
- throw error;
- }
+ if (error) {
+ throw error;
+ }
- if (!data?.allowed) {
- res.status(403).json({
- message:
- "You're not allowed to access the platform API. Please contact hey@useautumn.com to request access!",
- code: "not_allowed",
- });
- return;
- }
- next();
- } catch (error) {
- req.logger.error(`Failed to check if org is allowed to access platform`, {
- error,
- });
- res.status(500).json({
- message: "Failed to check if org is allowed to access platform",
- code: "internal_error",
- });
- }
+ if (!data?.allowed) {
+ res.status(403).json({
+ message:
+ "You're not allowed to access the platform API. Please contact hey@useautumn.com to request access!",
+ code: "not_allowed",
+ });
+ return;
+ }
+ next();
+ } catch (error) {
+ req.logger.error(`Failed to check if org is allowed to access platform`, {
+ error,
+ });
+ res.status(500).json({
+ message: "Failed to check if org is allowed to access platform",
+ code: "internal_error",
+ });
+ }
};
platformRouter.use(platformAuthMiddleware);
const ExchangeSchema = z.object({
- // organization_name: z.string().nullish(),
- // organization_slug: z.string().nullish(),
- organization: z
- .object({
- name: z.string().nonempty(),
- slug: z.string().nonempty(),
- })
- .nullish(),
- email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
- stripe_test_key: z.string().nonempty().optional(),
- stripe_live_key: z.string().nonempty().optional(),
+ // organization_name: z.string().nullish(),
+ // organization_slug: z.string().nullish(),
+ organization: z
+ .object({
+ name: z.string().nonempty(),
+ slug: z.string().nonempty(),
+ })
+ .nullish(),
+ email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
+ stripe_test_key: z.string().nonempty().optional(),
+ stripe_live_key: z.string().nonempty().optional(),
});
platformRouter.post("/exchange", (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "exchange",
- handler: async (req: ExtendedRequest, res: any) => {
- let { organization, email, stripe_test_key, stripe_live_key } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "exchange",
+ handler: async (req: ExtendedRequest, res: any) => {
+ let { organization, email, stripe_test_key, stripe_live_key } = req.body;
- const { db, logger } = req;
+ const { db, logger } = req;
- ExchangeSchema.parse({
- organization,
- email,
- stripe_test_key,
- stripe_live_key,
- });
+ ExchangeSchema.parse({
+ organization,
+ email,
+ stripe_test_key,
+ stripe_live_key,
+ });
- if (!stripe_test_key && !stripe_live_key) {
- res.status(400).json({
- message: "Either stripe_test_key or stripe_live_key is required",
- code: "invalid_request",
- });
- return;
- }
+ if (!stripe_test_key && !stripe_live_key) {
+ res.status(400).json({
+ message: "Either stripe_test_key or stripe_live_key is required",
+ code: "invalid_request",
+ });
+ return;
+ }
- // 1. Check if user with this email already exists
- let user = await db.query.user.findFirst({
- where: eq(userTable.email, email),
- });
+ // 1. Check if user with this email already exists
+ let user = await db.query.user.findFirst({
+ where: eq(userTable.email, email),
+ });
- if (!user) {
- [user] = await db
- .insert(userTable)
- .values({
- id: generateId(),
- name: "",
- email,
- emailVerified: true,
- createdAt: new Date(),
- updatedAt: new Date(),
- role: "user",
- banned: false,
- banReason: null,
- banExpires: null,
- createdBy: req.org.id,
- })
- .returning();
- }
+ if (!user) {
+ [user] = await db
+ .insert(userTable)
+ .values({
+ id: generateId(),
+ name: "",
+ email,
+ emailVerified: true,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ role: "user",
+ banned: false,
+ banReason: null,
+ banExpires: null,
+ createdBy: req.org.id,
+ })
+ .returning();
+ }
- logger.info(`User found / created: ${user.id} (${email})`);
+ logger.info(`User found / created: ${user.id} (${email})`);
- let org: Organization;
+ let org: Organization;
- // let membership = await db.query.member.findFirst({
- // with: {
- // organization: true,
- // },
- // where: and(
- // eq(member.userId, user.id!),
- // eq(member.role, "owner"),
- // organization_slug
- // ? eq(organizations.slug, organization_slug)
- // : undefined,
- // eq(organizations.created_by, req.org.id)
- // ),
- // });
- const orgSlug = organization?.slug
- ? `${organization.slug}_${req.org.id}`
- : undefined;
- const data = await db
- .select()
- .from(member)
- .innerJoin(organizations, eq(member.organizationId, organizations.id))
- .where(
- and(
- eq(member.userId, user.id!),
- eq(member.role, "owner"),
- orgSlug ? eq(organizations.slug, orgSlug) : undefined,
- eq(organizations.created_by, req.org.id)
- )
- );
+ // let membership = await db.query.member.findFirst({
+ // with: {
+ // organization: true,
+ // },
+ // where: and(
+ // eq(member.userId, user.id!),
+ // eq(member.role, "owner"),
+ // organization_slug
+ // ? eq(organizations.slug, organization_slug)
+ // : undefined,
+ // eq(organizations.created_by, req.org.id)
+ // ),
+ // });
+ const orgSlug = organization?.slug
+ ? `${organization.slug}_${req.org.id}`
+ : undefined;
+ const data = await db
+ .select()
+ .from(member)
+ .innerJoin(organizations, eq(member.organizationId, organizations.id))
+ .where(
+ and(
+ eq(member.userId, user.id!),
+ eq(member.role, "owner"),
+ orgSlug ? eq(organizations.slug, orgSlug) : undefined,
+ eq(organizations.created_by, req.org.id),
+ ),
+ );
- let membership = data.length > 0 ? data[0] : null;
+ let membership = data.length > 0 ? data[0] : null;
- if (!membership) {
- logger.info(`Connected to Stripe`);
+ if (!membership) {
+ logger.info(`Connected to Stripe`);
- // 2. Create org
- let orgId = generateId();
+ // 2. Create org
+ let orgId = generateId();
- [org] = (await db
- .insert(organizations)
- .values({
- id: orgId,
- slug: orgSlug
- ? orgSlug
- : `platform_org_${Math.floor(10000000 + Math.random() * 90000000)}`,
+ [org] = (await db
+ .insert(organizations)
+ .values({
+ id: orgId,
+ slug: orgSlug
+ ? orgSlug
+ : `platform_org_${Math.floor(10000000 + Math.random() * 90000000)}`,
- name: organization?.name || `Platform Org (${req.org.id})`,
- logo: "",
- createdAt: new Date(),
- metadata: "",
- created_by: req.org.id,
- })
- .returning()) as [Organization];
+ name: organization?.name || `Platform Org (${req.org.id})`,
+ logo: "",
+ createdAt: new Date(),
+ metadata: "",
+ created_by: req.org.id,
+ })
+ .returning()) as [Organization];
- await db.insert(member).values({
- id: generateId(),
- organizationId: orgId,
- userId: user.id!,
- role: "owner",
- createdAt: new Date(),
- });
+ await db.insert(member).values({
+ id: generateId(),
+ organizationId: orgId,
+ userId: user.id!,
+ role: "owner",
+ createdAt: new Date(),
+ });
- await afterOrgCreated({ org });
- } else {
- // org = (await db.query.organizations.findFirst({
- // where: eq(organizations.id, membership.organizationId),
- // })) as Organization;
- org = membership.organizations as Organization;
- }
+ await afterOrgCreated({ org });
+ } else {
+ // org = (await db.query.organizations.findFirst({
+ // where: eq(organizations.id, membership.organizationId),
+ // })) as Organization;
+ org = membership.organizations as Organization;
+ }
- let sandboxKey, prodKey;
+ let sandboxKey, prodKey;
- let finalStripeConfig: any = {};
- let defaultCurrency = org.default_currency || "usd";
+ let finalStripeConfig: any = {};
+ let defaultCurrency = org.default_currency || "usd";
- // Connect stripe if not exists...
- if (stripe_test_key) {
- const reconnectStripe = await shouldReconnectStripe({
- org,
- env: AppEnv.Sandbox,
- stripeKey: stripe_test_key,
- logger: req.logtail,
- });
+ // Connect stripe if not exists...
+ if (stripe_test_key) {
+ const reconnectStripe = await shouldReconnectStripe({
+ org,
+ env: AppEnv.Sandbox,
+ stripeKey: stripe_test_key,
+ logger: req.logtail,
+ });
- if (reconnectStripe) {
- let {
- test_api_key,
- test_webhook_secret,
- defaultCurrency: newDefaultCurrency,
- } = await connectStripe({
- orgId: org.id,
- apiKey: stripe_test_key,
- env: AppEnv.Sandbox,
- });
+ if (reconnectStripe) {
+ let {
+ test_api_key,
+ test_webhook_secret,
+ defaultCurrency: newDefaultCurrency,
+ } = await connectStripe({
+ orgId: org.id,
+ apiKey: stripe_test_key,
+ env: AppEnv.Sandbox,
+ });
- finalStripeConfig = {
- ...finalStripeConfig,
- test_api_key,
- test_webhook_secret,
- };
+ finalStripeConfig = {
+ ...finalStripeConfig,
+ test_api_key,
+ test_webhook_secret,
+ };
- if (!defaultCurrency) {
- defaultCurrency = newDefaultCurrency || "usd";
- }
- }
+ if (!defaultCurrency) {
+ defaultCurrency = newDefaultCurrency || "usd";
+ }
+ }
- sandboxKey = await createKey({
- db,
- orgId: org.id,
- env: AppEnv.Sandbox,
- name: "Platform API Key",
- prefix: "am_sk_test",
- meta: {},
- });
- }
+ sandboxKey = await createKey({
+ db,
+ orgId: org.id,
+ env: AppEnv.Sandbox,
+ name: "Platform API Key",
+ prefix: "am_sk_test",
+ meta: {},
+ });
+ }
- if (stripe_live_key) {
- const reconnectStripe = await shouldReconnectStripe({
- org,
- env: AppEnv.Live,
- stripeKey: stripe_live_key,
- logger: req.logger,
- });
+ if (stripe_live_key) {
+ const reconnectStripe = await shouldReconnectStripe({
+ org,
+ env: AppEnv.Live,
+ stripeKey: stripe_live_key,
+ logger: req.logger,
+ });
- if (reconnectStripe) {
- console.log("Reconnecting stripe live");
- let {
- live_api_key,
- live_webhook_secret,
- defaultCurrency: newDefaultCurrency,
- } = await connectStripe({
- orgId: org.id,
- apiKey: stripe_live_key,
- env: AppEnv.Live,
- });
+ if (reconnectStripe) {
+ console.log("Reconnecting stripe live");
+ let {
+ live_api_key,
+ live_webhook_secret,
+ defaultCurrency: newDefaultCurrency,
+ } = await connectStripe({
+ orgId: org.id,
+ apiKey: stripe_live_key,
+ env: AppEnv.Live,
+ });
- finalStripeConfig = {
- ...finalStripeConfig,
- live_api_key,
- live_webhook_secret,
- };
+ finalStripeConfig = {
+ ...finalStripeConfig,
+ live_api_key,
+ live_webhook_secret,
+ };
- if (!defaultCurrency) {
- defaultCurrency = newDefaultCurrency || "usd";
- }
- }
+ if (!defaultCurrency) {
+ defaultCurrency = newDefaultCurrency || "usd";
+ }
+ }
- prodKey = await createKey({
- db,
- orgId: org.id,
- env: AppEnv.Live,
- name: "Platform API Key",
- prefix: "am_sk_live",
- meta: {},
- });
- }
+ prodKey = await createKey({
+ db,
+ orgId: org.id,
+ env: AppEnv.Live,
+ name: "Platform API Key",
+ prefix: "am_sk_live",
+ meta: {},
+ });
+ }
- // if (!org.stripe_config?.success_url) {
- // finalStripeConfig.success_url = `https://useautumn.com`;
- // }
+ // if (!org.stripe_config?.success_url) {
+ // finalStripeConfig.success_url = `https://useautumn.com`;
+ // }
- await db
- .update(organizations)
- .set({
- default_currency: defaultCurrency,
- stripe_connected: true,
- stripe_config: {
- ...org.stripe_config,
- ...finalStripeConfig,
- } as StripeConfig,
- })
- .where(eq(organizations.id, org.id));
- res.status(200).json({
- // org: {
- // id: org.id,
- // slug: org.slug,
- // name: org.name,
- // },
- // user: {
- // id: user.id!,
- // email,
- // },
- api_keys: {
- sandbox: sandboxKey,
- production: prodKey,
- },
- });
- },
- })
+ await db
+ .update(organizations)
+ .set({
+ default_currency: defaultCurrency,
+ stripe_connected: true,
+ stripe_config: {
+ ...org.stripe_config,
+ ...finalStripeConfig,
+ } as StripeConfig,
+ })
+ .where(eq(organizations.id, org.id));
+ res.status(200).json({
+ // org: {
+ // id: org.id,
+ // slug: org.slug,
+ // name: org.name,
+ // },
+ // user: {
+ // id: user.id!,
+ // email,
+ // },
+ api_keys: {
+ sandbox: sandboxKey,
+ production: prodKey,
+ },
+ });
+ },
+ }),
);
export { platformRouter };
diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts
index a87e02d93..46cfda9f1 100644
--- a/server/src/internal/products/ProductService.ts
+++ b/server/src/internal/products/ProductService.ts
@@ -1,14 +1,14 @@
import {
- type AppEnv,
- customerProducts,
- customers,
- ErrCode,
- entitlements,
- type FullProduct,
- freeTrials,
- type Product,
- prices,
- products,
+ type AppEnv,
+ customerProducts,
+ customers,
+ ErrCode,
+ entitlements,
+ type FullProduct,
+ freeTrials,
+ type Product,
+ prices,
+ products,
} from "@autumn/shared";
import { and, desc, eq, exists, inArray, ne, or, sql } from "drizzle-orm";
import { StatusCodes } from "http-status-codes";
@@ -18,490 +18,490 @@ import { notNullish } from "@/utils/genUtils.js";
import { getLatestProducts } from "./productUtils.js";
const parseFreeTrials = ({
- products,
- product,
+ products,
+ product,
}: {
- products?: FullProduct[];
- product?: FullProduct;
+ products?: FullProduct[];
+ product?: FullProduct;
}) => {
- if (products) {
- for (const prod of products) {
- prod.free_trial =
- prod.free_trials && prod.free_trials.length > 0
- ? prod.free_trials[0]
- : null;
- }
- } else if (product) {
- product!.free_trial =
- product!.free_trials && product!.free_trials.length > 0
- ? product!.free_trials[0]
- : null;
- }
- return product;
+ if (products) {
+ for (const prod of products) {
+ prod.free_trial =
+ prod.free_trials && prod.free_trials.length > 0
+ ? prod.free_trials[0]
+ : null;
+ }
+ } else if (product) {
+ product!.free_trial =
+ product!.free_trials && product!.free_trials.length > 0
+ ? product!.free_trials[0]
+ : null;
+ }
+ return product;
};
// biome-ignore lint/complexity/noStaticOnlyClass: no thanks m8
export class ProductService {
- static async getByFeature({
- db,
- internalFeatureId,
- }: {
- db: DrizzleCli;
- internalFeatureId: string;
- }) {
- const fullProducts = (await db.query.products.findMany({
- where: exists(
- db
- .select()
- .from(entitlements)
- .where(
- and(
- eq(entitlements.internal_product_id, products.internal_id),
- eq(entitlements.internal_feature_id, internalFeatureId)
- )
- )
- ),
- with: {
- entitlements: {
- with: {
- feature: true,
- },
- },
- prices: { where: eq(prices.is_custom, false) },
- free_trials: { where: eq(freeTrials.is_custom, false) },
- },
- orderBy: [desc(products.version)],
- })) as FullProduct[];
+ static async getByFeature({
+ db,
+ internalFeatureId,
+ }: {
+ db: DrizzleCli;
+ internalFeatureId: string;
+ }) {
+ const fullProducts = (await db.query.products.findMany({
+ where: exists(
+ db
+ .select()
+ .from(entitlements)
+ .where(
+ and(
+ eq(entitlements.internal_product_id, products.internal_id),
+ eq(entitlements.internal_feature_id, internalFeatureId),
+ ),
+ ),
+ ),
+ with: {
+ entitlements: {
+ with: {
+ feature: true,
+ },
+ },
+ prices: { where: eq(prices.is_custom, false) },
+ free_trials: { where: eq(freeTrials.is_custom, false) },
+ },
+ orderBy: [desc(products.version)],
+ })) as FullProduct[];
- parseFreeTrials({ products: fullProducts });
+ parseFreeTrials({ products: fullProducts });
- const latestProducts = getLatestProducts(fullProducts);
+ const latestProducts = getLatestProducts(fullProducts);
- return latestProducts;
- }
+ return latestProducts;
+ }
- static async getByInternalId({
- db,
- internalId,
- }: {
- db: DrizzleCli;
- internalId: string;
- }) {
- return (await db.query.products.findFirst({
- where: eq(products.internal_id, internalId),
- })) as Product;
- }
+ static async getByInternalId({
+ db,
+ internalId,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ }) {
+ return (await db.query.products.findFirst({
+ where: eq(products.internal_id, internalId),
+ })) as Product;
+ }
- static async listByInternalIds({
- db,
- internalIds,
- }: {
- db: DrizzleCli;
- internalIds: string[];
- }) {
- return (await db.query.products.findMany({
- where: inArray(products.internal_id, internalIds),
- with: {
- entitlements: {
- with: {
- feature: true,
- },
- },
- prices: { where: eq(prices.is_custom, false) },
- },
- })) as FullProduct[];
- }
+ static async listByInternalIds({
+ db,
+ internalIds,
+ }: {
+ db: DrizzleCli;
+ internalIds: string[];
+ }) {
+ return (await db.query.products.findMany({
+ where: inArray(products.internal_id, internalIds),
+ with: {
+ entitlements: {
+ with: {
+ feature: true,
+ },
+ },
+ prices: { where: eq(prices.is_custom, false) },
+ },
+ })) as FullProduct[];
+ }
- static async listDefault({
- db,
- orgId,
- env,
- group,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- group?: string;
- }) {
- const prods = (await db.query.products.findMany({
- where: and(
- eq(products.org_id, orgId),
- eq(products.env, env),
- eq(products.is_default, true),
- ne(products.archived, true),
- group ? eq(products.group, group) : undefined
- ),
- with: {
- entitlements: {
- with: {
- feature: true,
- },
- where: eq(entitlements.is_custom, false),
- },
- prices: { where: eq(prices.is_custom, false) },
- free_trials: { where: eq(freeTrials.is_custom, false) },
- },
- })) as FullProduct[];
+ static async listDefault({
+ db,
+ orgId,
+ env,
+ group,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ group?: string;
+ }) {
+ const prods = (await db.query.products.findMany({
+ where: and(
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ eq(products.is_default, true),
+ ne(products.archived, true),
+ group ? eq(products.group, group) : undefined,
+ ),
+ with: {
+ entitlements: {
+ with: {
+ feature: true,
+ },
+ where: eq(entitlements.is_custom, false),
+ },
+ prices: { where: eq(prices.is_custom, false) },
+ free_trials: { where: eq(freeTrials.is_custom, false) },
+ },
+ })) as FullProduct[];
- parseFreeTrials({ products: prods });
+ parseFreeTrials({ products: prods });
- const latestProducts = getLatestProducts(prods);
+ const latestProducts = getLatestProducts(prods);
- return latestProducts as FullProduct[];
- }
+ return latestProducts as FullProduct[];
+ }
- static async insert({ db, product }: { db: DrizzleCli; product: Product }) {
- const prod = await db.insert(products).values(product).returning();
+ static async insert({ db, product }: { db: DrizzleCli; product: Product }) {
+ const prod = await db.insert(products).values(product).returning();
- if (!prod || prod.length === 0) {
- throw new RecaseError({
- message: "Failed to create product",
- code: ErrCode.InternalError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (!prod || prod.length === 0) {
+ throw new RecaseError({
+ message: "Failed to create product",
+ code: ErrCode.InternalError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- return prod[0] as Product;
- }
+ return prod[0] as Product;
+ }
- static async get({
- db,
- id,
- orgId,
- env,
- version,
- }: {
- db: DrizzleCli;
- id: string;
- orgId: string;
- env: AppEnv;
- version?: number;
- }) {
- const data = await db.query.products.findMany({
- where: and(
- eq(products.id, id),
- eq(products.org_id, orgId),
- eq(products.env, env),
- version ? eq(products.version, version) : undefined
- ),
- orderBy: [desc(products.version)],
- });
+ static async get({
+ db,
+ id,
+ orgId,
+ env,
+ version,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ orgId: string;
+ env: AppEnv;
+ version?: number;
+ }) {
+ const data = await db.query.products.findMany({
+ where: and(
+ eq(products.id, id),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ version ? eq(products.version, version) : undefined,
+ ),
+ orderBy: [desc(products.version)],
+ });
- if (!data || data.length === 0) {
- return null;
- }
+ if (!data || data.length === 0) {
+ return null;
+ }
- return data[0];
- }
+ return data[0];
+ }
- static async listFull({
- db,
- orgId,
- env,
- inIds,
- returnAll = false,
- version,
- excludeEnts = false,
- archived,
- includeAll = false,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- inIds?: string[];
- returnAll?: boolean;
- version?: number;
- excludeEnts?: boolean;
- archived?: boolean;
- includeAll?: boolean;
- }) {
- const data = (await db.query.products.findMany({
- where: and(
- eq(products.org_id, orgId),
- eq(products.env, env),
- inIds ? inArray(products.id, inIds) : undefined,
- version ? eq(products.version, version) : undefined
- ),
+ static async listFull({
+ db,
+ orgId,
+ env,
+ inIds,
+ returnAll = false,
+ version,
+ excludeEnts = false,
+ archived,
+ includeAll = false,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ inIds?: string[];
+ returnAll?: boolean;
+ version?: number;
+ excludeEnts?: boolean;
+ archived?: boolean;
+ includeAll?: boolean;
+ }) {
+ const data = (await db.query.products.findMany({
+ where: and(
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ inIds ? inArray(products.id, inIds) : undefined,
+ version ? eq(products.version, version) : undefined,
+ ),
- with: {
- entitlements: excludeEnts
- ? undefined
- : {
- with: {
- feature: true,
- },
- where: eq(entitlements.is_custom, false),
- },
- prices: { where: eq(prices.is_custom, false) },
- free_trials: { where: eq(freeTrials.is_custom, false) },
- },
- orderBy: [desc(products.internal_id)],
- })) as FullProduct[];
+ with: {
+ entitlements: excludeEnts
+ ? undefined
+ : {
+ with: {
+ feature: true,
+ },
+ where: eq(entitlements.is_custom, false),
+ },
+ prices: { where: eq(prices.is_custom, false) },
+ free_trials: { where: eq(freeTrials.is_custom, false) },
+ },
+ orderBy: [desc(products.internal_id)],
+ })) as FullProduct[];
- parseFreeTrials({ products: data });
+ parseFreeTrials({ products: data });
- if (returnAll) {
- return data;
- }
+ if (returnAll) {
+ return data;
+ }
- const latestProducts: FullProduct[] = getLatestProducts(data);
+ const latestProducts: FullProduct[] = getLatestProducts(data);
- if (inIds) {
- const newProducts: FullProduct[] = [];
- for (const id of inIds) {
- const prod = latestProducts.find((prod) => prod.id === id);
- if (!prod) {
- throw new RecaseError({
- message: `Product ${id} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
- newProducts.push(prod);
- }
- return newProducts;
- }
+ if (inIds) {
+ const newProducts: FullProduct[] = [];
+ for (const id of inIds) {
+ const prod = latestProducts.find((prod) => prod.id === id);
+ if (!prod) {
+ throw new RecaseError({
+ message: `Product ${id} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
+ newProducts.push(prod);
+ }
+ return newProducts;
+ }
- if (notNullish(archived)) {
- return latestProducts.filter((p) => p.archived === archived);
- }
+ if (notNullish(archived)) {
+ return latestProducts.filter((p) => p.archived === archived);
+ }
- return latestProducts as FullProduct[];
- }
+ return latestProducts as FullProduct[];
+ }
- static async getFull({
- db,
- idOrInternalId,
- orgId,
- env,
- version,
- allowNotFound = false,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: AppEnv;
- version?: number;
- allowNotFound?: boolean;
- }) {
- const data = (await db.query.products.findFirst({
- where: and(
- or(
- eq(products.id, idOrInternalId),
- eq(products.internal_id, idOrInternalId)
- ),
- eq(products.org_id, orgId),
- eq(products.env, env),
- version ? eq(products.version, version) : undefined
- ),
- orderBy: [desc(products.version)],
- with: {
- entitlements: {
- with: {
- feature: true,
- },
- where: eq(entitlements.is_custom, false),
- },
- prices: { where: eq(prices.is_custom, false) },
- free_trials: { where: eq(freeTrials.is_custom, false) },
- },
- })) as FullProduct;
+ static async getFull({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ version,
+ allowNotFound = false,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: AppEnv;
+ version?: number;
+ allowNotFound?: boolean;
+ }) {
+ const data = (await db.query.products.findFirst({
+ where: and(
+ or(
+ eq(products.id, idOrInternalId),
+ eq(products.internal_id, idOrInternalId),
+ ),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ version ? eq(products.version, version) : undefined,
+ ),
+ orderBy: [desc(products.version)],
+ with: {
+ entitlements: {
+ with: {
+ feature: true,
+ },
+ where: eq(entitlements.is_custom, false),
+ },
+ prices: { where: eq(prices.is_custom, false) },
+ free_trials: { where: eq(freeTrials.is_custom, false) },
+ },
+ })) as FullProduct;
- parseFreeTrials({ product: data });
+ parseFreeTrials({ product: data });
- if (!data) {
- if (allowNotFound) return null as unknown as FullProduct;
- throw new RecaseError({
- message: `Product ${idOrInternalId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!data) {
+ if (allowNotFound) return null as unknown as FullProduct;
+ throw new RecaseError({
+ message: `Product ${idOrInternalId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- return data as FullProduct;
- }
+ return data as FullProduct;
+ }
- static async getProductVersionCount({
- db,
- productId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const data = await db.query.products.findMany({
- columns: {
- version: true,
- },
- limit: 1,
- where: and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- ),
- orderBy: [desc(products.version)],
- });
+ static async getProductVersionCount({
+ db,
+ productId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const data = await db.query.products.findMany({
+ columns: {
+ version: true,
+ },
+ limit: 1,
+ where: and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ orderBy: [desc(products.version)],
+ });
- if (data.length === 0) {
- throw new RecaseError({
- message: `Product ${productId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (data.length === 0) {
+ throw new RecaseError({
+ message: `Product ${productId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- return data[0].version;
- }
+ return data[0].version;
+ }
- // UPDATES
- static async updateByInternalId({
- db,
- internalId,
- update,
- }: {
- db: DrizzleCli;
- internalId: string;
- update: any;
- }) {
- await db
- .update(products)
- .set(update)
- .where(eq(products.internal_id, internalId));
- }
+ // UPDATES
+ static async updateByInternalId({
+ db,
+ internalId,
+ update,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ update: any;
+ }) {
+ await db
+ .update(products)
+ .set(update)
+ .where(eq(products.internal_id, internalId));
+ }
- // DELETES
+ // DELETES
- static async deleteByInternalId({
- db,
- internalId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- internalId: string;
- orgId: string;
- env: AppEnv;
- }) {
- await db
- .delete(products)
- .where(
- and(
- eq(products.internal_id, internalId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- );
- }
+ static async deleteByInternalId({
+ db,
+ internalId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ await db
+ .delete(products)
+ .where(
+ and(
+ eq(products.internal_id, internalId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ );
+ }
- static async deleteByProductId({
- db,
- productId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const res = await db
- .select()
- .from(products)
- .where(
- and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- );
+ static async deleteByProductId({
+ db,
+ productId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const res = await db
+ .select()
+ .from(products)
+ .where(
+ and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ );
- const internalIds = res.map((r) => r.internal_id);
+ const internalIds = res.map((r) => r.internal_id);
- if (internalIds.length === 0) return;
- if (internalIds.length > 500) {
- throw new RecaseError({
- message: "Cannot delete more than 500 products at once",
- code: ErrCode.InternalError,
- statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
- });
- }
+ if (internalIds.length === 0) return;
+ if (internalIds.length > 500) {
+ throw new RecaseError({
+ message: "Cannot delete more than 500 products at once",
+ code: ErrCode.InternalError,
+ statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
+ });
+ }
- await db.delete(products).where(inArray(products.internal_id, internalIds));
- }
+ await db.delete(products).where(inArray(products.internal_id, internalIds));
+ }
- static async deleteByOrgId({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- await db
- .delete(products)
- .where(and(eq(products.org_id, orgId), eq(products.env, env)));
- }
+ static async deleteByOrgId({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ await db
+ .delete(products)
+ .where(and(eq(products.org_id, orgId), eq(products.env, env)));
+ }
- static async getDeletionText({
- db,
- productId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const internalProductIds = (
- await db
- .select({
- internal_product_id: products.internal_id,
- })
- .from(products)
- .where(
- and(
- eq(products.id, productId),
- eq(products.org_id, orgId),
- eq(products.env, env)
- )
- )
- ).map((r) => r.internal_product_id);
+ static async getDeletionText({
+ db,
+ productId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const internalProductIds = (
+ await db
+ .select({
+ internal_product_id: products.internal_id,
+ })
+ .from(products)
+ .where(
+ and(
+ eq(products.id, productId),
+ eq(products.org_id, orgId),
+ eq(products.env, env),
+ ),
+ )
+ ).map((r) => r.internal_product_id);
- const res = await db
- .select({
- internal_customer_id: customerProducts.internal_customer_id,
- name: customers.name,
- id: customers.id,
- email: customers.email,
- totalCount: sql`COUNT(*) OVER ()`,
- })
- .from(customerProducts)
- .innerJoin(
- customers,
- eq(customerProducts.internal_customer_id, customers.internal_id)
- )
- .where(
- and(
- inArray(customerProducts.internal_product_id, internalProductIds),
- eq(customers.env, env),
- eq(customers.org_id, orgId)
- )
- )
- .orderBy(desc(customers.created_at))
- .groupBy(
- customerProducts.internal_customer_id,
- customers.name,
- customers.created_at,
- customers.id,
- customers.email
- );
- // .limit(1);
+ const res = await db
+ .select({
+ internal_customer_id: customerProducts.internal_customer_id,
+ name: customers.name,
+ id: customers.id,
+ email: customers.email,
+ totalCount: sql`COUNT(*) OVER ()`,
+ })
+ .from(customerProducts)
+ .innerJoin(
+ customers,
+ eq(customerProducts.internal_customer_id, customers.internal_id),
+ )
+ .where(
+ and(
+ inArray(customerProducts.internal_product_id, internalProductIds),
+ eq(customers.env, env),
+ eq(customers.org_id, orgId),
+ ),
+ )
+ .orderBy(desc(customers.created_at))
+ .groupBy(
+ customerProducts.internal_customer_id,
+ customers.name,
+ customers.created_at,
+ customers.id,
+ customers.email,
+ );
+ // .limit(1);
- return res;
- }
+ return res;
+ }
}
diff --git a/server/src/internal/products/entitlements/EntitlementService.ts b/server/src/internal/products/entitlements/EntitlementService.ts
index c95ff0401..5b73617bc 100644
--- a/server/src/internal/products/entitlements/EntitlementService.ts
+++ b/server/src/internal/products/entitlements/EntitlementService.ts
@@ -1,111 +1,111 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- Entitlement,
- entitlements,
- features,
- EntitlementWithFeature,
+ Entitlement,
+ entitlements,
+ features,
+ EntitlementWithFeature,
} from "@autumn/shared";
import { eq, and, inArray } from "drizzle-orm";
import { buildConflictUpdateColumns } from "@/db/dbUtils.js";
export class EntitlementService {
- static async getByOrg({
- db,
- orgId,
- env,
- excludeCustom = true,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: string;
- excludeCustom?: boolean;
- }) {
- // 1. get features for org
- let featuresQuery = db
- .select({
- internal_id: features.internal_id,
- })
- .from(features)
- .where(and(eq(features.org_id, orgId), eq(features.env, env)));
+ static async getByOrg({
+ db,
+ orgId,
+ env,
+ excludeCustom = true,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: string;
+ excludeCustom?: boolean;
+ }) {
+ // 1. get features for org
+ let featuresQuery = db
+ .select({
+ internal_id: features.internal_id,
+ })
+ .from(features)
+ .where(and(eq(features.org_id, orgId), eq(features.env, env)));
- let ents = await db.query.entitlements.findMany({
- where: (entitlements, { inArray }) =>
- and(
- inArray(entitlements.internal_feature_id, featuresQuery),
- excludeCustom ? eq(entitlements.is_custom, false) : undefined,
- ),
- with: {
- feature: true,
- },
- });
+ let ents = await db.query.entitlements.findMany({
+ where: (entitlements, { inArray }) =>
+ and(
+ inArray(entitlements.internal_feature_id, featuresQuery),
+ excludeCustom ? eq(entitlements.is_custom, false) : undefined,
+ ),
+ with: {
+ feature: true,
+ },
+ });
- return ents as EntitlementWithFeature[];
- }
+ return ents as EntitlementWithFeature[];
+ }
- static async getByFeature({
- db,
- internalFeatureId,
- }: {
- db: DrizzleCli;
- internalFeatureId: string;
- }) {
- return await db.query.entitlements.findFirst({
- where: eq(entitlements.internal_feature_id, internalFeatureId),
- with: {
- feature: true,
- },
- });
- }
+ static async getByFeature({
+ db,
+ internalFeatureId,
+ }: {
+ db: DrizzleCli;
+ internalFeatureId: string;
+ }) {
+ return await db.query.entitlements.findFirst({
+ where: eq(entitlements.internal_feature_id, internalFeatureId),
+ with: {
+ feature: true,
+ },
+ });
+ }
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: Entitlement[] | Entitlement;
- }) {
- if (Array.isArray(data) && data.length == 0) {
- return;
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: Entitlement[] | Entitlement;
+ }) {
+ if (Array.isArray(data) && data.length == 0) {
+ return;
+ }
- return await db.insert(entitlements).values(data as any); // DRIZZLE TYPE REFACTOR
- }
+ return await db.insert(entitlements).values(data as any); // DRIZZLE TYPE REFACTOR
+ }
- static async upsert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: Entitlement[] | Entitlement;
- }) {
- if (Array.isArray(data) && data.length == 0) return;
+ static async upsert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: Entitlement[] | Entitlement;
+ }) {
+ if (Array.isArray(data) && data.length == 0) return;
- const updateColumns = buildConflictUpdateColumns(entitlements, ["id"]);
- await db
- .insert(entitlements)
- .values(data as any)
- .onConflictDoUpdate({
- target: entitlements.id,
- set: updateColumns,
- });
- }
+ const updateColumns = buildConflictUpdateColumns(entitlements, ["id"]);
+ await db
+ .insert(entitlements)
+ .values(data as any)
+ .onConflictDoUpdate({
+ target: entitlements.id,
+ set: updateColumns,
+ });
+ }
- static async update({
- db,
- id,
- updates,
- }: {
- db: DrizzleCli;
- id: string;
- updates: Partial;
- }) {
- return await db
- .update(entitlements)
- .set(updates)
- .where(eq(entitlements.id, id));
- }
+ static async update({
+ db,
+ id,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ updates: Partial;
+ }) {
+ return await db
+ .update(entitlements)
+ .set(updates)
+ .where(eq(entitlements.id, id));
+ }
- static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- await db.delete(entitlements).where(inArray(entitlements.id, ids));
- }
+ static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ await db.delete(entitlements).where(inArray(entitlements.id, ids));
+ }
}
diff --git a/server/src/internal/products/entitlements/entitlementUtils.ts b/server/src/internal/products/entitlements/entitlementUtils.ts
index 30e96d07f..709765a56 100644
--- a/server/src/internal/products/entitlements/entitlementUtils.ts
+++ b/server/src/internal/products/entitlements/entitlementUtils.ts
@@ -1,222 +1,222 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- EntInterval,
- FreeTrial,
- Entitlement,
- AllowanceType,
- EntitlementWithFeature,
- FeatureType,
- Feature,
- ErrCode,
- UsagePriceConfig,
- PriceType,
- Price,
- FullProduct,
- FullEntitlement,
- Rollover,
- RolloverConfig,
+ EntInterval,
+ FreeTrial,
+ Entitlement,
+ AllowanceType,
+ EntitlementWithFeature,
+ FeatureType,
+ Feature,
+ ErrCode,
+ UsagePriceConfig,
+ PriceType,
+ Price,
+ FullProduct,
+ FullEntitlement,
+ Rollover,
+ RolloverConfig,
} from "@autumn/shared";
import { addDays } from "date-fns";
export const entIntervalToTrialDuration = ({
- interval,
- intervalCount,
+ interval,
+ intervalCount,
}: {
- interval: EntInterval;
- intervalCount: number;
+ interval: EntInterval;
+ intervalCount: number;
}) => {
- switch (interval) {
- case EntInterval.Day:
- return intervalCount;
- case EntInterval.Week:
- return intervalCount * 7;
- case EntInterval.Month:
- return intervalCount * 30;
- case EntInterval.Quarter:
- return intervalCount * 90;
- case EntInterval.SemiAnnual:
- return intervalCount * 180;
- case EntInterval.Year:
- return intervalCount * 365;
- case EntInterval.Lifetime:
- return intervalCount * 1000;
- }
+ switch (interval) {
+ case EntInterval.Day:
+ return intervalCount;
+ case EntInterval.Week:
+ return intervalCount * 7;
+ case EntInterval.Month:
+ return intervalCount * 30;
+ case EntInterval.Quarter:
+ return intervalCount * 90;
+ case EntInterval.SemiAnnual:
+ return intervalCount * 180;
+ case EntInterval.Year:
+ return intervalCount * 365;
+ case EntInterval.Lifetime:
+ return intervalCount * 1000;
+ }
};
export const applyTrialToEntitlement = (
- entitlement: EntitlementWithFeature,
- freeTrial: FreeTrial | null
+ entitlement: EntitlementWithFeature,
+ freeTrial: FreeTrial | null,
) => {
- if (!freeTrial) return false;
+ if (!freeTrial) return false;
- if (entitlement.feature.type === FeatureType.Boolean) return false;
- if (!entitlement.interval || entitlement.interval === EntInterval.Lifetime)
- return false;
- if (entitlement.allowance_type === AllowanceType.Unlimited) return false;
+ if (entitlement.feature.type === FeatureType.Boolean) return false;
+ if (!entitlement.interval || entitlement.interval === EntInterval.Lifetime)
+ return false;
+ if (entitlement.allowance_type === AllowanceType.Unlimited) return false;
- const trialDays = freeTrial.length;
- const entDays = entIntervalToTrialDuration({
- interval: entitlement.interval!,
- intervalCount: entitlement.interval_count || 1,
- });
+ const trialDays = freeTrial.length;
+ const entDays = entIntervalToTrialDuration({
+ interval: entitlement.interval!,
+ intervalCount: entitlement.interval_count || 1,
+ });
- if (entDays && entDays > trialDays) {
- return true;
- }
+ if (entDays && entDays > trialDays) {
+ return true;
+ }
- return false;
+ return false;
};
export const addTrialToNextResetAt = (
- nextResetAt: number,
- freeTrial: FreeTrial | null
+ nextResetAt: number,
+ freeTrial: FreeTrial | null,
) => {
- if (!freeTrial) return nextResetAt;
+ if (!freeTrial) return nextResetAt;
- return addDays(new Date(nextResetAt), freeTrial.length).getTime();
+ return addDays(new Date(nextResetAt), freeTrial.length).getTime();
};
export const rolloversAreSame = ({
- rollover1,
- rollover2,
+ rollover1,
+ rollover2,
}: {
- rollover1?: RolloverConfig | null;
- rollover2?: RolloverConfig | null;
+ rollover1?: RolloverConfig | null;
+ rollover2?: RolloverConfig | null;
}) => {
- if (!rollover1 && !rollover2) return true;
- if (!rollover1 && rollover2) return false;
- if (rollover1 && !rollover2) return false;
+ if (!rollover1 && !rollover2) return true;
+ if (!rollover1 && rollover2) return false;
+ if (rollover1 && !rollover2) return false;
- return (
- rollover1!.max == rollover2!.max &&
- rollover1!.duration == rollover2!.duration &&
- rollover1!.length == rollover2!.length
- );
+ return (
+ rollover1!.max == rollover2!.max &&
+ rollover1!.duration == rollover2!.duration &&
+ rollover1!.length == rollover2!.length
+ );
};
export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
- // 1. Check if they have same internal_feature_id
- if (ent1.internal_feature_id !== ent2.internal_feature_id) {
- console.log(
- `Internal feature ID different: ${ent1.internal_feature_id} !== ${ent2.internal_feature_id}`
- );
- return false;
- }
+ // 1. Check if they have same internal_feature_id
+ if (ent1.internal_feature_id !== ent2.internal_feature_id) {
+ console.log(
+ `Internal feature ID different: ${ent1.internal_feature_id} !== ${ent2.internal_feature_id}`,
+ );
+ return false;
+ }
- // 2. Check if they have same allowance type
- if (ent1.allowance_type !== ent2.allowance_type) {
- console.log(
- `Allowance type different: ${ent1.allowance_type} !== ${ent2.allowance_type}`
- );
- return false;
- }
- // 3. Check if they have same interval
- let diffs = {
- interval: {
- condition: ent1.interval !== ent2.interval,
- message: `Interval different: ${ent1.interval} !== ${ent2.interval}`,
- },
- intervalCount: {
- condition: ent1.interval_count !== ent2.interval_count,
- message: `Interval count different: ${ent1.interval_count} !== ${ent2.interval_count}`,
- },
- allowance: {
- condition:
- ent1.allowance_type !== AllowanceType.Unlimited &&
- ent1.allowance !== ent2.allowance,
- message: `Allowance different: ${ent1.allowance} !== ${ent2.allowance}`,
- },
- carryFromPrevious: {
- condition: ent1.carry_from_previous !== ent2.carry_from_previous,
- message: `Carry from previous different: ${ent1.carry_from_previous} !== ${ent2.carry_from_previous}`,
- },
- entityFeatureId: {
- condition: ent1.entity_feature_id !== ent2.entity_feature_id,
- message: `Entity feature ID different: ${ent1.entity_feature_id} !== ${ent2.entity_feature_id}`,
- },
- usageLimit: {
- condition: ent1.usage_limit !== ent2.usage_limit,
- message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`,
- },
- rollover: {
- condition: !rolloversAreSame({
- rollover1: ent1.rollover,
- rollover2: ent2.rollover,
- }),
- message: `Rollover different: ${ent1.rollover} !== ${ent2.rollover}`,
- },
- };
+ // 2. Check if they have same allowance type
+ if (ent1.allowance_type !== ent2.allowance_type) {
+ console.log(
+ `Allowance type different: ${ent1.allowance_type} !== ${ent2.allowance_type}`,
+ );
+ return false;
+ }
+ // 3. Check if they have same interval
+ let diffs = {
+ interval: {
+ condition: ent1.interval !== ent2.interval,
+ message: `Interval different: ${ent1.interval} !== ${ent2.interval}`,
+ },
+ intervalCount: {
+ condition: ent1.interval_count !== ent2.interval_count,
+ message: `Interval count different: ${ent1.interval_count} !== ${ent2.interval_count}`,
+ },
+ allowance: {
+ condition:
+ ent1.allowance_type !== AllowanceType.Unlimited &&
+ ent1.allowance !== ent2.allowance,
+ message: `Allowance different: ${ent1.allowance} !== ${ent2.allowance}`,
+ },
+ carryFromPrevious: {
+ condition: ent1.carry_from_previous !== ent2.carry_from_previous,
+ message: `Carry from previous different: ${ent1.carry_from_previous} !== ${ent2.carry_from_previous}`,
+ },
+ entityFeatureId: {
+ condition: ent1.entity_feature_id !== ent2.entity_feature_id,
+ message: `Entity feature ID different: ${ent1.entity_feature_id} !== ${ent2.entity_feature_id}`,
+ },
+ usageLimit: {
+ condition: ent1.usage_limit !== ent2.usage_limit,
+ message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`,
+ },
+ rollover: {
+ condition: !rolloversAreSame({
+ rollover1: ent1.rollover,
+ rollover2: ent2.rollover,
+ }),
+ message: `Rollover different: ${ent1.rollover} !== ${ent2.rollover}`,
+ },
+ };
- let entsAreDiff = Object.values(diffs).some((d) => d.condition);
+ let entsAreDiff = Object.values(diffs).some((d) => d.condition);
- if (entsAreDiff) {
- console.log("Entitlements different");
- console.log(
- "Differences:",
- Object.values(diffs)
- .filter((d) => d.condition)
- .map((d) => d.message)
- );
- }
- return !entsAreDiff;
+ if (entsAreDiff) {
+ console.log("Entitlements different");
+ console.log(
+ "Differences:",
+ Object.values(diffs)
+ .filter((d) => d.condition)
+ .map((d) => d.message),
+ );
+ }
+ return !entsAreDiff;
};
// OTHERS
export const getEntRelatedPrice = (
- entitlement: Entitlement,
- prices: Price[],
- allowFeatureMatch = false
+ entitlement: Entitlement,
+ prices: Price[],
+ allowFeatureMatch = false,
) => {
- return prices.find((price) => {
- if (price.config?.type === PriceType.Fixed) {
- return false;
- }
+ return prices.find((price) => {
+ if (price.config?.type === PriceType.Fixed) {
+ return false;
+ }
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- if (allowFeatureMatch) {
- return entitlement.internal_feature_id == config.internal_feature_id;
- }
+ if (allowFeatureMatch) {
+ return entitlement.internal_feature_id == config.internal_feature_id;
+ }
- let entIdMatch = entitlement.id == price.entitlement_id;
- let productIdMatch =
- entitlement.internal_product_id == price.internal_product_id;
- return entIdMatch && productIdMatch;
- });
+ let entIdMatch = entitlement.id == price.entitlement_id;
+ let productIdMatch =
+ entitlement.internal_product_id == price.internal_product_id;
+ return entIdMatch && productIdMatch;
+ });
};
export const getEntitlementsForProduct = (
- product: FullProduct,
- entitlements: EntitlementWithFeature[]
+ product: FullProduct,
+ entitlements: EntitlementWithFeature[],
) => {
- return entitlements.filter(
- (ent) => ent.internal_product_id === product.internal_id
- );
+ return entitlements.filter(
+ (ent) => ent.internal_product_id === product.internal_id,
+ );
};
export const getEntsWithFeature = ({
- ents,
- features,
+ ents,
+ features,
}: {
- ents: Entitlement[];
- features: Feature[];
+ ents: Entitlement[];
+ features: Feature[];
}) => {
- return ents.map((ent) => {
- let feature = features.find(
- (f) => f.internal_id === ent.internal_feature_id
- );
- if (!feature) {
- throw new RecaseError({
- message: `Couldn't find feature ${ent.internal_feature_id} for entitlement ${ent.id}`,
- code: ErrCode.FeatureNotFound,
- });
- }
+ return ents.map((ent) => {
+ let feature = features.find(
+ (f) => f.internal_id === ent.internal_feature_id,
+ );
+ if (!feature) {
+ throw new RecaseError({
+ message: `Couldn't find feature ${ent.internal_feature_id} for entitlement ${ent.id}`,
+ code: ErrCode.FeatureNotFound,
+ });
+ }
- return {
- ...ent,
- feature,
- };
- }) as FullEntitlement[];
+ return {
+ ...ent,
+ feature,
+ };
+ }) as FullEntitlement[];
};
diff --git a/server/src/internal/products/free-trials/FreeTrialService.ts b/server/src/internal/products/free-trials/FreeTrialService.ts
index c37036fdf..842443ffc 100644
--- a/server/src/internal/products/free-trials/FreeTrialService.ts
+++ b/server/src/internal/products/free-trials/FreeTrialService.ts
@@ -86,11 +86,10 @@ export class FreeTrialService {
.from(freeTrials)
.innerJoin(
products,
- eq(freeTrials.internal_product_id, products.internal_id)
+ eq(freeTrials.internal_product_id, products.internal_id),
)
.where(and(eq(products.org_id, orgId), eq(products.env, env)));
return result[0];
}
-
}
diff --git a/server/src/internal/products/free-trials/freeTrialUtils.ts b/server/src/internal/products/free-trials/freeTrialUtils.ts
index 557f4791a..9f6debb92 100644
--- a/server/src/internal/products/free-trials/freeTrialUtils.ts
+++ b/server/src/internal/products/free-trials/freeTrialUtils.ts
@@ -1,11 +1,11 @@
import {
- type CreateFreeTrial,
- CreateFreeTrialSchema,
- ErrCode,
- type FreeProductConfig,
- type FreeTrial,
- FreeTrialDuration,
- type Price
+ type CreateFreeTrial,
+ CreateFreeTrialSchema,
+ ErrCode,
+ type FreeProductConfig,
+ type FreeTrial,
+ FreeTrialDuration,
+ type Price,
} from "@autumn/shared";
import { addDays, addMinutes, addMonths, addYears } from "date-fns";
import type { DrizzleCli } from "@/db/initDrizzle.js";
diff --git a/server/src/internal/products/handlers/handleCopyProduct.ts b/server/src/internal/products/handlers/handleCopyProduct.ts
index c51e883f8..f42183e3a 100644
--- a/server/src/internal/products/handlers/handleCopyProduct.ts
+++ b/server/src/internal/products/handlers/handleCopyProduct.ts
@@ -7,114 +7,114 @@ import { initNewFeature } from "@/internal/features/internalFeatureRouter.js";
import { copyProduct } from "@/internal/products/productUtils.js";
export const handleCopyProduct = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Copy Product",
- handler: async (req, res) => {
- let { db, logtail: logger } = req;
+ routeHandler({
+ req,
+ res,
+ action: "Copy Product",
+ handler: async (req, res) => {
+ let { db, logtail: logger } = req;
- const { productId: fromProductId } = req.params;
- const orgId = req.orgId;
- const fromEnv = req.env;
- const { env: toEnv, id: toId, name: toName } = req.body;
+ const { productId: fromProductId } = req.params;
+ const orgId = req.orgId;
+ const fromEnv = req.env;
+ const { env: toEnv, id: toId, name: toName } = req.body;
- if (!toEnv || !toId || !toName) {
- throw new RecaseError({
- message: "env, id, and name are required",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!toEnv || !toId || !toName) {
+ throw new RecaseError({
+ message: "env, id, and name are required",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (fromEnv == toEnv && fromProductId == toId) {
- throw new RecaseError({
- message: "Product ID already exists",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (fromEnv == toEnv && fromProductId == toId) {
+ throw new RecaseError({
+ message: "Product ID already exists",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- // 1. Check if product exists in live already...
- const toProduct = await ProductService.get({
- db,
- id: toId,
- orgId,
- env: toEnv,
- });
+ // 1. Check if product exists in live already...
+ const toProduct = await ProductService.get({
+ db,
+ id: toId,
+ orgId,
+ env: toEnv,
+ });
- if (toProduct) {
- throw new RecaseError({
- message: "Product already exists in live... can't copy again",
- code: ErrCode.ProductAlreadyExists,
- statusCode: 400,
- });
- }
+ if (toProduct) {
+ throw new RecaseError({
+ message: "Product already exists in live... can't copy again",
+ code: ErrCode.ProductAlreadyExists,
+ statusCode: 400,
+ });
+ }
- // 1. Get sandbox product
- const [fromFullProduct, fromFeatures, toFeatures] = await Promise.all([
- ProductService.getFull({
- db,
- idOrInternalId: fromProductId,
- orgId,
- env: fromEnv,
- }),
- FeatureService.list({
- db,
- orgId,
- env: fromEnv,
- }),
- FeatureService.list({
- db,
- orgId,
- env: toEnv,
- }),
- ]);
+ // 1. Get sandbox product
+ const [fromFullProduct, fromFeatures, toFeatures] = await Promise.all([
+ ProductService.getFull({
+ db,
+ idOrInternalId: fromProductId,
+ orgId,
+ env: fromEnv,
+ }),
+ FeatureService.list({
+ db,
+ orgId,
+ env: fromEnv,
+ }),
+ FeatureService.list({
+ db,
+ orgId,
+ env: toEnv,
+ }),
+ ]);
- if (fromEnv != toEnv) {
- for (const fromFeature of fromFeatures) {
- const toFeature = toFeatures.find((f) => f.id == fromFeature.id);
+ if (fromEnv != toEnv) {
+ for (const fromFeature of fromFeatures) {
+ const toFeature = toFeatures.find((f) => f.id == fromFeature.id);
- if (toFeature && fromFeature.type !== toFeature.type) {
- throw new RecaseError({
- message: `Feature ${fromFeature.name} exists in ${toEnv}, but has a different config. Please match them then try again.`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (toFeature && fromFeature.type !== toFeature.type) {
+ throw new RecaseError({
+ message: `Feature ${fromFeature.name} exists in ${toEnv}, but has a different config. Please match them then try again.`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (!toFeature) {
- let res = await FeatureService.insert({
- db,
- data: initNewFeature({
- data: CreateFeatureSchema.parse(fromFeature),
- orgId,
- env: toEnv,
- }),
- logger,
- });
+ if (!toFeature) {
+ let res = await FeatureService.insert({
+ db,
+ data: initNewFeature({
+ data: CreateFeatureSchema.parse(fromFeature),
+ orgId,
+ env: toEnv,
+ }),
+ logger,
+ });
- toFeatures.push(res![0]);
- }
- }
- }
+ toFeatures.push(res![0]);
+ }
+ }
+ }
- // // 2. Copy product
- await copyProduct({
- db,
- product: fromFullProduct,
- toOrgId: orgId,
- toId,
- toName,
- fromEnv,
- toEnv: toEnv,
- toFeatures,
- fromFeatures,
- org: req.org,
- logger,
- });
+ // // 2. Copy product
+ await copyProduct({
+ db,
+ product: fromFullProduct,
+ toOrgId: orgId,
+ toId,
+ toName,
+ fromEnv,
+ toEnv: toEnv,
+ toFeatures,
+ fromFeatures,
+ org: req.org,
+ logger,
+ });
- // 2. Get product from sandbox
- res.status(200).send({ message: "Product copied" });
- },
- });
+ // 2. Get product from sandbox
+ res.status(200).send({ message: "Product copied" });
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleCreateProduct.ts b/server/src/internal/products/handlers/handleCreateProduct.ts
index dd8327bcc..7ea695900 100644
--- a/server/src/internal/products/handlers/handleCreateProduct.ts
+++ b/server/src/internal/products/handlers/handleCreateProduct.ts
@@ -1,35 +1,35 @@
import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js";
import {
- handleNewFreeTrial,
- validateAndInitFreeTrial,
+ handleNewFreeTrial,
+ validateAndInitFreeTrial,
} from "@/internal/products/free-trials/freeTrialUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import {
- CreateProductSchema,
- Entitlement,
- ErrCode,
- FreeTrial,
- FullProduct,
- Price,
- Product,
- ProductItem,
- ProductResponseSchema,
- ProductV2,
+ CreateProductSchema,
+ Entitlement,
+ ErrCode,
+ FreeTrial,
+ FullProduct,
+ Price,
+ Product,
+ ProductItem,
+ ProductResponseSchema,
+ ProductV2,
} from "@autumn/shared";
import {
- keyToTitle,
- notNullish,
- nullish,
- validateId,
+ keyToTitle,
+ notNullish,
+ nullish,
+ validateId,
} from "@/utils/genUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import {
- constructProduct,
- getGroupToDefaults,
- initProductInStripe,
+ constructProduct,
+ getGroupToDefaults,
+ initProductInStripe,
} from "@/internal/products/productUtils.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
@@ -39,221 +39,221 @@ import { isDefaultTrial } from "../productUtils/classifyProduct.js";
import { validateOneOffTrial } from "../free-trials/freeTrialUtils.js";
const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => {
- let { free_trial, items } = req.body;
- let { orgId, env, db, features } = req;
+ let { free_trial, items } = req.body;
+ let { orgId, env, db, features } = req;
- let productData = CreateProductSchema.parse(req.body);
+ let productData = CreateProductSchema.parse(req.body);
- validateId("Product", productData.id);
+ validateId("Product", productData.id);
- if (nullish(req.body.name)) {
- productData.name = keyToTitle(productData.id);
- }
+ if (nullish(req.body.name)) {
+ productData.name = keyToTitle(productData.id);
+ }
- const existing = await ProductService.get({
- db,
- orgId,
- env,
- id: productData.id,
- });
+ const existing = await ProductService.get({
+ db,
+ orgId,
+ env,
+ id: productData.id,
+ });
- // 1. If existing product, throw error
- if (existing) {
- throw new RecaseError({
- message: `Product ${productData.id} already exists`,
- code: ErrCode.ProductAlreadyExists,
- statusCode: 400,
- });
- }
+ // 1. If existing product, throw error
+ if (existing) {
+ throw new RecaseError({
+ message: `Product ${productData.id} already exists`,
+ code: ErrCode.ProductAlreadyExists,
+ statusCode: 400,
+ });
+ }
- // 2. Validate items if exist
+ // 2. Validate items if exist
- if (items && !Array.isArray(items)) {
- throw new RecaseError({
- message: "Items must be an array",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- } else if (items) {
- validateProductItems({
- newItems: items,
- features,
- orgId: req.orgId,
- env: req.env,
- });
- }
+ if (items && !Array.isArray(items)) {
+ throw new RecaseError({
+ message: "Items must be an array",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ } else if (items) {
+ validateProductItems({
+ newItems: items,
+ features,
+ orgId: req.orgId,
+ env: req.env,
+ });
+ }
- // 3. Validate free trial if exist
- let freeTrial: FreeTrial | null = null;
- if (notNullish(free_trial)) {
- // console.log("Free trial before:", free_trial);
- freeTrial = validateAndInitFreeTrial({
- freeTrial: free_trial,
- internalProductId: productData.id,
- isCustom: false,
- });
- // console.log("Free trial after:", freeTrial);
- }
+ // 3. Validate free trial if exist
+ let freeTrial: FreeTrial | null = null;
+ if (notNullish(free_trial)) {
+ // console.log("Free trial before:", free_trial);
+ freeTrial = validateAndInitFreeTrial({
+ freeTrial: free_trial,
+ internalProductId: productData.id,
+ isCustom: false,
+ });
+ // console.log("Free trial after:", freeTrial);
+ }
- return {
- features,
- freeTrial,
- productData,
- };
+ return {
+ features,
+ freeTrial,
+ productData,
+ };
};
export const disableCurrentDefault = async ({
- req,
- newProduct,
- items,
- freeTrial,
+ req,
+ newProduct,
+ items,
+ freeTrial,
}: {
- req: ExtendedRequest;
- newProduct: Product;
- items: ProductItem[];
+ req: ExtendedRequest;
+ newProduct: Product;
+ items: ProductItem[];
- freeTrial: FreeTrial | null;
+ freeTrial: FreeTrial | null;
}) => {
- const { db, org, env, logger } = req;
- let defaultProds = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ const { db, org, env, logger } = req;
+ let defaultProds = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- defaultProds = defaultProds.filter((prod) => prod.id !== newProduct.id);
+ defaultProds = defaultProds.filter((prod) => prod.id !== newProduct.id);
- if (defaultProds.length === 0) return;
+ if (defaultProds.length === 0) return;
- const defaults = getGroupToDefaults({
- defaultProds,
- })?.[newProduct.group];
+ const defaults = getGroupToDefaults({
+ defaultProds,
+ })?.[newProduct.group];
- const willBeDefaultTrial = isDefaultTrial({
- product: {
- ...newProduct,
- free_trial: freeTrial,
- items: items || [],
- },
- });
+ const willBeDefaultTrial = isDefaultTrial({
+ product: {
+ ...newProduct,
+ free_trial: freeTrial,
+ items: items || [],
+ },
+ });
- if (willBeDefaultTrial) {
- // Disable current default trial
- const curDefault = defaults?.defaultTrial;
- if (curDefault) {
- logger.info(
- `Disabling trial on cur default trial product: ${curDefault.id}`
- );
- await ProductService.updateByInternalId({
- db,
- internalId: curDefault.internal_id,
- update: { is_default: false },
- });
- }
- } else if (newProduct.is_default) {
- const curDefault = defaults?.free;
- if (curDefault) {
- logger.info(`Disabling trial on cur default product: ${curDefault.id}`);
- await ProductService.updateByInternalId({
- db,
- internalId: curDefault.internal_id,
- update: { is_default: false },
- });
- }
- }
+ if (willBeDefaultTrial) {
+ // Disable current default trial
+ const curDefault = defaults?.defaultTrial;
+ if (curDefault) {
+ logger.info(
+ `Disabling trial on cur default trial product: ${curDefault.id}`,
+ );
+ await ProductService.updateByInternalId({
+ db,
+ internalId: curDefault.internal_id,
+ update: { is_default: false },
+ });
+ }
+ } else if (newProduct.is_default) {
+ const curDefault = defaults?.free;
+ if (curDefault) {
+ logger.info(`Disabling trial on cur default product: ${curDefault.id}`);
+ await ProductService.updateByInternalId({
+ db,
+ internalId: curDefault.internal_id,
+ update: { is_default: false },
+ });
+ }
+ }
};
export const handleCreateProduct = async (req: Request, res: any) =>
- routeHandler({
- req,
- res,
- action: "POST /products",
- handler: async (req, res) => {
- let { items } = req.body;
- let { logtail: logger, org, features, env, db } = req;
+ routeHandler({
+ req,
+ res,
+ action: "POST /products",
+ handler: async (req, res) => {
+ let { items } = req.body;
+ let { logtail: logger, org, features, env, db } = req;
- let { freeTrial, productData } = await validateCreateProduct({
- req,
- });
+ let { freeTrial, productData } = await validateCreateProduct({
+ req,
+ });
- let newProduct = constructProduct({
- productData,
- orgId: org.id,
- env,
- });
+ let newProduct = constructProduct({
+ productData,
+ orgId: org.id,
+ env,
+ });
- await disableCurrentDefault({
- req,
- newProduct,
- items,
- freeTrial: freeTrial || null,
- });
+ await disableCurrentDefault({
+ req,
+ newProduct,
+ items,
+ freeTrial: freeTrial || null,
+ });
- let product = await ProductService.insert({ db, product: newProduct });
+ let product = await ProductService.insert({ db, product: newProduct });
- let prices: Price[] = [];
- let entitlements: Entitlement[] = [];
- if (notNullish(items)) {
- const res = await handleNewProductItems({
- db,
- product,
- features,
- curPrices: [],
- curEnts: [],
- newItems: items,
- logger,
- isCustom: false,
- newVersion: false,
- });
- prices = res.prices;
- entitlements = res.entitlements;
- }
-
- await validateOneOffTrial({
- prices,
- freeTrial: freeTrial || null,
- });
+ let prices: Price[] = [];
+ let entitlements: Entitlement[] = [];
+ if (notNullish(items)) {
+ const res = await handleNewProductItems({
+ db,
+ product,
+ features,
+ curPrices: [],
+ curEnts: [],
+ newItems: items,
+ logger,
+ isCustom: false,
+ newVersion: false,
+ });
+ prices = res.prices;
+ entitlements = res.entitlements;
+ }
- await initProductInStripe({
- db,
- product: {
- ...product,
- prices,
- entitlements,
- } as FullProduct,
- org,
- env,
- logger,
- });
+ await validateOneOffTrial({
+ prices,
+ freeTrial: freeTrial || null,
+ });
- if (notNullish(freeTrial)) {
- await handleNewFreeTrial({
- db,
- newFreeTrial: freeTrial,
- curFreeTrial: null,
- internalProductId: product.internal_id,
- isCustom: false,
- });
- }
+ await initProductInStripe({
+ db,
+ product: {
+ ...product,
+ prices,
+ entitlements,
+ } as FullProduct,
+ org,
+ env,
+ logger,
+ });
- await addTaskToQueue({
- jobName: JobName.DetectBaseVariant,
- payload: {
- curProduct: {
- ...product,
- prices,
- entitlements: [],
- },
- },
- });
+ if (notNullish(freeTrial)) {
+ await handleNewFreeTrial({
+ db,
+ newFreeTrial: freeTrial,
+ curFreeTrial: null,
+ internalProductId: product.internal_id,
+ isCustom: false,
+ });
+ }
- res.status(200).json(
- ProductResponseSchema.parse({
- ...product,
- autumn_id: product.internal_id,
- items: items || [],
- free_trial: freeTrial,
- })
- );
- },
- });
+ await addTaskToQueue({
+ jobName: JobName.DetectBaseVariant,
+ payload: {
+ curProduct: {
+ ...product,
+ prices,
+ entitlements: [],
+ },
+ },
+ });
+
+ res.status(200).json(
+ ProductResponseSchema.parse({
+ ...product,
+ autumn_id: product.internal_id,
+ items: items || [],
+ free_trial: freeTrial,
+ }),
+ );
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleDeleteProduct.ts b/server/src/internal/products/handlers/handleDeleteProduct.ts
index b88cf2053..746b21d75 100644
--- a/server/src/internal/products/handlers/handleDeleteProduct.ts
+++ b/server/src/internal/products/handlers/handleDeleteProduct.ts
@@ -7,93 +7,93 @@ import { routeHandler } from "@/utils/routerUtils.js";
import { AppEnv, ErrCode } from "@autumn/shared";
export const handleDeleteProduct = (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "delete product",
- handler: async () => {
- const { db, orgId, env } = req;
- const { productId } = req.params;
- const { all_versions } = req.query;
+ routeHandler({
+ req,
+ res,
+ action: "delete product",
+ handler: async () => {
+ const { db, orgId, env } = req;
+ const { productId } = req.params;
+ const { all_versions } = req.query;
- const product = await ProductService.get({
- db,
- id: productId,
- orgId,
- env,
- });
+ const product = await ProductService.get({
+ db,
+ id: productId,
+ orgId,
+ env,
+ });
- if (!product) {
- throw new RecaseError({
- message: `Product ${productId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: 404,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${productId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: 404,
+ });
+ }
- let [latestCounts, allCounts] = await Promise.all([
- CusProdReadService.getCounts({
- db,
- internalProductId: product.internal_id,
- }),
- CusProdReadService.getCountsForAllVersions({
- db,
- productId: productId,
- orgId,
- env,
- }),
- ]);
+ let [latestCounts, allCounts] = await Promise.all([
+ CusProdReadService.getCounts({
+ db,
+ internalProductId: product.internal_id,
+ }),
+ CusProdReadService.getCountsForAllVersions({
+ db,
+ productId: productId,
+ orgId,
+ env,
+ }),
+ ]);
- let deleteAllVersions = all_versions === "true";
- let cusProdCount = deleteAllVersions ? allCounts.all : latestCounts.all;
+ let deleteAllVersions = all_versions === "true";
+ let cusProdCount = deleteAllVersions ? allCounts.all : latestCounts.all;
- if (cusProdCount > 0) {
- throw new RecaseError({
- message: "Cannot delete product with customers",
- code: ErrCode.ProductHasCustomers,
- statusCode: 400,
- });
- }
+ if (cusProdCount > 0) {
+ throw new RecaseError({
+ message: "Cannot delete product with customers",
+ code: ErrCode.ProductHasCustomers,
+ statusCode: 400,
+ });
+ }
- // if (cusProdCount > 0 && env == AppEnv.Sandbox) {
- // if (cusProdCount > 100) {
- // throw new RecaseError({
- // message:
- // "Cannot delete this product as it has more than 100 customers on it.",
- // code: ErrCode.ProductHasCustomers,
- // statusCode: 400,
- // });
- // }
+ // if (cusProdCount > 0 && env == AppEnv.Sandbox) {
+ // if (cusProdCount > 100) {
+ // throw new RecaseError({
+ // message:
+ // "Cannot delete this product as it has more than 100 customers on it.",
+ // code: ErrCode.ProductHasCustomers,
+ // statusCode: 400,
+ // });
+ // }
- // await CusProductService.deleteByProduct({
- // db,
- // productId: deleteAllVersions ? productId : undefined,
- // internalProductId: deleteAllVersions
- // ? undefined
- // : product.internal_id,
- // orgId,
- // env,
- // });
- // }
+ // await CusProductService.deleteByProduct({
+ // db,
+ // productId: deleteAllVersions ? productId : undefined,
+ // internalProductId: deleteAllVersions
+ // ? undefined
+ // : product.internal_id,
+ // orgId,
+ // env,
+ // });
+ // }
- // 2. Delete prices, entitlements, and product
- if (deleteAllVersions) {
- await ProductService.deleteByProductId({
- db,
- productId,
- orgId,
- env,
- });
- } else {
- await ProductService.deleteByInternalId({
- db,
- internalId: product.internal_id,
- orgId,
- env,
- });
- }
+ // 2. Delete prices, entitlements, and product
+ if (deleteAllVersions) {
+ await ProductService.deleteByProductId({
+ db,
+ productId,
+ orgId,
+ env,
+ });
+ } else {
+ await ProductService.deleteByInternalId({
+ db,
+ internalId: product.internal_id,
+ orgId,
+ env,
+ });
+ }
- res.status(200).json({ message: "Product deleted" });
- return;
- },
- });
+ res.status(200).json({ message: "Product deleted" });
+ return;
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleGetProduct.ts b/server/src/internal/products/handlers/handleGetProduct.ts
index 48764dba7..47689885a 100644
--- a/server/src/internal/products/handlers/handleGetProduct.ts
+++ b/server/src/internal/products/handlers/handleGetProduct.ts
@@ -8,53 +8,53 @@ import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const handleGetProduct = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "get product",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { productId } = req.params;
- let { schemaVersion } = req.query as { schemaVersion: string };
+ routeHandler({
+ req,
+ res,
+ action: "get product",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { productId } = req.params;
+ let { schemaVersion } = req.query as { schemaVersion: string };
- const { db, orgId, env } = req;
+ const { db, orgId, env } = req;
- if (!productId) {
- throw new RecaseError({
- message: "Product ID is required",
- code: ErrCode.InvalidRequest,
- });
- }
+ if (!productId) {
+ throw new RecaseError({
+ message: "Product ID is required",
+ code: ErrCode.InvalidRequest,
+ });
+ }
- let [product, features] = await Promise.all([
- ProductService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: productId,
- }),
- FeatureService.getFromReq(req),
- ]);
+ let [product, features] = await Promise.all([
+ ProductService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: productId,
+ }),
+ FeatureService.getFromReq(req),
+ ]);
- if (!product) {
- throw new RecaseError({
- message: `Product ${productId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${productId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let schemaVersionInt = schemaVersion ? parseInt(schemaVersion) : 2;
+ let schemaVersionInt = schemaVersion ? parseInt(schemaVersion) : 2;
- if (schemaVersionInt == 1) {
- res.status(200).json(product);
- } else {
- res.status(200).json(
- await getProductResponse({
- product,
- features,
- currency: req.org.default_currency,
- })
- );
- }
- },
- });
+ if (schemaVersionInt == 1) {
+ res.status(200).json(product);
+ } else {
+ res.status(200).json(
+ await getProductResponse({
+ product,
+ features,
+ currency: req.org.default_currency,
+ }),
+ );
+ }
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleGetProductDeleteInfo.ts b/server/src/internal/products/handlers/handleGetProductDeleteInfo.ts
index a10d9221e..fe5faf45c 100644
--- a/server/src/internal/products/handlers/handleGetProductDeleteInfo.ts
+++ b/server/src/internal/products/handlers/handleGetProductDeleteInfo.ts
@@ -7,65 +7,65 @@ import { ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
export const handleGetProductDeleteInfo = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Get product deletion info",
- handler: async (req: ExtendedRequest, res: any) => {
- try {
- // 1. Get number of versions
- const { db, orgId, env } = req;
- let product = await ProductService.get({
- db,
- id: req.params.productId,
- orgId: req.orgId,
- env: req.env,
- });
+ routeHandler({
+ req,
+ res,
+ action: "Get product deletion info",
+ handler: async (req: ExtendedRequest, res: any) => {
+ try {
+ // 1. Get number of versions
+ const { db, orgId, env } = req;
+ let product = await ProductService.get({
+ db,
+ id: req.params.productId,
+ orgId: req.orgId,
+ env: req.env,
+ });
- if (!product) {
- throw new RecaseError({
- message: `Product ${req.params.productId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${req.params.productId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let [allVersions, latestVersion, deletionText] = await Promise.all([
- CusProdReadService.existsForProduct({
- db,
- productId: req.params.productId,
- }),
- CusProdReadService.existsForProduct({
- db,
- internalProductId: product.internal_id,
- }),
- ProductService.getDeletionText({
- db,
- productId: req.params.productId,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ let [allVersions, latestVersion, deletionText] = await Promise.all([
+ CusProdReadService.existsForProduct({
+ db,
+ productId: req.params.productId,
+ }),
+ CusProdReadService.existsForProduct({
+ db,
+ internalProductId: product.internal_id,
+ }),
+ ProductService.getDeletionText({
+ db,
+ productId: req.params.productId,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- // 2. Get cus products
+ // 2. Get cus products
- res.status(200).send({
- numVersion: product.version,
- hasCusProducts: allVersions,
- hasCusProductsLatest: latestVersion,
- customerName:
- deletionText[0]?.name ||
- deletionText[0]?.email ||
- deletionText[0]?.id,
- totalCount: deletionText[0]?.totalCount,
- });
- } catch (error) {
- handleRequestError({
- error,
- req,
- res,
- action: "Get product info",
- });
- }
- },
- });
+ res.status(200).send({
+ numVersion: product.version,
+ hasCusProducts: allVersions,
+ hasCusProductsLatest: latestVersion,
+ customerName:
+ deletionText[0]?.name ||
+ deletionText[0]?.email ||
+ deletionText[0]?.id,
+ totalCount: deletionText[0]?.totalCount,
+ });
+ } catch (error) {
+ handleRequestError({
+ error,
+ req,
+ res,
+ action: "Get product info",
+ });
+ }
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleListProducts.ts b/server/src/internal/products/handlers/handleListProducts.ts
index 5631e324c..a042d2d14 100644
--- a/server/src/internal/products/handlers/handleListProducts.ts
+++ b/server/src/internal/products/handlers/handleListProducts.ts
@@ -5,35 +5,35 @@ import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js";
export const handleListProducts = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "List products",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { db, orgId, env } = req;
+ routeHandler({
+ req,
+ res,
+ action: "List products",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { db, orgId, env } = req;
- const [features, products] = await Promise.all([
- FeatureService.getFromReq(req),
- ProductService.listFull({
- db,
- orgId,
- env,
- }),
- ]);
+ const [features, products] = await Promise.all([
+ FeatureService.getFromReq(req),
+ ProductService.listFull({
+ db,
+ orgId,
+ env,
+ }),
+ ]);
- let prods = await Promise.all(
- products.map((p) => getProductResponse({ product: p, features }))
- );
+ let prods = await Promise.all(
+ products.map((p) => getProductResponse({ product: p, features })),
+ );
- if (req.query.v1_schema === "true") {
- res.status(200).json({
- list: products,
- });
- return;
- }
+ if (req.query.v1_schema === "true") {
+ res.status(200).json({
+ list: products,
+ });
+ return;
+ }
- res.status(200).json({
- list: prods,
- });
- },
- });
+ res.status(200).json({
+ list: prods,
+ });
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleListProductsBeta.ts b/server/src/internal/products/handlers/handleListProductsBeta.ts
index 1cd0eeed9..a3eaaca64 100644
--- a/server/src/internal/products/handlers/handleListProductsBeta.ts
+++ b/server/src/internal/products/handlers/handleListProductsBeta.ts
@@ -1,8 +1,8 @@
import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js";
import { ProductService } from "@/internal/products/ProductService.js";
import type {
- ExtendedRequest,
- ExtendedResponse,
+ ExtendedRequest,
+ ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js";
@@ -10,66 +10,66 @@ import { sortFullProducts } from "../productUtils/sortProductUtils.js";
// biome-ignore lint/suspicious/noExplicitAny: alright buddy WRAP it up 👉🚪
export const handleListProductsBeta = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "list products v2 (beta)",
- handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
- const { org, features, env, db } = req;
- const customerId = req.query.customer_id;
- const entityId = req.query.entity_id as string | undefined;
- const includeAll = req.query.include_archived as unknown as boolean;
+ routeHandler({
+ req,
+ res,
+ action: "list products v2 (beta)",
+ handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
+ const { org, features, env, db } = req;
+ const customerId = req.query.customer_id;
+ const entityId = req.query.entity_id as string | undefined;
+ const includeAll = req.query.include_archived as unknown as boolean;
- const [products, customer] = await Promise.all([
- ProductService.listFull({
- db,
- orgId: org.id,
- env,
- archived: includeAll ? undefined : false,
- }),
- (async () => {
- if (!customerId) {
- return undefined;
- }
+ const [products, customer] = await Promise.all([
+ ProductService.listFull({
+ db,
+ orgId: org.id,
+ env,
+ archived: includeAll ? undefined : false,
+ }),
+ (async () => {
+ if (!customerId) {
+ return undefined;
+ }
- return await getCusWithCache({
- db,
- org,
- idOrInternalId: customerId as string,
- allowNotFound: true,
- entityId: entityId as string,
- env,
- logger: req.logger,
- });
- })(),
- ]);
+ return await getCusWithCache({
+ db,
+ org,
+ idOrInternalId: customerId as string,
+ allowNotFound: true,
+ entityId: entityId as string,
+ env,
+ logger: req.logger,
+ });
+ })(),
+ ]);
- if (req.query.v1_schema === "true") {
- res.status(200).json({
- list: products,
- });
- return;
- }
+ if (req.query.v1_schema === "true") {
+ res.status(200).json({
+ list: products,
+ });
+ return;
+ }
- sortFullProducts({ products });
+ sortFullProducts({ products });
- const batchResponse = [];
- for (const p of products) {
- batchResponse.push(
- getProductResponse({
- product: p,
- features,
- currency: org.default_currency || undefined,
- db,
- fullCus: customer ? customer : undefined,
- })
- );
- }
+ const batchResponse = [];
+ for (const p of products) {
+ batchResponse.push(
+ getProductResponse({
+ product: p,
+ features,
+ currency: org.default_currency || undefined,
+ db,
+ fullCus: customer ? customer : undefined,
+ }),
+ );
+ }
- const productResponse = await Promise.all(batchResponse);
+ const productResponse = await Promise.all(batchResponse);
- res.status(200).json({
- list: productResponse,
- });
- },
- });
+ res.status(200).json({
+ list: productResponse,
+ });
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts
index d75744f94..787ee42e2 100644
--- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts
+++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts
@@ -17,204 +17,204 @@ import { productsAreSame } from "../../productUtils/compareProductUtils.js";
import { initProductInStripe } from "../../productUtils.js";
import { mapToProductItems } from "../../productV2Utils.js";
import {
- disableCurrentDefault,
- handleCreateProduct,
+ disableCurrentDefault,
+ handleCreateProduct,
} from "../handleCreateProduct.js";
import { handleVersionProductV2 } from "../handleVersionProduct.js";
import { handleUpdateProductDetails } from "./updateProductDetails.js";
import { formatPrice } from "../../prices/priceUtils.js";
export const handleUpdateProductV2 = async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Update product",
- handler: async () => {
- const { productId } = req.params;
- const { version, upsert, disable_version } = req.query;
- const { orgId, env, logger, db } = req;
+ routeHandler({
+ req,
+ res,
+ action: "Update product",
+ handler: async () => {
+ const { productId } = req.params;
+ const { version, upsert, disable_version } = req.query;
+ const { orgId, env, logger, db } = req;
- const [features, org, fullProduct, rewardPrograms, _defaultProds] =
- await Promise.all([
- FeatureService.getFromReq(req),
- OrgService.getFromReq(req),
- ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- version: version ? parseInt(version) : undefined,
- allowNotFound: upsert === "true",
- }),
- RewardProgramService.getByProductId({
- db,
- productIds: [productId],
- orgId,
- env,
- }),
- ProductService.listDefault({
- db,
- orgId,
- env,
- }),
- ]);
+ const [features, org, fullProduct, rewardPrograms, _defaultProds] =
+ await Promise.all([
+ FeatureService.getFromReq(req),
+ OrgService.getFromReq(req),
+ ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ version: version ? parseInt(version) : undefined,
+ allowNotFound: upsert === "true",
+ }),
+ RewardProgramService.getByProductId({
+ db,
+ productIds: [productId],
+ orgId,
+ env,
+ }),
+ ProductService.listDefault({
+ db,
+ orgId,
+ env,
+ }),
+ ]);
- if (!fullProduct) {
- if (upsert === "true") {
- await handleCreateProduct(req, res);
- return;
- }
+ if (!fullProduct) {
+ if (upsert === "true") {
+ await handleCreateProduct(req, res);
+ return;
+ }
- throw new RecaseError({
- message: "Product not found",
- code: ErrCode.ProductNotFound,
- statusCode: 404,
- });
- }
+ throw new RecaseError({
+ message: "Product not found",
+ code: ErrCode.ProductNotFound,
+ statusCode: 404,
+ });
+ }
- const cusProductsCurVersion =
- await CusProductService.getByInternalProductId({
- db,
- internalProductId: fullProduct.internal_id,
- });
+ const cusProductsCurVersion =
+ await CusProductService.getByInternalProductId({
+ db,
+ internalProductId: fullProduct.internal_id,
+ });
- const cusProductExists = cusProductsCurVersion.length > 0;
+ const cusProductExists = cusProductsCurVersion.length > 0;
- // console.log("Updating product", {
- // id: fullProduct.id,
- // body: req.body,
- // });
- await disableCurrentDefault({
- req,
- newProduct: {
- ...fullProduct,
- ...req.body,
- },
- items:
- req.body.items ||
- mapToProductItems({
- prices: fullProduct.prices,
- entitlements: fullProduct.entitlements,
- features,
- }),
- freeTrial: req.body.free_trial || fullProduct.free_trial || null,
- });
+ // console.log("Updating product", {
+ // id: fullProduct.id,
+ // body: req.body,
+ // });
+ await disableCurrentDefault({
+ req,
+ newProduct: {
+ ...fullProduct,
+ ...req.body,
+ },
+ items:
+ req.body.items ||
+ mapToProductItems({
+ prices: fullProduct.prices,
+ entitlements: fullProduct.entitlements,
+ features,
+ }),
+ freeTrial: req.body.free_trial || fullProduct.free_trial || null,
+ });
- await handleUpdateProductDetails({
- db,
- curProduct: fullProduct,
- newProduct: UpdateProductSchema.parse(req.body),
- newFreeTrial: req.body.free_trial,
- items: req.body.items,
- org,
- rewardPrograms,
- logger,
- });
+ await handleUpdateProductDetails({
+ db,
+ curProduct: fullProduct,
+ newProduct: UpdateProductSchema.parse(req.body),
+ newFreeTrial: req.body.free_trial,
+ items: req.body.items,
+ org,
+ rewardPrograms,
+ logger,
+ });
- const itemsExist = notNullish(req.body.items);
- if (cusProductExists && itemsExist) {
- if (disable_version === "true") {
- throw new RecaseError({
- message: "Cannot auto save product as there are existing customers",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ const itemsExist = notNullish(req.body.items);
+ if (cusProductExists && itemsExist) {
+ if (disable_version === "true") {
+ throw new RecaseError({
+ message: "Cannot auto save product as there are existing customers",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- const { itemsSame, freeTrialsSame } = productsAreSame({
- newProductV2: req.body,
- curProductV1: fullProduct,
- features,
- });
- const productSame = itemsSame && freeTrialsSame;
+ const { itemsSame, freeTrialsSame } = productsAreSame({
+ newProductV2: req.body,
+ curProductV1: fullProduct,
+ features,
+ });
+ const productSame = itemsSame && freeTrialsSame;
- if (!productSame) {
- await handleVersionProductV2({
- req,
- res,
- latestProduct: fullProduct,
- org,
- env,
- items: req.body.items,
- freeTrial: req.body.free_trial,
- });
- return;
- }
- res.status(200).send(fullProduct);
- return;
- }
+ if (!productSame) {
+ await handleVersionProductV2({
+ req,
+ res,
+ latestProduct: fullProduct,
+ org,
+ env,
+ items: req.body.items,
+ freeTrial: req.body.free_trial,
+ });
+ return;
+ }
+ res.status(200).send(fullProduct);
+ return;
+ }
- const { items, free_trial } = req.body;
+ const { items, free_trial } = req.body;
- if (free_trial !== undefined) {
- await validateOneOffTrial({
- prices: fullProduct.prices,
- freeTrial: free_trial,
- });
- }
+ if (free_trial !== undefined) {
+ await validateOneOffTrial({
+ prices: fullProduct.prices,
+ freeTrial: free_trial,
+ });
+ }
- await handleNewProductItems({
- db,
- curPrices: fullProduct.prices,
- curEnts: fullProduct.entitlements,
- newItems: items,
- features,
- product: fullProduct,
- logger,
- isCustom: false,
- });
+ await handleNewProductItems({
+ db,
+ curPrices: fullProduct.prices,
+ curEnts: fullProduct.entitlements,
+ newItems: items,
+ features,
+ product: fullProduct,
+ logger,
+ isCustom: false,
+ });
- // New full product
- const newFullProduct = await ProductService.getFull({
- db,
- idOrInternalId: fullProduct.id,
- orgId,
- env,
- });
+ // New full product
+ const newFullProduct = await ProductService.getFull({
+ db,
+ idOrInternalId: fullProduct.id,
+ orgId,
+ env,
+ });
- if (free_trial !== undefined) {
- await validateOneOffTrial({
- prices: newFullProduct.prices,
- freeTrial: free_trial,
- });
+ if (free_trial !== undefined) {
+ await validateOneOffTrial({
+ prices: newFullProduct.prices,
+ freeTrial: free_trial,
+ });
- await handleNewFreeTrial({
- db,
- curFreeTrial: fullProduct.free_trial,
- newFreeTrial: free_trial,
- internalProductId: fullProduct.internal_id,
- isCustom: false,
- product: fullProduct,
- });
- }
+ await handleNewFreeTrial({
+ db,
+ curFreeTrial: fullProduct.free_trial,
+ newFreeTrial: free_trial,
+ internalProductId: fullProduct.internal_id,
+ isCustom: false,
+ product: fullProduct,
+ });
+ }
- // New full product
- await initProductInStripe({
- db,
- product: newFullProduct,
- org,
- env,
- logger,
- });
+ // New full product
+ await initProductInStripe({
+ db,
+ product: newFullProduct,
+ org,
+ env,
+ logger,
+ });
- logger.info("Adding task to queue to detect base variant");
- await addTaskToQueue({
- jobName: JobName.DetectBaseVariant,
- payload: {
- curProduct: newFullProduct,
- },
- });
+ logger.info("Adding task to queue to detect base variant");
+ await addTaskToQueue({
+ jobName: JobName.DetectBaseVariant,
+ payload: {
+ curProduct: newFullProduct,
+ },
+ });
- await addTaskToQueue({
- jobName: JobName.RewardMigration,
- payload: {
- oldPrices: fullProduct.prices,
- productId: fullProduct.id,
- orgId: org.id,
- env,
- },
- });
- res.status(200).send({ message: "Product updated" });
- return;
- },
- });
+ await addTaskToQueue({
+ jobName: JobName.RewardMigration,
+ payload: {
+ oldPrices: fullProduct.prices,
+ productId: fullProduct.id,
+ orgId: org.id,
+ env,
+ },
+ });
+ res.status(200).send({ message: "Product updated" });
+ return;
+ },
+ });
diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts
index 9c422b2d8..dd60e2de0 100644
--- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts
+++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts
@@ -3,281 +3,281 @@ import { CusProductService } from "@/internal/customers/cusProducts/CusProductSe
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import {
- AppEnv,
- CreateFreeTrial,
- ErrCode,
- FreeTrial,
- FullProduct,
- isFreeProductV2,
- Organization,
- Product,
- ProductItem,
- RewardProgram,
- UpdateProduct,
+ AppEnv,
+ CreateFreeTrial,
+ ErrCode,
+ FreeTrial,
+ FullProduct,
+ isFreeProductV2,
+ Organization,
+ Product,
+ ProductItem,
+ RewardProgram,
+ UpdateProduct,
} from "@autumn/shared";
import { ProductService } from "../../ProductService.js";
import { FreeTrialService } from "../../free-trials/FreeTrialService.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { usagePriceToProductName } from "../../prices/priceUtils/usagePriceUtils/convertUsagePrice.js";
import {
- isFeaturePriceItem,
- isPriceItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "../../product-items/productItemUtils/getItemType.js";
import { isFreeProduct } from "../../productUtils.js";
import { isStripeConnected } from "@/internal/orgs/orgUtils.js";
import { isDefaultTrialFullProduct } from "../../productUtils/classifyProduct.js";
const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => {
- if (notNullish(prod2.id) && prod1.id != prod2.id) {
- return false;
- }
+ if (notNullish(prod2.id) && prod1.id != prod2.id) {
+ return false;
+ }
- if (notNullish(prod2.name) && prod1.name != prod2.name) {
- return false;
- }
+ if (notNullish(prod2.name) && prod1.name != prod2.name) {
+ return false;
+ }
- if (notNullish(prod2.group) && prod1.group != prod2.group) {
- return false;
- }
+ if (notNullish(prod2.group) && prod1.group != prod2.group) {
+ return false;
+ }
- if (notNullish(prod2.is_add_on) && prod1.is_add_on != prod2.is_add_on) {
- return false;
- }
+ if (notNullish(prod2.is_add_on) && prod1.is_add_on != prod2.is_add_on) {
+ return false;
+ }
- if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) {
- return false;
- }
+ if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) {
+ return false;
+ }
- if (notNullish(prod2.archived) && prod1.archived !== prod2.archived) {
- return false;
- }
+ if (notNullish(prod2.archived) && prod1.archived !== prod2.archived) {
+ return false;
+ }
- return true;
+ return true;
};
const updateStripeProductNames = async ({
- db,
- org,
- curProduct,
- newName,
- logger,
+ db,
+ org,
+ curProduct,
+ newName,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- curProduct: FullProduct;
- newName: string;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ curProduct: FullProduct;
+ newName: string;
+ logger: any;
}) => {
- if (!isStripeConnected({ org, env: curProduct.env as AppEnv })) return;
+ if (!isStripeConnected({ org, env: curProduct.env as AppEnv })) return;
- const stripeCli = createStripeCli({
- org,
- env: curProduct.env as AppEnv,
- });
- let stripeProdId = curProduct.processor?.id;
+ const stripeCli = createStripeCli({
+ org,
+ env: curProduct.env as AppEnv,
+ });
+ let stripeProdId = curProduct.processor?.id;
- if (!stripeProdId || !newName) {
- return;
- }
+ if (!stripeProdId || !newName) {
+ return;
+ }
- try {
- await stripeCli.products.update(stripeProdId, {
- name: newName,
- });
- } catch (error: any) {
- logger.error(
- `Error updating product ${curProduct.id} name in Stripe: ${error.message}`,
- {
- error,
- stripeProdId,
- newName,
- }
- );
- }
+ try {
+ await stripeCli.products.update(stripeProdId, {
+ name: newName,
+ });
+ } catch (error: any) {
+ logger.error(
+ `Error updating product ${curProduct.id} name in Stripe: ${error.message}`,
+ {
+ error,
+ stripeProdId,
+ newName,
+ },
+ );
+ }
- for (const price of curProduct.prices) {
- let stripeProdId = price.config?.stripe_product_id;
+ for (const price of curProduct.prices) {
+ let stripeProdId = price.config?.stripe_product_id;
- if (stripeProdId) {
- let name = usagePriceToProductName({
- price,
- fullProduct: {
- ...curProduct,
- name: newName,
- },
- });
+ if (stripeProdId) {
+ let name = usagePriceToProductName({
+ price,
+ fullProduct: {
+ ...curProduct,
+ name: newName,
+ },
+ });
- try {
- await stripeCli.products.update(stripeProdId, {
- name,
- });
- } catch (error: any) {
- logger.error(
- `Error updating price ${price.id} name in Stripe: ${error.message}`
- );
- }
- }
- }
+ try {
+ await stripeCli.products.update(stripeProdId, {
+ name,
+ });
+ } catch (error: any) {
+ logger.error(
+ `Error updating price ${price.id} name in Stripe: ${error.message}`,
+ );
+ }
+ }
+ }
};
const willBeDefaultTrial = ({
- newProduct,
- curProduct,
- newFreeTrial,
- newItems,
+ newProduct,
+ curProduct,
+ newFreeTrial,
+ newItems,
}: {
- newProduct: UpdateProduct;
- curProduct: FullProduct;
- newFreeTrial: FreeTrial;
- newItems: ProductItem[];
+ newProduct: UpdateProduct;
+ curProduct: FullProduct;
+ newFreeTrial: FreeTrial;
+ newItems: ProductItem[];
}) => {
- // 1. Get final default
- const finalDefault = notNullish(newProduct.is_default)
- ? newProduct.is_default
- : curProduct.is_default;
+ // 1. Get final default
+ const finalDefault = notNullish(newProduct.is_default)
+ ? newProduct.is_default
+ : curProduct.is_default;
- const finalFreeTrial = notNullish(newFreeTrial)
- ? newFreeTrial
- : curProduct.free_trial;
+ const finalFreeTrial = notNullish(newFreeTrial)
+ ? newFreeTrial
+ : curProduct.free_trial;
- const finalIsFree = notNullish(newItems)
- ? isFreeProductV2({ items: newItems })
- : isFreeProduct(curProduct.prices);
+ const finalIsFree = notNullish(newItems)
+ ? isFreeProductV2({ items: newItems })
+ : isFreeProduct(curProduct.prices);
- return finalDefault && !finalIsFree && finalFreeTrial;
+ return finalDefault && !finalIsFree && finalFreeTrial;
};
export const handleUpdateProductDetails = async ({
- db,
- newProduct,
- curProduct,
- newFreeTrial,
- items,
- org,
- rewardPrograms,
- logger,
+ db,
+ newProduct,
+ curProduct,
+ newFreeTrial,
+ items,
+ org,
+ rewardPrograms,
+ logger,
}: {
- db: DrizzleCli;
- curProduct: FullProduct;
- newProduct: UpdateProduct;
- newFreeTrial: FreeTrial;
- items: ProductItem[];
- org: Organization;
- rewardPrograms: RewardProgram[];
- logger: any;
+ db: DrizzleCli;
+ curProduct: FullProduct;
+ newProduct: UpdateProduct;
+ newFreeTrial: FreeTrial;
+ items: ProductItem[];
+ org: Organization;
+ rewardPrograms: RewardProgram[];
+ logger: any;
}) => {
- const customersOnAllVersions = await CusProductService.getByProductId({
- db,
- productId: curProduct.id,
- orgId: org.id,
- env: curProduct.env as AppEnv,
- });
+ const customersOnAllVersions = await CusProductService.getByProductId({
+ db,
+ productId: curProduct.id,
+ orgId: org.id,
+ env: curProduct.env as AppEnv,
+ });
- const trialConfig = await FreeTrialService.getByProductId({
- db,
- productId: curProduct.internal_id,
- });
+ const trialConfig = await FreeTrialService.getByProductId({
+ db,
+ productId: curProduct.internal_id,
+ });
- // Should error if:
- // - New product is a default product
- // - Org is not allowed to have paid default products
- // - Current product is not a default trial
+ // Should error if:
+ // - New product is a default product
+ // - Org is not allowed to have paid default products
+ // - Current product is not a default trial
- // Final prices are curProduct.prices or newProduct.prices
+ // Final prices are curProduct.prices or newProduct.prices
- if (
- newProduct.is_default &&
- !org.config.allow_paid_default &&
- !willBeDefaultTrial({
- newProduct,
- curProduct,
- newFreeTrial,
- newItems: items,
- })
- // && !isDefaultTrialFullProduct({
- // product: {
- // ...newProduct,
- // free_trial: newFreeTrial || curProduct.free_trial || null,
- // },
- // skipDefault: true,
- // })
- ) {
- // 1. Check if there are items
- if (items) {
- if (items.some((item) => isFeaturePriceItem(item) || isPriceItem(item))) {
- throw new RecaseError({
- message:
- "Cannot make a product default if it has fixed prices or paid features",
- code: ErrCode.InvalidProduct,
- statusCode: 400,
- });
- }
- } else {
- if (!isFreeProduct(curProduct.prices)) {
- throw new RecaseError({
- message:
- "Cannot make a product default if it has fixed prices or paid features",
- code: ErrCode.InvalidProduct,
- statusCode: 400,
- });
- }
- }
- }
+ if (
+ newProduct.is_default &&
+ !org.config.allow_paid_default &&
+ !willBeDefaultTrial({
+ newProduct,
+ curProduct,
+ newFreeTrial,
+ newItems: items,
+ })
+ // && !isDefaultTrialFullProduct({
+ // product: {
+ // ...newProduct,
+ // free_trial: newFreeTrial || curProduct.free_trial || null,
+ // },
+ // skipDefault: true,
+ // })
+ ) {
+ // 1. Check if there are items
+ if (items) {
+ if (items.some((item) => isFeaturePriceItem(item) || isPriceItem(item))) {
+ throw new RecaseError({
+ message:
+ "Cannot make a product default if it has fixed prices or paid features",
+ code: ErrCode.InvalidProduct,
+ statusCode: 400,
+ });
+ }
+ } else {
+ if (!isFreeProduct(curProduct.prices)) {
+ throw new RecaseError({
+ message:
+ "Cannot make a product default if it has fixed prices or paid features",
+ code: ErrCode.InvalidProduct,
+ statusCode: 400,
+ });
+ }
+ }
+ }
- if (productDetailsSame(curProduct, newProduct)) {
- return;
- }
+ if (productDetailsSame(curProduct, newProduct)) {
+ return;
+ }
- if (notNullish(newProduct.id) && newProduct.id !== curProduct.id) {
- if (customersOnAllVersions.length > 0) {
- throw new RecaseError({
- message: "Cannot change product ID because it has existing customers",
- code: ErrCode.ProductHasCustomers,
- statusCode: 400,
- });
- }
+ if (notNullish(newProduct.id) && newProduct.id !== curProduct.id) {
+ if (customersOnAllVersions.length > 0) {
+ throw new RecaseError({
+ message: "Cannot change product ID because it has existing customers",
+ code: ErrCode.ProductHasCustomers,
+ statusCode: 400,
+ });
+ }
- if (rewardPrograms.length > 0) {
- throw new RecaseError({
- message:
- "Cannot change product ID because existing reward programs are linked to it",
- code: ErrCode.ProductHasRewardPrograms,
- statusCode: 400,
- });
- }
- }
+ if (rewardPrograms.length > 0) {
+ throw new RecaseError({
+ message:
+ "Cannot change product ID because existing reward programs are linked to it",
+ code: ErrCode.ProductHasRewardPrograms,
+ statusCode: 400,
+ });
+ }
+ }
- // 2. Update product
- await ProductService.updateByInternalId({
- db,
- internalId: curProduct.internal_id,
- update: {
- id: newProduct.id,
- name: newProduct.name,
- group: newProduct.group,
- is_add_on: newProduct.is_add_on,
- is_default: newProduct.is_default,
- archived: newProduct.archived,
- },
- });
+ // 2. Update product
+ await ProductService.updateByInternalId({
+ db,
+ internalId: curProduct.internal_id,
+ update: {
+ id: newProduct.id,
+ name: newProduct.name,
+ group: newProduct.group,
+ is_add_on: newProduct.is_add_on,
+ is_default: newProduct.is_default,
+ archived: newProduct.archived,
+ },
+ });
- // Update product name in Stripe
- if (curProduct.name !== newProduct.name && notNullish(newProduct.name)) {
- logger.info(
- `Updating product (${curProduct.id}) name in Stripe to ${newProduct.name}`
- );
- await updateStripeProductNames({
- db,
- org,
- curProduct,
- newName: newProduct.name!,
- logger,
- });
- }
+ // Update product name in Stripe
+ if (curProduct.name !== newProduct.name && notNullish(newProduct.name)) {
+ logger.info(
+ `Updating product (${curProduct.id}) name in Stripe to ${newProduct.name}`,
+ );
+ await updateStripeProductNames({
+ db,
+ org,
+ curProduct,
+ newName: newProduct.name!,
+ logger,
+ });
+ }
- curProduct.name = newProduct.name || curProduct.name;
- curProduct.group = newProduct.group || curProduct.group;
- curProduct.is_add_on = newProduct.is_add_on ?? curProduct.is_add_on;
- curProduct.is_default = newProduct.is_default ?? curProduct.is_default;
- curProduct.archived = newProduct.archived ?? curProduct.archived;
+ curProduct.name = newProduct.name || curProduct.name;
+ curProduct.group = newProduct.group || curProduct.group;
+ curProduct.is_add_on = newProduct.is_add_on ?? curProduct.is_add_on;
+ curProduct.is_default = newProduct.is_default ?? curProduct.is_default;
+ curProduct.archived = newProduct.archived ?? curProduct.archived;
};
diff --git a/server/src/internal/products/handlers/handleVersionProduct.ts b/server/src/internal/products/handlers/handleVersionProduct.ts
index 7cb6142c3..37ed7b036 100644
--- a/server/src/internal/products/handlers/handleVersionProduct.ts
+++ b/server/src/internal/products/handlers/handleVersionProduct.ts
@@ -1,10 +1,10 @@
import {
- type AppEnv,
- CreateProductSchema,
- type FreeTrial,
- type FullProduct,
- type Organization,
- type ProductItem,
+ type AppEnv,
+ CreateProductSchema,
+ type FreeTrial,
+ type FullProduct,
+ type Organization,
+ type ProductItem,
} from "@autumn/shared";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
@@ -14,8 +14,8 @@ import { PriceService } from "@/internal/products/prices/PriceService.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js";
import {
- constructProduct,
- initProductInStripe,
+ constructProduct,
+ initProductInStripe,
} from "@/internal/products/productUtils.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts
index 32af65272..6be0d048c 100644
--- a/server/src/internal/products/internalProductRouter.ts
+++ b/server/src/internal/products/internalProductRouter.ts
@@ -18,8 +18,8 @@ import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js";
import { createOrgResponse } from "../orgs/orgUtils.js";
import {
- sortFullProducts,
- sortProductsByPrice,
+ sortFullProducts,
+ sortProductsByPrice,
} from "./productUtils/sortProductUtils.js";
import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js";
@@ -27,500 +27,500 @@ export const productRouter: Router = Router({ mergeParams: true });
// Get list of products
productRouter.get("/products", async (req: any, res) => {
- try {
- const { db } = req;
- const products = await ProductService.listFull({
- db,
- orgId: req.orgId,
- env: req.env,
- });
+ try {
+ const { db } = req;
+ const products = await ProductService.listFull({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ });
- sortFullProducts({ products });
+ sortFullProducts({ products });
- const groupToDefaults = getGroupToDefaults({
- defaultProds: products,
- });
+ const groupToDefaults = getGroupToDefaults({
+ defaultProds: products,
+ });
- res.status(200).json({
- products: products.map((p) =>
- mapToProductV2({ product: p, features: req.features })
- ),
- groupToDefaults,
- });
- } catch (error) {
- console.error("Failed to get products", error);
- res.status(500).send(error);
- }
+ res.status(200).json({
+ products: products.map((p) =>
+ mapToProductV2({ product: p, features: req.features }),
+ ),
+ groupToDefaults,
+ });
+ } catch (error) {
+ console.error("Failed to get products", error);
+ res.status(500).send(error);
+ }
});
// Get counts for all products
productRouter.get("/product_counts", async (req: any, res) => {
- try {
- let { db } = req;
- let products = await ProductService.listFull({
- db,
- orgId: req.orgId,
- env: req.env,
- });
+ try {
+ let { db } = req;
+ let products = await ProductService.listFull({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ });
- let counts = await Promise.all(
- products.map(async (product) => {
- // if (latestVersion) {
- // return CusProdReadService.getCounts({
- // db,
- // internalProductId: product.internal_id,
- // });
- // }
+ let counts = await Promise.all(
+ products.map(async (product) => {
+ // if (latestVersion) {
+ // return CusProdReadService.getCounts({
+ // db,
+ // internalProductId: product.internal_id,
+ // });
+ // }
- return CusProdReadService.getCountsForAllVersions({
- db,
- productId: product.id,
- orgId: req.orgId,
- env: req.env,
- });
- })
- );
+ return CusProdReadService.getCountsForAllVersions({
+ db,
+ productId: product.id,
+ orgId: req.orgId,
+ env: req.env,
+ });
+ }),
+ );
- let result: { [key: string]: any } = {};
- for (let i = 0; i < products.length; i++) {
- if (!result[products[i].id]) {
- result[products[i].id] = counts[i];
- }
- }
+ let result: { [key: string]: any } = {};
+ for (let i = 0; i < products.length; i++) {
+ if (!result[products[i].id]) {
+ result[products[i].id] = counts[i];
+ }
+ }
- res.status(200).send(result);
- } catch (error) {
- console.error("Failed to get products", error);
- res.status(500).send(error);
- }
+ res.status(200).send(result);
+ } catch (error) {
+ console.error("Failed to get products", error);
+ res.status(500).send(error);
+ }
});
// Get list of features
productRouter.get("/features", async (req: any, res) => {
- try {
- res.status(200).json({ features: req.features });
- } catch (error) {
- console.error("Failed to get features", error);
- res.status(500).send(error);
- }
+ try {
+ res.status(200).json({ features: req.features });
+ } catch (error) {
+ console.error("Failed to get features", error);
+ res.status(500).send(error);
+ }
});
// Get list of rewards
productRouter.get("/rewards", async (req: any, res) => {
- try {
- const { db, orgId, env } = req;
- const rewards = await RewardService.list({ db, orgId, env });
- const rewardPrograms = await RewardProgramService.list({
- db,
- orgId,
- env,
- });
- res.status(200).send({ rewards, rewardPrograms });
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get rewards",
- });
- }
+ try {
+ const { db, orgId, env } = req;
+ const rewards = await RewardService.list({ db, orgId, env });
+ const rewardPrograms = await RewardProgramService.list({
+ db,
+ orgId,
+ env,
+ });
+ res.status(200).send({ rewards, rewardPrograms });
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get rewards",
+ });
+ }
});
// Get single product data
productRouter.get("/:productId/data2", async (req: any, res) => {
- try {
- const { productId } = req.params;
- const { version } = req.query;
- const { db, orgId, env } = req;
+ try {
+ const { productId } = req.params;
+ const { version } = req.query;
+ const { db, orgId, env } = req;
- const [product, latestProduct] = await Promise.all([
- ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- version: version ? parseInt(version) : undefined,
- }),
- ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- }),
- ]);
+ const [product, latestProduct] = await Promise.all([
+ ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ version: version ? parseInt(version) : undefined,
+ }),
+ ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ }),
+ ]);
- if (!product) {
- throw new RecaseError({
- message: `Product ${productId} ${
- version ? `(v${version})` : ""
- } not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${productId} ${
+ version ? `(v${version})` : ""
+ } not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let productV2 = mapToProductV2({
- product: product,
- features: req.features,
- });
+ let productV2 = mapToProductV2({
+ product: product,
+ features: req.features,
+ });
- res
- .status(200)
- .json({ product: productV2, numVersions: latestProduct.version });
- } catch (error) {
- console.error("Failed to get product", error);
- res.status(500).send(error);
- }
+ res
+ .status(200)
+ .json({ product: productV2, numVersions: latestProduct.version });
+ } catch (error) {
+ console.error("Failed to get product", error);
+ res.status(500).send(error);
+ }
});
// Get counts for a single product
productRouter.get("/:productId/count", async (req: any, res) => {
- try {
- const { db, orgId, env } = req;
- const { productId } = req.params;
- const { version } = req.query;
+ try {
+ const { db, orgId, env } = req;
+ const { productId } = req.params;
+ const { version } = req.query;
- const product = await ProductService.get({
- db,
- id: productId,
- orgId,
- env,
- version: version ? parseInt(version) : undefined,
- });
+ const product = await ProductService.get({
+ db,
+ id: productId,
+ orgId,
+ env,
+ version: version ? parseInt(version) : undefined,
+ });
- if (!product) {
- throw new RecaseError({
- message: `Product ${productId} ${
- version ? `(v${version})` : ""
- } not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${productId} ${
+ version ? `(v${version})` : ""
+ } not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- // Get counts from postgres
- const counts = await CusProdReadService.getCounts({
- db,
- internalProductId: product.internal_id,
- });
+ // Get counts from postgres
+ const counts = await CusProdReadService.getCounts({
+ db,
+ internalProductId: product.internal_id,
+ });
- res.status(200).send(counts);
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get product counts (internal)",
- });
- }
+ res.status(200).send(counts);
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get product counts (internal)",
+ });
+ }
});
// Get list of migrations
productRouter.get("/migrations", async (req: any, res) => {
- try {
- const { db, orgId, env } = req;
- const migrations = await MigrationService.getExistingJobs({
- db,
- orgId,
- env,
- });
- res.status(200).send({ migrations });
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get migrations",
- });
- }
+ try {
+ const { db, orgId, env } = req;
+ const migrations = await MigrationService.getExistingJobs({
+ db,
+ orgId,
+ env,
+ });
+ res.status(200).send({ migrations });
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get migrations",
+ });
+ }
});
productRouter.get("/data", async (req: any, res) => {
- try {
- let { db } = req;
+ try {
+ let { db } = req;
- const allVersions = req.query.all_versions === "true";
+ const allVersions = req.query.all_versions === "true";
- const [products, features, org, coupons, rewardPrograms, defaultProds] =
- await Promise.all([
- ProductService.listFull({
- db,
- orgId: req.orgId,
- env: req.env,
- archived: false,
- returnAll: allVersions,
- }),
- FeatureService.getFromReq(req),
- OrgService.getFromReq(req),
- RewardService.list({ db, orgId: req.orgId, env: req.env }),
- RewardProgramService.list({
- db,
- orgId: req.orgId,
- env: req.env,
- }),
- ProductService.listDefault({
- db,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ const [products, features, org, coupons, rewardPrograms, defaultProds] =
+ await Promise.all([
+ ProductService.listFull({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ archived: false,
+ returnAll: allVersions,
+ }),
+ FeatureService.getFromReq(req),
+ OrgService.getFromReq(req),
+ RewardService.list({ db, orgId: req.orgId, env: req.env }),
+ RewardProgramService.list({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ProductService.listDefault({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- sortFullProducts({
- products: getLatestProducts(products),
- });
+ sortFullProducts({
+ products: getLatestProducts(products),
+ });
- const groupToDefaultProd = getGroupToDefaults({
- defaultProds,
- });
+ const groupToDefaultProd = getGroupToDefaults({
+ defaultProds,
+ });
- res.status(200).json({
- products: products.map((product) => {
- return mapToProductV2({ product, features });
- }),
- versionCounts: getProductVersionCounts(products),
- features,
- org: createOrgResponse({ org, env: req.env }),
- rewards: coupons,
- rewardPrograms,
- groupToDefaults: groupToDefaultProd,
- });
- } catch (error) {
- console.error("Failed to get products", error);
- res.status(500).send(error);
- }
+ res.status(200).json({
+ products: products.map((product) => {
+ return mapToProductV2({ product, features });
+ }),
+ versionCounts: getProductVersionCounts(products),
+ features,
+ org: createOrgResponse({ org, env: req.env }),
+ rewards: coupons,
+ rewardPrograms,
+ groupToDefaults: groupToDefaultProd,
+ });
+ } catch (error) {
+ console.error("Failed to get products", error);
+ res.status(500).send(error);
+ }
});
productRouter.post("/data", async (req: any, res) => {
- try {
- let { db } = req;
- let { showArchived } = req.body;
+ try {
+ let { db } = req;
+ let { showArchived } = req.body;
- const [products, defaultProds, features, org, coupons, rewardPrograms] =
- await Promise.all([
- ProductService.listFull({
- db,
- orgId: req.orgId,
- env: req.env,
- // returnAll: true,
- archived: showArchived,
- }),
- ProductService.listDefault({
- db,
- orgId: req.orgId,
- env: req.env,
- }),
- FeatureService.getFromReq(req),
- OrgService.getFromReq(req),
- RewardService.list({ db, orgId: req.orgId, env: req.env }),
- RewardProgramService.list({
- db,
- orgId: req.orgId,
- env: req.env,
- }),
- ]);
+ const [products, defaultProds, features, org, coupons, rewardPrograms] =
+ await Promise.all([
+ ProductService.listFull({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ // returnAll: true,
+ archived: showArchived,
+ }),
+ ProductService.listDefault({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ FeatureService.getFromReq(req),
+ OrgService.getFromReq(req),
+ RewardService.list({ db, orgId: req.orgId, env: req.env }),
+ RewardProgramService.list({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ }),
+ ]);
- // Group to default product
- const groupToDefaultProd = getGroupToDefaults({
- defaultProds,
- });
+ // Group to default product
+ const groupToDefaultProd = getGroupToDefaults({
+ defaultProds,
+ });
- res.status(200).json({
- products: sortFullProducts({ products }).map((product) => {
- return mapToProductV2({ product, features });
- }),
- groupToDefaults: groupToDefaultProd,
- versionCounts: getProductVersionCounts(products),
- features,
- org: createOrgResponse({ org, env: req.env }),
- rewards: coupons,
- rewardPrograms,
- });
- } catch (error) {
- console.error("Failed to get products", error);
- res.status(500).send(error);
- }
+ res.status(200).json({
+ products: sortFullProducts({ products }).map((product) => {
+ return mapToProductV2({ product, features });
+ }),
+ groupToDefaults: groupToDefaultProd,
+ versionCounts: getProductVersionCounts(products),
+ features,
+ org: createOrgResponse({ org, env: req.env }),
+ rewards: coupons,
+ rewardPrograms,
+ });
+ } catch (error) {
+ console.error("Failed to get products", error);
+ res.status(500).send(error);
+ }
});
productRouter.get("/counts", async (req: any, res) => {
- try {
- let { db } = req;
- let products = await ProductService.listFull({
- db,
- orgId: req.orgId,
- env: req.env,
- // returnAll: true,
- });
+ try {
+ let { db } = req;
+ let products = await ProductService.listFull({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ // returnAll: true,
+ });
- const latestVersion = req.query.latest_version === "true";
+ const latestVersion = req.query.latest_version === "true";
- let counts = await Promise.all(
- products.map(async (product) => {
- if (latestVersion) {
- return CusProdReadService.getCounts({
- db,
- internalProductId: product.internal_id,
- });
- }
+ let counts = await Promise.all(
+ products.map(async (product) => {
+ if (latestVersion) {
+ return CusProdReadService.getCounts({
+ db,
+ internalProductId: product.internal_id,
+ });
+ }
- return CusProdReadService.getCountsForAllVersions({
- db,
- productId: product.id,
- orgId: req.orgId,
- env: req.env,
- });
- })
- );
+ return CusProdReadService.getCountsForAllVersions({
+ db,
+ productId: product.id,
+ orgId: req.orgId,
+ env: req.env,
+ });
+ }),
+ );
- let result: { [key: string]: any } = {};
- for (let i = 0; i < products.length; i++) {
- if (!result[products[i].id]) {
- result[products[i].id] = counts[i];
- }
- }
+ let result: { [key: string]: any } = {};
+ for (let i = 0; i < products.length; i++) {
+ if (!result[products[i].id]) {
+ result[products[i].id] = counts[i];
+ }
+ }
- res.status(200).send(result);
- } catch (error) {
- console.error("Failed to get product counts", error);
- res.status(500).send(error);
- }
+ res.status(200).send(result);
+ } catch (error) {
+ console.error("Failed to get product counts", error);
+ res.status(500).send(error);
+ }
});
productRouter.get("/:productId/data", async (req: any, res) => {
- try {
- const { productId } = req.params;
- const { version } = req.query;
- const { db, orgId, env } = req;
+ try {
+ const { productId } = req.params;
+ const { version } = req.query;
+ const { db, orgId, env } = req;
- const [product, features, org, numVersions, existingMigrations] =
- await Promise.all([
- ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId,
- env,
- version: version ? parseInt(version) : undefined,
- }),
- FeatureService.getFromReq(req),
- OrgService.getFromReq(req),
- ProductService.getProductVersionCount({
- db,
- productId,
- orgId,
- env,
- }),
- MigrationService.getExistingJobs({
- db,
- orgId,
- env,
- }),
- ]);
+ const [product, features, org, numVersions, existingMigrations] =
+ await Promise.all([
+ ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId,
+ env,
+ version: version ? parseInt(version) : undefined,
+ }),
+ FeatureService.getFromReq(req),
+ OrgService.getFromReq(req),
+ ProductService.getProductVersionCount({
+ db,
+ productId,
+ orgId,
+ env,
+ }),
+ MigrationService.getExistingJobs({
+ db,
+ orgId,
+ env,
+ }),
+ ]);
- if (!product) {
- throw new RecaseError({
- message: `Product ${productId} ${
- version ? `(v${version})` : ""
- } not found`,
- code: ErrCode.ProductNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product ${productId} ${
+ version ? `(v${version})` : ""
+ } not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- const defaultProds = await ProductService.listDefault({
- db,
- orgId: req.orgId,
- env: req.env,
- group: product.group,
- });
+ const defaultProds = await ProductService.listDefault({
+ db,
+ orgId: req.orgId,
+ env: req.env,
+ group: product.group,
+ });
- const groupDefaults = getGroupToDefaults({
- defaultProds,
- })?.[product.group];
+ const groupDefaults = getGroupToDefaults({
+ defaultProds,
+ })?.[product.group];
- let entitlements = product.entitlements;
- let prices = product.prices;
+ let entitlements = product.entitlements;
+ let prices = product.prices;
- entitlements = entitlements.sort((a: any, b: any) => {
- return b.feature.id.localeCompare(a.feature.id);
- });
+ entitlements = entitlements.sort((a: any, b: any) => {
+ return b.feature.id.localeCompare(a.feature.id);
+ });
- prices = prices.sort((a: any, b: any) => {
- return b.id.localeCompare(a.id);
- });
+ prices = prices.sort((a: any, b: any) => {
+ return b.id.localeCompare(a.id);
+ });
- let productV2 = mapToProductV2({ product, features });
+ let productV2 = mapToProductV2({ product, features });
- res.status(200).send({
- product: productV2,
- entitlements,
- prices,
- features,
- org: {
- id: org.id,
- name: org.name,
- test_pkey: org.test_pkey,
- live_pkey: org.live_pkey,
- default_currency: org.default_currency,
- },
- numVersions,
- existingMigrations,
- groupDefaults: groupDefaults,
- });
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get product data (internal)",
- });
- }
+ res.status(200).send({
+ product: productV2,
+ entitlements,
+ prices,
+ features,
+ org: {
+ id: org.id,
+ name: org.name,
+ test_pkey: org.test_pkey,
+ live_pkey: org.live_pkey,
+ default_currency: org.default_currency,
+ },
+ numVersions,
+ existingMigrations,
+ groupDefaults: groupDefaults,
+ });
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get product data (internal)",
+ });
+ }
});
productRouter.post("/product_options", async (req: any, res: any) => {
- try {
- const { items } = req.body;
+ try {
+ const { items } = req.body;
- const features = await FeatureService.getFromReq(req);
- const featureToOptions: { [key: string]: FeatureOptions } = {};
+ const features = await FeatureService.getFromReq(req);
+ const featureToOptions: { [key: string]: FeatureOptions } = {};
- for (const item of items) {
- if (isFeaturePriceItem(item) && item.usage_model == UsageModel.Prepaid) {
- featureToOptions[item.feature_id] = {
- feature_id: item.feature_id,
- quantity: 0,
- };
- }
- }
+ for (const item of items) {
+ if (isFeaturePriceItem(item) && item.usage_model == UsageModel.Prepaid) {
+ featureToOptions[item.feature_id] = {
+ feature_id: item.feature_id,
+ quantity: 0,
+ };
+ }
+ }
- res.status(200).send({ options: Object.values(featureToOptions) });
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get product options",
- });
- }
+ res.status(200).send({ options: Object.values(featureToOptions) });
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get product options",
+ });
+ }
});
productRouter.get("/:productId/info", handleGetProductDeleteInfo);
productRouter.get("/rewards", async (req: any, res: any) => {
- try {
- const { db, orgId, env } = req;
+ try {
+ const { db, orgId, env } = req;
- const rewards = await RewardService.list({
- db,
- orgId,
- env,
- });
+ const rewards = await RewardService.list({
+ db,
+ orgId,
+ env,
+ });
- res.status(200).send({ rewards });
- } catch (error) {
- handleFrontendReqError({
- error,
- req,
- res,
- action: "Get rewards",
- });
- }
+ res.status(200).send({ rewards });
+ } catch (error) {
+ handleFrontendReqError({
+ error,
+ req,
+ res,
+ action: "Get rewards",
+ });
+ }
});
diff --git a/server/src/internal/products/pricecn/pricecnUtils.ts b/server/src/internal/products/pricecn/pricecnUtils.ts
index ff7613fa0..27891df07 100644
--- a/server/src/internal/products/pricecn/pricecnUtils.ts
+++ b/server/src/internal/products/pricecn/pricecnUtils.ts
@@ -4,18 +4,18 @@ import { notNullish, nullish } from "@/utils/genUtils.js";
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
import {
- ProductV2,
- Feature,
- ProductItem,
- Organization,
- ProductItemFeatureType,
- ErrCode,
- Infinite,
- FullCusProduct,
- numberWithCommas,
- AttachScenario,
- FullProduct,
- FullCustomer,
+ ProductV2,
+ Feature,
+ ProductItem,
+ Organization,
+ ProductItemFeatureType,
+ ErrCode,
+ Infinite,
+ FullCusProduct,
+ numberWithCommas,
+ AttachScenario,
+ FullProduct,
+ FullCustomer,
} from "@autumn/shared";
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
@@ -26,425 +26,425 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFreeTrialAfterFingerprint } from "../free-trials/freeTrialUtils.js";
export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
- items.sort((a, b) => {
- let aIsPriceItem = isPriceItem(a);
- let bIsPriceItem = isPriceItem(b);
+ items.sort((a, b) => {
+ let aIsPriceItem = isPriceItem(a);
+ let bIsPriceItem = isPriceItem(b);
- if (aIsPriceItem && bIsPriceItem) {
- return 0;
- }
+ if (aIsPriceItem && bIsPriceItem) {
+ return 0;
+ }
- if (aIsPriceItem && !bIsPriceItem) {
- return -1;
- }
+ if (aIsPriceItem && !bIsPriceItem) {
+ return -1;
+ }
- if (!aIsPriceItem && bIsPriceItem) {
- return 1;
- }
+ if (!aIsPriceItem && bIsPriceItem) {
+ return 1;
+ }
- // 2. Put feature price next
- let aIsFeatureItem = isFeaturePriceItem(a);
- let bIsFeatureItem = isFeaturePriceItem(b);
+ // 2. Put feature price next
+ let aIsFeatureItem = isFeaturePriceItem(a);
+ let bIsFeatureItem = isFeaturePriceItem(b);
- if (aIsFeatureItem && !bIsFeatureItem) {
- return -1;
- }
+ if (aIsFeatureItem && !bIsFeatureItem) {
+ return -1;
+ }
- if (!aIsFeatureItem && bIsFeatureItem) {
- return 1;
- }
+ if (!aIsFeatureItem && bIsFeatureItem) {
+ return 1;
+ }
- // 3. Put feature price items in alphabetical order
- let feature = features.find((f) => f.id == a.feature_id);
- let aFeatureName = feature?.name;
- let bFeatureName = features.find((f) => f.id == b.feature_id)?.name;
+ // 3. Put feature price items in alphabetical order
+ let feature = features.find((f) => f.id == a.feature_id);
+ let aFeatureName = feature?.name;
+ let bFeatureName = features.find((f) => f.id == b.feature_id)?.name;
- if (!aFeatureName || !bFeatureName) {
- return 0;
- }
+ if (!aFeatureName || !bFeatureName) {
+ return 0;
+ }
- return aFeatureName.localeCompare(bFeatureName);
- });
+ return aFeatureName.localeCompare(bFeatureName);
+ });
- return items;
+ return items;
};
export const getIncludedFeatureName = ({
- item,
- feature,
+ item,
+ feature,
}: {
- item: ProductItem;
- feature: Feature;
+ item: ProductItem;
+ feature: Feature;
}) => {
- return getFeatureName({
- feature,
- plural: typeof item.included_usage === "number" && item.included_usage > 1,
- });
+ return getFeatureName({
+ feature,
+ plural: typeof item.included_usage === "number" && item.included_usage > 1,
+ });
};
export const getPriceText = ({
- item,
- org,
+ item,
+ org,
}: {
- item: ProductItem;
- org: Organization;
+ item: ProductItem;
+ org: Organization;
}) => {
- const formatAmount = (amount: number) => {
- return new Intl.NumberFormat(undefined, {
- style: "currency",
- currency: org.default_currency || "USD",
- minimumFractionDigits: 0,
- maximumFractionDigits: 10,
- }).format(amount);
- };
- if (item.price) {
- return formatAmount(item.price as number);
- }
+ const formatAmount = (amount: number) => {
+ return new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency: org.default_currency || "USD",
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 10,
+ }).format(amount);
+ };
+ if (item.price) {
+ return formatAmount(item.price as number);
+ }
- let tiers = item.tiers;
- if (tiers) {
- if (tiers.length == 1) {
- return formatAmount(tiers[0].amount);
- }
+ let tiers = item.tiers;
+ if (tiers) {
+ if (tiers.length == 1) {
+ return formatAmount(tiers[0].amount);
+ }
- let firstPrice = tiers[0].amount;
- let lastPrice = tiers[tiers.length - 1].amount;
+ let firstPrice = tiers[0].amount;
+ let lastPrice = tiers[tiers.length - 1].amount;
- return `${formatAmount(firstPrice)} - ${formatAmount(lastPrice)}`;
- }
+ return `${formatAmount(firstPrice)} - ${formatAmount(lastPrice)}`;
+ }
};
// Deprecate
export const getPricecnPrice = ({
- org,
- items,
- features,
- isMainPrice = true,
+ org,
+ items,
+ features,
+ isMainPrice = true,
}: {
- org: Organization;
- features: Feature[];
- items: ProductItem[];
- isMainPrice?: boolean;
+ org: Organization;
+ features: Feature[];
+ items: ProductItem[];
+ isMainPrice?: boolean;
}) => {
- let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
+ let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
- if (!priceExists) {
- return {
- primaryText: "Free",
- secondaryText: " ",
- };
- }
+ if (!priceExists) {
+ return {
+ primaryText: "Free",
+ secondaryText: " ",
+ };
+ }
- let priceItem = items[0];
+ let priceItem = items[0];
- if (isPriceItem(priceItem)) {
- return {
- ...priceItem,
- primaryText: getPriceText({ item: priceItem, org }),
- secondaryText: priceItem.interval ? `per ${priceItem.interval}` : " ",
- };
- } else {
- let feature = features.find((f) => f.id == priceItem.feature_id);
- let texts = featurePricetoPricecnItem({
- feature,
- item: priceItem,
- org,
- isMainPrice,
- });
- return {
- ...priceItem,
- primaryText: texts.primaryText,
- secondaryText: texts.secondaryText,
- };
- }
+ if (isPriceItem(priceItem)) {
+ return {
+ ...priceItem,
+ primaryText: getPriceText({ item: priceItem, org }),
+ secondaryText: priceItem.interval ? `per ${priceItem.interval}` : " ",
+ };
+ } else {
+ let feature = features.find((f) => f.id == priceItem.feature_id);
+ let texts = featurePricetoPricecnItem({
+ feature,
+ item: priceItem,
+ org,
+ isMainPrice,
+ });
+ return {
+ ...priceItem,
+ primaryText: texts.primaryText,
+ secondaryText: texts.secondaryText,
+ };
+ }
};
export const featureToPricecnItem = ({
- feature,
- item,
+ feature,
+ item,
}: {
- feature?: Feature;
- item: ProductItem;
+ feature?: Feature;
+ item: ProductItem;
}) => {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
- // 1. If feature
- if (item.feature_type == ProductItemFeatureType.Static) {
- return {
- primaryText: feature.name,
- };
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
+ // 1. If feature
+ if (item.feature_type == ProductItemFeatureType.Static) {
+ return {
+ primaryText: feature.name,
+ };
+ }
- let featureName = getIncludedFeatureName({
- feature,
- item,
- });
+ let featureName = getIncludedFeatureName({
+ feature,
+ item,
+ });
- let includedUsageTxt =
- item.included_usage == Infinite
- ? "Unlimited "
- : nullish(item.included_usage) || item.included_usage == 0
- ? ""
- : `${numberWithCommas(item.included_usage!)} `;
+ let includedUsageTxt =
+ item.included_usage == Infinite
+ ? "Unlimited "
+ : nullish(item.included_usage) || item.included_usage == 0
+ ? ""
+ : `${numberWithCommas(item.included_usage!)} `;
- return {
- primaryText: `${includedUsageTxt}${featureName}`,
- };
+ return {
+ primaryText: `${includedUsageTxt}${featureName}`,
+ };
};
export const featurePricetoPricecnItem = ({
- feature,
- item,
- org,
- isMainPrice = false,
- withNameAfterIncluded = false,
+ feature,
+ item,
+ org,
+ isMainPrice = false,
+ withNameAfterIncluded = false,
}: {
- feature?: Feature;
- item: ProductItem;
- org: Organization;
- isMainPrice?: boolean;
- withNameAfterIncluded?: boolean;
+ feature?: Feature;
+ item: ProductItem;
+ org: Organization;
+ isMainPrice?: boolean;
+ withNameAfterIncluded?: boolean;
}) => {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- // 1. Get included usage
- let includedFeatureName = getIncludedFeatureName({
- feature,
- item,
- });
+ // 1. Get included usage
+ let includedFeatureName = getIncludedFeatureName({
+ feature,
+ item,
+ });
- let includedUsageStr = "";
- if (notNullish(item.included_usage) && (item.included_usage as number) > 0) {
- let includedUsage = numberWithCommas(item.included_usage as number);
- if (withNameAfterIncluded) {
- includedUsageStr = `${includedUsage} ${includedFeatureName}`;
- } else {
- includedUsageStr = `${includedUsage} included`;
- }
- }
+ let includedUsageStr = "";
+ if (notNullish(item.included_usage) && (item.included_usage as number) > 0) {
+ let includedUsage = numberWithCommas(item.included_usage as number);
+ if (withNameAfterIncluded) {
+ includedUsageStr = `${includedUsage} ${includedFeatureName}`;
+ } else {
+ includedUsageStr = `${includedUsage} included`;
+ }
+ }
- let priceStr = getPriceText({ item, org });
- let billingFeatureName = getFeatureName({
- feature,
- plural: typeof item.billing_units == "number" && item.billing_units > 1,
- });
+ let priceStr = getPriceText({ item, org });
+ let billingFeatureName = getFeatureName({
+ feature,
+ plural: typeof item.billing_units == "number" && item.billing_units > 1,
+ });
- let priceStr2 = "";
- if (item.billing_units && item.billing_units > 1) {
- priceStr2 = `${numberWithCommas(item.billing_units)} ${billingFeatureName}`;
- } else {
- priceStr2 = `${billingFeatureName}`;
- }
+ let priceStr2 = "";
+ if (item.billing_units && item.billing_units > 1) {
+ priceStr2 = `${numberWithCommas(item.billing_units)} ${billingFeatureName}`;
+ } else {
+ priceStr2 = `${billingFeatureName}`;
+ }
- let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
+ let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
- if (includedUsageStr) {
- return {
- primaryText: includedUsageStr,
- secondaryText: `then ${priceStr} per ${priceStr2}${intervalStr}`,
- };
- }
+ if (includedUsageStr) {
+ return {
+ primaryText: includedUsageStr,
+ secondaryText: `then ${priceStr} per ${priceStr2}${intervalStr}`,
+ };
+ }
- return {
- primaryText: priceStr,
- secondaryText: `per ${priceStr2}${intervalStr}`,
- };
+ return {
+ primaryText: priceStr,
+ secondaryText: `per ${priceStr2}${intervalStr}`,
+ };
};
export const getAttachScenario = ({
- curMainProduct,
- curScheduledProduct,
- fullProduct,
+ curMainProduct,
+ curScheduledProduct,
+ fullProduct,
}: {
- curMainProduct?: FullCusProduct | null;
- curScheduledProduct?: FullCusProduct | null;
- fullProduct: FullProduct;
+ curMainProduct?: FullCusProduct | null;
+ curScheduledProduct?: FullCusProduct | null;
+ fullProduct: FullProduct;
}) => {
- if (!curMainProduct) return AttachScenario.New;
+ if (!curMainProduct) return AttachScenario.New;
- if (fullProduct.is_add_on) {
- return AttachScenario.New;
- }
+ if (fullProduct.is_add_on) {
+ return AttachScenario.New;
+ }
- // 1. If current product is the same as the product, return active
- if (curMainProduct?.product.id == fullProduct.id) {
- if (curMainProduct.canceled_at != null) {
- return AttachScenario.Renew;
- } else return AttachScenario.Active;
- }
+ // 1. If current product is the same as the product, return active
+ if (curMainProduct?.product.id == fullProduct.id) {
+ if (curMainProduct.canceled_at != null) {
+ return AttachScenario.Renew;
+ } else return AttachScenario.Active;
+ }
- if (curScheduledProduct?.product.id == fullProduct.id) {
- return AttachScenario.Scheduled;
- }
+ if (curScheduledProduct?.product.id == fullProduct.id) {
+ return AttachScenario.Scheduled;
+ }
- let curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
+ let curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
- let isUpgrade = isProductUpgrade({
- prices1: curFullProduct.prices,
- prices2: fullProduct.prices,
- });
+ let isUpgrade = isProductUpgrade({
+ prices1: curFullProduct.prices,
+ prices2: fullProduct.prices,
+ });
- return isUpgrade ? AttachScenario.Upgrade : AttachScenario.Downgrade;
+ return isUpgrade ? AttachScenario.Upgrade : AttachScenario.Downgrade;
};
export const toPricecnProduct = async ({
- db,
- org,
- product,
- fullProduct,
- otherProducts,
- features,
- curMainProduct,
- curScheduledProduct,
- fullCus,
+ db,
+ org,
+ product,
+ fullProduct,
+ otherProducts,
+ features,
+ curMainProduct,
+ curScheduledProduct,
+ fullCus,
}: {
- db: DrizzleCli;
- org: Organization;
- product: ProductV2;
- fullProduct: FullProduct;
- otherProducts: FullProduct[];
- features: Feature[];
- curMainProduct?: FullCusProduct | null;
- curScheduledProduct?: FullCusProduct | null;
- fullCus?: FullCustomer;
+ db: DrizzleCli;
+ org: Organization;
+ product: ProductV2;
+ fullProduct: FullProduct;
+ otherProducts: FullProduct[];
+ features: Feature[];
+ curMainProduct?: FullCusProduct | null;
+ curScheduledProduct?: FullCusProduct | null;
+ fullCus?: FullCustomer;
}) => {
- let items = structuredClone(product.items);
+ let items = structuredClone(product.items);
- sortProductItems(items, features);
+ sortProductItems(items, features);
- let price = getPricecnPrice({ org, items, features });
- let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
- let itemsWithoutPrice = priceExists ? items.slice(1) : items;
+ let price = getPricecnPrice({ org, items, features });
+ let priceExists = items.some((i) => isPriceItem(i) || isFeaturePriceItem(i));
+ let itemsWithoutPrice = priceExists ? items.slice(1) : items;
- let pricecnItems = itemsWithoutPrice.map((i) => {
- let data: {
- primaryText?: string;
- secondaryText?: string;
- };
+ let pricecnItems = itemsWithoutPrice.map((i) => {
+ let data: {
+ primaryText?: string;
+ secondaryText?: string;
+ };
- if (isPriceItem(i)) {
- let priceTxt = getPriceText({ item: i, org });
- data = {
- primaryText: priceTxt,
- secondaryText: i.interval ? `per ${i.interval}` : undefined,
- };
- }
+ if (isPriceItem(i)) {
+ let priceTxt = getPriceText({ item: i, org });
+ data = {
+ primaryText: priceTxt,
+ secondaryText: i.interval ? `per ${i.interval}` : undefined,
+ };
+ }
- let feature = features.find((f) => f.id == i.feature_id);
- if (isFeaturePriceItem(i)) {
- data = featurePricetoPricecnItem({
- feature,
- item: i,
- org,
- withNameAfterIncluded: true,
- });
- } else {
- data = featureToPricecnItem({ feature, item: i });
- }
+ let feature = features.find((f) => f.id == i.feature_id);
+ if (isFeaturePriceItem(i)) {
+ data = featurePricetoPricecnItem({
+ feature,
+ item: i,
+ org,
+ withNameAfterIncluded: true,
+ });
+ } else {
+ data = featureToPricecnItem({ feature, item: i });
+ }
- return {
- ...i,
+ return {
+ ...i,
- primary_text: data?.primaryText,
- secondary_text: data?.secondaryText,
+ primary_text: data?.primaryText,
+ secondary_text: data?.secondaryText,
- // To deprecate
- ...data,
- };
- });
+ // To deprecate
+ ...data,
+ };
+ });
- let isCurrent = curMainProduct?.product.id == product.id;
- let isScheduled = curScheduledProduct?.product.id == product.id;
+ let isCurrent = curMainProduct?.product.id == product.id;
+ let isScheduled = curScheduledProduct?.product.id == product.id;
- let buttonText = "Get Started";
+ let buttonText = "Get Started";
- if (isCurrent) {
- let isCanceled = curMainProduct!.canceled_at != null;
- buttonText = isCanceled ? "Renew" : "Current Plan";
- } else if (isScheduled) {
- buttonText = "Scheduled";
- }
+ if (isCurrent) {
+ let isCanceled = curMainProduct!.canceled_at != null;
+ buttonText = isCanceled ? "Renew" : "Current Plan";
+ } else if (isScheduled) {
+ buttonText = "Scheduled";
+ }
- let scenario = getAttachScenario({
- curMainProduct,
- curScheduledProduct,
- fullProduct,
- });
+ let scenario = getAttachScenario({
+ curMainProduct,
+ curScheduledProduct,
+ fullProduct,
+ });
- let freeTrial = fullProduct.free_trial;
+ let freeTrial = fullProduct.free_trial;
- let baseVariant = null;
- if (fullProduct.base_variant_id) {
- baseVariant = otherProducts.find(
- (p) => p.id == fullProduct.base_variant_id
- );
- }
+ let baseVariant = null;
+ if (fullProduct.base_variant_id) {
+ baseVariant = otherProducts.find(
+ (p) => p.id == fullProduct.base_variant_id,
+ );
+ }
- let name = product.name;
- if (baseVariant) {
- name = `${baseVariant.name}`;
- }
+ let name = product.name;
+ if (baseVariant) {
+ name = `${baseVariant.name}`;
+ }
- let intervalGroup = null;
- if (
- baseVariant ||
- otherProducts.some((p) => p.base_variant_id == product.id)
- ) {
- let intervalSet = getLargestInterval({ prices: fullProduct.prices });
- intervalGroup = intervalSet?.interval;
- }
+ let intervalGroup = null;
+ if (
+ baseVariant ||
+ otherProducts.some((p) => p.base_variant_id == product.id)
+ ) {
+ let intervalSet = getLargestInterval({ prices: fullProduct.prices });
+ intervalGroup = intervalSet?.interval;
+ }
- let trialAvailable = false;
- if (product.free_trial && fullCus) {
- let trial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: product.free_trial,
- fingerprint: fullCus.fingerprint,
- internalCustomerId: fullCus.internal_id,
- multipleAllowed: org.config.multiple_trials,
- productId: product.id,
- });
+ let trialAvailable = false;
+ if (product.free_trial && fullCus) {
+ let trial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: product.free_trial,
+ fingerprint: fullCus.fingerprint,
+ internalCustomerId: fullCus.internal_id,
+ multipleAllowed: org.config.multiple_trials,
+ productId: product.id,
+ });
- if (scenario == AttachScenario.Downgrade) trial = null;
- trialAvailable = notNullish(trial) ? true : false;
- }
+ if (scenario == AttachScenario.Downgrade) trial = null;
+ trialAvailable = notNullish(trial) ? true : false;
+ }
- return {
- id: product.id,
- name,
- is_add_on: product.is_add_on,
- price: price
- ? {
- primary_text: price.primaryText,
- secondary_text: price.secondaryText,
+ return {
+ id: product.id,
+ name,
+ is_add_on: product.is_add_on,
+ price: price
+ ? {
+ primary_text: price.primaryText,
+ secondary_text: price.secondaryText,
- // To deprecate
- ...price,
- }
- : null,
- items: pricecnItems,
- scenario,
- // free_trial: freeTrial
- // ? FreeTrialResponseSchema.parse({
- // ...freeTrial,
- // trial_available: trialAvailable,
- // })
- // : null,
+ // To deprecate
+ ...price,
+ }
+ : null,
+ items: pricecnItems,
+ scenario,
+ // free_trial: freeTrial
+ // ? FreeTrialResponseSchema.parse({
+ // ...freeTrial,
+ // trial_available: trialAvailable,
+ // })
+ // : null,
- // interval_group: intervalGroup,
+ // interval_group: intervalGroup,
- // To deprecate
- buttonText,
- };
+ // To deprecate
+ buttonText,
+ };
};
diff --git a/server/src/internal/products/prices/PriceService.ts b/server/src/internal/products/prices/PriceService.ts
index c3c26bafb..6d03ab336 100644
--- a/server/src/internal/products/prices/PriceService.ts
+++ b/server/src/internal/products/prices/PriceService.ts
@@ -4,131 +4,131 @@ import { Price, prices, Product } from "@autumn/shared";
import { and, eq, inArray, or, sql } from "drizzle-orm";
export class PriceService {
- static async get({ db, id }: { db: DrizzleCli; id: string }) {
- return (await db.query.prices.findFirst({
- where: eq(prices.id, id),
- })) as Price;
- }
+ static async get({ db, id }: { db: DrizzleCli; id: string }) {
+ return (await db.query.prices.findFirst({
+ where: eq(prices.id, id),
+ })) as Price;
+ }
- static async getCustomInEntIds({
- db,
- entitlementIds,
- }: {
- db: DrizzleCli;
- entitlementIds: string[];
- }) {
- return await db.query.prices.findMany({
- where: and(
- inArray(prices.entitlement_id, entitlementIds),
- eq(prices.is_custom, true)
- ),
- });
- }
+ static async getCustomInEntIds({
+ db,
+ entitlementIds,
+ }: {
+ db: DrizzleCli;
+ entitlementIds: string[];
+ }) {
+ return await db.query.prices.findMany({
+ where: and(
+ inArray(prices.entitlement_id, entitlementIds),
+ eq(prices.is_custom, true),
+ ),
+ });
+ }
- static async getInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- if (!ids || ids.length === 0) {
- return [];
- }
+ static async getInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ if (!ids || ids.length === 0) {
+ return [];
+ }
- return (await db.query.prices.findMany({
- where: inArray(prices.id, ids),
- with: {
- product: true,
- },
- })) as (Price & { product: Product })[];
- }
+ return (await db.query.prices.findMany({
+ where: inArray(prices.id, ids),
+ with: {
+ product: true,
+ },
+ })) as (Price & { product: Product })[];
+ }
- static async insert({ db, data }: { db: DrizzleCli; data: Price | Price[] }) {
- if (Array.isArray(data) && data.length === 0) {
- return;
- }
+ static async insert({ db, data }: { db: DrizzleCli; data: Price | Price[] }) {
+ if (Array.isArray(data) && data.length === 0) {
+ return;
+ }
- await db.insert(prices).values(data as any);
- }
+ await db.insert(prices).values(data as any);
+ }
- static async update({
- db,
- id,
- update,
- }: {
- db: DrizzleCli;
- id: string;
- update: Partial;
- }) {
- await db.update(prices).set(update).where(eq(prices.id, id));
- }
+ static async update({
+ db,
+ id,
+ update,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ update: Partial;
+ }) {
+ await db.update(prices).set(update).where(eq(prices.id, id));
+ }
- static async upsert({ db, data }: { db: DrizzleCli; data: Price | Price[] }) {
- if (Array.isArray(data) && data.length == 0) return;
+ static async upsert({ db, data }: { db: DrizzleCli; data: Price | Price[] }) {
+ if (Array.isArray(data) && data.length == 0) return;
- const updateColumns = buildConflictUpdateColumns(prices, ["id"]);
+ const updateColumns = buildConflictUpdateColumns(prices, ["id"]);
- await db
- .insert(prices)
- .values(data as any)
- .onConflictDoUpdate({
- target: prices.id,
- set: updateColumns,
- });
- }
+ await db
+ .insert(prices)
+ .values(data as any)
+ .onConflictDoUpdate({
+ target: prices.id,
+ set: updateColumns,
+ });
+ }
- static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- await db.delete(prices).where(inArray(prices.id, ids));
- }
+ static async deleteInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ await db.delete(prices).where(inArray(prices.id, ids));
+ }
- static async getByStripeId({
- db,
- stripePriceId,
- }: {
- db: DrizzleCli;
- stripePriceId: string;
- }) {
- return (await db.query.prices.findFirst({
- where: or(
- sql`${prices.config} ->> 'stripe_price_id' = ${stripePriceId}`,
- sql`${prices.config} ->> 'stripe_empty_price_id' = ${stripePriceId}`
- ),
- with: {
- product: true,
- },
- })) as (Price & { product: Product }) | undefined;
- }
+ static async getByStripeId({
+ db,
+ stripePriceId,
+ }: {
+ db: DrizzleCli;
+ stripePriceId: string;
+ }) {
+ return (await db.query.prices.findFirst({
+ where: or(
+ sql`${prices.config} ->> 'stripe_price_id' = ${stripePriceId}`,
+ sql`${prices.config} ->> 'stripe_empty_price_id' = ${stripePriceId}`,
+ ),
+ with: {
+ product: true,
+ },
+ })) as (Price & { product: Product }) | undefined;
+ }
- static async getByStripeIds({
- db,
- stripePriceIds,
- }: {
- db: DrizzleCli;
- stripePriceIds: string[];
- }) {
- if (!stripePriceIds || stripePriceIds.length === 0)
- return {} as Record;
+ static async getByStripeIds({
+ db,
+ stripePriceIds,
+ }: {
+ db: DrizzleCli;
+ stripePriceIds: string[];
+ }) {
+ if (!stripePriceIds || stripePriceIds.length === 0)
+ return {} as Record;
- const perIdExprs = stripePriceIds.map((id) =>
- or(
- sql`${prices.config} ->> 'stripe_price_id' = ${id}`,
- sql`${prices.config} ->> 'stripe_empty_price_id' = ${id}`
- )
- );
+ const perIdExprs = stripePriceIds.map((id) =>
+ or(
+ sql`${prices.config} ->> 'stripe_price_id' = ${id}`,
+ sql`${prices.config} ->> 'stripe_empty_price_id' = ${id}`,
+ ),
+ );
- let whereExpr = perIdExprs[0];
- for (let i = 1; i < perIdExprs.length; i++) {
- whereExpr = or(whereExpr, perIdExprs[i]);
- }
+ let whereExpr = perIdExprs[0];
+ for (let i = 1; i < perIdExprs.length; i++) {
+ whereExpr = or(whereExpr, perIdExprs[i]);
+ }
- const rows = (await db.query.prices.findMany({
- where: whereExpr,
- with: { product: true },
- })) as (Price & { product: Product })[];
+ const rows = (await db.query.prices.findMany({
+ where: whereExpr,
+ with: { product: true },
+ })) as (Price & { product: Product })[];
- const byStripeId: Record = {};
- for (const row of rows) {
- const cfg: any = (row as any).config || {};
- const ids = [cfg.stripe_price_id, cfg.stripe_empty_price_id].filter(
- Boolean
- ) as string[];
- for (const id of ids) byStripeId[id] = row;
- }
- return byStripeId;
- }
+ const byStripeId: Record = {};
+ for (const row of rows) {
+ const cfg: any = (row as any).config || {};
+ const ids = [cfg.stripe_price_id, cfg.stripe_empty_price_id].filter(
+ Boolean,
+ ) as string[];
+ for (const id of ids) byStripeId[id] = row;
+ }
+ return byStripeId;
+ }
}
diff --git a/server/src/internal/products/prices/billingIntervalUtils.ts b/server/src/internal/products/prices/billingIntervalUtils.ts
index 31e778c00..8150d1f9d 100644
--- a/server/src/internal/products/prices/billingIntervalUtils.ts
+++ b/server/src/internal/products/prices/billingIntervalUtils.ts
@@ -1,418 +1,418 @@
import { BillingInterval, IntervalConfig } from "@autumn/shared";
import {
- addMinutes,
- addMonths,
- addSeconds,
- addWeeks,
- addYears,
- differenceInSeconds,
- getDate,
- getHours,
- getMinutes,
- getSeconds,
- getTime,
- setDate,
- setHours,
- setMinutes,
- setSeconds,
- startOfMonth,
- subMonths,
- subWeeks,
- subYears,
+ addMinutes,
+ addMonths,
+ addSeconds,
+ addWeeks,
+ addYears,
+ differenceInSeconds,
+ getDate,
+ getHours,
+ getMinutes,
+ getSeconds,
+ getTime,
+ setDate,
+ setHours,
+ setMinutes,
+ setSeconds,
+ startOfMonth,
+ subMonths,
+ subWeeks,
+ subYears,
} from "date-fns";
import { UTCDate } from "@date-fns/utc";
import { formatUnixToDateTime } from "@/utils/genUtils.js";
export const subtractBillingIntervalUnix = ({
- unixTimestamp,
- interval,
- intervalCount = 1,
+ unixTimestamp,
+ interval,
+ intervalCount = 1,
}: {
- unixTimestamp: number;
- interval: BillingInterval;
- intervalCount: number;
+ unixTimestamp: number;
+ interval: BillingInterval;
+ intervalCount: number;
}) => {
- const date = new UTCDate(unixTimestamp);
- let subtractedDate = date;
- switch (interval) {
- case BillingInterval.Week:
- subtractedDate = subWeeks(date, 1 * intervalCount);
- break;
- case BillingInterval.Month:
- subtractedDate = subMonths(date, 1 * intervalCount);
- break;
- case BillingInterval.Quarter:
- subtractedDate = subMonths(date, 3 * intervalCount);
- break;
- case BillingInterval.SemiAnnual:
- subtractedDate = subMonths(date, 6 * intervalCount);
- break;
- case BillingInterval.Year:
- subtractedDate = subYears(date, 1 * intervalCount);
- break;
- default:
- throw new Error(`Invalid billing interval: ${interval}`);
- }
- return subtractedDate.getTime();
+ const date = new UTCDate(unixTimestamp);
+ let subtractedDate = date;
+ switch (interval) {
+ case BillingInterval.Week:
+ subtractedDate = subWeeks(date, 1 * intervalCount);
+ break;
+ case BillingInterval.Month:
+ subtractedDate = subMonths(date, 1 * intervalCount);
+ break;
+ case BillingInterval.Quarter:
+ subtractedDate = subMonths(date, 3 * intervalCount);
+ break;
+ case BillingInterval.SemiAnnual:
+ subtractedDate = subMonths(date, 6 * intervalCount);
+ break;
+ case BillingInterval.Year:
+ subtractedDate = subYears(date, 1 * intervalCount);
+ break;
+ default:
+ throw new Error(`Invalid billing interval: ${interval}`);
+ }
+ return subtractedDate.getTime();
};
export const addBillingIntervalUnix = ({
- unixTimestamp,
- interval,
- intervalCount = 1,
+ unixTimestamp,
+ interval,
+ intervalCount = 1,
}: {
- unixTimestamp: number;
- interval?: BillingInterval;
- intervalCount?: number;
+ unixTimestamp: number;
+ interval?: BillingInterval;
+ intervalCount?: number;
}) => {
- if (!interval || !intervalCount) {
- return unixTimestamp;
- }
+ if (!interval || !intervalCount) {
+ return unixTimestamp;
+ }
- const date = new UTCDate(unixTimestamp);
- let addedDate = date;
- switch (interval) {
- case BillingInterval.Week:
- addedDate = addWeeks(date, 1 * intervalCount);
- break;
- case BillingInterval.Month:
- addedDate = addMonths(date, intervalCount);
- break;
- case BillingInterval.Quarter:
- addedDate = addMonths(date, 3 * intervalCount);
- break;
- case BillingInterval.SemiAnnual:
- addedDate = addMonths(date, 6 * intervalCount);
- break;
- case BillingInterval.Year:
- addedDate = addYears(date, 1 * intervalCount);
- break;
- default:
- throw new Error(`Invalid billing interval: ${interval}`);
- }
- return addedDate.getTime();
+ const date = new UTCDate(unixTimestamp);
+ let addedDate = date;
+ switch (interval) {
+ case BillingInterval.Week:
+ addedDate = addWeeks(date, 1 * intervalCount);
+ break;
+ case BillingInterval.Month:
+ addedDate = addMonths(date, intervalCount);
+ break;
+ case BillingInterval.Quarter:
+ addedDate = addMonths(date, 3 * intervalCount);
+ break;
+ case BillingInterval.SemiAnnual:
+ addedDate = addMonths(date, 6 * intervalCount);
+ break;
+ case BillingInterval.Year:
+ addedDate = addYears(date, 1 * intervalCount);
+ break;
+ default:
+ throw new Error(`Invalid billing interval: ${interval}`);
+ }
+ return addedDate.getTime();
};
export const getNextStartOfMonthUnix = ({
- interval,
- intervalCount,
+ interval,
+ intervalCount,
}: {
- interval: BillingInterval;
- intervalCount: number;
+ interval: BillingInterval;
+ intervalCount: number;
}) => {
- const nextBillingCycle = addIntervalForProration({
- unixTimestamp: Date.now(),
- intervalConfig: {
- interval,
- intervalCount,
- },
- });
+ const nextBillingCycle = addIntervalForProration({
+ unixTimestamp: Date.now(),
+ intervalConfig: {
+ interval,
+ intervalCount,
+ },
+ });
- // Subtract till it hits first
- const date = new UTCDate(nextBillingCycle);
- const firstDayOfMonth = startOfMonth(date);
- const twelveOClock = setHours(firstDayOfMonth, 12);
+ // Subtract till it hits first
+ const date = new UTCDate(nextBillingCycle);
+ const firstDayOfMonth = startOfMonth(date);
+ const twelveOClock = setHours(firstDayOfMonth, 12);
- return twelveOClock.getTime();
+ return twelveOClock.getTime();
};
export const getAlignedIntervalUnix = ({
- alignWithUnix,
- interval,
- intervalCount,
- now,
- alwaysReturn,
+ alignWithUnix,
+ interval,
+ intervalCount,
+ now,
+ alwaysReturn,
}: {
- alignWithUnix: number;
- interval: BillingInterval;
- intervalCount: number;
- now?: number;
- alwaysReturn?: boolean;
+ alignWithUnix: number;
+ interval: BillingInterval;
+ intervalCount: number;
+ now?: number;
+ alwaysReturn?: boolean;
}) => {
- // alignWithUnix = addSeconds(alignWithUnix, 20).getTime();
+ // alignWithUnix = addSeconds(alignWithUnix, 20).getTime();
- let nextCycleAnchorUnix = alignWithUnix;
+ let nextCycleAnchorUnix = alignWithUnix;
- now = now || Date.now();
+ now = now || Date.now();
- const naturalBillingDate = addIntervalForProration({
- unixTimestamp: now,
- intervalConfig: {
- interval,
- intervalCount,
- },
- });
+ const naturalBillingDate = addIntervalForProration({
+ unixTimestamp: now,
+ intervalConfig: {
+ interval,
+ intervalCount,
+ },
+ });
- const maxIterations = 10000;
- let iterations = 0;
+ const maxIterations = 10000;
+ let iterations = 0;
- const printLogs = false;
- if (printLogs) {
- console.log(
- "Natural billing date:",
- formatUnixToDateTime(naturalBillingDate)
- );
- console.log(
- "Next cycle anchor unix:",
- formatUnixToDateTime(nextCycleAnchorUnix)
- );
- }
+ const printLogs = false;
+ if (printLogs) {
+ console.log(
+ "Natural billing date:",
+ formatUnixToDateTime(naturalBillingDate),
+ );
+ console.log(
+ "Next cycle anchor unix:",
+ formatUnixToDateTime(nextCycleAnchorUnix),
+ );
+ }
- while (true) {
- const subtractedUnix = subtractBillingIntervalUnix({
- unixTimestamp: nextCycleAnchorUnix,
- interval,
- intervalCount,
- });
+ while (true) {
+ const subtractedUnix = subtractBillingIntervalUnix({
+ unixTimestamp: nextCycleAnchorUnix,
+ interval,
+ intervalCount,
+ });
- if (printLogs) {
- console.log("Subtracted unix:", formatUnixToDateTime(subtractedUnix));
- }
+ if (printLogs) {
+ console.log("Subtracted unix:", formatUnixToDateTime(subtractedUnix));
+ }
- if (subtractedUnix <= now) {
- break;
- }
+ if (subtractedUnix <= now) {
+ break;
+ }
- nextCycleAnchorUnix = subtractedUnix;
+ nextCycleAnchorUnix = subtractedUnix;
- iterations++;
- if (iterations > maxIterations) {
- throw new Error("Max iterations reached");
- }
- }
+ iterations++;
+ if (iterations > maxIterations) {
+ throw new Error("Max iterations reached");
+ }
+ }
- let billingCycleAnchorUnix: number | undefined = nextCycleAnchorUnix;
+ let billingCycleAnchorUnix: number | undefined = nextCycleAnchorUnix;
- if (printLogs) {
- console.log(
- "Next cycle anchor:",
- formatUnixToDateTime(nextCycleAnchorUnix)
- );
- console.log("Now:", formatUnixToDateTime(now));
- console.log("--------------------------------");
- }
+ if (printLogs) {
+ console.log(
+ "Next cycle anchor:",
+ formatUnixToDateTime(nextCycleAnchorUnix),
+ );
+ console.log("Now:", formatUnixToDateTime(now));
+ console.log("--------------------------------");
+ }
- let anchorAndNaturalDiff = differenceInSeconds(
- naturalBillingDate,
- nextCycleAnchorUnix
- );
+ let anchorAndNaturalDiff = differenceInSeconds(
+ naturalBillingDate,
+ nextCycleAnchorUnix,
+ );
- // For insurance, also means you can't set billing cycle anchor to a minute in the future...
- let anchorAndNowDiff = Math.abs(
- differenceInSeconds(now, nextCycleAnchorUnix)
- );
+ // For insurance, also means you can't set billing cycle anchor to a minute in the future...
+ let anchorAndNowDiff = Math.abs(
+ differenceInSeconds(now, nextCycleAnchorUnix),
+ );
- if (anchorAndNaturalDiff < 60 || anchorAndNowDiff < 20) {
- if (alwaysReturn) {
- return naturalBillingDate;
- } else {
- billingCycleAnchorUnix = undefined;
- }
- }
+ if (anchorAndNaturalDiff < 60 || anchorAndNowDiff < 20) {
+ if (alwaysReturn) {
+ return naturalBillingDate;
+ } else {
+ billingCycleAnchorUnix = undefined;
+ }
+ }
- return billingCycleAnchorUnix;
+ return billingCycleAnchorUnix;
};
export const subtractFromUnixTillAligned = ({
- targetUnix,
- originalUnix,
+ targetUnix,
+ originalUnix,
}: {
- targetUnix: number;
- originalUnix: number;
+ targetUnix: number;
+ originalUnix: number;
}) => {
- const targetDate = new UTCDate(targetUnix);
- const originalDate = new UTCDate(originalUnix);
+ const targetDate = new UTCDate(targetUnix);
+ const originalDate = new UTCDate(originalUnix);
- // Get target date components
- const targetDay = getDate(targetDate);
- const targetHours = getHours(targetDate);
- const targetMinutes = getMinutes(targetDate);
- const targetSeconds = getSeconds(targetDate);
- const originalDay = getDate(originalDate);
+ // Get target date components
+ const targetDay = getDate(targetDate);
+ const targetHours = getHours(targetDate);
+ const targetMinutes = getMinutes(targetDate);
+ const targetSeconds = getSeconds(targetDate);
+ const originalDay = getDate(originalDate);
- // Create aligned date using date-fns functions
- let alignedDate = originalDate;
+ // Create aligned date using date-fns functions
+ let alignedDate = originalDate;
- // If target day is greater than original day, subtract a month
- if (targetDay > originalDay) {
- alignedDate = subMonths(alignedDate, 1);
- }
+ // If target day is greater than original day, subtract a month
+ if (targetDay > originalDay) {
+ alignedDate = subMonths(alignedDate, 1);
+ }
- // Calculate last day of the month to handle month length differences
- const lastDayOfMonth = new UTCDate(
- alignedDate.getFullYear(),
- alignedDate.getMonth() + 1,
- 0
- ).getDate();
+ // Calculate last day of the month to handle month length differences
+ const lastDayOfMonth = new UTCDate(
+ alignedDate.getFullYear(),
+ alignedDate.getMonth() + 1,
+ 0,
+ ).getDate();
- // Apply target day (capped to last day of month) and time components
- alignedDate = setDate(alignedDate, Math.min(targetDay, lastDayOfMonth));
- alignedDate = setHours(alignedDate, targetHours);
- alignedDate = setMinutes(alignedDate, targetMinutes);
- alignedDate = setSeconds(alignedDate, targetSeconds);
+ // Apply target day (capped to last day of month) and time components
+ alignedDate = setDate(alignedDate, Math.min(targetDay, lastDayOfMonth));
+ alignedDate = setHours(alignedDate, targetHours);
+ alignedDate = setMinutes(alignedDate, targetMinutes);
+ alignedDate = setSeconds(alignedDate, targetSeconds);
- return getTime(alignedDate);
+ return getTime(alignedDate);
};
// Subtracts an interval from a period end, preserving end-of-month anchoring
// e.g. 30 Sep -> 31 Aug (not 30 Aug)
export const subtractIntervalForProration = ({
- unixTimestamp,
- interval,
- intervalCount = 1,
+ unixTimestamp,
+ interval,
+ intervalCount = 1,
}: {
- unixTimestamp: number;
- interval: BillingInterval;
- intervalCount?: number;
+ unixTimestamp: number;
+ interval: BillingInterval;
+ intervalCount?: number;
}) => {
- const endDate = new UTCDate(unixTimestamp);
+ const endDate = new UTCDate(unixTimestamp);
- const isEndOfMonth = () => {
- const lastDay = new UTCDate(
- endDate.getFullYear(),
- endDate.getMonth() + 1,
- 0
- ).getDate();
- return getDate(endDate) === lastDay;
- };
+ const isEndOfMonth = () => {
+ const lastDay = new UTCDate(
+ endDate.getFullYear(),
+ endDate.getMonth() + 1,
+ 0,
+ ).getDate();
+ return getDate(endDate) === lastDay;
+ };
- const preserveTime = (d: UTCDate) => {
- let preserved = new UTCDate(d.getTime());
- preserved = new UTCDate(setHours(preserved, getHours(endDate)).getTime());
- preserved = new UTCDate(
- setMinutes(preserved, getMinutes(endDate)).getTime()
- );
- preserved = new UTCDate(
- setSeconds(preserved, getSeconds(endDate)).getTime()
- );
- return preserved;
- };
+ const preserveTime = (d: UTCDate) => {
+ let preserved = new UTCDate(d.getTime());
+ preserved = new UTCDate(setHours(preserved, getHours(endDate)).getTime());
+ preserved = new UTCDate(
+ setMinutes(preserved, getMinutes(endDate)).getTime(),
+ );
+ preserved = new UTCDate(
+ setSeconds(preserved, getSeconds(endDate)).getTime(),
+ );
+ return preserved;
+ };
- const setToLastDayOfMonth = (d: UTCDate) => {
- const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
- return new UTCDate(setDate(d, last).getTime());
- };
+ const setToLastDayOfMonth = (d: UTCDate) => {
+ const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
+ return new UTCDate(setDate(d, last).getTime());
+ };
- switch (interval) {
- case BillingInterval.Week: {
- const sub = new UTCDate(subWeeks(endDate, 1 * intervalCount).getTime());
- return getTime(sub);
- }
- case BillingInterval.Month: {
- let sub = new UTCDate(subMonths(endDate, 1 * intervalCount).getTime());
- if (isEndOfMonth()) {
- sub = setToLastDayOfMonth(sub);
- }
- sub = preserveTime(sub);
- return getTime(sub);
- }
- case BillingInterval.Quarter: {
- let sub = new UTCDate(subMonths(endDate, 3 * intervalCount).getTime());
- if (isEndOfMonth()) {
- sub = setToLastDayOfMonth(sub);
- }
- sub = preserveTime(sub);
- return getTime(sub);
- }
- case BillingInterval.SemiAnnual: {
- let sub = new UTCDate(subMonths(endDate, 6 * intervalCount).getTime());
- if (isEndOfMonth()) {
- sub = setToLastDayOfMonth(sub);
- }
- sub = preserveTime(sub);
- return getTime(sub);
- }
- case BillingInterval.Year: {
- let sub = new UTCDate(subYears(endDate, 1 * intervalCount).getTime());
- if (isEndOfMonth()) {
- sub = setToLastDayOfMonth(sub);
- }
- sub = preserveTime(sub);
- return getTime(sub);
- }
- default:
- throw new Error(`Invalid billing interval: ${interval}`);
- }
+ switch (interval) {
+ case BillingInterval.Week: {
+ const sub = new UTCDate(subWeeks(endDate, 1 * intervalCount).getTime());
+ return getTime(sub);
+ }
+ case BillingInterval.Month: {
+ let sub = new UTCDate(subMonths(endDate, 1 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ sub = setToLastDayOfMonth(sub);
+ }
+ sub = preserveTime(sub);
+ return getTime(sub);
+ }
+ case BillingInterval.Quarter: {
+ let sub = new UTCDate(subMonths(endDate, 3 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ sub = setToLastDayOfMonth(sub);
+ }
+ sub = preserveTime(sub);
+ return getTime(sub);
+ }
+ case BillingInterval.SemiAnnual: {
+ let sub = new UTCDate(subMonths(endDate, 6 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ sub = setToLastDayOfMonth(sub);
+ }
+ sub = preserveTime(sub);
+ return getTime(sub);
+ }
+ case BillingInterval.Year: {
+ let sub = new UTCDate(subYears(endDate, 1 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ sub = setToLastDayOfMonth(sub);
+ }
+ sub = preserveTime(sub);
+ return getTime(sub);
+ }
+ default:
+ throw new Error(`Invalid billing interval: ${interval}`);
+ }
};
// Adds an interval to a period start, preserving end-of-month anchoring
// e.g. 30 Sep -> 31 Oct (not 30 Oct)
export const addIntervalForProration = ({
- unixTimestamp,
- intervalConfig,
+ unixTimestamp,
+ intervalConfig,
}: {
- unixTimestamp: number;
- intervalConfig: IntervalConfig;
+ unixTimestamp: number;
+ intervalConfig: IntervalConfig;
}) => {
- if (!intervalConfig) return unixTimestamp;
- let { interval, intervalCount } = intervalConfig;
- intervalCount = intervalCount ?? 1;
- const startDate = new UTCDate(unixTimestamp);
+ if (!intervalConfig) return unixTimestamp;
+ let { interval, intervalCount } = intervalConfig;
+ intervalCount = intervalCount ?? 1;
+ const startDate = new UTCDate(unixTimestamp);
- const isEndOfMonth = () => {
- const lastDay = new UTCDate(
- startDate.getFullYear(),
- startDate.getMonth() + 1,
- 0
- ).getDate();
- return getDate(startDate) === lastDay;
- };
+ const isEndOfMonth = () => {
+ const lastDay = new UTCDate(
+ startDate.getFullYear(),
+ startDate.getMonth() + 1,
+ 0,
+ ).getDate();
+ return getDate(startDate) === lastDay;
+ };
- const preserveTime = (d: UTCDate) => {
- let preserved = new UTCDate(d.getTime());
- preserved = new UTCDate(setHours(preserved, getHours(startDate)).getTime());
- preserved = new UTCDate(
- setMinutes(preserved, getMinutes(startDate)).getTime()
- );
- preserved = new UTCDate(
- setSeconds(preserved, getSeconds(startDate)).getTime()
- );
- return preserved;
- };
+ const preserveTime = (d: UTCDate) => {
+ let preserved = new UTCDate(d.getTime());
+ preserved = new UTCDate(setHours(preserved, getHours(startDate)).getTime());
+ preserved = new UTCDate(
+ setMinutes(preserved, getMinutes(startDate)).getTime(),
+ );
+ preserved = new UTCDate(
+ setSeconds(preserved, getSeconds(startDate)).getTime(),
+ );
+ return preserved;
+ };
- const setToLastDayOfMonth = (d: UTCDate) => {
- const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
- return new UTCDate(setDate(d, last).getTime());
- };
+ const setToLastDayOfMonth = (d: UTCDate) => {
+ const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
+ return new UTCDate(setDate(d, last).getTime());
+ };
- switch (interval) {
- case BillingInterval.Week: {
- const add = new UTCDate(addWeeks(startDate, 1 * intervalCount).getTime());
- return getTime(add);
- }
- case BillingInterval.Month: {
- let add = new UTCDate(addMonths(startDate, 1 * intervalCount).getTime());
- if (isEndOfMonth()) {
- add = setToLastDayOfMonth(add);
- }
- add = preserveTime(add);
- return getTime(add);
- }
- case BillingInterval.Quarter: {
- let add = new UTCDate(addMonths(startDate, 3 * intervalCount).getTime());
- if (isEndOfMonth()) {
- add = setToLastDayOfMonth(add);
- }
- add = preserveTime(add);
- return getTime(add);
- }
- case BillingInterval.SemiAnnual: {
- let add = new UTCDate(addMonths(startDate, 6 * intervalCount).getTime());
- if (isEndOfMonth()) {
- add = setToLastDayOfMonth(add);
- }
- add = preserveTime(add);
- return getTime(add);
- }
- case BillingInterval.Year: {
- let add = new UTCDate(addYears(startDate, 1 * intervalCount).getTime());
- if (isEndOfMonth()) {
- add = setToLastDayOfMonth(add);
- }
- add = preserveTime(add);
- return getTime(add);
- }
- default:
- throw new Error(`Invalid billing interval: ${interval}`);
- }
+ switch (interval) {
+ case BillingInterval.Week: {
+ const add = new UTCDate(addWeeks(startDate, 1 * intervalCount).getTime());
+ return getTime(add);
+ }
+ case BillingInterval.Month: {
+ let add = new UTCDate(addMonths(startDate, 1 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ add = setToLastDayOfMonth(add);
+ }
+ add = preserveTime(add);
+ return getTime(add);
+ }
+ case BillingInterval.Quarter: {
+ let add = new UTCDate(addMonths(startDate, 3 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ add = setToLastDayOfMonth(add);
+ }
+ add = preserveTime(add);
+ return getTime(add);
+ }
+ case BillingInterval.SemiAnnual: {
+ let add = new UTCDate(addMonths(startDate, 6 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ add = setToLastDayOfMonth(add);
+ }
+ add = preserveTime(add);
+ return getTime(add);
+ }
+ case BillingInterval.Year: {
+ let add = new UTCDate(addYears(startDate, 1 * intervalCount).getTime());
+ if (isEndOfMonth()) {
+ add = setToLastDayOfMonth(add);
+ }
+ add = preserveTime(add);
+ return getTime(add);
+ }
+ default:
+ throw new Error(`Invalid billing interval: ${interval}`);
+ }
};
diff --git a/server/src/internal/products/prices/billingIntervalUtils2.ts b/server/src/internal/products/prices/billingIntervalUtils2.ts
index 35de8b5dc..4c4c3f4fd 100644
--- a/server/src/internal/products/prices/billingIntervalUtils2.ts
+++ b/server/src/internal/products/prices/billingIntervalUtils2.ts
@@ -1,86 +1,86 @@
import { IntervalConfig } from "@autumn/shared";
import {
- addIntervalForProration,
- subtractIntervalForProration,
+ addIntervalForProration,
+ subtractIntervalForProration,
} from "./billingIntervalUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
export const addIntervalToAnchor = ({
- intervalConfig,
- anchorUnix,
- now,
+ intervalConfig,
+ anchorUnix,
+ now,
}: {
- intervalConfig: IntervalConfig;
- anchorUnix: number;
- now?: number;
+ intervalConfig: IntervalConfig;
+ anchorUnix: number;
+ now?: number;
}) => {
- now = now || Date.now();
+ now = now || Date.now();
- let nextInterval = anchorUnix;
- for (let i = 0; i < 50; i++) {
- if (nextInterval > now) return nextInterval;
+ let nextInterval = anchorUnix;
+ for (let i = 0; i < 50; i++) {
+ if (nextInterval > now) return nextInterval;
- nextInterval = addIntervalForProration({
- unixTimestamp: nextInterval,
- intervalConfig,
- });
- }
+ nextInterval = addIntervalForProration({
+ unixTimestamp: nextInterval,
+ intervalConfig,
+ });
+ }
- return addIntervalForProration({
- unixTimestamp: anchorUnix,
- intervalConfig,
- });
+ return addIntervalForProration({
+ unixTimestamp: anchorUnix,
+ intervalConfig,
+ });
};
export const subtractIntervalFromAnchor = ({
- anchor,
- intervalConfig,
- now,
+ anchor,
+ intervalConfig,
+ now,
}: {
- anchor: number;
- intervalConfig: IntervalConfig;
- now?: number;
+ anchor: number;
+ intervalConfig: IntervalConfig;
+ now?: number;
}) => {
- let curAnchor = anchor;
- now = now || Date.now();
+ let curAnchor = anchor;
+ now = now || Date.now();
- for (let i = 0; i < 50; i++) {
- let newAnchor = subtractIntervalForProration({
- unixTimestamp: anchor,
- interval: intervalConfig.interval,
- intervalCount: intervalConfig.intervalCount ?? 1,
- });
+ for (let i = 0; i < 50; i++) {
+ let newAnchor = subtractIntervalForProration({
+ unixTimestamp: anchor,
+ interval: intervalConfig.interval,
+ intervalCount: intervalConfig.intervalCount ?? 1,
+ });
- // Return anchor before it goes below now
- if (newAnchor <= now) return curAnchor;
+ // Return anchor before it goes below now
+ if (newAnchor <= now) return curAnchor;
- curAnchor = newAnchor;
- }
+ curAnchor = newAnchor;
+ }
- return now;
+ return now;
};
export const getAlignedUnix = ({
- anchor,
- intervalConfig,
- now,
+ anchor,
+ intervalConfig,
+ now,
}: {
- anchor: number; // can be in the future or past
- intervalConfig: IntervalConfig;
- now?: number;
+ anchor: number; // can be in the future or past
+ intervalConfig: IntervalConfig;
+ now?: number;
}) => {
- now = now || Date.now();
+ now = now || Date.now();
- if (anchor <= now)
- return addIntervalToAnchor({
- intervalConfig,
- anchorUnix: anchor,
- now,
- });
+ if (anchor <= now)
+ return addIntervalToAnchor({
+ intervalConfig,
+ anchorUnix: anchor,
+ now,
+ });
- return subtractIntervalFromAnchor({
- anchor,
- intervalConfig,
- now,
- });
+ return subtractIntervalFromAnchor({
+ anchor,
+ intervalConfig,
+ now,
+ });
};
diff --git a/server/src/internal/products/prices/priceInitUtils.ts b/server/src/internal/products/prices/priceInitUtils.ts
index b8715cac9..424ebade9 100644
--- a/server/src/internal/products/prices/priceInitUtils.ts
+++ b/server/src/internal/products/prices/priceInitUtils.ts
@@ -2,240 +2,240 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { generateId } from "@/utils/genUtils.js";
import {
- AllowanceType,
- BillingInterval,
- BillingType,
- Entitlement,
- ErrCode,
- FixedPriceConfig,
- FixedPriceConfigSchema,
- Price,
- PriceType,
- UsagePriceConfig,
- UsagePriceConfigSchema,
+ AllowanceType,
+ BillingInterval,
+ BillingType,
+ Entitlement,
+ ErrCode,
+ FixedPriceConfig,
+ FixedPriceConfigSchema,
+ Price,
+ PriceType,
+ UsagePriceConfig,
+ UsagePriceConfigSchema,
} from "@autumn/shared";
export const constructPrice = ({
- name,
- config,
- orgId,
- internalProductId,
- isCustom = false,
+ name,
+ config,
+ orgId,
+ internalProductId,
+ isCustom = false,
}: {
- name: string;
- config: UsagePriceConfig | FixedPriceConfig;
- orgId: string;
- internalProductId: string;
- isCustom: boolean;
+ name: string;
+ config: UsagePriceConfig | FixedPriceConfig;
+ orgId: string;
+ internalProductId: string;
+ isCustom: boolean;
}) => {
- return {
- id: generateId("pr"),
- org_id: orgId,
- internal_product_id: internalProductId,
- created_at: Date.now(),
- billing_type: getBillingType(config),
- is_custom: isCustom,
+ return {
+ id: generateId("pr"),
+ org_id: orgId,
+ internal_product_id: internalProductId,
+ created_at: Date.now(),
+ billing_type: getBillingType(config),
+ is_custom: isCustom,
- name,
- config,
- };
+ name,
+ config,
+ };
};
// GET PRICES
const validatePrice = (
- price: Price,
- relatedEnt?: Entitlement | undefined | null
+ price: Price,
+ relatedEnt?: Entitlement | undefined | null,
) => {
- if (!price.config?.type) {
- throw new RecaseError({
- message: "Missing `type` field in price config",
- code: ErrCode.InvalidPriceConfig,
- statusCode: 400,
- });
- }
+ if (!price.config?.type) {
+ throw new RecaseError({
+ message: "Missing `type` field in price config",
+ code: ErrCode.InvalidPriceConfig,
+ statusCode: 400,
+ });
+ }
- if (price.config?.type == PriceType.Fixed) {
- FixedPriceConfigSchema.parse(price.config);
- } else {
- UsagePriceConfigSchema.parse(price.config);
+ if (price.config?.type == PriceType.Fixed) {
+ FixedPriceConfigSchema.parse(price.config);
+ } else {
+ UsagePriceConfigSchema.parse(price.config);
- const config = price.config! as UsagePriceConfig;
+ const config = price.config! as UsagePriceConfig;
- if (config.usage_tiers.length == 0) {
- throw new RecaseError({
- message: "Usage based prices should have at least one tier",
- code: ErrCode.InvalidPriceConfig,
- statusCode: 400,
- });
- }
+ if (config.usage_tiers.length == 0) {
+ throw new RecaseError({
+ message: "Usage based prices should have at least one tier",
+ code: ErrCode.InvalidPriceConfig,
+ statusCode: 400,
+ });
+ }
- if (relatedEnt?.allowance_type == AllowanceType.Unlimited) {
- if (config.interval == BillingInterval.OneOff) {
- throw new RecaseError({
- message: `Usage-based price cannot have unlimited allowance (${relatedEnt.feature_id})`,
- code: ErrCode.InvalidPriceConfig,
- statusCode: 400,
- });
- }
- }
+ if (relatedEnt?.allowance_type == AllowanceType.Unlimited) {
+ if (config.interval == BillingInterval.OneOff) {
+ throw new RecaseError({
+ message: `Usage-based price cannot have unlimited allowance (${relatedEnt.feature_id})`,
+ code: ErrCode.InvalidPriceConfig,
+ statusCode: 400,
+ });
+ }
+ }
- const billingType = getBillingType(config);
- if (billingType == BillingType.UsageInArrear) {
- if (config.interval == BillingInterval.OneOff) {
- throw new RecaseError({
- message: "One off prices must be billed at start of period",
- code: ErrCode.InvalidPriceConfig,
- statusCode: 400,
- });
- }
- }
- }
+ const billingType = getBillingType(config);
+ if (billingType == BillingType.UsageInArrear) {
+ if (config.interval == BillingInterval.OneOff) {
+ throw new RecaseError({
+ message: "One off prices must be billed at start of period",
+ code: ErrCode.InvalidPriceConfig,
+ statusCode: 400,
+ });
+ }
+ }
+ }
- return {
- valid: true,
- error: null,
- };
+ return {
+ valid: true,
+ error: null,
+ };
};
export const tiersAreSame = (tiers1: any[], tiers2: any[]) => {
- if (tiers1.length !== tiers2.length) return false;
- for (let i = 0; i < tiers1.length; i++) {
- let tier1 = tiers1[i];
- let tier2 = tiers2[i];
- // if (tier1.to !== tier2.to) return false;
+ if (tiers1.length !== tiers2.length) return false;
+ for (let i = 0; i < tiers1.length; i++) {
+ let tier1 = tiers1[i];
+ let tier2 = tiers2[i];
+ // if (tier1.to !== tier2.to) return false;
- // Only compare to if not last tier
- if (i !== tiers1.length - 1) {
- if (tier1.to !== tier2.to) return false;
- }
+ // Only compare to if not last tier
+ if (i !== tiers1.length - 1) {
+ if (tier1.to !== tier2.to) return false;
+ }
- if (tier1.amount !== tier2.amount) return false;
- }
- return true;
+ if (tier1.amount !== tier2.amount) return false;
+ }
+ return true;
};
export const pricesAreSame = (
- price1: Price,
- price2: Price,
- logDifferences = false
+ price1: Price,
+ price2: Price,
+ logDifferences = false,
) => {
- // if (price1.name !== price2.name) return false;
+ // if (price1.name !== price2.name) return false;
- const config1 = price1.config!;
- const config2 = price2.config!;
- // if (config1.type !== config2.type) return false; // shouldn't be able to change, but just in case...
+ const config1 = price1.config!;
+ const config2 = price2.config!;
+ // if (config1.type !== config2.type) return false; // shouldn't be able to change, but just in case...
- if (config1.type === PriceType.Fixed) {
- const fixedConfig1 = FixedPriceConfigSchema.parse(config1);
- const fixedConfig2 = FixedPriceConfigSchema.parse(config2);
+ if (config1.type === PriceType.Fixed) {
+ const fixedConfig1 = FixedPriceConfigSchema.parse(config1);
+ const fixedConfig2 = FixedPriceConfigSchema.parse(config2);
- // 1. Check amount same
- let diffs = {
- amount: {
- condition: fixedConfig1.amount !== fixedConfig2.amount,
- message: `Amount different: ${fixedConfig1.amount} !== ${fixedConfig2.amount}`,
- },
- interval: {
- condition: fixedConfig1.interval !== fixedConfig2.interval,
- message: `Interval different: ${fixedConfig1.interval} !== ${fixedConfig2.interval}`,
- },
- interval_count: {
- condition: fixedConfig1.interval_count !== fixedConfig2.interval_count,
- message: `Interval count different: ${fixedConfig1.interval_count} !== ${fixedConfig2.interval_count}`,
- },
- };
+ // 1. Check amount same
+ let diffs = {
+ amount: {
+ condition: fixedConfig1.amount !== fixedConfig2.amount,
+ message: `Amount different: ${fixedConfig1.amount} !== ${fixedConfig2.amount}`,
+ },
+ interval: {
+ condition: fixedConfig1.interval !== fixedConfig2.interval,
+ message: `Interval different: ${fixedConfig1.interval} !== ${fixedConfig2.interval}`,
+ },
+ interval_count: {
+ condition: fixedConfig1.interval_count !== fixedConfig2.interval_count,
+ message: `Interval count different: ${fixedConfig1.interval_count} !== ${fixedConfig2.interval_count}`,
+ },
+ };
- let pricesAreDiff = Object.values(diffs).some((d) => d.condition);
+ let pricesAreDiff = Object.values(diffs).some((d) => d.condition);
- if (pricesAreDiff) {
- console.log("Fixed price different");
- console.log(
- "Differences:",
- Object.values(diffs)
- .filter((d) => d.condition)
- .map((d) => d.message)
- );
- }
+ if (pricesAreDiff) {
+ console.log("Fixed price different");
+ console.log(
+ "Differences:",
+ Object.values(diffs)
+ .filter((d) => d.condition)
+ .map((d) => d.message),
+ );
+ }
- return !pricesAreDiff;
- } else {
- const usageConfig1 = UsagePriceConfigSchema.parse(config1);
- const usageConfig2 = UsagePriceConfigSchema.parse(config2);
+ return !pricesAreDiff;
+ } else {
+ const usageConfig1 = UsagePriceConfigSchema.parse(config1);
+ const usageConfig2 = UsagePriceConfigSchema.parse(config2);
- let diffs = {
- should_prorate: {
- condition: usageConfig1.should_prorate !== usageConfig2.should_prorate,
- message: `Should prorate different: ${usageConfig1.should_prorate} !== ${usageConfig2.should_prorate}`,
- },
- bill_when: {
- condition: usageConfig1.bill_when !== usageConfig2.bill_when,
- message: `Bill when different: ${usageConfig1.bill_when} !== ${usageConfig2.bill_when}`,
- },
- billing_units: {
- condition: usageConfig1.billing_units !== usageConfig2.billing_units,
- message: `Billing units different: ${usageConfig1.billing_units} !== ${usageConfig2.billing_units}`,
- },
- interval: {
- condition: usageConfig1.interval !== usageConfig2.interval,
- message: `Interval different: ${usageConfig1.interval} !== ${usageConfig2.interval}`,
- },
- interval_count: {
- condition: usageConfig1.interval_count !== usageConfig2.interval_count,
- message: `Interval count different: ${usageConfig1.interval_count} !== ${usageConfig2.interval_count}`,
- },
- internal_feature_id: {
- condition:
- usageConfig1.internal_feature_id !== usageConfig2.internal_feature_id,
- message: `Internal feature ID different: ${usageConfig1.internal_feature_id} !== ${usageConfig2.internal_feature_id}`,
- },
- feature_id: {
- condition: usageConfig1.feature_id !== usageConfig2.feature_id,
- message: `Feature ID different: ${usageConfig1.feature_id} !== ${usageConfig2.feature_id}`,
- },
- usage_tiers: {
- condition: !tiersAreSame(
- usageConfig1.usage_tiers,
- usageConfig2.usage_tiers
- ),
- message: `Usage tiers different: ${usageConfig1.usage_tiers.map(
- (t) => `${t.to} (${t.amount})`
- )} !== ${usageConfig2.usage_tiers.map((t) => `${t.to} (${t.amount})`)}`,
- },
- };
+ let diffs = {
+ should_prorate: {
+ condition: usageConfig1.should_prorate !== usageConfig2.should_prorate,
+ message: `Should prorate different: ${usageConfig1.should_prorate} !== ${usageConfig2.should_prorate}`,
+ },
+ bill_when: {
+ condition: usageConfig1.bill_when !== usageConfig2.bill_when,
+ message: `Bill when different: ${usageConfig1.bill_when} !== ${usageConfig2.bill_when}`,
+ },
+ billing_units: {
+ condition: usageConfig1.billing_units !== usageConfig2.billing_units,
+ message: `Billing units different: ${usageConfig1.billing_units} !== ${usageConfig2.billing_units}`,
+ },
+ interval: {
+ condition: usageConfig1.interval !== usageConfig2.interval,
+ message: `Interval different: ${usageConfig1.interval} !== ${usageConfig2.interval}`,
+ },
+ interval_count: {
+ condition: usageConfig1.interval_count !== usageConfig2.interval_count,
+ message: `Interval count different: ${usageConfig1.interval_count} !== ${usageConfig2.interval_count}`,
+ },
+ internal_feature_id: {
+ condition:
+ usageConfig1.internal_feature_id !== usageConfig2.internal_feature_id,
+ message: `Internal feature ID different: ${usageConfig1.internal_feature_id} !== ${usageConfig2.internal_feature_id}`,
+ },
+ feature_id: {
+ condition: usageConfig1.feature_id !== usageConfig2.feature_id,
+ message: `Feature ID different: ${usageConfig1.feature_id} !== ${usageConfig2.feature_id}`,
+ },
+ usage_tiers: {
+ condition: !tiersAreSame(
+ usageConfig1.usage_tiers,
+ usageConfig2.usage_tiers,
+ ),
+ message: `Usage tiers different: ${usageConfig1.usage_tiers.map(
+ (t) => `${t.to} (${t.amount})`,
+ )} !== ${usageConfig2.usage_tiers.map((t) => `${t.to} (${t.amount})`)}`,
+ },
+ };
- const prorationConfig1 = price1.proration_config;
- const prorationConfig2 = price2.proration_config;
+ const prorationConfig1 = price1.proration_config;
+ const prorationConfig2 = price2.proration_config;
- const prorationConfigDiff = {
- on_increase: {
- condition:
- prorationConfig1?.on_increase != prorationConfig2?.on_increase,
- message: `On increase different: ${prorationConfig1?.on_increase} != ${prorationConfig2?.on_increase}`,
- },
- on_decrease: {
- condition:
- prorationConfig1?.on_decrease != prorationConfig2?.on_decrease,
- message: `On decrease different: ${prorationConfig1?.on_decrease} != ${prorationConfig2?.on_decrease}`,
- },
- };
+ const prorationConfigDiff = {
+ on_increase: {
+ condition:
+ prorationConfig1?.on_increase != prorationConfig2?.on_increase,
+ message: `On increase different: ${prorationConfig1?.on_increase} != ${prorationConfig2?.on_increase}`,
+ },
+ on_decrease: {
+ condition:
+ prorationConfig1?.on_decrease != prorationConfig2?.on_decrease,
+ message: `On decrease different: ${prorationConfig1?.on_decrease} != ${prorationConfig2?.on_decrease}`,
+ },
+ };
- let pricesAreDiff =
- Object.values(diffs).some((d) => d.condition) ||
- Object.values(prorationConfigDiff).some((d) => d.condition);
+ let pricesAreDiff =
+ Object.values(diffs).some((d) => d.condition) ||
+ Object.values(prorationConfigDiff).some((d) => d.condition);
- if (pricesAreDiff && logDifferences) {
- console.log(`Usage price different: ${usageConfig1.feature_id}`);
- console.log(
- "Differences:",
- Object.values(diffs)
- .filter((d) => d.condition)
- .map((d) => d.message),
- Object.values(prorationConfigDiff)
- .filter((d) => d.condition)
- .map((d) => d.message)
- );
- }
+ if (pricesAreDiff && logDifferences) {
+ console.log(`Usage price different: ${usageConfig1.feature_id}`);
+ console.log(
+ "Differences:",
+ Object.values(diffs)
+ .filter((d) => d.condition)
+ .map((d) => d.message),
+ Object.values(prorationConfigDiff)
+ .filter((d) => d.condition)
+ .map((d) => d.message),
+ );
+ }
- return !pricesAreDiff;
- }
+ return !pricesAreDiff;
+ }
};
diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts
index 1c9631b21..5c81b615f 100644
--- a/server/src/internal/products/prices/priceUtils.ts
+++ b/server/src/internal/products/prices/priceUtils.ts
@@ -1,21 +1,21 @@
import { compareObjects, generateId, notNullish } from "@/utils/genUtils.js";
import {
- BillWhen,
- BillingInterval,
- BillingType,
- FixedPriceConfig,
- Price,
- PriceType,
- UsagePriceConfig,
- Entitlement,
- EntitlementWithFeature,
- FeatureOptions,
- ErrCode,
- FullProduct,
- TierInfinite,
- OnIncrease,
- OnDecrease,
- Product,
+ BillWhen,
+ BillingInterval,
+ BillingType,
+ FixedPriceConfig,
+ Price,
+ PriceType,
+ UsagePriceConfig,
+ Entitlement,
+ EntitlementWithFeature,
+ FeatureOptions,
+ ErrCode,
+ FullProduct,
+ TierInfinite,
+ OnIncrease,
+ OnDecrease,
+ Product,
} from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js";
@@ -24,439 +24,440 @@ import { Decimal } from "decimal.js";
import { compareBillingIntervals } from "./priceUtils/priceIntervalUtils.js";
const BillingIntervalOrder = [
- BillingInterval.Year,
- BillingInterval.SemiAnnual,
- BillingInterval.Quarter,
- BillingInterval.Month,
- BillingInterval.OneOff,
+ BillingInterval.Year,
+ BillingInterval.SemiAnnual,
+ BillingInterval.Quarter,
+ BillingInterval.Month,
+ BillingInterval.OneOff,
];
export const constructPrice = ({
- internalProductId,
- entitlementId,
- orgId,
- fixedConfig,
- usageConfig,
- isCustom,
+ internalProductId,
+ entitlementId,
+ orgId,
+ fixedConfig,
+ usageConfig,
+ isCustom,
}: {
- internalProductId: string;
- isCustom: boolean;
- orgId: string;
- entitlementId?: string;
- fixedConfig?: FixedPriceConfig;
- usageConfig?: UsagePriceConfig;
+ internalProductId: string;
+ isCustom: boolean;
+ orgId: string;
+ entitlementId?: string;
+ fixedConfig?: FixedPriceConfig;
+ usageConfig?: UsagePriceConfig;
}) => {
- if (!usageConfig && !fixedConfig) {
- throw new RecaseError({
- message: "Usage config or fixed config must be provided",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (!usageConfig && !fixedConfig) {
+ throw new RecaseError({
+ message: "Usage config or fixed config must be provided",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- let newPrice: Price = {
- id: generateId("pr"),
- org_id: orgId,
- internal_product_id: internalProductId,
- created_at: Date.now(),
- is_custom: isCustom,
- config: (usageConfig || fixedConfig)!,
- entitlement_id: entitlementId,
- proration_config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
- };
+ let newPrice: Price = {
+ id: generateId("pr"),
+ org_id: orgId,
+ internal_product_id: internalProductId,
+ created_at: Date.now(),
+ is_custom: isCustom,
+ config: (usageConfig || fixedConfig)!,
+ entitlement_id: entitlementId,
+ proration_config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
+ };
- return newPrice;
+ return newPrice;
};
export const getBillingType = (config: FixedPriceConfig | UsagePriceConfig) => {
- // 1. Fixed cycle / one off
- if (
- config.type == PriceType.Fixed &&
- config.interval == BillingInterval.OneOff
- ) {
- return BillingType.OneOff;
- } else if (config.type == PriceType.Fixed) {
- return BillingType.FixedCycle;
- }
+ // 1. Fixed cycle / one off
+ if (
+ config.type == PriceType.Fixed &&
+ config.interval == BillingInterval.OneOff
+ ) {
+ return BillingType.OneOff;
+ } else if (config.type == PriceType.Fixed) {
+ return BillingType.FixedCycle;
+ }
- // 2. Prepaid
+ // 2. Prepaid
- let usageConfig = config as UsagePriceConfig;
- if (
- usageConfig.bill_when == BillWhen.InAdvance ||
- usageConfig.bill_when == BillWhen.StartOfPeriod
- ) {
- return BillingType.UsageInAdvance;
- } else if (usageConfig.bill_when == BillWhen.EndOfPeriod) {
- if (usageConfig.should_prorate) {
- return BillingType.InArrearProrated;
- }
- return BillingType.UsageInArrear;
- }
+ let usageConfig = config as UsagePriceConfig;
+ if (
+ usageConfig.bill_when == BillWhen.InAdvance ||
+ usageConfig.bill_when == BillWhen.StartOfPeriod
+ ) {
+ return BillingType.UsageInAdvance;
+ } else if (usageConfig.bill_when == BillWhen.EndOfPeriod) {
+ if (usageConfig.should_prorate) {
+ return BillingType.InArrearProrated;
+ }
+ return BillingType.UsageInArrear;
+ }
- return BillingType.UsageInArrear;
+ return BillingType.UsageInArrear;
};
export const getBillingInterval = (prices: Price[]) => {
- if (prices.length === 0) {
- return {
- interval: BillingInterval.OneOff,
- intervalCount: 1,
- };
- }
+ if (prices.length === 0) {
+ return {
+ interval: BillingInterval.OneOff,
+ intervalCount: 1,
+ };
+ }
- const pricesCopy = structuredClone(prices);
+ const pricesCopy = structuredClone(prices);
- try {
- pricesCopy.sort((a, b) => {
- return compareBillingIntervals({
- configA: {
- interval: a.config!.interval as BillingInterval,
- intervalCount: a.config!.interval_count || 1,
- },
- configB: {
- interval: b.config!.interval as BillingInterval,
- intervalCount: b.config!.interval_count || 1,
- },
- });
- // return (
- // BillingIntervalOrder.indexOf(b.config!.interval!) -
- // BillingIntervalOrder.indexOf(a.config!.interval!)
- // );
- });
- } catch (error) {
- console.log("Error sorting prices:", error);
- throw error;
- }
+ try {
+ pricesCopy.sort((a, b) => {
+ return compareBillingIntervals({
+ configA: {
+ interval: a.config!.interval as BillingInterval,
+ intervalCount: a.config!.interval_count || 1,
+ },
+ configB: {
+ interval: b.config!.interval as BillingInterval,
+ intervalCount: b.config!.interval_count || 1,
+ },
+ });
+ // return (
+ // BillingIntervalOrder.indexOf(b.config!.interval!) -
+ // BillingIntervalOrder.indexOf(a.config!.interval!)
+ // );
+ });
+ } catch (error) {
+ console.log("Error sorting prices:", error);
+ throw error;
+ }
- // console.log(
- // "pricesCopy",
- // pricesCopy.map((p) => ({
- // interval: p.config!.interval,
- // intervalCount: p.config!.interval_count,
- // }))
- // );
+ // console.log(
+ // "pricesCopy",
+ // pricesCopy.map((p) => ({
+ // interval: p.config!.interval,
+ // intervalCount: p.config!.interval_count,
+ // }))
+ // );
- if (pricesCopy.length == 0) {
- throw new RecaseError({
- message: "No prices found, can't get billing interval",
- code: ErrCode.InvalidRequest,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (pricesCopy.length == 0) {
+ throw new RecaseError({
+ message: "No prices found, can't get billing interval",
+ code: ErrCode.InvalidRequest,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- return {
- interval: pricesCopy[0].config!.interval as BillingInterval,
- intervalCount: pricesCopy[0].config!.interval_count || 1,
- };
- // return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval;
+ return {
+ interval: pricesCopy[0].config!.interval as BillingInterval,
+ intervalCount: pricesCopy[0].config!.interval_count || 1,
+ };
+ // return pricesCopy[pricesCopy.length - 1].config!.interval as BillingInterval;
};
export const pricesOnlyOneOff = (prices: Price[]) => {
- if (prices.length == 0) return false;
+ if (prices.length == 0) return false;
- return prices.every((price) => {
- let interval = price.config?.interval;
+ return prices.every((price) => {
+ let interval = price.config?.interval;
- if (!interval || interval != BillingInterval.OneOff) {
- return false;
- }
- return true;
- });
+ if (!interval || interval != BillingInterval.OneOff) {
+ return false;
+ }
+ return true;
+ });
};
export const pricesContainRecurring = (prices: Price[]) => {
- // TODO: Look into this...
- return prices.some((price) => {
- const interval = price.config?.interval;
+ // TODO: Look into this...
+ return prices.some((price) => {
+ const interval = price.config?.interval;
- if (interval && interval != BillingInterval.OneOff) {
- return true;
- }
+ if (interval && interval != BillingInterval.OneOff) {
+ return true;
+ }
- return false;
- });
+ return false;
+ });
};
// Get price options
export const getEntOptions = (
- optionsList: FeatureOptions[] | null | undefined,
- entitlement: Entitlement | EntitlementWithFeature
+ optionsList: FeatureOptions[] | null | undefined,
+ entitlement: Entitlement | EntitlementWithFeature,
) => {
- if (!entitlement || !optionsList || !Array.isArray(optionsList)) {
- return null;
- }
- const options = optionsList.find(
- (options) => options.internal_feature_id === entitlement.internal_feature_id
- );
- return options;
+ if (!entitlement || !optionsList || !Array.isArray(optionsList)) {
+ return null;
+ }
+ const options = optionsList.find(
+ (options) =>
+ options.internal_feature_id === entitlement.internal_feature_id,
+ );
+ return options;
};
export const getPriceEntitlement = (
- price: Price,
- entitlements: EntitlementWithFeature[],
- allowFeatureMatch = false
+ price: Price,
+ entitlements: EntitlementWithFeature[],
+ allowFeatureMatch = false,
) => {
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- const entitlement = entitlements.find((ent) => {
- let entIdMatch =
- notNullish(price.entitlement_id) && price.entitlement_id == ent.id;
+ const entitlement = entitlements.find((ent) => {
+ let entIdMatch =
+ notNullish(price.entitlement_id) && price.entitlement_id == ent.id;
- let featureIdMatch =
- notNullish(config.internal_feature_id) &&
- config.internal_feature_id == ent.internal_feature_id;
+ let featureIdMatch =
+ notNullish(config.internal_feature_id) &&
+ config.internal_feature_id == ent.internal_feature_id;
- let productIdMatch = ent.internal_product_id == price.internal_product_id;
+ let productIdMatch = ent.internal_product_id == price.internal_product_id;
- if (allowFeatureMatch) {
- return (entIdMatch || featureIdMatch) && productIdMatch;
- }
+ if (allowFeatureMatch) {
+ return (entIdMatch || featureIdMatch) && productIdMatch;
+ }
- return entIdMatch && productIdMatch;
- });
+ return entIdMatch && productIdMatch;
+ });
- return entitlement as EntitlementWithFeature;
+ return entitlement as EntitlementWithFeature;
};
export const getPriceOptions = (
- price: Price,
- optionsList: FeatureOptions[]
+ price: Price,
+ optionsList: FeatureOptions[],
) => {
- let config = price.config as UsagePriceConfig;
+ let config = price.config as UsagePriceConfig;
- const options = optionsList.find(
- (options) => options.internal_feature_id === config.internal_feature_id
- );
+ const options = optionsList.find(
+ (options) => options.internal_feature_id === config.internal_feature_id,
+ );
- return options;
+ return options;
};
export const pricesAreSame = (price1: Price, price2: Price) => {
- for (const key in price1.config) {
- const originalValue = (price1.config as any)[key];
- const newValue = (price2.config as any)[key];
+ for (const key in price1.config) {
+ const originalValue = (price1.config as any)[key];
+ const newValue = (price2.config as any)[key];
- if (key == "usage_tiers") {
- for (let i = 0; i < originalValue.length; i++) {
- const originalTier = originalValue[i];
- const newTier = newValue[i];
- if (!compareObjects(originalTier, newTier)) {
- return false;
- }
- }
- } else if (originalValue !== newValue) {
- return false;
- }
- }
+ if (key == "usage_tiers") {
+ for (let i = 0; i < originalValue.length; i++) {
+ const originalTier = originalValue[i];
+ const newTier = newValue[i];
+ if (!compareObjects(originalTier, newTier)) {
+ return false;
+ }
+ }
+ } else if (originalValue !== newValue) {
+ return false;
+ }
+ }
- return true;
+ return true;
};
export const getUsageTier = (price: Price, quantity: number) => {
- let usageConfig = price.config as UsagePriceConfig;
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- if (i == usageConfig.usage_tiers.length - 1) {
- return usageConfig.usage_tiers[i];
- }
+ let usageConfig = price.config as UsagePriceConfig;
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ if (i == usageConfig.usage_tiers.length - 1) {
+ return usageConfig.usage_tiers[i];
+ }
- let tier = usageConfig.usage_tiers[i];
- if (tier.to == TierInfinite || tier.to >= quantity) {
- return tier;
- }
- }
- return usageConfig.usage_tiers[0];
+ let tier = usageConfig.usage_tiers[i];
+ if (tier.to == TierInfinite || tier.to >= quantity) {
+ return tier;
+ }
+ }
+ return usageConfig.usage_tiers[0];
};
export const getPriceAmount = ({
- price,
- options,
- relatedEnt,
+ price,
+ options,
+ relatedEnt,
}: {
- price: Price;
- options?: FeatureOptions;
- relatedEnt?: EntitlementWithFeature;
- quantity?: number;
+ price: Price;
+ options?: FeatureOptions;
+ relatedEnt?: EntitlementWithFeature;
+ quantity?: number;
}) => {
- let billingType = getBillingType(price.config!);
- if (billingType == BillingType.OneOff) {
- let config = price.config as FixedPriceConfig;
- return Number(config.amount.toFixed(2));
- } else if (billingType == BillingType.UsageInAdvance) {
- let quantity = options?.quantity!;
- let config = price.config as UsagePriceConfig;
+ let billingType = getBillingType(price.config!);
+ if (billingType == BillingType.OneOff) {
+ let config = price.config as FixedPriceConfig;
+ return Number(config.amount.toFixed(2));
+ } else if (billingType == BillingType.UsageInAdvance) {
+ let quantity = options?.quantity!;
+ let config = price.config as UsagePriceConfig;
- let overage = new Decimal(quantity)
- .mul(config.billing_units || 1)
- .toNumber();
+ let overage = new Decimal(quantity)
+ .mul(config.billing_units || 1)
+ .toNumber();
- return getPriceForOverage(price, overage);
- }
+ return getPriceForOverage(price, overage);
+ }
- return 0;
+ return 0;
};
export const getPriceForOverage = (price: Price, overage?: number) => {
- let usageConfig = price.config as UsagePriceConfig;
- let billingType = getBillingType(usageConfig);
+ let usageConfig = price.config as UsagePriceConfig;
+ let billingType = getBillingType(usageConfig);
- if (
- billingType == BillingType.FixedCycle ||
- billingType == BillingType.OneOff
- ) {
- const config = price.config as FixedPriceConfig;
- return config.amount;
- }
+ if (
+ billingType == BillingType.FixedCycle ||
+ billingType == BillingType.OneOff
+ ) {
+ const config = price.config as FixedPriceConfig;
+ return config.amount;
+ }
- let amount = 0;
- let billingUnits = usageConfig.billing_units || 1;
- let remainingUsage = new Decimal(
- Math.ceil(new Decimal(overage!).div(billingUnits).toNumber())
- )
- .mul(billingUnits)
- .toNumber();
+ let amount = 0;
+ let billingUnits = usageConfig.billing_units || 1;
+ let remainingUsage = new Decimal(
+ Math.ceil(new Decimal(overage!).div(billingUnits).toNumber()),
+ )
+ .mul(billingUnits)
+ .toNumber();
- let lastTo: number = 0;
- for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
- let tier = usageConfig.usage_tiers[i];
+ let lastTo: number = 0;
+ for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
+ let tier = usageConfig.usage_tiers[i];
- let amountUsed = 0;
- if (tier.to == TierInfinite || tier.to == -1) {
- amountUsed = remainingUsage;
- } else {
- amountUsed = Math.min(remainingUsage, tier.to - lastTo);
- lastTo = tier.to;
- }
+ let amountUsed = 0;
+ if (tier.to == TierInfinite || tier.to == -1) {
+ amountUsed = remainingUsage;
+ } else {
+ amountUsed = Math.min(remainingUsage, tier.to - lastTo);
+ lastTo = tier.to;
+ }
- // Divide amount by billing units
- let amountPerUnit = new Decimal(tier.amount)
- .div(usageConfig.billing_units!)
- .toNumber();
+ // Divide amount by billing units
+ let amountPerUnit = new Decimal(tier.amount)
+ .div(usageConfig.billing_units!)
+ .toNumber();
- amount += amountPerUnit * amountUsed;
- remainingUsage -= amountUsed;
+ amount += amountPerUnit * amountUsed;
+ remainingUsage -= amountUsed;
- if (remainingUsage <= 0) {
- break;
- }
- }
+ if (remainingUsage <= 0) {
+ break;
+ }
+ }
- return Number(amount.toFixed(10));
+ return Number(amount.toFixed(10));
};
export const priceToEventName = (productName: string, featureName: string) => {
- return `${productName} - ${featureName}`;
+ return `${productName} - ${featureName}`;
};
export const roundPriceAmounts = (price: Price) => {
- if (price.config!.type == PriceType.Fixed) {
- const config = price.config as FixedPriceConfig;
- config.amount = Number(config.amount.toFixed(10));
- price.config = config;
- } else if (price.config!.type == PriceType.Usage) {
- const config = price.config as UsagePriceConfig;
- for (let i = 0; i < config.usage_tiers.length; i++) {
- config.usage_tiers[i].amount = Number(
- config.usage_tiers[i].amount.toFixed(10)
- );
- }
+ if (price.config!.type == PriceType.Fixed) {
+ const config = price.config as FixedPriceConfig;
+ config.amount = Number(config.amount.toFixed(10));
+ price.config = config;
+ } else if (price.config!.type == PriceType.Usage) {
+ const config = price.config as UsagePriceConfig;
+ for (let i = 0; i < config.usage_tiers.length; i++) {
+ config.usage_tiers[i].amount = Number(
+ config.usage_tiers[i].amount.toFixed(10),
+ );
+ }
- price.config = config;
- }
+ price.config = config;
+ }
};
export const priceIsOneOffAndTiered = (
- price: Price,
- relatedEnt: EntitlementWithFeature
+ price: Price,
+ relatedEnt: EntitlementWithFeature,
) => {
- let config = price.config as UsagePriceConfig;
- if (config.type == PriceType.Fixed) {
- return false;
- }
+ let config = price.config as UsagePriceConfig;
+ if (config.type == PriceType.Fixed) {
+ return false;
+ }
- return (
- config.interval == BillingInterval.OneOff && config.usage_tiers.length > 1
- );
+ return (
+ config.interval == BillingInterval.OneOff && config.usage_tiers.length > 1
+ );
};
export const getProductForPrice = (price: Price, products: FullProduct[]) => {
- return products.find(
- (product) => product.internal_id === price.internal_product_id
- );
+ return products.find(
+ (product) => product.internal_id === price.internal_product_id,
+ );
};
// Price to price / tiers
export const priceToAmountOrTiers = (price: Price) => {
- if (price.config!.type == PriceType.Fixed) {
- let config = price.config as FixedPriceConfig;
- return {
- price: config.amount,
- };
- } else {
- let config = price.config as UsagePriceConfig;
- if (config.usage_tiers.length > 1) {
- return {
- tiers: config.usage_tiers,
- };
- } else {
- return {
- price: config.usage_tiers[0].amount,
- };
- }
- }
+ if (price.config!.type == PriceType.Fixed) {
+ let config = price.config as FixedPriceConfig;
+ return {
+ price: config.amount,
+ };
+ } else {
+ let config = price.config as UsagePriceConfig;
+ if (config.usage_tiers.length > 1) {
+ return {
+ tiers: config.usage_tiers,
+ };
+ } else {
+ return {
+ price: config.usage_tiers[0].amount,
+ };
+ }
+ }
};
export const roundUsage = ({
- usage,
- billingUnits,
+ usage,
+ billingUnits,
}: {
- usage: number;
- billingUnits: number;
+ usage: number;
+ billingUnits: number;
}) => {
- if (!billingUnits || billingUnits == 1) {
- return usage;
- }
+ if (!billingUnits || billingUnits == 1) {
+ return usage;
+ }
- return new Decimal(usage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
+ return new Decimal(usage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
};
export const formatPrice = ({
- price,
- product,
+ price,
+ product,
}: {
- price: Price;
- product?: Product;
+ price: Price;
+ product?: Product;
}) => {
- if (price.config.type == PriceType.Fixed) {
- const config = price.config as FixedPriceConfig;
- const formatted = `${config.amount}${config.interval == BillingInterval.OneOff ? "(one off)" : `/ ${config.interval}`}`;
- if (product) {
- return `${product.name} - ${formatted}`;
- }
- return formatted;
- } else {
- const config = price.config as UsagePriceConfig;
- let billingType = getBillingType(config);
- let formatBillingType = {
- [BillingType.UsageInAdvance]: "prepaid",
- [BillingType.UsageInArrear]: "usage",
- [BillingType.InArrearProrated]: "cont_use",
- [BillingType.FixedCycle]: "cont_use",
- };
+ if (price.config.type == PriceType.Fixed) {
+ const config = price.config as FixedPriceConfig;
+ const formatted = `${config.amount}${config.interval == BillingInterval.OneOff ? "(one off)" : `/ ${config.interval}`}`;
+ if (product) {
+ return `${product.name} - ${formatted}`;
+ }
+ return formatted;
+ } else {
+ const config = price.config as UsagePriceConfig;
+ let billingType = getBillingType(config);
+ let formatBillingType = {
+ [BillingType.UsageInAdvance]: "prepaid",
+ [BillingType.UsageInArrear]: "usage",
+ [BillingType.InArrearProrated]: "cont_use",
+ [BillingType.FixedCycle]: "cont_use",
+ };
- let featureId = config.feature_id;
+ let featureId = config.feature_id;
- const formatted = `${formatBillingType[billingType as keyof typeof formatBillingType]} price for feature ${featureId}: $${config.usage_tiers[0].amount}${config.billing_units ? ` ${config.billing_units}` : ""}`;
- if (product) {
- return `${product.name} - ${formatted}`;
- }
- return formatted;
- }
+ const formatted = `${formatBillingType[billingType as keyof typeof formatBillingType]} price for feature ${featureId}: $${config.usage_tiers[0].amount}${config.billing_units ? ` ${config.billing_units}` : ""}`;
+ if (product) {
+ return `${product.name} - ${formatted}`;
+ }
+ return formatted;
+ }
};
diff --git a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.ts b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.ts
index 8b9d352a3..bb962708c 100644
--- a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.ts
+++ b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.ts
@@ -1,90 +1,90 @@
import { generateId } from "@/utils/genUtils.js";
import {
- FullCustomerEntitlement,
- InsertReplaceable,
- InsertReplaceableSchema,
- OnDecrease,
- Price,
+ FullCustomerEntitlement,
+ InsertReplaceable,
+ InsertReplaceableSchema,
+ OnDecrease,
+ Price,
} from "@autumn/shared";
export const getReplaceables = ({
- cusEnt,
- prevOverage,
- newOverage,
- deleteNextCycle = true,
+ cusEnt,
+ prevOverage,
+ newOverage,
+ deleteNextCycle = true,
}: {
- cusEnt: FullCustomerEntitlement;
- prevOverage: number;
- newOverage: number;
- deleteNextCycle?: boolean;
+ cusEnt: FullCustomerEntitlement;
+ prevOverage: number;
+ newOverage: number;
+ deleteNextCycle?: boolean;
}): InsertReplaceable[] => {
- if (prevOverage <= newOverage) {
- return [];
- }
+ if (prevOverage <= newOverage) {
+ return [];
+ }
- let numReplaceables = prevOverage - newOverage;
- let newReplaceables = Array.from({ length: numReplaceables }, (_, i) =>
- InsertReplaceableSchema.parse({
- id: generateId("rep"),
- cus_ent_id: cusEnt.id,
- created_at: Date.now(),
- delete_next_cycle: deleteNextCycle,
- }),
- );
+ let numReplaceables = prevOverage - newOverage;
+ let newReplaceables = Array.from({ length: numReplaceables }, (_, i) =>
+ InsertReplaceableSchema.parse({
+ id: generateId("rep"),
+ cus_ent_id: cusEnt.id,
+ created_at: Date.now(),
+ delete_next_cycle: deleteNextCycle,
+ }),
+ );
- return newReplaceables;
+ return newReplaceables;
};
export const getContUsageDowngradeItem = ({
- price,
- cusEnt,
- prevOverage,
- newOverage,
+ price,
+ cusEnt,
+ prevOverage,
+ newOverage,
}: {
- price: Price;
- cusEnt: FullCustomerEntitlement;
- prevOverage: number;
- newOverage: number;
+ price: Price;
+ cusEnt: FullCustomerEntitlement;
+ prevOverage: number;
+ newOverage: number;
}) => {
- let noProration = price.proration_config?.on_decrease == OnDecrease.None;
+ let noProration = price.proration_config?.on_decrease == OnDecrease.None;
- if (noProration) {
- let newReplaceables = getReplaceables({
- cusEnt,
- prevOverage,
- newOverage,
- });
+ if (noProration) {
+ let newReplaceables = getReplaceables({
+ cusEnt,
+ prevOverage,
+ newOverage,
+ });
- // let description = getFeatureInvoiceDescription({
- // feature,
- // usage: newRoundedUsage,
- // billingUnits: (price.config as UsagePriceConfig).billing_units,
- // prodName: product.name,
- // });
+ // let description = getFeatureInvoiceDescription({
+ // feature,
+ // usage: newRoundedUsage,
+ // billingUnits: (price.config as UsagePriceConfig).billing_units,
+ // prodName: product.name,
+ // });
- return {
- newReplaceables,
- amount: null,
- };
- } else {
- }
+ return {
+ newReplaceables,
+ amount: null,
+ };
+ } else {
+ }
- // let shouldProrate =
- // price.config.proration_config?.on_decrease == OnDecrease.Prorate;
+ // let shouldProrate =
+ // price.config.proration_config?.on_decrease == OnDecrease.Prorate;
- // if (shouldProrate) {
- // invoice = await createDowngradeProrationInvoice({
- // org,
- // cusPrice,
- // stripeCli,
- // sub,
- // newPrice,
- // prevPrice,
- // newRoundedUsage,
- // feature,
- // product,
- // onDecrease,
- // logger,
- // });
- // }
+ // if (shouldProrate) {
+ // invoice = await createDowngradeProrationInvoice({
+ // org,
+ // cusPrice,
+ // stripeCli,
+ // sub,
+ // newPrice,
+ // prevPrice,
+ // newRoundedUsage,
+ // feature,
+ // product,
+ // onDecrease,
+ // logger,
+ // });
+ // }
};
diff --git a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts
index d00bd5a85..513c839af 100644
--- a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts
+++ b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.ts
@@ -2,34 +2,34 @@ import { Entitlement, Price, UsagePriceConfig } from "@autumn/shared";
import { Decimal } from "decimal.js";
export const getUsageFromBalance = ({
- ent,
- price,
- balance,
+ ent,
+ price,
+ balance,
}: {
- ent: Entitlement;
- price: Price;
- balance: number;
+ ent: Entitlement;
+ price: Price;
+ balance: number;
}) => {
- let config = price.config as UsagePriceConfig;
- let billingUnits = config.billing_units || 1;
+ let config = price.config as UsagePriceConfig;
+ let billingUnits = config.billing_units || 1;
- let overage = -Math.min(0, balance);
- let roundedOverage = new Decimal(overage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
+ let overage = -Math.min(0, balance);
+ let roundedOverage = new Decimal(overage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
- let usage = new Decimal(ent.allowance!).sub(balance).toNumber();
+ let usage = new Decimal(ent.allowance!).sub(balance).toNumber();
- let roundedUsage = usage;
- if (overage > 0) {
- roundedUsage = new Decimal(usage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
- }
+ let roundedUsage = usage;
+ if (overage > 0) {
+ roundedUsage = new Decimal(usage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
+ }
- return { usage, roundedUsage, overage, roundedOverage };
+ return { usage, roundedUsage, overage, roundedOverage };
};
diff --git a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getUsageDiffLineItem.ts b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getUsageDiffLineItem.ts
index 6306e2592..d151f434a 100644
--- a/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getUsageDiffLineItem.ts
+++ b/server/src/internal/products/prices/priceUtils/arrearProratedUtils/getUsageDiffLineItem.ts
@@ -1,72 +1,72 @@
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
import {
- FullEntitlement,
- getFeatureInvoiceDescription,
- Organization,
- Price,
- Product,
- UsagePriceConfig,
- usageToFeatureName,
+ FullEntitlement,
+ getFeatureInvoiceDescription,
+ Organization,
+ Price,
+ Product,
+ UsagePriceConfig,
+ usageToFeatureName,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import { shouldProrate } from "../prorationConfigUtils.js";
export const getUsageDiffLineItem = ({
- prevBalance,
- newBalance,
- org,
- price,
- newUsageAmount,
- ent,
- product,
+ prevBalance,
+ newBalance,
+ org,
+ price,
+ newUsageAmount,
+ ent,
+ product,
}: {
- prevBalance: number;
- newBalance: number;
- org: Organization;
- price: Price;
- newUsageAmount: number;
- ent: FullEntitlement;
- product: Product;
+ prevBalance: number;
+ newBalance: number;
+ org: Organization;
+ price: Price;
+ newUsageAmount: number;
+ ent: FullEntitlement;
+ product: Product;
}) => {
- const usageDiff = new Decimal(prevBalance).sub(newBalance).abs().toNumber();
- const isIncrease = newBalance <= prevBalance;
- const willProrate = isIncrease
- ? shouldProrate(price.proration_config?.on_increase)
- : shouldProrate(price.proration_config?.on_decrease);
+ const usageDiff = new Decimal(prevBalance).sub(newBalance).abs().toNumber();
+ const isIncrease = newBalance <= prevBalance;
+ const willProrate = isIncrease
+ ? shouldProrate(price.proration_config?.on_increase)
+ : shouldProrate(price.proration_config?.on_decrease);
- let description = getFeatureInvoiceDescription({
- feature: ent.feature,
- usage: usageDiff,
- billingUnits: (price.config as UsagePriceConfig).billing_units,
- });
+ let description = getFeatureInvoiceDescription({
+ feature: ent.feature,
+ usage: usageDiff,
+ billingUnits: (price.config as UsagePriceConfig).billing_units,
+ });
- if (isIncrease) {
- description = `${product.name} - Additional ${description}`;
- } else {
- description = `Unused ${product.name} - ${description}`;
- }
+ if (isIncrease) {
+ description = `${product.name} - Additional ${description}`;
+ } else {
+ description = `Unused ${product.name} - ${description}`;
+ }
- let previewLineItem = constructPreviewItem({
- price,
- org,
- amount: newUsageAmount,
- description,
- });
+ let previewLineItem = constructPreviewItem({
+ price,
+ org,
+ amount: newUsageAmount,
+ description,
+ });
- if (!isIncrease && !willProrate) {
- let featureName = usageToFeatureName({
- usage: usageDiff,
- feature: ent.feature,
- });
+ if (!isIncrease && !willProrate) {
+ let featureName = usageToFeatureName({
+ usage: usageDiff,
+ feature: ent.feature,
+ });
- previewLineItem = constructPreviewItem({
- priceStr: `${usageDiff} free ${featureName}`,
- price,
- org,
- // description: `${product.name} - ${usageDiff} free ${featureName}`,
- description: `${product.name} - ${featureName}`,
- });
- }
+ previewLineItem = constructPreviewItem({
+ priceStr: `${usageDiff} free ${featureName}`,
+ price,
+ org,
+ // description: `${product.name} - ${usageDiff} free ${featureName}`,
+ description: `${product.name} - ${featureName}`,
+ });
+ }
- return previewLineItem;
+ return previewLineItem;
};
diff --git a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts
index 398d91a23..a8aa9fdd6 100644
--- a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts
@@ -4,27 +4,27 @@ import { FullProduct, PriceType } from "@autumn/shared";
import { subItemToAutumnInterval } from "@/external/stripe/utils.js";
export const subItemToFixedPrice = ({
- subItem,
- product,
- basePrice,
+ subItem,
+ product,
+ basePrice,
}: {
- subItem: Stripe.SubscriptionItem;
- product: FullProduct;
- basePrice?: number;
+ subItem: Stripe.SubscriptionItem;
+ product: FullProduct;
+ basePrice?: number;
}) => {
- const { price } = subItem;
+ const { price } = subItem;
- const { interval, intervalCount } = subItemToAutumnInterval(subItem);
- return constructPrice({
- internalProductId: product.internal_id,
- isCustom: true,
- orgId: product.org_id,
- fixedConfig: {
- type: PriceType.Fixed,
- amount: basePrice || (price.unit_amount || 0) / 100,
- interval,
- interval_count: intervalCount,
- stripe_price_id: price.id,
- },
- });
+ const { interval, intervalCount } = subItemToAutumnInterval(subItem);
+ return constructPrice({
+ internalProductId: product.internal_id,
+ isCustom: true,
+ orgId: product.org_id,
+ fixedConfig: {
+ type: PriceType.Fixed,
+ amount: basePrice || (price.unit_amount || 0) / 100,
+ interval,
+ interval_count: intervalCount,
+ stripe_price_id: price.id,
+ },
+ });
};
diff --git a/server/src/internal/products/prices/priceUtils/convertPrice.ts b/server/src/internal/products/prices/priceUtils/convertPrice.ts
index 7753d204a..5ae41dd93 100644
--- a/server/src/internal/products/prices/priceUtils/convertPrice.ts
+++ b/server/src/internal/products/prices/priceUtils/convertPrice.ts
@@ -1,15 +1,15 @@
import {
- BillingType,
- EntitlementWithFeature,
- Feature,
- UsageModel,
- UsagePriceConfig,
- BillingInterval,
- CustomerPrice,
- FullCustomerEntitlement,
- FullCustomerPrice,
- ProductOptions,
- FullProduct,
+ BillingType,
+ EntitlementWithFeature,
+ Feature,
+ UsageModel,
+ UsagePriceConfig,
+ BillingInterval,
+ CustomerPrice,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ ProductOptions,
+ FullProduct,
} from "@autumn/shared";
import { Price } from "@autumn/shared";
@@ -17,107 +17,107 @@ import { getBillingType, getPriceEntitlement } from "../priceUtils.js";
import { isFixedPrice } from "./usagePriceUtils/classifyUsagePrice.js";
export const priceToIntervalKey = (price: Price) => {
- return toIntervalKey({
- interval: price.config?.interval,
- intervalCount: price.config?.interval_count ?? 1,
- });
+ return toIntervalKey({
+ interval: price.config?.interval,
+ intervalCount: price.config?.interval_count ?? 1,
+ });
};
export const toIntervalKey = ({
- interval,
- intervalCount,
+ interval,
+ intervalCount,
}: {
- interval: BillingInterval;
- intervalCount: number;
+ interval: BillingInterval;
+ intervalCount: number;
}) => {
- if (interval == BillingInterval.OneOff) {
- return BillingInterval.OneOff;
- } else if (interval == BillingInterval.Quarter) {
- let finalCount = (intervalCount ?? 1) * 3;
- return `${BillingInterval.Month}-${finalCount}`;
- } else if (interval == BillingInterval.SemiAnnual) {
- let finalCount = (intervalCount ?? 1) * 6;
- return `${BillingInterval.Month}-${finalCount}`;
- }
+ if (interval == BillingInterval.OneOff) {
+ return BillingInterval.OneOff;
+ } else if (interval == BillingInterval.Quarter) {
+ let finalCount = (intervalCount ?? 1) * 3;
+ return `${BillingInterval.Month}-${finalCount}`;
+ } else if (interval == BillingInterval.SemiAnnual) {
+ let finalCount = (intervalCount ?? 1) * 6;
+ return `${BillingInterval.Month}-${finalCount}`;
+ }
- if (interval == BillingInterval.Week) {
- return `${BillingInterval.Week}-${intervalCount}`;
- } else if (interval == BillingInterval.Year) {
- return `${BillingInterval.Year}-${intervalCount}`;
- }
- return `${interval}-${intervalCount}`;
+ if (interval == BillingInterval.Week) {
+ return `${BillingInterval.Week}-${intervalCount}`;
+ } else if (interval == BillingInterval.Year) {
+ return `${BillingInterval.Year}-${intervalCount}`;
+ }
+ return `${interval}-${intervalCount}`;
};
export const intervalKeyToPrice = (intervalKey: string) => {
- const [interval, intervalCount] = intervalKey.split("-");
- return {
- interval: interval as BillingInterval,
- intervalCount: intervalCount ? parseInt(intervalCount) : 1,
- };
+ const [interval, intervalCount] = intervalKey.split("-");
+ return {
+ interval: interval as BillingInterval,
+ intervalCount: intervalCount ? parseInt(intervalCount) : 1,
+ };
};
export const priceToFeature = ({
- price,
- ents,
- features,
+ price,
+ ents,
+ features,
}: {
- price: Price;
- ents?: EntitlementWithFeature[];
- features?: Feature[];
+ price: Price;
+ ents?: EntitlementWithFeature[];
+ features?: Feature[];
}) => {
- if (!features && !ents) {
- throw new Error("priceToFeature requires either ents or features as arg");
- }
+ if (!features && !ents) {
+ throw new Error("priceToFeature requires either ents or features as arg");
+ }
- if (features) {
- return features.find(
- (f) =>
- f.internal_id == (price.config as UsagePriceConfig).internal_feature_id
- );
- }
+ if (features) {
+ return features.find(
+ (f) =>
+ f.internal_id == (price.config as UsagePriceConfig).internal_feature_id,
+ );
+ }
- const ent = getPriceEntitlement(price, ents!);
- return ent?.feature;
+ const ent = getPriceEntitlement(price, ents!);
+ return ent?.feature;
};
export const priceToUsageModel = (price: Price) => {
- let billingType = getBillingType(price.config);
- if (isFixedPrice({ price })) {
- return undefined;
- }
- if (billingType == BillingType.UsageInAdvance) {
- return UsageModel.Prepaid;
- }
- return UsageModel.PayPerUse;
+ let billingType = getBillingType(price.config);
+ if (isFixedPrice({ price })) {
+ return undefined;
+ }
+ if (billingType == BillingType.UsageInAdvance) {
+ return UsageModel.Prepaid;
+ }
+ return UsageModel.PayPerUse;
};
export const cusPriceToCusEnt = ({
- cusPrice,
- cusEnts,
+ cusPrice,
+ cusEnts,
}: {
- cusPrice: FullCustomerPrice;
- cusEnts: FullCustomerEntitlement[];
+ cusPrice: FullCustomerPrice;
+ cusEnts: FullCustomerEntitlement[];
}) => {
- return cusEnts.find(
- (ce) => ce.entitlement?.id == cusPrice.price.entitlement_id
- );
+ return cusEnts.find(
+ (ce) => ce.entitlement?.id == cusPrice.price.entitlement_id,
+ );
};
export const priceToProductOptions = ({
- price,
- options,
- products,
+ price,
+ options,
+ products,
}: {
- price: Price;
- options: ProductOptions[] | undefined;
- products: FullProduct[];
+ price: Price;
+ options: ProductOptions[] | undefined;
+ products: FullProduct[];
}) => {
- if (!options) return undefined;
+ if (!options) return undefined;
- const productId = products.find(
- (p) => p.internal_id == price.internal_product_id
- )?.id;
+ const productId = products.find(
+ (p) => p.internal_id == price.internal_product_id,
+ )?.id;
- const productOptions = options.find((o) => o.product_id == productId);
- return productOptions;
+ const productOptions = options.find((o) => o.product_id == productId);
+ return productOptions;
};
diff --git a/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts
index 5190aec29..60fedde3d 100644
--- a/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/copyPriceUtils.ts
@@ -6,54 +6,54 @@ import { generateId } from "@/utils/genUtils.js";
import { Price, UsagePriceConfig } from "@autumn/shared";
export const copyPrice = async ({
- db,
- priceId,
- usagePriceConfig,
- fixedPriceConfig,
- isCustom,
- withPrevConfig = true,
+ db,
+ priceId,
+ usagePriceConfig,
+ fixedPriceConfig,
+ isCustom,
+ withPrevConfig = true,
}: {
- db: DrizzleCli;
- priceId: string;
- usagePriceConfig?: Partial;
- fixedPriceConfig?: Partial;
- isCustom?: boolean;
- withPrevConfig?: boolean;
+ db: DrizzleCli;
+ priceId: string;
+ usagePriceConfig?: Partial;
+ fixedPriceConfig?: Partial;
+ isCustom?: boolean;
+ withPrevConfig?: boolean;
}) => {
- let price = (await db.query.prices.findFirst({
- where: eq(prices.id, priceId),
- })) as Price;
+ let price = (await db.query.prices.findFirst({
+ where: eq(prices.id, priceId),
+ })) as Price;
- let newPrice = structuredClone(price);
+ let newPrice = structuredClone(price);
- newPrice = {
- ...newPrice,
- id: generateId("pr"),
- created_at: Date.now(),
- is_custom: isCustom || newPrice.is_custom,
- };
+ newPrice = {
+ ...newPrice,
+ id: generateId("pr"),
+ created_at: Date.now(),
+ is_custom: isCustom || newPrice.is_custom,
+ };
- if (fixedPriceConfig) {
- newPrice = {
- ...newPrice,
- entitlement_id: null,
- config: {
- ...(withPrevConfig ? (newPrice.config as FixedPriceConfig) : {}),
- type: PriceType.Fixed,
- ...fixedPriceConfig,
- } as FixedPriceConfig,
- };
- }
+ if (fixedPriceConfig) {
+ newPrice = {
+ ...newPrice,
+ entitlement_id: null,
+ config: {
+ ...(withPrevConfig ? (newPrice.config as FixedPriceConfig) : {}),
+ type: PriceType.Fixed,
+ ...fixedPriceConfig,
+ } as FixedPriceConfig,
+ };
+ }
- if (usagePriceConfig) {
- newPrice = {
- ...newPrice,
- config: {
- ...(newPrice.config as UsagePriceConfig),
- ...usagePriceConfig,
- },
- };
- }
+ if (usagePriceConfig) {
+ newPrice = {
+ ...newPrice,
+ config: {
+ ...(newPrice.config as UsagePriceConfig),
+ ...usagePriceConfig,
+ },
+ };
+ }
- return newPrice;
+ return newPrice;
};
diff --git a/server/src/internal/products/prices/priceUtils/findPriceUtils.ts b/server/src/internal/products/prices/priceUtils/findPriceUtils.ts
index 8a6fdbe5e..914bc36dc 100644
--- a/server/src/internal/products/prices/priceUtils/findPriceUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/findPriceUtils.ts
@@ -1,11 +1,11 @@
import {
- BillingType,
- ErrCode,
- FullProduct,
- Price,
- PriceType,
- Product,
- UsagePriceConfig,
+ BillingType,
+ ErrCode,
+ FullProduct,
+ Price,
+ PriceType,
+ Product,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Feature } from "@autumn/shared";
@@ -14,115 +14,115 @@ import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
export const findPrepaidPrice = ({
- prices,
- internalFeatureId,
+ prices,
+ internalFeatureId,
}: {
- prices: Price[];
- internalFeatureId?: string;
+ prices: Price[];
+ internalFeatureId?: string;
}) => {
- return prices.find((p: Price) => {
- if (p.config.type != PriceType.Usage) return false;
+ return prices.find((p: Price) => {
+ if (p.config.type != PriceType.Usage) return false;
- const billingType = getBillingType(p.config);
- const config = p.config as UsagePriceConfig;
+ const billingType = getBillingType(p.config);
+ const config = p.config as UsagePriceConfig;
- if (billingType != BillingType.UsageInAdvance) return false;
+ if (billingType != BillingType.UsageInAdvance) return false;
- if (internalFeatureId) {
- return config.internal_feature_id == internalFeatureId;
- } else return true;
- });
+ if (internalFeatureId) {
+ return config.internal_feature_id == internalFeatureId;
+ } else return true;
+ });
};
export const findContUsePrice = ({ prices }: { prices: Price[] }) => {
- return prices.find((p: Price) => {
- const billingType = getBillingType(p.config);
- return billingType == BillingType.InArrearProrated;
- });
+ return prices.find((p: Price) => {
+ const billingType = getBillingType(p.config);
+ return billingType == BillingType.InArrearProrated;
+ });
};
export const findPriceForFeature = ({
- prices,
- feature,
- internalFeatureId,
+ prices,
+ feature,
+ internalFeatureId,
}: {
- prices: Price[];
- feature?: Feature;
- internalFeatureId?: string;
+ prices: Price[];
+ feature?: Feature;
+ internalFeatureId?: string;
}) => {
- if (!feature && !internalFeatureId) {
- throw new RecaseError({
- message: "findPriceForFeature: No feature or internalFeatureId provided",
- code: ErrCode.InternalError,
- });
- }
+ if (!feature && !internalFeatureId) {
+ throw new RecaseError({
+ message: "findPriceForFeature: No feature or internalFeatureId provided",
+ code: ErrCode.InternalError,
+ });
+ }
- return prices.find((p: Price) => {
- const config = p.config as UsagePriceConfig;
+ return prices.find((p: Price) => {
+ const config = p.config as UsagePriceConfig;
- if (!config.internal_feature_id) {
- return false;
- }
+ if (!config.internal_feature_id) {
+ return false;
+ }
- if (internalFeatureId) {
- return config.internal_feature_id == internalFeatureId;
- } else {
- return config.internal_feature_id == feature!.internal_id;
- }
- });
+ if (internalFeatureId) {
+ return config.internal_feature_id == internalFeatureId;
+ } else {
+ return config.internal_feature_id == feature!.internal_id;
+ }
+ });
};
export const findPriceFromPlaceholderId = ({
- prices,
- placeholderId,
+ prices,
+ placeholderId,
}: {
- prices: Price[];
- placeholderId: string;
+ prices: Price[];
+ placeholderId: string;
}) => {
- return prices.find((p: Price) => {
- const config = p.config as UsagePriceConfig;
- return config.stripe_placeholder_price_id == placeholderId;
- });
+ return prices.find((p: Price) => {
+ const config = p.config as UsagePriceConfig;
+ return config.stripe_placeholder_price_id == placeholderId;
+ });
};
export const findPriceFromStripeId = ({
- prices,
- stripePriceId,
- billingType,
+ prices,
+ stripePriceId,
+ billingType,
}: {
- prices: Price[];
- stripePriceId: string;
- billingType?: BillingType;
+ prices: Price[];
+ stripePriceId: string;
+ billingType?: BillingType;
}) => {
- return prices.find((p: Price) => {
- const config = p.config as UsagePriceConfig;
- const idMatch = config.stripe_price_id == stripePriceId;
- const typeMatch = billingType
- ? getBillingType(config) == billingType
- : true;
+ return prices.find((p: Price) => {
+ const config = p.config as UsagePriceConfig;
+ const idMatch = config.stripe_price_id == stripePriceId;
+ const typeMatch = billingType
+ ? getBillingType(config) == billingType
+ : true;
- return idMatch && typeMatch;
- });
+ return idMatch && typeMatch;
+ });
};
export const priceToProduct = ({
- price,
- products,
+ price,
+ products,
}: {
- price: Price;
- products: FullProduct[];
+ price: Price;
+ products: FullProduct[];
}) => {
- return products.find(
- (p: Product) => p.internal_id == price.internal_product_id,
- );
+ return products.find(
+ (p: Product) => p.internal_id == price.internal_product_id,
+ );
};
export const filterByBillingType = ({
- prices,
- billingType,
+ prices,
+ billingType,
}: {
- prices: Price[];
- billingType: BillingType;
+ prices: Price[];
+ billingType: BillingType;
}) => {
- return prices.filter((p) => getBillingType(p.config) == billingType);
+ return prices.filter((p) => getBillingType(p.config) == billingType);
};
diff --git a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts
index 4d7232d08..8ea8bb60c 100644
--- a/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/priceIntervalUtils.ts
@@ -1,207 +1,207 @@
import {
- BillingInterval,
- EntInterval,
- Entitlement,
- FixedPriceConfig,
- Price,
- UsagePriceConfig,
+ BillingInterval,
+ EntInterval,
+ Entitlement,
+ FixedPriceConfig,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
const BillingIntervalOrder = [
- BillingInterval.Year,
- BillingInterval.SemiAnnual,
- BillingInterval.Quarter,
- BillingInterval.Month,
- BillingInterval.OneOff,
+ BillingInterval.Year,
+ BillingInterval.SemiAnnual,
+ BillingInterval.Quarter,
+ BillingInterval.Month,
+ BillingInterval.OneOff,
];
const ReversedBillingIntervalOrder = [
- // BillingInterval.OneOff,
- BillingInterval.Month,
- BillingInterval.Quarter,
- BillingInterval.SemiAnnual,
- BillingInterval.Year,
+ // BillingInterval.OneOff,
+ BillingInterval.Month,
+ BillingInterval.Quarter,
+ BillingInterval.SemiAnnual,
+ BillingInterval.Year,
];
const entToBillingInterval = (entInterval: EntInterval | null | undefined) => {
- if (entInterval == EntInterval.Lifetime || !entInterval) {
- return BillingInterval.OneOff;
- } else return entInterval as unknown as BillingInterval;
+ if (entInterval == EntInterval.Lifetime || !entInterval) {
+ return BillingInterval.OneOff;
+ } else return entInterval as unknown as BillingInterval;
};
const intervalToValue = (
- interval: BillingInterval,
- intervalCount?: number | null
+ interval: BillingInterval,
+ intervalCount?: number | null,
) => {
- const intervalToBaseVal: Record = {
- [BillingInterval.OneOff]: 0,
- [BillingInterval.Week]: 0.25,
- [BillingInterval.Month]: 1,
- [BillingInterval.Quarter]: 3,
- [BillingInterval.SemiAnnual]: 6,
- [BillingInterval.Year]: 12,
- };
+ const intervalToBaseVal: Record = {
+ [BillingInterval.OneOff]: 0,
+ [BillingInterval.Week]: 0.25,
+ [BillingInterval.Month]: 1,
+ [BillingInterval.Quarter]: 3,
+ [BillingInterval.SemiAnnual]: 6,
+ [BillingInterval.Year]: 12,
+ };
- return intervalToBaseVal[interval] * (intervalCount ?? 1);
+ return intervalToBaseVal[interval] * (intervalCount ?? 1);
};
type IntervalConfig = {
- interval: BillingInterval;
- intervalCount?: number | null;
+ interval: BillingInterval;
+ intervalCount?: number | null;
};
export function compareBillingIntervals({
- configA,
- configB,
+ configA,
+ configB,
}: {
- configA: IntervalConfig;
- configB: IntervalConfig;
+ configA: IntervalConfig;
+ configB: IntervalConfig;
}): number {
- // if (nullish(a)) {
- // return 1;
- // } else if (nullish(b)) {
- // return -1;
- // }
+ // if (nullish(a)) {
+ // return 1;
+ // } else if (nullish(b)) {
+ // return -1;
+ // }
- // how to compare these... convert to months?
+ // how to compare these... convert to months?
- const a = intervalToValue(configA.interval, configA.intervalCount);
- const b = intervalToValue(configB.interval, configB.intervalCount);
+ const a = intervalToValue(configA.interval, configA.intervalCount);
+ const b = intervalToValue(configB.interval, configB.intervalCount);
- return b - a;
+ return b - a;
}
export const getLargestInterval = ({
- prices,
- excludeOneOff = false,
+ prices,
+ excludeOneOff = false,
}: {
- prices: Price[];
- excludeOneOff?: boolean;
+ prices: Price[];
+ excludeOneOff?: boolean;
}) => {
- let sortedPrices = structuredClone(prices);
- sortPricesByInterval(sortedPrices);
+ let sortedPrices = structuredClone(prices);
+ sortPricesByInterval(sortedPrices);
- if (excludeOneOff) {
- sortedPrices = sortedPrices.filter(
- (price) => price.config.interval !== BillingInterval.OneOff
- );
- }
+ if (excludeOneOff) {
+ sortedPrices = sortedPrices.filter(
+ (price) => price.config.interval !== BillingInterval.OneOff,
+ );
+ }
- if (sortedPrices.length === 0) {
- return null;
- }
+ if (sortedPrices.length === 0) {
+ return null;
+ }
- return {
- interval: sortedPrices[0].config.interval,
- intervalCount: sortedPrices[0].config.interval_count ?? 1,
- };
+ return {
+ interval: sortedPrices[0].config.interval,
+ intervalCount: sortedPrices[0].config.interval_count ?? 1,
+ };
- // return BillingIntervalOrder.find((interval) =>
- // prices.some((price) => {
- // let intervalMatch = price.config.interval === interval;
- // let oneOffMatch = excludeOneOff
- // ? price.config.interval !== BillingInterval.OneOff
- // : true;
- // return intervalMatch && oneOffMatch;
- // })
- // )!;
+ // return BillingIntervalOrder.find((interval) =>
+ // prices.some((price) => {
+ // let intervalMatch = price.config.interval === interval;
+ // let oneOffMatch = excludeOneOff
+ // ? price.config.interval !== BillingInterval.OneOff
+ // : true;
+ // return intervalMatch && oneOffMatch;
+ // })
+ // )!;
};
export const getSmallestInterval = ({
- prices,
- ents,
- excludeOneOff = false,
+ prices,
+ ents,
+ excludeOneOff = false,
}: {
- prices: Price[];
- ents?: Entitlement[];
- excludeOneOff?: boolean;
+ prices: Price[];
+ ents?: Entitlement[];
+ excludeOneOff?: boolean;
}) => {
- // let sortedPrices = structuredClone(prices);
- // sortPricesByInterval(sortedPrices);
- let allPriceIntervals = prices.map((p) => {
- return {
- interval: p.config.interval,
- intervalCount: p.config.interval_count ?? 1,
- };
- });
+ // let sortedPrices = structuredClone(prices);
+ // sortPricesByInterval(sortedPrices);
+ let allPriceIntervals = prices.map((p) => {
+ return {
+ interval: p.config.interval,
+ intervalCount: p.config.interval_count ?? 1,
+ };
+ });
- if (excludeOneOff) {
- allPriceIntervals = allPriceIntervals.filter(
- (p) => p.interval !== BillingInterval.OneOff
- );
- }
+ if (excludeOneOff) {
+ allPriceIntervals = allPriceIntervals.filter(
+ (p) => p.interval !== BillingInterval.OneOff,
+ );
+ }
- const allEntIntervals = ents?.map((e) => {
- return {
- interval: entToBillingInterval(e.interval),
- intervalCount: e.interval_count ?? 1,
- };
- });
+ const allEntIntervals = ents?.map((e) => {
+ return {
+ interval: entToBillingInterval(e.interval),
+ intervalCount: e.interval_count ?? 1,
+ };
+ });
- const allIntervals = [...allPriceIntervals, ...(allEntIntervals || [])];
+ const allIntervals = [...allPriceIntervals, ...(allEntIntervals || [])];
- if (allIntervals.length === 0) {
- return null;
- }
+ if (allIntervals.length === 0) {
+ return null;
+ }
- allIntervals.sort((a, b) => {
- return compareBillingIntervals({ configA: a, configB: b });
- });
+ allIntervals.sort((a, b) => {
+ return compareBillingIntervals({ configA: a, configB: b });
+ });
- const smallestInterval = allIntervals?.[allIntervals.length - 1];
+ const smallestInterval = allIntervals?.[allIntervals.length - 1];
- return {
- interval: smallestInterval.interval,
- intervalCount: smallestInterval.intervalCount,
- };
+ return {
+ interval: smallestInterval.interval,
+ intervalCount: smallestInterval.intervalCount,
+ };
- // if (!smallestIntervalPrice) {
- // return null;
- // }
+ // if (!smallestIntervalPrice) {
+ // return null;
+ // }
- // return {
- // interval: smallestIntervalPrice.config.interval,
- // intervalCount: smallestIntervalPrice.config.interval_count ?? 1,
- // };
+ // return {
+ // interval: smallestIntervalPrice.config.interval,
+ // intervalCount: smallestIntervalPrice.config.interval_count ?? 1,
+ // };
- // return ReversedBillingIntervalOrder.find(
- // (interval) =>
- // prices.some((price) => price.config.interval === interval) ||
- // (ents &&
- // ents?.some((ent) => entToBillingInterval(ent.interval) === interval))
- // )!;
+ // return ReversedBillingIntervalOrder.find(
+ // (interval) =>
+ // prices.some((price) => price.config.interval === interval) ||
+ // (ents &&
+ // ents?.some((ent) => entToBillingInterval(ent.interval) === interval))
+ // )!;
};
export const sortPricesByInterval = (prices: Price[]) => {
- return prices.sort((a, b) => {
- return compareBillingIntervals({ configA: a.config, configB: b.config });
- });
+ return prices.sort((a, b) => {
+ return compareBillingIntervals({ configA: a.config, configB: b.config });
+ });
};
export const intervalsDifferent = ({
- intervalA,
- intervalB,
+ intervalA,
+ intervalB,
}: {
- intervalA: IntervalConfig | null;
- intervalB: IntervalConfig | null;
+ intervalA: IntervalConfig | null;
+ intervalB: IntervalConfig | null;
}) => {
- // return compareBillingIntervals({ configA: intervalA, configB: intervalB }) !== 0;
- if (nullish(intervalA) && nullish(intervalB)) {
- return false;
- }
+ // return compareBillingIntervals({ configA: intervalA, configB: intervalB }) !== 0;
+ if (nullish(intervalA) && nullish(intervalB)) {
+ return false;
+ }
- if (nullish(intervalA) || nullish(intervalB)) {
- return true;
- }
+ if (nullish(intervalA) || nullish(intervalB)) {
+ return true;
+ }
- const intervalCountA = intervalToValue(
- intervalA!.interval,
- intervalA!.intervalCount
- );
- const intervalCountB = intervalToValue(
- intervalB!.interval,
- intervalB!.intervalCount
- );
- return intervalCountA !== intervalCountB;
+ const intervalCountA = intervalToValue(
+ intervalA!.interval,
+ intervalA!.intervalCount,
+ );
+ const intervalCountB = intervalToValue(
+ intervalB!.interval,
+ intervalB!.intervalCount,
+ );
+ return intervalCountA !== intervalCountB;
};
diff --git a/server/src/internal/products/prices/priceUtils/priceToInvoiceAmount.ts b/server/src/internal/products/prices/priceUtils/priceToInvoiceAmount.ts
index c83c3d471..8dd01303d 100644
--- a/server/src/internal/products/prices/priceUtils/priceToInvoiceAmount.ts
+++ b/server/src/internal/products/prices/priceUtils/priceToInvoiceAmount.ts
@@ -1,171 +1,171 @@
import {
- BillingType,
- Feature,
- FixedPriceConfig,
- Infinite,
- Price,
- ProductItem,
- UsageModel,
- UsagePriceConfig,
+ BillingType,
+ Feature,
+ FixedPriceConfig,
+ Infinite,
+ Price,
+ ProductItem,
+ UsageModel,
+ UsagePriceConfig,
} from "@autumn/shared";
import { isFixedPrice } from "./usagePriceUtils/classifyUsagePrice.js";
import { getBillingType } from "../priceUtils.js";
import { Decimal } from "decimal.js";
import { nullish } from "@/utils/genUtils.js";
import {
- calculateProrationAmount,
- Proration,
+ calculateProrationAmount,
+ Proration,
} from "@/internal/invoices/prorationUtils.js";
import { itemToPriceAndEnt } from "../../product-items/productItemUtils/itemToPriceAndEnt.js";
import { isPriceItem } from "../../product-items/productItemUtils/getItemType.js";
export const getAmountForQuantity = ({
- price,
- quantity,
+ price,
+ quantity,
}: {
- price: Price;
- quantity: number;
+ price: Price;
+ quantity: number;
}) => {
- const config = price.config as UsagePriceConfig;
+ const config = price.config as UsagePriceConfig;
- let billingUnits = config.billing_units || 1;
+ let billingUnits = config.billing_units || 1;
- const roundedQuantity = new Decimal(quantity)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
+ const roundedQuantity = new Decimal(quantity)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
- let lastTierTo: number = 0;
+ let lastTierTo: number = 0;
- let amount = new Decimal(0);
- let remainingUsage = new Decimal(roundedQuantity);
+ let amount = new Decimal(0);
+ let remainingUsage = new Decimal(roundedQuantity);
- // console.log("Getting amount for quantity:", roundedQuantity);
- // console.log("Usage tiers:", config.usage_tiers);
+ // console.log("Getting amount for quantity:", roundedQuantity);
+ // console.log("Usage tiers:", config.usage_tiers);
- for (let i = 0; i < config.usage_tiers.length; i++) {
- let tier = config.usage_tiers[i];
+ for (let i = 0; i < config.usage_tiers.length; i++) {
+ let tier = config.usage_tiers[i];
- let usageWithinTier = new Decimal(0);
- if (tier.to == Infinite || tier.to == -1) {
- usageWithinTier = remainingUsage;
- } else {
- let tierUsage = new Decimal(tier.to).minus(lastTierTo);
- usageWithinTier = Decimal.min(remainingUsage, tierUsage);
- lastTierTo = tier.to;
- }
+ let usageWithinTier = new Decimal(0);
+ if (tier.to == Infinite || tier.to == -1) {
+ usageWithinTier = remainingUsage;
+ } else {
+ let tierUsage = new Decimal(tier.to).minus(lastTierTo);
+ usageWithinTier = Decimal.min(remainingUsage, tierUsage);
+ lastTierTo = tier.to;
+ }
- let amountPerUnit = new Decimal(tier.amount).div(billingUnits);
- let amountWithinTier = amountPerUnit.mul(usageWithinTier);
- amount = amount.plus(amountWithinTier);
- remainingUsage = remainingUsage.minus(usageWithinTier);
+ let amountPerUnit = new Decimal(tier.amount).div(billingUnits);
+ let amountWithinTier = amountPerUnit.mul(usageWithinTier);
+ amount = amount.plus(amountWithinTier);
+ remainingUsage = remainingUsage.minus(usageWithinTier);
- if (remainingUsage.lte(0)) {
- break;
- }
- }
+ if (remainingUsage.lte(0)) {
+ break;
+ }
+ }
- return amount.toDecimalPlaces(10).toNumber();
+ return amount.toDecimalPlaces(10).toNumber();
};
export const itemToInvoiceAmount = ({
- item,
- quantity,
- overage,
+ item,
+ quantity,
+ overage,
}: {
- item: ProductItem;
- quantity?: number;
- overage?: number;
+ item: ProductItem;
+ quantity?: number;
+ overage?: number;
}) => {
- let amount = 0;
- if (isPriceItem(item)) {
- amount = item.price!;
- }
+ let amount = 0;
+ if (isPriceItem(item)) {
+ amount = item.price!;
+ }
- if (!nullish(quantity) && !nullish(overage)) {
- throw new Error(
- `itemToInvoiceAmount: quantity or overage is required, autumn item: ${item.feature_id}`
- );
- }
+ if (!nullish(quantity) && !nullish(overage)) {
+ throw new Error(
+ `itemToInvoiceAmount: quantity or overage is required, autumn item: ${item.feature_id}`,
+ );
+ }
- let price = {
- config: {
- usage_tiers: item.tiers || [
- {
- to: Infinite,
- amount: item.price!,
- },
- ],
- billing_units: item.billing_units || 1,
- },
- } as unknown as Price;
+ let price = {
+ config: {
+ usage_tiers: item.tiers || [
+ {
+ to: Infinite,
+ amount: item.price!,
+ },
+ ],
+ billing_units: item.billing_units || 1,
+ },
+ } as unknown as Price;
- if (item.usage_model == UsageModel.Prepaid) {
- amount = getAmountForQuantity({ price, quantity: quantity! });
- } else {
- amount = getAmountForQuantity({ price, quantity: overage! });
- }
+ if (item.usage_model == UsageModel.Prepaid) {
+ amount = getAmountForQuantity({ price, quantity: quantity! });
+ } else {
+ amount = getAmountForQuantity({ price, quantity: overage! });
+ }
- return amount;
+ return amount;
};
export const priceToInvoiceAmount = ({
- price,
- item,
- quantity,
- productQuantity,
- overage,
- proration,
- now,
+ price,
+ item,
+ quantity,
+ productQuantity,
+ overage,
+ proration,
+ now,
}: {
- price?: Price;
- item?: ProductItem;
- quantity?: number; // quantity should be multiplied by billing units
- productQuantity?: number;
- overage?: number;
- proration?: Proration;
- now?: number;
+ price?: Price;
+ item?: ProductItem;
+ quantity?: number; // quantity should be multiplied by billing units
+ productQuantity?: number;
+ overage?: number;
+ proration?: Proration;
+ now?: number;
}) => {
- // 1. If fixed price, just return amount
+ // 1. If fixed price, just return amount
- let amount = 0;
+ let amount = 0;
- if (price) {
- if (isFixedPrice({ price })) {
- amount = (price.config as FixedPriceConfig).amount;
- if (productQuantity) {
- amount = new Decimal(amount).mul(productQuantity).toNumber();
- }
- } else {
- const config = price.config as UsagePriceConfig;
- let billingType = getBillingType(config);
+ if (price) {
+ if (isFixedPrice({ price })) {
+ amount = (price.config as FixedPriceConfig).amount;
+ if (productQuantity) {
+ amount = new Decimal(amount).mul(productQuantity).toNumber();
+ }
+ } else {
+ const config = price.config as UsagePriceConfig;
+ let billingType = getBillingType(config);
- if (!nullish(quantity) && !nullish(overage)) {
- throw new Error(
- `getAmountForPrice: quantity or overage is required, autumn price: ${price.id}`
- );
- }
+ if (!nullish(quantity) && !nullish(overage)) {
+ throw new Error(
+ `getAmountForPrice: quantity or overage is required, autumn price: ${price.id}`,
+ );
+ }
- if (billingType == BillingType.UsageInAdvance) {
- amount = getAmountForQuantity({ price, quantity: quantity! });
- } else {
- amount = getAmountForQuantity({ price, quantity: overage! });
- }
- }
- } else {
- amount = itemToInvoiceAmount({ item: item!, quantity, overage });
- }
+ if (billingType == BillingType.UsageInAdvance) {
+ amount = getAmountForQuantity({ price, quantity: quantity! });
+ } else {
+ amount = getAmountForQuantity({ price, quantity: overage! });
+ }
+ }
+ } else {
+ amount = itemToInvoiceAmount({ item: item!, quantity, overage });
+ }
- if (proration) {
- return calculateProrationAmount({
- periodEnd: proration.end,
- periodStart: proration.start,
- now: now || Date.now(),
- amount,
- allowNegative: true,
- });
- }
+ if (proration) {
+ return calculateProrationAmount({
+ periodEnd: proration.end,
+ periodStart: proration.start,
+ now: now || Date.now(),
+ amount,
+ allowNegative: true,
+ });
+ }
- return amount;
+ return amount;
};
diff --git a/server/src/internal/products/prices/priceUtils/priceToInvoiceItem.ts b/server/src/internal/products/prices/priceUtils/priceToInvoiceItem.ts
index 36d4bb3dd..3fa33d247 100644
--- a/server/src/internal/products/prices/priceUtils/priceToInvoiceItem.ts
+++ b/server/src/internal/products/prices/priceUtils/priceToInvoiceItem.ts
@@ -1,69 +1,69 @@
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
import {
- FullEntitlement,
- getFeatureInvoiceDescription,
- Organization,
- Price,
- UsagePriceConfig,
+ FullEntitlement,
+ getFeatureInvoiceDescription,
+ Organization,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import { priceToInvoiceAmount } from "./priceToInvoiceAmount.js";
import { Proration } from "@/internal/invoices/prorationUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
export const priceToInvoiceItem = ({
- price,
- ent,
- usage,
- prodName,
- org,
- proration,
- now,
- allowNegative,
+ price,
+ ent,
+ usage,
+ prodName,
+ org,
+ proration,
+ now,
+ allowNegative,
}: {
- price: Price;
- ent: FullEntitlement;
- usage: number;
- prodName: string;
- org: Organization;
- proration?: Proration;
- now?: number;
- allowNegative?: boolean;
+ price: Price;
+ ent: FullEntitlement;
+ usage: number;
+ prodName: string;
+ org: Organization;
+ proration?: Proration;
+ now?: number;
+ allowNegative?: boolean;
}) => {
- const config = price.config as UsagePriceConfig;
- const billingUnits = config.billing_units || 1;
- now = now || Date.now();
+ const config = price.config as UsagePriceConfig;
+ const billingUnits = config.billing_units || 1;
+ now = now || Date.now();
- let overageDescription = getFeatureInvoiceDescription({
- feature: ent.feature,
- usage: usage,
- billingUnits: billingUnits,
- prodName,
- });
+ let overageDescription = getFeatureInvoiceDescription({
+ feature: ent.feature,
+ usage: usage,
+ billingUnits: billingUnits,
+ prodName,
+ });
- if (proration) {
- overageDescription = `${overageDescription} (from ${formatUnixToDate(now)})`;
- }
+ if (proration) {
+ overageDescription = `${overageDescription} (from ${formatUnixToDate(now)})`;
+ }
- // Get overage
- const overage = usage - ent.allowance!;
+ // Get overage
+ const overage = usage - ent.allowance!;
- let invoiceAmount = priceToInvoiceAmount({
- price,
- overage,
- proration,
- now,
- });
+ let invoiceAmount = priceToInvoiceAmount({
+ price,
+ overage,
+ proration,
+ now,
+ });
- if (!allowNegative && invoiceAmount < 0) {
- invoiceAmount = 0;
- }
+ if (!allowNegative && invoiceAmount < 0) {
+ invoiceAmount = 0;
+ }
- let newPreviewItem = constructPreviewItem({
- price,
- org,
- amount: invoiceAmount,
- description: overageDescription,
- });
+ let newPreviewItem = constructPreviewItem({
+ price,
+ org,
+ amount: invoiceAmount,
+ description: overageDescription,
+ });
- return newPreviewItem;
+ return newPreviewItem;
};
diff --git a/server/src/internal/products/prices/priceUtils/prorationConfigUtils.ts b/server/src/internal/products/prices/priceUtils/prorationConfigUtils.ts
index 03a03ba7e..872e32590 100644
--- a/server/src/internal/products/prices/priceUtils/prorationConfigUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/prorationConfigUtils.ts
@@ -1,30 +1,30 @@
import { OnDecrease, OnIncrease } from "@autumn/shared";
export const shouldCreateInvoiceItem = (onIncrease: OnIncrease) => {
- return (
- onIncrease === OnIncrease.BillImmediately ||
- onIncrease === OnIncrease.ProrateImmediately ||
- onIncrease === OnIncrease.ProrateNextCycle
- );
+ return (
+ onIncrease === OnIncrease.BillImmediately ||
+ onIncrease === OnIncrease.ProrateImmediately ||
+ onIncrease === OnIncrease.ProrateNextCycle
+ );
};
export const shouldBillNow = (onIncrease: OnIncrease | OnDecrease) => {
- return (
- onIncrease === OnIncrease.BillImmediately ||
- onIncrease === OnIncrease.ProrateImmediately ||
- onIncrease === OnDecrease.ProrateImmediately
- );
+ return (
+ onIncrease === OnIncrease.BillImmediately ||
+ onIncrease === OnIncrease.ProrateImmediately ||
+ onIncrease === OnDecrease.ProrateImmediately
+ );
};
export const shouldProrate = (onIncrease?: OnIncrease | OnDecrease) => {
- if (!onIncrease) {
- return true;
- }
+ if (!onIncrease) {
+ return true;
+ }
- return (
- onIncrease === OnIncrease.ProrateNextCycle ||
- onIncrease === OnIncrease.ProrateImmediately ||
- onIncrease === OnDecrease.ProrateImmediately ||
- onIncrease === OnDecrease.ProrateNextCycle
- );
+ return (
+ onIncrease === OnIncrease.ProrateNextCycle ||
+ onIncrease === OnIncrease.ProrateImmediately ||
+ onIncrease === OnDecrease.ProrateImmediately ||
+ onIncrease === OnDecrease.ProrateNextCycle
+ );
};
diff --git a/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts b/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts
index 0e9a2c879..957c3ec04 100644
--- a/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts
+++ b/server/src/internal/products/prices/priceUtils/sortPriceUtils.ts
@@ -2,17 +2,17 @@ import { BillingType, Price } from "@autumn/shared";
import { getBillingType } from "../priceUtils.js";
const BillingTypeOrder = [
- BillingType.OneOff,
- BillingType.FixedCycle,
- BillingType.UsageInArrear,
- BillingType.InArrearProrated,
- BillingType.UsageInAdvance,
+ BillingType.OneOff,
+ BillingType.FixedCycle,
+ BillingType.UsageInArrear,
+ BillingType.InArrearProrated,
+ BillingType.UsageInAdvance,
];
export const sortPricesByType = (prices: Price[]) => {
- return prices.sort((a, b) => {
- let aType = getBillingType(a.config);
- let bType = getBillingType(b.config);
- return BillingTypeOrder.indexOf(aType) - BillingTypeOrder.indexOf(bType);
- });
+ return prices.sort((a, b) => {
+ let aType = getBillingType(a.config);
+ let bType = getBillingType(b.config);
+ return BillingTypeOrder.indexOf(aType) - BillingTypeOrder.indexOf(bType);
+ });
};
diff --git a/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts b/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts
index 9c25269eb..f8af727d1 100644
--- a/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts
+++ b/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts
@@ -1,12 +1,12 @@
import {
- APIVersion,
- BillingInterval,
- BillingType,
- FullCusProduct,
- OnDecrease,
- OnIncrease,
- Price,
- UsagePriceConfig,
+ APIVersion,
+ BillingInterval,
+ BillingType,
+ FullCusProduct,
+ OnDecrease,
+ OnIncrease,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import { formatPrice, getBillingType } from "../../priceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
@@ -14,147 +14,147 @@ import Stripe from "stripe";
import { Decimal } from "decimal.js";
export const isOneOffPrice = ({ price }: { price: Price }) => {
- return price.config.interval == BillingInterval.OneOff;
+ return price.config.interval == BillingInterval.OneOff;
};
export const isUsagePrice = ({
- price,
- featureId,
+ price,
+ featureId,
}: {
- price: Price;
- featureId?: string;
+ price: Price;
+ featureId?: string;
}) => {
- let billingType = getBillingType(price.config);
+ let billingType = getBillingType(price.config);
- let isUsage =
- billingType == BillingType.UsageInArrear ||
- billingType == BillingType.InArrearProrated ||
- billingType == BillingType.UsageInAdvance;
+ let isUsage =
+ billingType == BillingType.UsageInArrear ||
+ billingType == BillingType.InArrearProrated ||
+ billingType == BillingType.UsageInAdvance;
- if (featureId) {
- return (
- isUsage && (price.config as UsagePriceConfig).feature_id == featureId
- );
- }
+ if (featureId) {
+ return (
+ isUsage && (price.config as UsagePriceConfig).feature_id == featureId
+ );
+ }
- return isUsage;
+ return isUsage;
};
export const isArrearPrice = ({ price }: { price?: Price }) => {
- if (!price) return false;
- let billingType = getBillingType(price.config);
- return billingType == BillingType.UsageInArrear;
+ if (!price) return false;
+ let billingType = getBillingType(price.config);
+ return billingType == BillingType.UsageInArrear;
};
export const isContUsePrice = ({ price }: { price?: Price }) => {
- if (!price) return false;
- let billingType = getBillingType(price.config);
- return billingType == BillingType.InArrearProrated;
+ if (!price) return false;
+ let billingType = getBillingType(price.config);
+ return billingType == BillingType.InArrearProrated;
};
export const isPrepaidPrice = ({ price }: { price: Price }) => {
- let billingType = getBillingType(price.config);
- return billingType == BillingType.UsageInAdvance;
+ let billingType = getBillingType(price.config);
+ return billingType == BillingType.UsageInAdvance;
};
export const isPayPerUse = ({ price }: { price: Price }) => {
- let billingType = getBillingType(price.config);
- return (
- billingType == BillingType.UsageInArrear ||
- billingType == BillingType.InArrearProrated
- );
+ let billingType = getBillingType(price.config);
+ return (
+ billingType == BillingType.UsageInArrear ||
+ billingType == BillingType.InArrearProrated
+ );
};
export const isFixedPrice = ({ price }: { price: Price }) => {
- let billingType = getBillingType(price.config);
+ let billingType = getBillingType(price.config);
- return (
- billingType == BillingType.FixedCycle || billingType == BillingType.OneOff
- );
+ return (
+ billingType == BillingType.FixedCycle || billingType == BillingType.OneOff
+ );
};
export const hasPrepaidPrice = ({
- prices,
- excludeOneOff,
+ prices,
+ excludeOneOff,
}: {
- prices: Price[];
- excludeOneOff?: boolean;
+ prices: Price[];
+ excludeOneOff?: boolean;
}) => {
- return prices.some((price) => {
- let isUsage = getBillingType(price.config) == BillingType.UsageInAdvance;
- let isOneOff = price.config.interval == BillingInterval.OneOff;
+ return prices.some((price) => {
+ let isUsage = getBillingType(price.config) == BillingType.UsageInAdvance;
+ let isOneOff = price.config.interval == BillingInterval.OneOff;
- return isUsage && (excludeOneOff ? !isOneOff : true);
- });
+ return isUsage && (excludeOneOff ? !isOneOff : true);
+ });
};
export const isV4Usage = ({
- price,
- cusProduct,
+ price,
+ cusProduct,
}: {
- price: Price;
- cusProduct: FullCusProduct;
+ price: Price;
+ cusProduct: FullCusProduct;
}) => {
- const billingType = getBillingType(price.config);
+ const billingType = getBillingType(price.config);
- return (
- billingType == BillingType.UsageInArrear &&
- (cusProduct.api_version == APIVersion.v1_4 ||
- notNullish(cusProduct.internal_entity_id))
- );
+ return (
+ billingType == BillingType.UsageInArrear &&
+ (cusProduct.api_version == APIVersion.v1_4 ||
+ notNullish(cusProduct.internal_entity_id))
+ );
};
// export const
export const onIncreaseToStripeProration = ({
- onIncrease,
+ onIncrease,
}: {
- onIncrease: OnIncrease;
+ onIncrease: OnIncrease;
}) => {
- let behavior = "none";
- if (onIncrease === OnIncrease.ProrateImmediately) {
- behavior = "always_invoice";
- } else if (onIncrease === OnIncrease.ProrateNextCycle) {
- behavior = "create_prorations";
- }
+ let behavior = "none";
+ if (onIncrease === OnIncrease.ProrateImmediately) {
+ behavior = "always_invoice";
+ } else if (onIncrease === OnIncrease.ProrateNextCycle) {
+ behavior = "create_prorations";
+ }
- return behavior as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
+ return behavior as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
};
export const onDecreaseToStripeProration = ({
- onDecrease,
+ onDecrease,
}: {
- onDecrease: OnDecrease;
+ onDecrease: OnDecrease;
}) => {
- let behavior = "none";
- if (onDecrease === OnDecrease.ProrateImmediately) {
- behavior = "always_invoice";
- } else if (onDecrease === OnDecrease.ProrateNextCycle) {
- behavior = "create_prorations";
- }
+ let behavior = "none";
+ if (onDecrease === OnDecrease.ProrateImmediately) {
+ behavior = "always_invoice";
+ } else if (onDecrease === OnDecrease.ProrateNextCycle) {
+ behavior = "create_prorations";
+ }
- return behavior as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
+ return behavior as Stripe.SubscriptionItemUpdateParams.ProrationBehavior;
};
export const roundUsage = ({
- usage,
- price,
- pos = true,
+ usage,
+ price,
+ pos = true,
}: {
- usage: number;
- price: Price;
- pos?: boolean;
+ usage: number;
+ price: Price;
+ pos?: boolean;
}) => {
- let config = price.config as UsagePriceConfig;
- let billingUnits = config.billing_units || 1;
+ let config = price.config as UsagePriceConfig;
+ let billingUnits = config.billing_units || 1;
- let rounded = new Decimal(usage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
+ let rounded = new Decimal(usage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
- if (pos) {
- return Math.max(rounded, 0);
- }
+ if (pos) {
+ return Math.max(rounded, 0);
+ }
- return rounded;
+ return rounded;
};
diff --git a/server/src/internal/products/prices/priceUtils/usagePriceUtils/convertUsagePrice.ts b/server/src/internal/products/prices/priceUtils/usagePriceUtils/convertUsagePrice.ts
index a5ec6130f..55baadb06 100644
--- a/server/src/internal/products/prices/priceUtils/usagePriceUtils/convertUsagePrice.ts
+++ b/server/src/internal/products/prices/priceUtils/usagePriceUtils/convertUsagePrice.ts
@@ -1,38 +1,38 @@
import {
- BillingType,
- Feature,
- FullProduct,
- Price,
- UsagePriceConfig,
+ BillingType,
+ Feature,
+ FullProduct,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import { priceToFeature } from "../convertPrice.js";
import { getBillingType } from "../../priceUtils.js";
export const usagePriceToProductName = ({
- price,
- fullProduct,
+ price,
+ fullProduct,
}: {
- price: Price;
- fullProduct: FullProduct;
+ price: Price;
+ fullProduct: FullProduct;
}) => {
- let feature = priceToFeature({
- price,
- ents: fullProduct.entitlements,
- });
+ let feature = priceToFeature({
+ price,
+ ents: fullProduct.entitlements,
+ });
- if (!feature) {
- return fullProduct.name;
- }
+ if (!feature) {
+ return fullProduct.name;
+ }
- let billingType = getBillingType(price.config);
- const billingUnits = (price.config as UsagePriceConfig).billing_units;
- if (
- billingType == BillingType.UsageInAdvance &&
- billingUnits &&
- billingUnits > 1
- ) {
- return `${fullProduct.name} - ${billingUnits} ${feature.name}`;
- }
+ let billingType = getBillingType(price.config);
+ const billingUnits = (price.config as UsagePriceConfig).billing_units;
+ if (
+ billingType == BillingType.UsageInAdvance &&
+ billingUnits &&
+ billingUnits > 1
+ ) {
+ return `${fullProduct.name} - ${billingUnits} ${feature.name}`;
+ }
- return `${fullProduct.name} - ${feature.name}`;
+ return `${fullProduct.name} - ${feature.name}`;
};
diff --git a/server/src/internal/products/product-items/compareItemUtils.ts b/server/src/internal/products/product-items/compareItemUtils.ts
index 01a77ff6b..1eb89331e 100644
--- a/server/src/internal/products/product-items/compareItemUtils.ts
+++ b/server/src/internal/products/product-items/compareItemUtils.ts
@@ -1,267 +1,267 @@
import {
- Feature,
- FeatureItem,
- FeatureItemSchema,
- FeaturePriceItem,
- FeaturePriceItemSchema,
- FeatureUsageType,
- PriceItem,
- PriceItemSchema,
- ProductItem,
+ Feature,
+ FeatureItem,
+ FeatureItemSchema,
+ FeaturePriceItem,
+ FeaturePriceItemSchema,
+ FeatureUsageType,
+ PriceItem,
+ PriceItemSchema,
+ ProductItem,
} from "@autumn/shared";
import {
- isFeatureItem,
- isFeaturePriceItem,
- isPriceItem,
+ isFeatureItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "./productItemUtils/getItemType.js";
import RecaseError from "@/utils/errorUtils.js";
import { itemToFeature } from "./productItemUtils/convertItem.js";
export const findSimilarItem = ({
- item,
- items,
+ item,
+ items,
}: {
- item: ProductItem;
- items: ProductItem[];
+ item: ProductItem;
+ items: ProductItem[];
}) => {
- // 1. If feature item
- if (isFeatureItem(item) || isFeaturePriceItem(item)) {
- return items.find((i) => i.feature_id === item.feature_id);
- }
+ // 1. If feature item
+ if (isFeatureItem(item) || isFeaturePriceItem(item)) {
+ return items.find((i) => i.feature_id === item.feature_id);
+ }
- // 2. If price item
- if (isPriceItem(item)) {
- return items.find((i) => {
- return (
- isPriceItem(i) &&
- i.price == item.price &&
- i.interval == item.interval &&
- (i.interval_count || 1) == (item.interval_count || 1)
- );
- });
- }
+ // 2. If price item
+ if (isPriceItem(item)) {
+ return items.find((i) => {
+ return (
+ isPriceItem(i) &&
+ i.price == item.price &&
+ i.interval == item.interval &&
+ (i.interval_count || 1) == (item.interval_count || 1)
+ );
+ });
+ }
- return null;
+ return null;
};
const tiersAreSame = (tiers1: any, tiers2: any) => {
- if (!tiers1 && !tiers2) {
- return true;
- }
+ if (!tiers1 && !tiers2) {
+ return true;
+ }
- if (!tiers1 || !tiers2) {
- return false;
- }
+ if (!tiers1 || !tiers2) {
+ return false;
+ }
- if (tiers1.length !== tiers2.length) {
- return false;
- }
+ if (tiers1.length !== tiers2.length) {
+ return false;
+ }
- return tiers1.every(
- (tier: any, index: number) =>
- tier.amount === tiers2[index].amount && tier.to === tiers2[index].to
- );
+ return tiers1.every(
+ (tier: any, index: number) =>
+ tier.amount === tiers2[index].amount && tier.to === tiers2[index].to,
+ );
};
export const featureItemsAreSame = ({
- item1,
- item2,
+ item1,
+ item2,
}: {
- item1: FeatureItem;
- item2: FeatureItem;
+ item1: FeatureItem;
+ item2: FeatureItem;
}) => {
- // Compare config objects (including rollover)
- const configsAreSame =
- JSON.stringify(item1.config) === JSON.stringify(item2.config);
+ // Compare config objects (including rollover)
+ const configsAreSame =
+ JSON.stringify(item1.config) === JSON.stringify(item2.config);
- const same =
- item1.feature_id === item2.feature_id &&
- item1.included_usage == item2.included_usage &&
- item1.interval == item2.interval &&
- (item1.interval_count || 1) == (item2.interval_count || 1) &&
- item1.entity_feature_id == item2.entity_feature_id &&
- item1.reset_usage_when_enabled == item2.reset_usage_when_enabled &&
- configsAreSame;
+ const same =
+ item1.feature_id === item2.feature_id &&
+ item1.included_usage == item2.included_usage &&
+ item1.interval == item2.interval &&
+ (item1.interval_count || 1) == (item2.interval_count || 1) &&
+ item1.entity_feature_id == item2.entity_feature_id &&
+ item1.reset_usage_when_enabled == item2.reset_usage_when_enabled &&
+ configsAreSame;
- return same;
+ return same;
};
export const priceItemsAreSame = ({
- item1,
- item2,
+ item1,
+ item2,
}: {
- item1: PriceItem;
- item2: PriceItem;
+ item1: PriceItem;
+ item2: PriceItem;
}) => {
- const same =
- item1.price === item2.price &&
- item1.interval == item2.interval &&
- (item1.interval_count || 1) == (item2.interval_count || 1);
+ const same =
+ item1.price === item2.price &&
+ item1.interval == item2.interval &&
+ (item1.interval_count || 1) == (item2.interval_count || 1);
- if (!same) {
- console.log(`Price items different: ${item1.price}`);
- }
+ if (!same) {
+ console.log(`Price items different: ${item1.price}`);
+ }
- return same;
+ return same;
};
export const featurePriceItemsAreSame = ({
- item1,
- item2,
+ item1,
+ item2,
}: {
- item1: FeaturePriceItem;
- item2: FeaturePriceItem;
+ item1: FeaturePriceItem;
+ item2: FeaturePriceItem;
}) => {
- const entsSame = {
- included_usage: {
- condition: item1.included_usage == item2.included_usage,
- message: `Included usage different: ${item1.included_usage} != ${item2.included_usage}`,
- },
- usage_limit: {
- condition: item1.usage_limit == item2.usage_limit,
- message: `Usage limit different: ${item1.usage_limit} !== ${item2.usage_limit}`,
- },
- reset_usage_when_enabled: {
- condition:
- item1.reset_usage_when_enabled == item2.reset_usage_when_enabled,
- message: `Reset usage when enabled different: ${item1.reset_usage_when_enabled} !== ${item2.reset_usage_when_enabled}`,
- },
- config: {
- condition: JSON.stringify(item1.config) === JSON.stringify(item2.config),
- message: `Config different: ${JSON.stringify(item1.config)} !== ${JSON.stringify(item2.config)}`,
- },
- };
+ const entsSame = {
+ included_usage: {
+ condition: item1.included_usage == item2.included_usage,
+ message: `Included usage different: ${item1.included_usage} != ${item2.included_usage}`,
+ },
+ usage_limit: {
+ condition: item1.usage_limit == item2.usage_limit,
+ message: `Usage limit different: ${item1.usage_limit} !== ${item2.usage_limit}`,
+ },
+ reset_usage_when_enabled: {
+ condition:
+ item1.reset_usage_when_enabled == item2.reset_usage_when_enabled,
+ message: `Reset usage when enabled different: ${item1.reset_usage_when_enabled} !== ${item2.reset_usage_when_enabled}`,
+ },
+ config: {
+ condition: JSON.stringify(item1.config) === JSON.stringify(item2.config),
+ message: `Config different: ${JSON.stringify(item1.config)} !== ${JSON.stringify(item2.config)}`,
+ },
+ };
- const pricesSame = {
- feature_id: {
- condition: item1.feature_id === item2.feature_id,
- message: `Feature ID different: ${item1.feature_id} != ${item2.feature_id}`,
- },
- interval: {
- condition: item1.interval == item2.interval,
- message: `Interval different: ${item1.interval} != ${item2.interval}`,
- },
- interval_count: {
- condition: (item1.interval_count || 1) == (item2.interval_count || 1),
- message: `Interval count different: ${item1.interval_count} != ${item2.interval_count}`,
- },
- usage_model: {
- condition: item1.usage_model === item2.usage_model,
- message: `Usage model different: ${item1.usage_model} != ${item2.usage_model}`,
- },
- price: {
- condition: item1.price == item2.price,
- message: `Price different: ${item1.price} != ${item2.price}`,
- },
- tiers: {
- condition: tiersAreSame(item1.tiers, item2.tiers),
- message: `Tiers different`,
- },
- billing_units: {
- condition: item1.billing_units == item2.billing_units,
- message: `Billing units different: ${item1.billing_units} !== ${item2.billing_units}`,
- },
- reset_usage_when_enabled: {
- condition:
- item1.reset_usage_when_enabled == item2.reset_usage_when_enabled,
- message: `Reset usage when enabled different: ${item1.reset_usage_when_enabled} !== ${item2.reset_usage_when_enabled}`,
- },
- };
+ const pricesSame = {
+ feature_id: {
+ condition: item1.feature_id === item2.feature_id,
+ message: `Feature ID different: ${item1.feature_id} != ${item2.feature_id}`,
+ },
+ interval: {
+ condition: item1.interval == item2.interval,
+ message: `Interval different: ${item1.interval} != ${item2.interval}`,
+ },
+ interval_count: {
+ condition: (item1.interval_count || 1) == (item2.interval_count || 1),
+ message: `Interval count different: ${item1.interval_count} != ${item2.interval_count}`,
+ },
+ usage_model: {
+ condition: item1.usage_model === item2.usage_model,
+ message: `Usage model different: ${item1.usage_model} != ${item2.usage_model}`,
+ },
+ price: {
+ condition: item1.price == item2.price,
+ message: `Price different: ${item1.price} != ${item2.price}`,
+ },
+ tiers: {
+ condition: tiersAreSame(item1.tiers, item2.tiers),
+ message: `Tiers different`,
+ },
+ billing_units: {
+ condition: item1.billing_units == item2.billing_units,
+ message: `Billing units different: ${item1.billing_units} !== ${item2.billing_units}`,
+ },
+ reset_usage_when_enabled: {
+ condition:
+ item1.reset_usage_when_enabled == item2.reset_usage_when_enabled,
+ message: `Reset usage when enabled different: ${item1.reset_usage_when_enabled} !== ${item2.reset_usage_when_enabled}`,
+ },
+ };
- const same =
- Object.values(pricesSame).every((d) => d.condition) &&
- Object.values(entsSame).every((d) => d.condition);
+ const same =
+ Object.values(pricesSame).every((d) => d.condition) &&
+ Object.values(entsSame).every((d) => d.condition);
- const pricesChanged = Object.values(pricesSame).some((d) => !d.condition);
+ const pricesChanged = Object.values(pricesSame).some((d) => !d.condition);
- if (!same) {
- console.log(
- "Feature price items different:",
- Object.values(entsSame)
- .filter((d) => !d.condition)
- .map((d) => d.message),
- Object.values(pricesSame)
- .filter((d) => !d.condition)
- .map((d) => d.message)
- );
- }
+ if (!same) {
+ console.log(
+ "Feature price items different:",
+ Object.values(entsSame)
+ .filter((d) => !d.condition)
+ .map((d) => d.message),
+ Object.values(pricesSame)
+ .filter((d) => !d.condition)
+ .map((d) => d.message),
+ );
+ }
- return {
- same,
- pricesChanged,
- };
+ return {
+ same,
+ pricesChanged,
+ };
};
export const itemsAreSame = ({
- item1,
- item2,
- features,
+ item1,
+ item2,
+ features,
}: {
- item1: ProductItem;
- item2: ProductItem;
- features?: Feature[];
+ item1: ProductItem;
+ item2: ProductItem;
+ features?: Feature[];
}) => {
- // 1. If feature item
- let same = false;
- let pricesChanged = false;
+ // 1. If feature item
+ let same = false;
+ let pricesChanged = false;
- if (isFeatureItem(item1)) {
- if (!isFeatureItem(item2)) {
- return {
- same: false,
- pricesChanged: true,
- };
- }
+ if (isFeatureItem(item1)) {
+ if (!isFeatureItem(item2)) {
+ return {
+ same: false,
+ pricesChanged: true,
+ };
+ }
- same = featureItemsAreSame({
- item1: FeatureItemSchema.parse(item1),
- item2: item2 as FeatureItem,
- });
+ same = featureItemsAreSame({
+ item1: FeatureItemSchema.parse(item1),
+ item2: item2 as FeatureItem,
+ });
- pricesChanged = false;
- }
+ pricesChanged = false;
+ }
- if (isFeaturePriceItem(item1)) {
- if (!isFeaturePriceItem(item2)) {
- return {
- same: false,
- pricesChanged: true,
- };
- }
+ if (isFeaturePriceItem(item1)) {
+ if (!isFeaturePriceItem(item2)) {
+ return {
+ same: false,
+ pricesChanged: true,
+ };
+ }
- const { same: same_, pricesChanged: pricesChanged_ } =
- featurePriceItemsAreSame({
- item1: FeaturePriceItemSchema.parse(item1),
- item2: FeaturePriceItemSchema.parse(item2),
- });
+ const { same: same_, pricesChanged: pricesChanged_ } =
+ featurePriceItemsAreSame({
+ item1: FeaturePriceItemSchema.parse(item1),
+ item2: FeaturePriceItemSchema.parse(item2),
+ });
- same = same_;
+ same = same_;
- let feature = itemToFeature({
- item: item1,
- features: features || [],
- });
+ let feature = itemToFeature({
+ item: item1,
+ features: features || [],
+ });
- if (feature?.config?.usage_type === FeatureUsageType.Continuous) {
- pricesChanged = true;
- } else {
- pricesChanged = pricesChanged_;
- }
- }
+ if (feature?.config?.usage_type === FeatureUsageType.Continuous) {
+ pricesChanged = true;
+ } else {
+ pricesChanged = pricesChanged_;
+ }
+ }
- // 2. If price item
- if (isPriceItem(item1)) {
- same = priceItemsAreSame({
- item1: PriceItemSchema.parse(item1),
- item2: PriceItemSchema.parse(item2),
- });
- if (!same) {
- pricesChanged = true;
- }
- }
+ // 2. If price item
+ if (isPriceItem(item1)) {
+ same = priceItemsAreSame({
+ item1: PriceItemSchema.parse(item1),
+ item2: PriceItemSchema.parse(item2),
+ });
+ if (!same) {
+ pricesChanged = true;
+ }
+ }
- return {
- same,
- pricesChanged: !same && pricesChanged,
- };
+ return {
+ same,
+ pricesChanged: !same && pricesChanged,
+ };
};
diff --git a/server/src/internal/products/product-items/createFeaturesFromItems.ts b/server/src/internal/products/product-items/createFeaturesFromItems.ts
index d4d21952c..067616373 100644
--- a/server/src/internal/products/product-items/createFeaturesFromItems.ts
+++ b/server/src/internal/products/product-items/createFeaturesFromItems.ts
@@ -1,100 +1,100 @@
import {
- AppEnv,
- ErrCode,
- Feature,
- FeatureType,
- FeatureUsageType,
- ProductItem,
- ProductItemFeatureType,
+ AppEnv,
+ ErrCode,
+ Feature,
+ FeatureType,
+ FeatureUsageType,
+ ProductItem,
+ ProductItemFeatureType,
} from "@autumn/shared";
import { isBooleanFeatureItem } from "./productItemUtils/getItemType.js";
import { validateFeatureId } from "@/internal/features/featureUtils.js";
import {
- constructBooleanFeature,
- constructMeteredFeature,
+ constructBooleanFeature,
+ constructMeteredFeature,
} from "@/internal/features/utils/constructFeatureUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";
export const createFeaturesFromItems = ({
- items,
- curFeatures,
- orgId,
- env,
+ items,
+ curFeatures,
+ orgId,
+ env,
}: {
- items: ProductItem[];
- curFeatures: Feature[];
- orgId: string;
- env: AppEnv;
+ items: ProductItem[];
+ curFeatures: Feature[];
+ orgId: string;
+ env: AppEnv;
}) => {
- let newFeatures: Feature[] = [];
- for (const item of items) {
- if (!item.feature_id) {
- continue;
- }
+ let newFeatures: Feature[] = [];
+ for (const item of items) {
+ if (!item.feature_id) {
+ continue;
+ }
- let feature = curFeatures.find((f) => f.id == item.feature_id);
- if (feature) {
- if (nullish(item.feature_type)) {
- continue;
- }
- // 1. Check that feature_type matches
- if (item.feature_type == ProductItemFeatureType.Static) {
- let booleanFail =
- item.feature_type == ProductItemFeatureType.Static &&
- feature.type != FeatureType.Boolean;
+ let feature = curFeatures.find((f) => f.id == item.feature_id);
+ if (feature) {
+ if (nullish(item.feature_type)) {
+ continue;
+ }
+ // 1. Check that feature_type matches
+ if (item.feature_type == ProductItemFeatureType.Static) {
+ let booleanFail =
+ item.feature_type == ProductItemFeatureType.Static &&
+ feature.type != FeatureType.Boolean;
- if (booleanFail) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} already exists but is not a static feature`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- } else {
- let usageFail = item.feature_type != feature.config?.usage_type;
- if (usageFail) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} already exists but is not a ${item.feature_type} feature`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- }
+ if (booleanFail) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} already exists but is not a static feature`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ } else {
+ let usageFail = item.feature_type != feature.config?.usage_type;
+ if (usageFail) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} already exists but is not a ${item.feature_type} feature`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ }
- continue;
- }
+ continue;
+ }
- validateFeatureId(item.feature_id);
+ validateFeatureId(item.feature_id);
- if (isBooleanFeatureItem(item)) {
- const feature = constructBooleanFeature({
- featureId: item.feature_id!,
- orgId,
- env,
- });
- newFeatures.push(feature);
- } else {
- if (!item.feature_type) {
- throw new RecaseError({
- message: `Feature type is required for ${item.feature_id}. Either 'continuous_use' or 'single_use'`,
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
- const feature = constructMeteredFeature({
- featureId: item.feature_id!,
- orgId,
- env,
- usageType:
- item.feature_type == ProductItemFeatureType.ContinuousUse
- ? FeatureUsageType.Continuous
- : FeatureUsageType.Single,
- });
- newFeatures.push(feature);
- }
- }
+ if (isBooleanFeatureItem(item)) {
+ const feature = constructBooleanFeature({
+ featureId: item.feature_id!,
+ orgId,
+ env,
+ });
+ newFeatures.push(feature);
+ } else {
+ if (!item.feature_type) {
+ throw new RecaseError({
+ message: `Feature type is required for ${item.feature_id}. Either 'continuous_use' or 'single_use'`,
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
+ const feature = constructMeteredFeature({
+ featureId: item.feature_id!,
+ orgId,
+ env,
+ usageType:
+ item.feature_type == ProductItemFeatureType.ContinuousUse
+ ? FeatureUsageType.Continuous
+ : FeatureUsageType.Single,
+ });
+ newFeatures.push(feature);
+ }
+ }
- return { allFeatures: [...curFeatures, ...newFeatures], newFeatures };
+ return { allFeatures: [...curFeatures, ...newFeatures], newFeatures };
};
diff --git a/server/src/internal/products/product-items/itemIntervalUtils.ts b/server/src/internal/products/product-items/itemIntervalUtils.ts
index 8681bec26..2f3d4e3c0 100644
--- a/server/src/internal/products/product-items/itemIntervalUtils.ts
+++ b/server/src/internal/products/product-items/itemIntervalUtils.ts
@@ -1,48 +1,48 @@
import { nullish } from "@/utils/genUtils.js";
import {
- BillingInterval,
- EntInterval,
- ProductItem,
- ProductItemFeatureType,
- ProductItemInterval,
+ BillingInterval,
+ EntInterval,
+ ProductItem,
+ ProductItemFeatureType,
+ ProductItemInterval,
} from "@autumn/shared";
export const billingToItemInterval = (billingInterval: BillingInterval) => {
- if (billingInterval == BillingInterval.OneOff) {
- return null;
- }
+ if (billingInterval == BillingInterval.OneOff) {
+ return null;
+ }
- return billingInterval as unknown as ProductItemInterval;
+ return billingInterval as unknown as ProductItemInterval;
};
export const entToItemInterval = (entInterval?: EntInterval) => {
- if (nullish(entInterval)) {
- return null;
- }
+ if (nullish(entInterval)) {
+ return null;
+ }
- if (entInterval == EntInterval.Lifetime) {
- return null;
- }
+ if (entInterval == EntInterval.Lifetime) {
+ return null;
+ }
- return entInterval as unknown as ProductItemInterval;
+ return entInterval as unknown as ProductItemInterval;
};
export const itemToBillingInterval = (item: ProductItem) => {
- if (nullish(item.interval)) {
- return BillingInterval.OneOff;
- }
+ if (nullish(item.interval)) {
+ return BillingInterval.OneOff;
+ }
- return item.interval;
+ return item.interval;
};
export const itemToEntInterval = (item: ProductItem) => {
- if (nullish(item.interval)) {
- return EntInterval.Lifetime;
- }
+ if (nullish(item.interval)) {
+ return EntInterval.Lifetime;
+ }
- if (item.feature_type == ProductItemFeatureType.ContinuousUse) {
- return EntInterval.Lifetime;
- }
+ if (item.feature_type == ProductItemFeatureType.ContinuousUse) {
+ return EntInterval.Lifetime;
+ }
- return item.interval;
+ return item.interval;
};
diff --git a/server/src/internal/products/product-items/productItemUtils.ts b/server/src/internal/products/product-items/productItemUtils.ts
index 98e778537..2169b98ee 100644
--- a/server/src/internal/products/product-items/productItemUtils.ts
+++ b/server/src/internal/products/product-items/productItemUtils.ts
@@ -1,158 +1,158 @@
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- EntInterval,
- ProductItemInterval,
- BillingInterval,
- ProductItem,
- ProductItemType,
- UsageModel,
- Infinite,
- ProductItemFeatureType,
- Feature,
- FeatureType,
+ EntInterval,
+ ProductItemInterval,
+ BillingInterval,
+ ProductItem,
+ ProductItemType,
+ UsageModel,
+ Infinite,
+ ProductItemFeatureType,
+ Feature,
+ FeatureType,
} from "@autumn/shared";
import { isFeatureItem } from "./productItemUtils/getItemType.js";
import {
- billingToItemInterval,
- entToItemInterval,
+ billingToItemInterval,
+ entToItemInterval,
} from "./itemIntervalUtils.js";
import {
- calculateProrationAmount,
- Proration,
+ calculateProrationAmount,
+ Proration,
} from "@/internal/invoices/prorationUtils.js";
export const itemToPriceOrTiers = ({
- item,
- proration,
- now,
+ item,
+ proration,
+ now,
}: {
- item: ProductItem;
- proration?: Proration;
- now?: number;
+ item: ProductItem;
+ proration?: Proration;
+ now?: number;
}) => {
- now = now || Date.now();
- if (item.price) {
- return {
- price: proration
- ? calculateProrationAmount({
- periodEnd: proration.end,
- periodStart: proration.start,
- now,
- amount: item.price,
- })
- : item.price,
- tiers: undefined,
- };
- } else if (item.tiers) {
- if (item.tiers.length > 1) {
- return {
- price: undefined,
- tiers: item.tiers.map((tier) => ({
- ...tier,
- amount: proration
- ? calculateProrationAmount({
- periodEnd: proration.end,
- periodStart: proration.start,
- now,
- amount: tier.amount,
- })
- : tier.amount,
- })),
- };
- } else {
- return {
- price: proration
- ? calculateProrationAmount({
- periodEnd: proration.end,
- periodStart: proration.start,
- now,
- amount: item.tiers[0].amount,
- })
- : item.tiers[0].amount,
- tiers: undefined,
- };
- }
- }
+ now = now || Date.now();
+ if (item.price) {
+ return {
+ price: proration
+ ? calculateProrationAmount({
+ periodEnd: proration.end,
+ periodStart: proration.start,
+ now,
+ amount: item.price,
+ })
+ : item.price,
+ tiers: undefined,
+ };
+ } else if (item.tiers) {
+ if (item.tiers.length > 1) {
+ return {
+ price: undefined,
+ tiers: item.tiers.map((tier) => ({
+ ...tier,
+ amount: proration
+ ? calculateProrationAmount({
+ periodEnd: proration.end,
+ periodStart: proration.start,
+ now,
+ amount: tier.amount,
+ })
+ : tier.amount,
+ })),
+ };
+ } else {
+ return {
+ price: proration
+ ? calculateProrationAmount({
+ periodEnd: proration.end,
+ periodStart: proration.start,
+ now,
+ amount: item.tiers[0].amount,
+ })
+ : item.tiers[0].amount,
+ tiers: undefined,
+ };
+ }
+ }
};
// FOR TESTS?
export const constructFeatureItem = ({
- feature_id,
- included_usage,
- interval = EntInterval.Month,
- entitlement_id,
- entity_feature_id,
+ feature_id,
+ included_usage,
+ interval = EntInterval.Month,
+ entitlement_id,
+ entity_feature_id,
}: {
- feature_id: string;
- included_usage?: number | string;
- interval?: EntInterval;
- entitlement_id?: string;
- entity_feature_id?: string;
+ feature_id: string;
+ included_usage?: number | string;
+ interval?: EntInterval;
+ entitlement_id?: string;
+ entity_feature_id?: string;
}) => {
- let item: ProductItem = {
- feature_id,
- included_usage: included_usage as number,
- interval: entToItemInterval(interval),
- entitlement_id,
- entity_feature_id,
- };
+ let item: ProductItem = {
+ feature_id,
+ included_usage: included_usage as number,
+ interval: entToItemInterval(interval),
+ entitlement_id,
+ entity_feature_id,
+ };
- return item;
+ return item;
};
export const constructPriceItem = ({
- price,
- interval,
- intervalCount,
+ price,
+ interval,
+ intervalCount,
}: {
- price: number;
- interval: BillingInterval | null;
- intervalCount?: number;
+ price: number;
+ interval: BillingInterval | null;
+ intervalCount?: number;
}) => {
- let item: ProductItem = {
- price: price,
- interval: interval as any,
- interval_count: intervalCount || 1,
- };
+ let item: ProductItem = {
+ price: price,
+ interval: interval as any,
+ interval_count: intervalCount || 1,
+ };
- return item;
+ return item;
};
export const constructFeaturePriceItem = ({
- feature_id,
+ feature_id,
- feature_type,
- included_usage,
- price,
- interval,
- usage_model,
- billing_units = 1,
- reset_usage_when_enabled = false,
- entity_feature_id,
+ feature_type,
+ included_usage,
+ price,
+ interval,
+ usage_model,
+ billing_units = 1,
+ reset_usage_when_enabled = false,
+ entity_feature_id,
}: {
- feature_id: string;
- feature_type?: ProductItemFeatureType;
- included_usage?: number;
- price: number;
- interval: BillingInterval;
- usage_model?: UsageModel;
- billing_units?: number;
- reset_usage_when_enabled?: boolean;
- entity_feature_id?: string;
+ feature_id: string;
+ feature_type?: ProductItemFeatureType;
+ included_usage?: number;
+ price: number;
+ interval: BillingInterval;
+ usage_model?: UsageModel;
+ billing_units?: number;
+ reset_usage_when_enabled?: boolean;
+ entity_feature_id?: string;
}) => {
- let item: ProductItem & {
- included_usage: number;
- } = {
- feature_id,
- feature_type,
- included_usage: included_usage as number,
- price,
- interval: billingToItemInterval(interval),
- usage_model,
- billing_units,
- reset_usage_when_enabled,
- entity_feature_id,
- };
+ let item: ProductItem & {
+ included_usage: number;
+ } = {
+ feature_id,
+ feature_type,
+ included_usage: included_usage as number,
+ price,
+ interval: billingToItemInterval(interval),
+ usage_model,
+ billing_units,
+ reset_usage_when_enabled,
+ entity_feature_id,
+ };
- return item;
+ return item;
};
diff --git a/server/src/internal/products/product-items/productItemUtils/classifyItem.ts b/server/src/internal/products/product-items/productItemUtils/classifyItem.ts
index 2ea5e1473..99d98a120 100644
--- a/server/src/internal/products/product-items/productItemUtils/classifyItem.ts
+++ b/server/src/internal/products/product-items/productItemUtils/classifyItem.ts
@@ -5,22 +5,22 @@ import { ProductItem } from "@autumn/shared";
import { itemToUsageType } from "./convertItem.js";
export const itemCanBeProrated = ({
- item,
- features,
+ item,
+ features,
}: {
- item: ProductItem;
- features: Feature[];
+ item: ProductItem;
+ features: Feature[];
}) => {
- const usageType = itemToUsageType({ item, features });
+ const usageType = itemToUsageType({ item, features });
- if (
- usageType == ProductItemFeatureType.SingleUse &&
- item.usage_model == UsageModel.Prepaid
- ) {
- return true;
- } else if (usageType == ProductItemFeatureType.ContinuousUse) {
- return true;
- }
+ if (
+ usageType == ProductItemFeatureType.SingleUse &&
+ item.usage_model == UsageModel.Prepaid
+ ) {
+ return true;
+ } else if (usageType == ProductItemFeatureType.ContinuousUse) {
+ return true;
+ }
- return false;
+ return false;
};
diff --git a/server/src/internal/products/product-items/productItemUtils/convertItem.ts b/server/src/internal/products/product-items/productItemUtils/convertItem.ts
index 8507a7bf4..643799dba 100644
--- a/server/src/internal/products/product-items/productItemUtils/convertItem.ts
+++ b/server/src/internal/products/product-items/productItemUtils/convertItem.ts
@@ -1,40 +1,40 @@
import {
- Feature,
- FeatureType,
- ProductItem,
- ProductItemFeatureType,
- UsageModel,
+ Feature,
+ FeatureType,
+ ProductItem,
+ ProductItemFeatureType,
+ UsageModel,
} from "@autumn/shared";
export const itemToFeature = ({
- item,
- features,
+ item,
+ features,
}: {
- item: ProductItem;
- features: Feature[];
+ item: ProductItem;
+ features: Feature[];
}) => {
- const feature = features.find((f) => f.id === item.feature_id);
+ const feature = features.find((f) => f.id === item.feature_id);
- return feature;
+ return feature;
};
export const itemToUsageType = ({
- item,
- features,
+ item,
+ features,
}: {
- item: ProductItem;
- features: Feature[];
+ item: ProductItem;
+ features: Feature[];
}) => {
- const feature = itemToFeature({ item, features });
- if (!feature) return null;
+ const feature = itemToFeature({ item, features });
+ if (!feature) return null;
- if (feature.type == FeatureType.Boolean) {
- return ProductItemFeatureType.Static;
- }
+ if (feature.type == FeatureType.Boolean) {
+ return ProductItemFeatureType.Static;
+ }
- if (feature.type == FeatureType.CreditSystem) {
- return ProductItemFeatureType.SingleUse;
- }
+ if (feature.type == FeatureType.CreditSystem) {
+ return ProductItemFeatureType.SingleUse;
+ }
- return feature.config!.usage_type as ProductItemFeatureType;
+ return feature.config!.usage_type as ProductItemFeatureType;
};
diff --git a/server/src/internal/products/product-items/productItemUtils/getItemType.ts b/server/src/internal/products/product-items/productItemUtils/getItemType.ts
index 707b3134a..8866bb9bd 100644
--- a/server/src/internal/products/product-items/productItemUtils/getItemType.ts
+++ b/server/src/internal/products/product-items/productItemUtils/getItemType.ts
@@ -3,40 +3,40 @@ import { ProductItem, ProductItemType } from "@autumn/shared";
import { nullish } from "@/utils/genUtils.js";
export const isBooleanFeatureItem = (item: ProductItem) => {
- return (
- notNullish(item.feature_id) &&
- (nullish(item.price) || item.price == 0) &&
- nullish(item.tiers) &&
- nullish(item.interval) &&
- nullish(item.included_usage)
- );
+ return (
+ notNullish(item.feature_id) &&
+ (nullish(item.price) || item.price == 0) &&
+ nullish(item.tiers) &&
+ nullish(item.interval) &&
+ nullish(item.included_usage)
+ );
};
export const isFeatureItem = (item: ProductItem) => {
- return (
- notNullish(item.feature_id) &&
- (nullish(item.price) || item.price == 0) &&
- nullish(item.tiers)
- );
+ return (
+ notNullish(item.feature_id) &&
+ (nullish(item.price) || item.price == 0) &&
+ nullish(item.tiers)
+ );
};
export const isPriceItem = (item: ProductItem) => {
- return notNullish(item.price) && nullish(item.feature_id);
+ return notNullish(item.price) && nullish(item.feature_id);
};
export const isFeaturePriceItem = (item: ProductItem) => {
- return (
- notNullish(item.feature_id) &&
- (notNullish(item.price) || notNullish(item.tiers))
- );
+ return (
+ notNullish(item.feature_id) &&
+ (notNullish(item.price) || notNullish(item.tiers))
+ );
};
export const getItemType = (item: ProductItem) => {
- if (isFeatureItem(item)) {
- return ProductItemType.Feature;
- } else if (isFeaturePriceItem(item)) {
- return ProductItemType.FeaturePrice;
- }
+ if (isFeatureItem(item)) {
+ return ProductItemType.Feature;
+ } else if (isFeaturePriceItem(item)) {
+ return ProductItemType.FeaturePrice;
+ }
- return ProductItemType.Price;
+ return ProductItemType.Price;
};
diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts
index 8d3ca4e7b..97a935f6b 100644
--- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts
+++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts
@@ -1,10 +1,10 @@
import {
- AppEnv,
- Entitlement,
- Feature,
- Price,
- Product,
- ProductItem,
+ AppEnv,
+ Entitlement,
+ Feature,
+ Price,
+ Product,
+ ProductItem,
} from "@autumn/shared";
import { itemToPriceAndEnt } from "./itemToPriceAndEnt.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
@@ -16,284 +16,284 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { formatPrice } from "../../prices/priceUtils.js";
const updateDbPricesAndEnts = async ({
- db,
- newPrices,
- newEnts,
- updatedPrices,
- updatedEnts,
- deletedPrices,
- deletedEnts,
+ db,
+ newPrices,
+ newEnts,
+ updatedPrices,
+ updatedEnts,
+ deletedPrices,
+ deletedEnts,
}: {
- db: DrizzleCli;
- newPrices: Price[];
- newEnts: Entitlement[];
- updatedPrices: Price[];
- updatedEnts: Entitlement[];
- deletedPrices: Price[];
- deletedEnts: Entitlement[];
+ db: DrizzleCli;
+ newPrices: Price[];
+ newEnts: Entitlement[];
+ updatedPrices: Price[];
+ updatedEnts: Entitlement[];
+ deletedPrices: Price[];
+ deletedEnts: Entitlement[];
}) => {
- // 1. Create new ents
+ // 1. Create new ents
- await Promise.all([
- EntitlementService.insert({
- db,
- data: newEnts,
- }),
- EntitlementService.upsert({
- db,
- data: updatedEnts,
- }),
- ]);
+ await Promise.all([
+ EntitlementService.insert({
+ db,
+ data: newEnts,
+ }),
+ EntitlementService.upsert({
+ db,
+ data: updatedEnts,
+ }),
+ ]);
- // 2. Create new prices
- await Promise.all([
- PriceService.insert({
- db,
- data: newPrices,
- }),
- PriceService.upsert({
- db,
- data: updatedPrices,
- }),
- PriceService.deleteInIds({
- db,
- ids: deletedPrices.map((price) => price.id!),
- }),
- ]);
+ // 2. Create new prices
+ await Promise.all([
+ PriceService.insert({
+ db,
+ data: newPrices,
+ }),
+ PriceService.upsert({
+ db,
+ data: updatedPrices,
+ }),
+ PriceService.deleteInIds({
+ db,
+ ids: deletedPrices.map((price) => price.id!),
+ }),
+ ]);
- // Check if any custom prices use this entitlement...
- let deletedEntIds = deletedEnts.map((ent) => ent.id!);
- let customPrices = await PriceService.getCustomInEntIds({
- db,
- entitlementIds: deletedEntIds,
- });
+ // Check if any custom prices use this entitlement...
+ let deletedEntIds = deletedEnts.map((ent) => ent.id!);
+ let customPrices = await PriceService.getCustomInEntIds({
+ db,
+ entitlementIds: deletedEntIds,
+ });
- if (customPrices.length == 0) {
- // Update the entitlement to be custom...
- await EntitlementService.deleteInIds({
- db,
- ids: deletedEntIds,
- });
- } else {
- let updateOrDelete: any = [];
- for (const ent of deletedEnts) {
- let hasCustomPrice = customPrices.some(
- (price) => price.entitlement_id == ent.id
- );
+ if (customPrices.length == 0) {
+ // Update the entitlement to be custom...
+ await EntitlementService.deleteInIds({
+ db,
+ ids: deletedEntIds,
+ });
+ } else {
+ let updateOrDelete: any = [];
+ for (const ent of deletedEnts) {
+ let hasCustomPrice = customPrices.some(
+ (price) => price.entitlement_id == ent.id,
+ );
- if (hasCustomPrice) {
- updateOrDelete.push(
- EntitlementService.update({
- db,
- id: ent.id!,
- updates: {
- is_custom: true,
- },
- })
- );
- } else {
- updateOrDelete.push(
- EntitlementService.deleteInIds({
- db,
- ids: [ent.id!],
- })
- );
- }
- }
+ if (hasCustomPrice) {
+ updateOrDelete.push(
+ EntitlementService.update({
+ db,
+ id: ent.id!,
+ updates: {
+ is_custom: true,
+ },
+ }),
+ );
+ } else {
+ updateOrDelete.push(
+ EntitlementService.deleteInIds({
+ db,
+ ids: [ent.id!],
+ }),
+ );
+ }
+ }
- await Promise.all(updateOrDelete);
- }
+ await Promise.all(updateOrDelete);
+ }
};
const handleCustomProductItems = async ({
- db,
- newPrices,
- newEnts,
- updatedPrices,
- updatedEnts,
- samePrices,
- sameEnts,
- features,
+ db,
+ newPrices,
+ newEnts,
+ updatedPrices,
+ updatedEnts,
+ samePrices,
+ sameEnts,
+ features,
}: {
- db: DrizzleCli;
- newPrices: Price[];
- newEnts: Entitlement[];
- updatedPrices: Price[];
- updatedEnts: Entitlement[];
- samePrices: Price[];
- sameEnts: Entitlement[];
- features: Feature[];
+ db: DrizzleCli;
+ newPrices: Price[];
+ newEnts: Entitlement[];
+ updatedPrices: Price[];
+ updatedEnts: Entitlement[];
+ samePrices: Price[];
+ sameEnts: Entitlement[];
+ features: Feature[];
}) => {
- // await EntitlementService.insert({
- // db,
- // data: [...newEnts, ...updatedEnts],
- // });
+ // await EntitlementService.insert({
+ // db,
+ // data: [...newEnts, ...updatedEnts],
+ // });
- // await PriceService.insert({
- // db,
- // data: [...newPrices, ...updatedPrices],
- // });
+ // await PriceService.insert({
+ // db,
+ // data: [...newPrices, ...updatedPrices],
+ // });
- return {
- prices: [...newPrices, ...updatedPrices, ...samePrices],
- entitlements: [...newEnts, ...updatedEnts, ...sameEnts].map((ent) => ({
- ...ent,
- feature: features.find((f) => f.id == ent.feature_id),
- })),
- customPrices: [...newPrices, ...updatedPrices],
- customEnts: [...newEnts, ...updatedEnts],
- };
+ return {
+ prices: [...newPrices, ...updatedPrices, ...samePrices],
+ entitlements: [...newEnts, ...updatedEnts, ...sameEnts].map((ent) => ({
+ ...ent,
+ feature: features.find((f) => f.id == ent.feature_id),
+ })),
+ customPrices: [...newPrices, ...updatedPrices],
+ customEnts: [...newEnts, ...updatedEnts],
+ };
};
export const handleNewProductItems = async ({
- db,
- curPrices,
- curEnts,
- newItems,
- features,
- product,
- logger,
- isCustom,
- newVersion,
- saveToDb = true,
+ db,
+ curPrices,
+ curEnts,
+ newItems,
+ features,
+ product,
+ logger,
+ isCustom,
+ newVersion,
+ saveToDb = true,
}: {
- db: DrizzleCli;
- curPrices: Price[];
- curEnts: Entitlement[];
- newItems: ProductItem[];
- features: Feature[];
- product: Product;
- logger: any;
- isCustom: boolean;
- newVersion?: boolean;
- saveToDb?: boolean;
+ db: DrizzleCli;
+ curPrices: Price[];
+ curEnts: Entitlement[];
+ newItems: ProductItem[];
+ features: Feature[];
+ product: Product;
+ logger: any;
+ isCustom: boolean;
+ newVersion?: boolean;
+ saveToDb?: boolean;
}) => {
- // Create features if not exist...
- if (!newItems) {
- return {
- prices: [],
- entitlements: [],
- customPrices: [],
- customEnts: [],
- };
- }
+ // Create features if not exist...
+ if (!newItems) {
+ return {
+ prices: [],
+ entitlements: [],
+ customPrices: [],
+ customEnts: [],
+ };
+ }
- // Validate product items...
- let { allFeatures, newFeatures } = validateProductItems({
- newItems,
- features,
- orgId: product.org_id!,
- env: product.env as AppEnv,
- });
+ // Validate product items...
+ let { allFeatures, newFeatures } = validateProductItems({
+ newItems,
+ features,
+ orgId: product.org_id!,
+ env: product.env as AppEnv,
+ });
- features = allFeatures;
+ features = allFeatures;
- let newPrices: Price[] = [];
- let newEnts: Entitlement[] = [];
+ let newPrices: Price[] = [];
+ let newEnts: Entitlement[] = [];
- let updatedPrices: Price[] = [];
- let updatedEnts: Entitlement[] = [];
+ let updatedPrices: Price[] = [];
+ let updatedEnts: Entitlement[] = [];
- let deletedPrices: Price[] = curPrices.filter((price) => {
- let item = newItems.find((item) => item.price_id == price.id);
- if (!item) {
- return true;
- }
+ let deletedPrices: Price[] = curPrices.filter((price) => {
+ let item = newItems.find((item) => item.price_id == price.id);
+ if (!item) {
+ return true;
+ }
- return isFeatureItem(item);
- });
+ return isFeatureItem(item);
+ });
- let deletedEnts: Entitlement[] = curEnts.filter(
- (ent) => !newItems.some((item) => item.entitlement_id == ent.id)
- );
+ let deletedEnts: Entitlement[] = curEnts.filter(
+ (ent) => !newItems.some((item) => item.entitlement_id == ent.id),
+ );
- let samePrices: Price[] = [];
- let sameEnts: Entitlement[] = [];
+ let samePrices: Price[] = [];
+ let sameEnts: Entitlement[] = [];
- for (const item of newItems) {
- let feature = features.find((f) => f.id == item.feature_id);
- let curEnt = curEnts.find((ent) => ent.id == item.entitlement_id);
- let curPrice = curPrices.find((price) => price.id == item.price_id);
+ for (const item of newItems) {
+ let feature = features.find((f) => f.id == item.feature_id);
+ let curEnt = curEnts.find((ent) => ent.id == item.entitlement_id);
+ let curPrice = curPrices.find((price) => price.id == item.price_id);
- // 2. Update price and entitlement?
- let { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } =
- itemToPriceAndEnt({
- item,
- orgId: product.org_id!,
- internalProductId: product.internal_id!,
- feature: feature,
- curPrice,
- curEnt,
- isCustom,
- newVersion,
- features,
- });
+ // 2. Update price and entitlement?
+ let { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } =
+ itemToPriceAndEnt({
+ item,
+ orgId: product.org_id!,
+ internalProductId: product.internal_id!,
+ feature: feature,
+ curPrice,
+ curEnt,
+ isCustom,
+ newVersion,
+ features,
+ });
- if (newPrice) {
- newPrices.push(newPrice);
- }
+ if (newPrice) {
+ newPrices.push(newPrice);
+ }
- if (newEnt) {
- newEnts.push(newEnt);
- }
+ if (newEnt) {
+ newEnts.push(newEnt);
+ }
- if (updatedPrice) {
- updatedPrices.push(updatedPrice);
- }
+ if (updatedPrice) {
+ updatedPrices.push(updatedPrice);
+ }
- if (updatedEnt) {
- updatedEnts.push(updatedEnt);
- }
+ if (updatedEnt) {
+ updatedEnts.push(updatedEnt);
+ }
- if (samePrice) {
- samePrices.push(samePrice);
- }
+ if (samePrice) {
+ samePrices.push(samePrice);
+ }
- if (sameEnt) {
- sameEnts.push(sameEnt);
- }
- }
+ if (sameEnt) {
+ sameEnts.push(sameEnt);
+ }
+ }
- if (newFeatures.length > 0 && saveToDb) {
- await FeatureService.insert({
- db,
- data: newFeatures,
- logger,
- });
- }
+ if (newFeatures.length > 0 && saveToDb) {
+ await FeatureService.insert({
+ db,
+ data: newFeatures,
+ logger,
+ });
+ }
- if ((isCustom || newVersion) && saveToDb) {
- return handleCustomProductItems({
- db,
- newPrices,
- newEnts,
- updatedPrices,
- updatedEnts,
- samePrices,
- sameEnts,
- features,
- });
- }
+ if ((isCustom || newVersion) && saveToDb) {
+ return handleCustomProductItems({
+ db,
+ newPrices,
+ newEnts,
+ updatedPrices,
+ updatedEnts,
+ samePrices,
+ sameEnts,
+ features,
+ });
+ }
- if (saveToDb) {
- await updateDbPricesAndEnts({
- db,
- newPrices,
- newEnts,
- updatedPrices,
- updatedEnts,
- deletedPrices,
- deletedEnts,
- });
- }
+ if (saveToDb) {
+ await updateDbPricesAndEnts({
+ db,
+ newPrices,
+ newEnts,
+ updatedPrices,
+ updatedEnts,
+ deletedPrices,
+ deletedEnts,
+ });
+ }
- return {
- prices: [...newPrices, ...updatedPrices],
- entitlements: [...newEnts, ...updatedEnts].map((ent) => ({
- ...ent,
- feature: features.find((f) => f.id == ent.feature_id),
- })),
- customPrices: [],
- customEnts: [],
- };
+ return {
+ prices: [...newPrices, ...updatedPrices],
+ entitlements: [...newEnts, ...updatedEnts].map((ent) => ({
+ ...ent,
+ feature: features.find((f) => f.id == ent.feature_id),
+ })),
+ customPrices: [],
+ customEnts: [],
+ };
};
diff --git a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts
index 84cbc93f9..1ecb0fc27 100644
--- a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts
+++ b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts
@@ -1,25 +1,25 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- AllowanceType,
- BillingInterval,
- BillingType,
- BillWhen,
- EntInterval,
- Entitlement,
- ErrCode,
- Feature,
- FeatureType,
- FixedPriceConfig,
- Infinite,
- Price,
- PriceType,
- ProductItem,
- UsageModel,
- TierInfinite,
- UsagePriceConfig,
- OnIncrease,
- OnDecrease,
- FeatureUsageType,
+ AllowanceType,
+ BillingInterval,
+ BillingType,
+ BillWhen,
+ EntInterval,
+ Entitlement,
+ ErrCode,
+ Feature,
+ FeatureType,
+ FixedPriceConfig,
+ Infinite,
+ Price,
+ PriceType,
+ ProductItem,
+ UsageModel,
+ TierInfinite,
+ UsagePriceConfig,
+ OnIncrease,
+ OnDecrease,
+ FeatureUsageType,
} from "@autumn/shared";
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
import { pricesAreSame } from "@/internal/products/prices/priceInitUtils.js";
@@ -27,416 +27,416 @@ import { entsAreSame } from "../../entitlements/entitlementUtils.js";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import {
- isFeatureItem,
- isFeaturePriceItem,
- isPriceItem,
+ isFeatureItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "./getItemType.js";
import {
- itemToBillingInterval,
- itemToEntInterval,
+ itemToBillingInterval,
+ itemToEntInterval,
} from "../itemIntervalUtils.js";
import { itemCanBeProrated } from "./classifyItem.js";
import { shouldProrate } from "../../prices/priceUtils/prorationConfigUtils.js";
export const getResetUsage = ({
- item,
- feature,
+ item,
+ feature,
}: {
- item: ProductItem;
- feature?: Feature;
+ item: ProductItem;
+ feature?: Feature;
}) => {
- if (!item.feature_id) {
- return undefined;
- }
- if (
- nullish(item.reset_usage_when_enabled) &&
- (isFeatureItem(item) || isFeaturePriceItem(item)) &&
- feature
- ) {
- return feature?.config?.usage_type == FeatureUsageType.Single;
- }
- return item.reset_usage_when_enabled;
+ if (!item.feature_id) {
+ return undefined;
+ }
+ if (
+ nullish(item.reset_usage_when_enabled) &&
+ (isFeatureItem(item) || isFeaturePriceItem(item)) &&
+ feature
+ ) {
+ return feature?.config?.usage_type == FeatureUsageType.Single;
+ }
+ return item.reset_usage_when_enabled;
};
// ITEM TO PRICE AND ENTITLEMENT
export const toPrice = ({
- item,
- orgId,
- internalProductId,
- isCustom,
- newVersion,
+ item,
+ orgId,
+ internalProductId,
+ isCustom,
+ newVersion,
}: {
- item: ProductItem;
- orgId: string;
- internalProductId: string;
- isCustom: boolean;
- newVersion?: boolean;
+ item: ProductItem;
+ orgId: string;
+ internalProductId: string;
+ isCustom: boolean;
+ newVersion?: boolean;
}) => {
- let config: FixedPriceConfig = {
- type: PriceType.Fixed,
- amount: notNullish(item.price) ? item.price! : item.tiers![0].amount!,
- interval: itemToBillingInterval(item) as BillingInterval,
- interval_count: item.interval_count || 1,
- };
+ let config: FixedPriceConfig = {
+ type: PriceType.Fixed,
+ amount: notNullish(item.price) ? item.price! : item.tiers![0].amount!,
+ interval: itemToBillingInterval(item) as BillingInterval,
+ interval_count: item.interval_count || 1,
+ };
- let price: Price = {
- id: item.price_id || generateId("pr"),
- created_at: item.created_at || Date.now(),
- org_id: orgId,
- internal_product_id: internalProductId,
- is_custom: isCustom,
- config,
- proration_config: null,
- };
+ let price: Price = {
+ id: item.price_id || generateId("pr"),
+ created_at: item.created_at || Date.now(),
+ org_id: orgId,
+ internal_product_id: internalProductId,
+ is_custom: isCustom,
+ config,
+ proration_config: null,
+ };
- if (isCustom || newVersion) {
- price = {
- ...price,
- id: generateId("pr"),
- created_at: Date.now(),
- };
- }
+ if (isCustom || newVersion) {
+ price = {
+ ...price,
+ id: generateId("pr"),
+ created_at: Date.now(),
+ };
+ }
- return { price, ent: null };
+ return { price, ent: null };
};
export const toFeature = ({
- item,
- orgId,
- internalFeatureId,
- internalProductId,
- isCustom,
- newVersion,
- feature,
+ item,
+ orgId,
+ internalFeatureId,
+ internalProductId,
+ isCustom,
+ newVersion,
+ feature,
}: {
- item: ProductItem;
- orgId: string;
- internalFeatureId: string;
- internalProductId: string;
- isCustom: boolean;
- newVersion?: boolean;
- feature?: Feature;
+ item: ProductItem;
+ orgId: string;
+ internalFeatureId: string;
+ internalProductId: string;
+ isCustom: boolean;
+ newVersion?: boolean;
+ feature?: Feature;
}) => {
- let isBoolean = feature?.type == FeatureType.Boolean;
+ let isBoolean = feature?.type == FeatureType.Boolean;
- let resetUsage = getResetUsage({ item, feature });
+ let resetUsage = getResetUsage({ item, feature });
- let ent: Entitlement = {
- id: item.entitlement_id || generateId("ent"),
- org_id: orgId,
- created_at: item.created_at || Date.now(),
- is_custom: isCustom,
- internal_product_id: internalProductId,
+ let ent: Entitlement = {
+ id: item.entitlement_id || generateId("ent"),
+ org_id: orgId,
+ created_at: item.created_at || Date.now(),
+ is_custom: isCustom,
+ internal_product_id: internalProductId,
- internal_feature_id: internalFeatureId,
- feature_id: item.feature_id!,
+ internal_feature_id: internalFeatureId,
+ feature_id: item.feature_id!,
- allowance: item.included_usage == Infinite ? null : item.included_usage!,
- allowance_type: isBoolean
- ? null
- : item.included_usage == Infinite
- ? AllowanceType.Unlimited
- : AllowanceType.Fixed,
+ allowance: item.included_usage == Infinite ? null : item.included_usage!,
+ allowance_type: isBoolean
+ ? null
+ : item.included_usage == Infinite
+ ? AllowanceType.Unlimited
+ : AllowanceType.Fixed,
- interval: isBoolean ? null : (itemToEntInterval(item) as EntInterval),
- interval_count: item.interval_count || 1,
+ interval: isBoolean ? null : (itemToEntInterval(item) as EntInterval),
+ interval_count: item.interval_count || 1,
- carry_from_previous: !resetUsage,
- entity_feature_id: item.entity_feature_id,
- usage_limit: null,
+ carry_from_previous: !resetUsage,
+ entity_feature_id: item.entity_feature_id,
+ usage_limit: null,
- rollover: item.config?.rollover,
- };
+ rollover: item.config?.rollover,
+ };
- if (isCustom || newVersion) {
- ent = {
- ...ent,
- id: generateId("ent"),
- created_at: Date.now(),
- };
- }
- return { price: null, ent };
+ if (isCustom || newVersion) {
+ ent = {
+ ...ent,
+ id: generateId("ent"),
+ created_at: Date.now(),
+ };
+ }
+ return { price: null, ent };
};
export const toFeatureAndPrice = ({
- item,
- orgId,
- internalFeatureId,
- internalProductId,
- isCustom,
- curPrice,
- curEnt,
- newVersion,
- features,
+ item,
+ orgId,
+ internalFeatureId,
+ internalProductId,
+ isCustom,
+ curPrice,
+ curEnt,
+ newVersion,
+ features,
}: {
- item: ProductItem;
- orgId: string;
- internalFeatureId: string;
- internalProductId: string;
- isCustom: boolean;
- curPrice?: Price;
- curEnt?: Entitlement;
- newVersion?: boolean;
- features: Feature[];
+ item: ProductItem;
+ orgId: string;
+ internalFeatureId: string;
+ internalProductId: string;
+ isCustom: boolean;
+ curPrice?: Price;
+ curEnt?: Entitlement;
+ newVersion?: boolean;
+ features: Feature[];
}) => {
- let resetUsage = getResetUsage({
- item,
- feature: features.find((f) => f.id == item.feature_id),
- });
+ let resetUsage = getResetUsage({
+ item,
+ feature: features.find((f) => f.id == item.feature_id),
+ });
- let ent: Entitlement = {
- id: item.entitlement_id || generateId("ent"),
- org_id: orgId,
- created_at: item.created_at || Date.now(),
- is_custom: isCustom,
- internal_product_id: internalProductId,
+ let ent: Entitlement = {
+ id: item.entitlement_id || generateId("ent"),
+ org_id: orgId,
+ created_at: item.created_at || Date.now(),
+ is_custom: isCustom,
+ internal_product_id: internalProductId,
- internal_feature_id: internalFeatureId,
- feature_id: item.feature_id!,
+ internal_feature_id: internalFeatureId,
+ feature_id: item.feature_id!,
- allowance: (item.included_usage as number) || 0,
- allowance_type: AllowanceType.Fixed,
- interval: itemToEntInterval(item) as EntInterval,
- interval_count: item.interval_count || 1,
+ allowance: (item.included_usage as number) || 0,
+ allowance_type: AllowanceType.Fixed,
+ interval: itemToEntInterval(item) as EntInterval,
+ interval_count: item.interval_count || 1,
- carry_from_previous: !resetUsage,
- entity_feature_id: item.entity_feature_id,
- usage_limit: item.usage_limit || null,
+ carry_from_previous: !resetUsage,
+ entity_feature_id: item.entity_feature_id,
+ usage_limit: item.usage_limit || null,
- rollover: item.config?.rollover,
- };
+ rollover: item.config?.rollover,
+ };
- // Will only create new ent id if
- let newEnt = !curEnt || (isCustom && !entsAreSame(curEnt, ent));
- if (newEnt || newVersion) {
- ent = {
- ...ent,
- id: generateId("ent"),
- created_at: Date.now(),
- };
- }
+ // Will only create new ent id if
+ let newEnt = !curEnt || (isCustom && !entsAreSame(curEnt, ent));
+ if (newEnt || newVersion) {
+ ent = {
+ ...ent,
+ id: generateId("ent"),
+ created_at: Date.now(),
+ };
+ }
- let entInterval = itemToEntInterval(item);
+ let entInterval = itemToEntInterval(item);
- let config: UsagePriceConfig = {
- type: PriceType.Usage,
+ let config: UsagePriceConfig = {
+ type: PriceType.Usage,
- bill_when:
- item.usage_model == UsageModel.Prepaid
- ? BillWhen.StartOfPeriod
- : BillWhen.EndOfPeriod,
+ bill_when:
+ item.usage_model == UsageModel.Prepaid
+ ? BillWhen.StartOfPeriod
+ : BillWhen.EndOfPeriod,
- billing_units: item.billing_units || 1,
- should_prorate: entInterval == EntInterval.Lifetime,
+ billing_units: item.billing_units || 1,
+ should_prorate: entInterval == EntInterval.Lifetime,
- internal_feature_id: internalFeatureId,
- feature_id: item.feature_id!,
- usage_tiers: notNullish(item.price)
- ? [
- {
- amount: item.price,
- to: TierInfinite,
- },
- ]
- : (item.tiers as any),
- interval: itemToBillingInterval(item) as BillingInterval,
- interval_count: item.interval_count || 1,
- };
+ internal_feature_id: internalFeatureId,
+ feature_id: item.feature_id!,
+ usage_tiers: notNullish(item.price)
+ ? [
+ {
+ amount: item.price,
+ to: TierInfinite,
+ },
+ ]
+ : (item.tiers as any),
+ interval: itemToBillingInterval(item) as BillingInterval,
+ interval_count: item.interval_count || 1,
+ };
- let prorationConfig = null;
- if (itemCanBeProrated({ item, features })) {
- let onIncrease = item.config?.on_increase || OnIncrease.ProrateImmediately;
- let onDecrease = item.config?.on_decrease || OnDecrease.Prorate;
+ let prorationConfig = null;
+ if (itemCanBeProrated({ item, features })) {
+ let onIncrease = item.config?.on_increase || OnIncrease.ProrateImmediately;
+ let onDecrease = item.config?.on_decrease || OnDecrease.Prorate;
- // console.log("Item config:", item.config);
- if (shouldProrate(onDecrease) || onDecrease == OnDecrease.Prorate) {
- onDecrease =
- onIncrease == OnIncrease.ProrateImmediately
- ? OnDecrease.ProrateImmediately
- : OnDecrease.ProrateNextCycle;
- }
+ // console.log("Item config:", item.config);
+ if (shouldProrate(onDecrease) || onDecrease == OnDecrease.Prorate) {
+ onDecrease =
+ onIncrease == OnIncrease.ProrateImmediately
+ ? OnDecrease.ProrateImmediately
+ : OnDecrease.ProrateNextCycle;
+ }
- prorationConfig = {
- on_increase: onIncrease,
- on_decrease: onDecrease,
- };
+ prorationConfig = {
+ on_increase: onIncrease,
+ on_decrease: onDecrease,
+ };
- // console.log("Proration config:", prorationConfig);
- }
+ // console.log("Proration config:", prorationConfig);
+ }
- let price: Price = {
- id: item.price_id || generateId("pr"),
- created_at: item.created_at || Date.now(),
- org_id: orgId,
- internal_product_id: internalProductId,
- is_custom: isCustom,
- config,
- entitlement_id: ent.id,
- proration_config: prorationConfig,
- };
+ let price: Price = {
+ id: item.price_id || generateId("pr"),
+ created_at: item.created_at || Date.now(),
+ org_id: orgId,
+ internal_product_id: internalProductId,
+ is_custom: isCustom,
+ config,
+ entitlement_id: ent.id,
+ proration_config: prorationConfig,
+ };
- let billingType = getBillingType(price.config!);
- if (
- (billingType == BillingType.UsageInArrear ||
- billingType == BillingType.InArrearProrated) &&
- price.config!.interval == BillingInterval.OneOff
- ) {
- throw new RecaseError({
- message: `Usage prices cannot be one-off if not set to prepaid (feature: ${item.feature_id})`,
- code: ErrCode.InvalidPrice,
- statusCode: 400,
- });
- }
+ let billingType = getBillingType(price.config!);
+ if (
+ (billingType == BillingType.UsageInArrear ||
+ billingType == BillingType.InArrearProrated) &&
+ price.config!.interval == BillingInterval.OneOff
+ ) {
+ throw new RecaseError({
+ message: `Usage prices cannot be one-off if not set to prepaid (feature: ${item.feature_id})`,
+ code: ErrCode.InvalidPrice,
+ statusCode: 400,
+ });
+ }
- let priceOrEntDifferent =
- (curPrice && !pricesAreSame(curPrice, price, true)) ||
- (curEnt && !entsAreSame(curEnt, ent));
+ let priceOrEntDifferent =
+ (curPrice && !pricesAreSame(curPrice, price, true)) ||
+ (curEnt && !entsAreSame(curEnt, ent));
- if (curPrice && (priceOrEntDifferent || newVersion)) {
- let newConfig = price.config as UsagePriceConfig;
- let curConfig = curPrice.config as UsagePriceConfig;
- newConfig.stripe_meter_id = curConfig.stripe_meter_id;
- newConfig.stripe_product_id = curConfig.stripe_product_id;
- price.config = newConfig;
- }
+ if (curPrice && (priceOrEntDifferent || newVersion)) {
+ let newConfig = price.config as UsagePriceConfig;
+ let curConfig = curPrice.config as UsagePriceConfig;
+ newConfig.stripe_meter_id = curConfig.stripe_meter_id;
+ newConfig.stripe_product_id = curConfig.stripe_product_id;
+ price.config = newConfig;
+ }
- if (isCustom || newVersion) {
- price = {
- ...price,
- id: generateId("pr"),
- created_at: Date.now(),
- };
- }
+ if (isCustom || newVersion) {
+ price = {
+ ...price,
+ id: generateId("pr"),
+ created_at: Date.now(),
+ };
+ }
- return { price, ent };
+ return { price, ent };
};
export const itemToPriceAndEnt = ({
- item,
- orgId,
- internalProductId,
- feature,
- curPrice,
- curEnt,
- isCustom,
- newVersion,
- features,
+ item,
+ orgId,
+ internalProductId,
+ feature,
+ curPrice,
+ curEnt,
+ isCustom,
+ newVersion,
+ features,
}: {
- item: ProductItem;
- orgId: string;
- internalProductId: string;
- feature?: Feature;
- curPrice?: Price;
- curEnt?: Entitlement;
- isCustom: boolean;
- newVersion?: boolean;
- features: Feature[];
+ item: ProductItem;
+ orgId: string;
+ internalProductId: string;
+ feature?: Feature;
+ curPrice?: Price;
+ curEnt?: Entitlement;
+ isCustom: boolean;
+ newVersion?: boolean;
+ features: Feature[];
}) => {
- let newPrice: Price | null = null;
- let newEnt: Entitlement | null = null;
+ let newPrice: Price | null = null;
+ let newEnt: Entitlement | null = null;
- let updatedPrice: Price | null = null;
- let updatedEnt: Entitlement | null = null;
+ let updatedPrice: Price | null = null;
+ let updatedEnt: Entitlement | null = null;
- let samePrice: Price | null = null;
- let sameEnt: Entitlement | null = null;
+ let samePrice: Price | null = null;
+ let sameEnt: Entitlement | null = null;
- if (isPriceItem(item)) {
- let { price } = toPrice({
- item,
- orgId,
- internalProductId,
- isCustom,
- newVersion,
- });
+ if (isPriceItem(item)) {
+ let { price } = toPrice({
+ item,
+ orgId,
+ internalProductId,
+ isCustom,
+ newVersion,
+ });
- if (!curPrice || newVersion) {
- newPrice = price;
- } else if (!pricesAreSame(curPrice, price, true)) {
- updatedPrice = price;
- } else {
- samePrice = curPrice;
- }
- } else if (isFeatureItem(item)) {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.InvalidRequest,
- });
- }
- let isBoolean = feature?.type == FeatureType.Boolean;
+ if (!curPrice || newVersion) {
+ newPrice = price;
+ } else if (!pricesAreSame(curPrice, price, true)) {
+ updatedPrice = price;
+ } else {
+ samePrice = curPrice;
+ }
+ } else if (isFeatureItem(item)) {
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ let isBoolean = feature?.type == FeatureType.Boolean;
- let { ent } = toFeature({
- item,
- orgId,
- internalFeatureId: feature!.internal_id!,
- internalProductId,
- isCustom,
- newVersion,
- feature,
- });
+ let { ent } = toFeature({
+ item,
+ orgId,
+ internalFeatureId: feature!.internal_id!,
+ internalProductId,
+ isCustom,
+ newVersion,
+ feature,
+ });
- if (!curEnt || newVersion) {
- newEnt = ent;
- }
+ if (!curEnt || newVersion) {
+ newEnt = ent;
+ }
- // Boolean features can't be updated
- else if (!entsAreSame(curEnt, ent)) {
- updatedEnt = ent;
- } else {
- sameEnt = curEnt;
- }
- } else {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.InvalidRequest,
- });
- }
+ // Boolean features can't be updated
+ else if (!entsAreSame(curEnt, ent)) {
+ updatedEnt = ent;
+ } else {
+ sameEnt = curEnt;
+ }
+ } else {
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- let { price, ent } = toFeatureAndPrice({
- item,
- orgId,
- internalFeatureId: feature!.internal_id!,
- internalProductId,
- isCustom,
- curPrice,
- curEnt,
- newVersion,
- features,
- });
+ let { price, ent } = toFeatureAndPrice({
+ item,
+ orgId,
+ internalFeatureId: feature!.internal_id!,
+ internalProductId,
+ isCustom,
+ curPrice,
+ curEnt,
+ newVersion,
+ features,
+ });
- let entSame = curEnt && entsAreSame(curEnt, ent);
+ let entSame = curEnt && entsAreSame(curEnt, ent);
- // 1. If no curPrice, price is new
- if (!curPrice || newVersion) {
- newPrice = price;
- }
+ // 1. If no curPrice, price is new
+ if (!curPrice || newVersion) {
+ newPrice = price;
+ }
- // 2. If ent or price aren't same, price is updated
- else if (!entSame || !pricesAreSame(curPrice, price, false)) {
- updatedPrice = price;
- }
+ // 2. If ent or price aren't same, price is updated
+ else if (!entSame || !pricesAreSame(curPrice, price, false)) {
+ updatedPrice = price;
+ }
- // 3. price is same
- else {
- samePrice = curPrice;
- }
+ // 3. price is same
+ else {
+ samePrice = curPrice;
+ }
- // 1. If no curEnt, ent is new
- if (!curEnt || newVersion) {
- newEnt = ent;
- }
+ // 1. If no curEnt, ent is new
+ if (!curEnt || newVersion) {
+ newEnt = ent;
+ }
- // 2. If ent is different, ent is updated
- else if (!entSame) {
- updatedEnt = ent;
- }
+ // 2. If ent is different, ent is updated
+ else if (!entSame) {
+ updatedEnt = ent;
+ }
- // 3. ent is same
- else {
- sameEnt = curEnt;
- }
- }
+ // 3. ent is same
+ else {
+ sameEnt = curEnt;
+ }
+ }
- return { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt };
+ return { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt };
};
diff --git a/server/src/internal/products/product-items/validateProductItems.ts b/server/src/internal/products/product-items/validateProductItems.ts
index 0c59c990e..e4d32da60 100644
--- a/server/src/internal/products/product-items/validateProductItems.ts
+++ b/server/src/internal/products/product-items/validateProductItems.ts
@@ -1,271 +1,271 @@
import RecaseError from "@/utils/errorUtils.js";
import {
- ProductItem,
- EntInterval,
- ErrCode,
- TierInfinite,
- ProductItemSchema,
- Infinite,
- ProductItemInterval,
- Feature,
- FeatureType,
- AppEnv,
- OnIncrease,
- UsageModel,
- RolloverDuration,
+ ProductItem,
+ EntInterval,
+ ErrCode,
+ TierInfinite,
+ ProductItemSchema,
+ Infinite,
+ ProductItemInterval,
+ Feature,
+ FeatureType,
+ AppEnv,
+ OnIncrease,
+ UsageModel,
+ RolloverDuration,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- isBooleanFeatureItem,
- isFeatureItem,
- isFeaturePriceItem,
- isPriceItem,
+ isBooleanFeatureItem,
+ isFeatureItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "./productItemUtils/getItemType.js";
import { itemToEntInterval } from "./itemIntervalUtils.js";
import { createFeaturesFromItems } from "./createFeaturesFromItems.js";
const validateProductItem = ({
- item,
- features,
+ item,
+ features,
}: {
- item: ProductItem;
- features: Feature[];
+ item: ProductItem;
+ features: Feature[];
}) => {
- item = ProductItemSchema.parse(item);
+ item = ProductItemSchema.parse(item);
- if (nullish(item.feature_id) && nullish(item.price) && nullish(item.tiers)) {
- throw new RecaseError({
- message: `Either 'feature_id', 'price', or both should be set`,
- code: ErrCode.InvalidProductItem,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (nullish(item.feature_id) && nullish(item.price) && nullish(item.tiers)) {
+ throw new RecaseError({
+ message: `Either 'feature_id', 'price', or both should be set`,
+ code: ErrCode.InvalidProductItem,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // 1. Check if amount and tiers are not null
- if (notNullish(item.price) && notNullish(item.tiers)) {
- throw new RecaseError({
- message: `Either 'price' or 'tiers' should be set, not both`,
- code: ErrCode.InvalidProductItem,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // 1. Check if amount and tiers are not null
+ if (notNullish(item.price) && notNullish(item.tiers)) {
+ throw new RecaseError({
+ message: `Either 'price' or 'tiers' should be set, not both`,
+ code: ErrCode.InvalidProductItem,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // 2. If amount is set, it must be greater than 0
- if (notNullish(item.price) && item.price! <= 0) {
- throw new RecaseError({
- message: `Price must be greater than 0`,
- code: ErrCode.InvalidProductItem,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // 2. If amount is set, it must be greater than 0
+ if (notNullish(item.price) && item.price! <= 0) {
+ throw new RecaseError({
+ message: `Price must be greater than 0`,
+ code: ErrCode.InvalidProductItem,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // 4. One off prices / fixed prices can have at most 2 decimal places
- // (isFeaturePriceItem(item) && !item.interval && isOneOff) ||
- if (isPriceItem(item)) {
- // One off price..., can't have more than 2 DP
- if (item.price && item.price.toString().split(".")[1]?.length > 2) {
- throw new RecaseError({
- message: `One off prices can have at most 2 decimal places`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // 4. One off prices / fixed prices can have at most 2 decimal places
+ // (isFeaturePriceItem(item) && !item.interval && isOneOff) ||
+ if (isPriceItem(item)) {
+ // One off price..., can't have more than 2 DP
+ if (item.price && item.price.toString().split(".")[1]?.length > 2) {
+ throw new RecaseError({
+ message: `One off prices can have at most 2 decimal places`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // if (item.tiers) {
- // item.tiers.forEach((tier) => {
- // if (tier.amount.toString().split(".")[1]?.length > 2) {
- // throw new RecaseError({
- // message: `One off prices can have at most 2 decimal places`,
- // code: ErrCode.InvalidInputs,
- // statusCode: StatusCodes.BAD_REQUEST,
- // });
- // }
- // });
- // }
- }
+ // if (item.tiers) {
+ // item.tiers.forEach((tier) => {
+ // if (tier.amount.toString().split(".")[1]?.length > 2) {
+ // throw new RecaseError({
+ // message: `One off prices can have at most 2 decimal places`,
+ // code: ErrCode.InvalidInputs,
+ // statusCode: StatusCodes.BAD_REQUEST,
+ // });
+ // }
+ // });
+ // }
+ }
- // 4. If it's a feature item, it should have included usage as number or inf
- if (isFeaturePriceItem(item) || isFeatureItem(item)) {
- if (
- typeof item.included_usage !== "number" &&
- item.included_usage !== Infinite &&
- notNullish(item.included_usage)
- ) {
- throw new RecaseError({
- message: `Included usage must be a number or '${Infinite}'`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ // 4. If it's a feature item, it should have included usage as number or inf
+ if (isFeaturePriceItem(item) || isFeatureItem(item)) {
+ if (
+ typeof item.included_usage !== "number" &&
+ item.included_usage !== Infinite &&
+ notNullish(item.included_usage)
+ ) {
+ throw new RecaseError({
+ message: `Included usage must be a number or '${Infinite}'`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (nullish(item.included_usage)) {
- item.included_usage = 0;
- }
- }
+ if (nullish(item.included_usage)) {
+ item.included_usage = 0;
+ }
+ }
- // 5. If it's a price, can't have day, minute or hour interval
- if (isFeaturePriceItem(item) || isPriceItem(item)) {
- if (
- item.interval == ProductItemInterval.Day ||
- item.interval == ProductItemInterval.Minute ||
- item.interval == ProductItemInterval.Hour
- ) {
- throw new RecaseError({
- message: `Price can't have day, minute or hour interval`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ // 5. If it's a price, can't have day, minute or hour interval
+ if (isFeaturePriceItem(item) || isPriceItem(item)) {
+ if (
+ item.interval == ProductItemInterval.Day ||
+ item.interval == ProductItemInterval.Minute ||
+ item.interval == ProductItemInterval.Hour
+ ) {
+ throw new RecaseError({
+ message: `Price can't have day, minute or hour interval`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- if (
- item.usage_model == UsageModel.Prepaid &&
- item.config?.on_increase == OnIncrease.BillImmediately
- ) {
- throw new RecaseError({
- message: `Bill immediately is not supported for prepaid just yet, contact us at hey@useautumn.com if you're interested!`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (
+ item.usage_model == UsageModel.Prepaid &&
+ item.config?.on_increase == OnIncrease.BillImmediately
+ ) {
+ throw new RecaseError({
+ message: `Bill immediately is not supported for prepaid just yet, contact us at hey@useautumn.com if you're interested!`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- // Rollover
- // if (item.config?.rollover) {
- // let rollover = item.config.rollover;
+ // Rollover
+ // if (item.config?.rollover) {
+ // let rollover = item.config.rollover;
- // if (rollover.duration == RolloverDuration.Month) {
- // }
- // }
+ // if (rollover.duration == RolloverDuration.Month) {
+ // }
+ // }
};
export const validateProductItems = ({
- newItems,
- features,
- orgId,
- env,
+ newItems,
+ features,
+ orgId,
+ env,
}: {
- newItems: ProductItem[];
- features: Feature[];
- orgId: string;
- env: AppEnv;
+ newItems: ProductItem[];
+ features: Feature[];
+ orgId: string;
+ env: AppEnv;
}) => {
- let { allFeatures, newFeatures } = createFeaturesFromItems({
- items: newItems,
- curFeatures: features,
- orgId,
- env,
- });
+ let { allFeatures, newFeatures } = createFeaturesFromItems({
+ items: newItems,
+ curFeatures: features,
+ orgId,
+ env,
+ });
- features = allFeatures;
+ features = allFeatures;
- // const isOneOff =
- // newItems.every((item) => {
- // if (isFeatureItem(item)) return true;
- // return nullish(item.interval);
- // }) &&
- // newItems.some((item) => isFeaturePriceItem(item) || isPriceItem(item));
+ // const isOneOff =
+ // newItems.every((item) => {
+ // if (isFeatureItem(item)) return true;
+ // return nullish(item.interval);
+ // }) &&
+ // newItems.some((item) => isFeaturePriceItem(item) || isPriceItem(item));
- // 1. Check values
- for (let index = 0; index < newItems.length; index++) {
- validateProductItem({ item: newItems[index], features });
- let feature = features.find((f) => f.id == newItems[index].feature_id);
+ // 1. Check values
+ for (let index = 0; index < newItems.length; index++) {
+ validateProductItem({ item: newItems[index], features });
+ let feature = features.find((f) => f.id == newItems[index].feature_id);
- if (feature && feature.type == FeatureType.Metered) {
- newItems[index].feature_type = feature.config?.usage_type;
- }
- }
+ if (feature && feature.type == FeatureType.Metered) {
+ newItems[index].feature_type = feature.config?.usage_type;
+ }
+ }
- for (let index = 0; index < newItems.length; index++) {
- let item = newItems[index];
- let entInterval = itemToEntInterval(item);
- const intervalCount = item.interval_count || 1;
+ for (let index = 0; index < newItems.length; index++) {
+ let item = newItems[index];
+ let entInterval = itemToEntInterval(item);
+ const intervalCount = item.interval_count || 1;
- if (isFeaturePriceItem(item) && entInterval == EntInterval.Lifetime) {
- let otherItem = newItems.find((i: any, index2: any) => {
- return i.feature_id == item.feature_id && index2 != index;
- });
+ if (isFeaturePriceItem(item) && entInterval == EntInterval.Lifetime) {
+ let otherItem = newItems.find((i: any, index2: any) => {
+ return i.feature_id == item.feature_id && index2 != index;
+ });
- if (otherItem && isFeaturePriceItem(otherItem)) {
- throw new RecaseError({
- message: `If feature is lifetime and paid, can't have any other features`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ if (otherItem && isFeaturePriceItem(otherItem)) {
+ throw new RecaseError({
+ message: `If feature is lifetime and paid, can't have any other features`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- // Boolean duplicate
- if (isBooleanFeatureItem(item)) {
- let otherItem = newItems.find((i: any, index2: any) => {
- return (
- i.feature_id == item.feature_id &&
- index2 != index &&
- item.entity_feature_id == i.entity_feature_id
- );
- });
+ // Boolean duplicate
+ if (isBooleanFeatureItem(item)) {
+ let otherItem = newItems.find((i: any, index2: any) => {
+ return (
+ i.feature_id == item.feature_id &&
+ index2 != index &&
+ item.entity_feature_id == i.entity_feature_id
+ );
+ });
- if (otherItem) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} is duplicated`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
+ if (otherItem) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} is duplicated`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
- let otherItem = newItems.find((i: any, index2: any) => {
- return (
- i.feature_id == item.feature_id &&
- index2 != index &&
- itemToEntInterval(i) == entInterval &&
- (i.interval_count || 1) == intervalCount &&
- i.entity_feature_id == item.entity_feature_id
- );
- });
+ let otherItem = newItems.find((i: any, index2: any) => {
+ return (
+ i.feature_id == item.feature_id &&
+ index2 != index &&
+ itemToEntInterval(i) == entInterval &&
+ (i.interval_count || 1) == intervalCount &&
+ i.entity_feature_id == item.entity_feature_id
+ );
+ });
- if (!otherItem) {
- continue;
- }
+ if (!otherItem) {
+ continue;
+ }
- if (isFeatureItem(otherItem) && isFeatureItem(item)) {
- throw new RecaseError({
- message: `You're trying to create two items for the same feature (${item.feature_id}) with the same interval. Please make them into one item.`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isFeatureItem(otherItem) && isFeatureItem(item)) {
+ throw new RecaseError({
+ message: `You're trying to create two items for the same feature (${item.feature_id}) with the same interval. Please make them into one item.`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (isFeatureItem(otherItem)) {
- throw new RecaseError({
- message: `You have a usage-based price for for this feature (${item.feature_id}). If you're looking to create an overage item (eg. 100 free, then $0.5 thereafter), you should add it to the existing item.`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (isFeatureItem(otherItem)) {
+ throw new RecaseError({
+ message: `You have a usage-based price for for this feature (${item.feature_id}). If you're looking to create an overage item (eg. 100 free, then $0.5 thereafter), you should add it to the existing item.`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (item.usage_model && item.usage_model == otherItem?.usage_model) {
- throw new RecaseError({
- message: `You're trying to add the same feature (${item.feature_id}), with the same reset interval. You should either change the reset interval of one of the items, or make one of them a prepaid quantity`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (item.usage_model && item.usage_model == otherItem?.usage_model) {
+ throw new RecaseError({
+ message: `You're trying to add the same feature (${item.feature_id}), with the same reset interval. You should either change the reset interval of one of the items, or make one of them a prepaid quantity`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- if (isPriceItem(item)) {
- let otherItem = newItems.find((i: any, index2: any) => {
- return i.interval === item.interval && index2 != index;
- });
+ if (isPriceItem(item)) {
+ let otherItem = newItems.find((i: any, index2: any) => {
+ return i.interval === item.interval && index2 != index;
+ });
- if (otherItem) {
- throw new RecaseError({
- message: `Can't have two fixed prices with the same interval`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
- }
- }
+ if (otherItem) {
+ throw new RecaseError({
+ message: `Can't have two fixed prices with the same interval`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
+ }
+ }
- return { allFeatures, newFeatures };
+ return { allFeatures, newFeatures };
};
diff --git a/server/src/internal/products/productRouter.ts b/server/src/internal/products/productRouter.ts
index 22c305a2b..4f71b5183 100644
--- a/server/src/internal/products/productRouter.ts
+++ b/server/src/internal/products/productRouter.ts
@@ -36,120 +36,120 @@ productRouter.delete("/:productId", handleDeleteProduct);
productRouter.post("/:productId/copy", handleCopyProduct);
productRouter.post("/all/init_stripe", async (req: any, res) => {
- try {
- const { orgId, env, logtail: logger, db } = req;
+ try {
+ const { orgId, env, logtail: logger, db } = req;
- const [fullProducts, org] = await Promise.all([
- ProductService.listFull({
- db,
- orgId,
- env,
- }),
- OrgService.getFromReq(req),
- ]);
+ const [fullProducts, org] = await Promise.all([
+ ProductService.listFull({
+ db,
+ orgId,
+ env,
+ }),
+ OrgService.getFromReq(req),
+ ]);
- console.log(
- "fullProducts",
- fullProducts.map((p) => p.id)
- );
+ console.log(
+ "fullProducts",
+ fullProducts.map((p) => p.id),
+ );
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- const productBatchSize = 5;
- for (let i = 0; i < fullProducts.length; i += productBatchSize) {
- const batch = fullProducts.slice(i, i + productBatchSize);
- const batchPromises = batch.map((product) =>
- checkStripeProductExists({
- db,
- org,
- env,
- product,
- logger,
- })
- );
- await Promise.all(batchPromises);
- }
+ const productBatchSize = 5;
+ for (let i = 0; i < fullProducts.length; i += productBatchSize) {
+ const batch = fullProducts.slice(i, i + productBatchSize);
+ const batchPromises = batch.map((product) =>
+ checkStripeProductExists({
+ db,
+ org,
+ env,
+ product,
+ logger,
+ }),
+ );
+ await Promise.all(batchPromises);
+ }
- const entitlements = fullProducts.flatMap((p) => p.entitlements);
- const prices = fullProducts.flatMap((p) => p.prices);
+ const entitlements = fullProducts.flatMap((p) => p.entitlements);
+ const prices = fullProducts.flatMap((p) => p.prices);
- const batchSize = 3;
- for (let i = 0; i < prices.length; i += batchSize) {
- const batch = prices.slice(i, i + batchSize);
- const batchPriceUpdate = [];
- for (const price of batch) {
- batchPriceUpdate.push(
- createStripePriceIFNotExist({
- db,
- org,
- stripeCli: stripeCli,
- price,
- entitlements,
- product: fullProducts.find(
- (p) => p.internal_id === price.internal_product_id
- )!,
- logger,
- })
- );
- }
+ const batchSize = 3;
+ for (let i = 0; i < prices.length; i += batchSize) {
+ const batch = prices.slice(i, i + batchSize);
+ const batchPriceUpdate = [];
+ for (const price of batch) {
+ batchPriceUpdate.push(
+ createStripePriceIFNotExist({
+ db,
+ org,
+ stripeCli: stripeCli,
+ price,
+ entitlements,
+ product: fullProducts.find(
+ (p) => p.internal_id === price.internal_product_id,
+ )!,
+ logger,
+ }),
+ );
+ }
- await Promise.all(batchPriceUpdate);
- }
- res.status(200).json({ message: "Stripe products initialized" });
- } catch (error) {
- handleRequestError({ req, error, res, action: "Init stripe products" });
- }
+ await Promise.all(batchPriceUpdate);
+ }
+ res.status(200).json({ message: "Stripe products initialized" });
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "Init stripe products" });
+ }
});
productRouter.get("/:productId/has_customers", async (req: any, res: any) =>
- routeHandler({
- req,
- res,
- action: "Get product has customers",
- handler: async () => {
- const { productId } = req.params;
- const { db, features } = req;
- const { id, items, free_trial } = req.body;
+ routeHandler({
+ req,
+ res,
+ action: "Get product has customers",
+ handler: async () => {
+ const { productId } = req.params;
+ const { db, features } = req;
+ const { id, items, free_trial } = req.body;
- const product = await ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId: req.orgId,
- env: req.env,
- });
+ const product = await ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId: req.orgId,
+ env: req.env,
+ });
- if (!product) {
- throw new RecaseError({
- message: `Product with id ${productId} not found`,
- code: ErrCode.ProductNotFound,
- statusCode: 404,
- });
- }
+ if (!product) {
+ throw new RecaseError({
+ message: `Product with id ${productId} not found`,
+ code: ErrCode.ProductNotFound,
+ statusCode: 404,
+ });
+ }
- const cusProductsCurVersion =
- await CusProductService.getByInternalProductId({
- db,
- internalProductId: product.internal_id,
- });
+ const cusProductsCurVersion =
+ await CusProductService.getByInternalProductId({
+ db,
+ internalProductId: product.internal_id,
+ });
- const { itemsSame, freeTrialsSame } = productsAreSame({
- newProductV2: req.body,
- curProductV1: product,
- features,
- });
+ const { itemsSame, freeTrialsSame } = productsAreSame({
+ newProductV2: req.body,
+ curProductV1: product,
+ features,
+ });
- const productSame = itemsSame && freeTrialsSame;
+ const productSame = itemsSame && freeTrialsSame;
- res.status(200).json({
- current_version: product.version,
- will_version: !productSame && cusProductsCurVersion.length > 0,
- archived: product.archived,
- });
- },
- })
+ res.status(200).json({
+ current_version: product.version,
+ will_version: !productSame && cusProductsCurVersion.length > 0,
+ archived: product.archived,
+ });
+ },
+ }),
);
productRouter.get("/:productId/deletion_info", handleGetProductDeleteInfo);
diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts
index a87f6ea59..3490c9706 100644
--- a/server/src/internal/products/productUtils.ts
+++ b/server/src/internal/products/productUtils.ts
@@ -1,38 +1,38 @@
import {
- type AppEnv,
- BillingInterval,
- BillingType,
- type CreateProduct,
- EntInterval,
- type Entitlement,
- EntitlementSchema,
- ErrCode,
- type Feature,
- type FixedPriceConfig,
- type FullProduct,
- intervalsSame,
- type Organization,
- type Price,
- PriceSchema,
- PriceType,
- ProcessorType,
- type Product,
- ProductSchema,
- type UsagePriceConfig,
+ type AppEnv,
+ BillingInterval,
+ BillingType,
+ type CreateProduct,
+ EntInterval,
+ type Entitlement,
+ EntitlementSchema,
+ ErrCode,
+ type Feature,
+ type FixedPriceConfig,
+ type FullProduct,
+ intervalsSame,
+ type Organization,
+ type Price,
+ PriceSchema,
+ PriceType,
+ ProcessorType,
+ type Product,
+ ProductSchema,
+ type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import {
- getBillingInterval,
- getBillingType,
+ getBillingInterval,
+ getBillingType,
} from "@/internal/products/prices/priceUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { generateId, notNullish } from "@/utils/genUtils.js";
import type {
- AttachParams,
- InsertCusProductParams,
+ AttachParams,
+ InsertCusProductParams,
} from "../customers/cusProducts/AttachParams.js";
import { isStripeConnected } from "../orgs/orgUtils.js";
import { EntitlementService } from "./entitlements/EntitlementService.js";
@@ -44,544 +44,544 @@ import { compareBillingIntervals } from "./prices/priceUtils/priceIntervalUtils.
import { isDefaultTrialFullProduct } from "./productUtils/classifyProduct.js";
export const getLatestProducts = (products: FullProduct[]) => {
- const latestProducts = products.reduce((acc: any, product: any) => {
- if (!acc[product.id]) {
- acc[product.id] = product;
- } else if (product.version > acc[product.id].version) {
- acc[product.id] = product;
- }
- return acc;
- }, {});
+ const latestProducts = products.reduce((acc: any, product: any) => {
+ if (!acc[product.id]) {
+ acc[product.id] = product;
+ } else if (product.version > acc[product.id].version) {
+ acc[product.id] = product;
+ }
+ return acc;
+ }, {});
- return Object.values(latestProducts) as FullProduct[];
+ return Object.values(latestProducts) as FullProduct[];
};
export const getProductVersionCounts = (products: FullProduct[]) => {
- const versionCounts = products.reduce((acc: any, product: any) => {
- if (!acc[product.id]) {
- acc[product.id] = 1;
- } else {
- acc[product.id]++;
- }
- return acc;
- }, {});
+ const versionCounts = products.reduce((acc: any, product: any) => {
+ if (!acc[product.id]) {
+ acc[product.id] = 1;
+ } else {
+ acc[product.id]++;
+ }
+ return acc;
+ }, {});
- return versionCounts;
+ return versionCounts;
};
// Construct product
export const constructProduct = ({
- productData,
- orgId,
- env,
- processor,
- baseVariantId,
+ productData,
+ orgId,
+ env,
+ processor,
+ baseVariantId,
}: {
- productData: CreateProduct;
- orgId: string;
- env: AppEnv;
- processor?: any;
- baseVariantId?: string | null;
+ productData: CreateProduct;
+ orgId: string;
+ env: AppEnv;
+ processor?: any;
+ baseVariantId?: string | null;
}) => {
- const newProduct: Product = {
- ...productData,
- org_id: orgId,
- env,
- processor,
- internal_id: generateId("prod"),
- created_at: Date.now(),
- base_variant_id: null,
- archived: false,
- };
+ const newProduct: Product = {
+ ...productData,
+ org_id: orgId,
+ env,
+ processor,
+ internal_id: generateId("prod"),
+ created_at: Date.now(),
+ base_variant_id: null,
+ archived: false,
+ };
- return newProduct;
+ return newProduct;
};
export const isProductUpgrade = ({
- prices1,
- prices2,
- usageAlwaysUpgrade = true,
+ prices1,
+ prices2,
+ usageAlwaysUpgrade = true,
}: {
- prices1: Price[];
- prices2: Price[];
- usageAlwaysUpgrade?: boolean;
+ prices1: Price[];
+ prices2: Price[];
+ usageAlwaysUpgrade?: boolean;
}) => {
- if (isFreeProduct(prices1) && !isFreeProduct(prices2)) {
- return true;
- }
+ if (isFreeProduct(prices1) && !isFreeProduct(prices2)) {
+ return true;
+ }
- if (!isFreeProduct(prices1) && isFreeProduct(prices2)) {
- return false;
- }
+ if (!isFreeProduct(prices1) && isFreeProduct(prices2)) {
+ return false;
+ }
- if (
- prices1.every(
- (p) => getBillingType(p.config!) === BillingType.UsageInArrear
- ) &&
- prices2.every(
- (p) => getBillingType(p.config!) === BillingType.UsageInArrear
- ) &&
- usageAlwaysUpgrade
- ) {
- return true;
- }
+ if (
+ prices1.every(
+ (p) => getBillingType(p.config!) === BillingType.UsageInArrear,
+ ) &&
+ prices2.every(
+ (p) => getBillingType(p.config!) === BillingType.UsageInArrear,
+ ) &&
+ usageAlwaysUpgrade
+ ) {
+ return true;
+ }
- const billingInterval1 = getBillingInterval(prices1); // pro quarter
- const billingInterval2 = getBillingInterval(prices2); // premium
+ const billingInterval1 = getBillingInterval(prices1); // pro quarter
+ const billingInterval2 = getBillingInterval(prices2); // premium
- // 2. Get total price for each product
- const getTotalPrice = (prices: Price[]) => {
- let totalPrice = new Decimal(0);
- for (const price of prices) {
- if ("usage_tiers" in price.config!) {
- totalPrice = totalPrice.plus(price.config!.usage_tiers[0].amount);
- } else {
- totalPrice = totalPrice.plus(price.config!.amount);
- }
- }
- return totalPrice.toNumber();
- };
+ // 2. Get total price for each product
+ const getTotalPrice = (prices: Price[]) => {
+ let totalPrice = new Decimal(0);
+ for (const price of prices) {
+ if ("usage_tiers" in price.config!) {
+ totalPrice = totalPrice.plus(price.config!.usage_tiers[0].amount);
+ } else {
+ totalPrice = totalPrice.plus(price.config!.amount);
+ }
+ }
+ return totalPrice.toNumber();
+ };
- // 3. Compare prices
+ // 3. Compare prices
- if (
- intervalsSame({
- intervalA: billingInterval1,
- intervalB: billingInterval2,
- })
- ) {
- return getTotalPrice(prices1) < getTotalPrice(prices2);
- } else {
- return (
- compareBillingIntervals({
- configA: billingInterval1,
- configB: billingInterval2,
- }) > 0
- );
- }
+ if (
+ intervalsSame({
+ intervalA: billingInterval1,
+ intervalB: billingInterval2,
+ })
+ ) {
+ return getTotalPrice(prices1) < getTotalPrice(prices2);
+ } else {
+ return (
+ compareBillingIntervals({
+ configA: billingInterval1,
+ configB: billingInterval2,
+ }) > 0
+ );
+ }
};
export const isFreeProduct = (prices: Price[]) => {
- if (prices.length === 0) {
- return true;
- }
+ if (prices.length === 0) {
+ return true;
+ }
- let totalPrice = 0;
- for (const price of prices) {
- if ("usage_tiers" in price.config!) {
- totalPrice += price.config!.usage_tiers.reduce(
- (acc, tier) => acc + tier.amount,
- 0
- );
- } else {
- totalPrice += price.config!.amount;
- }
- }
- return totalPrice === 0;
+ let totalPrice = 0;
+ for (const price of prices) {
+ if ("usage_tiers" in price.config!) {
+ totalPrice += price.config!.usage_tiers.reduce(
+ (acc, tier) => acc + tier.amount,
+ 0,
+ );
+ } else {
+ totalPrice += price.config!.amount;
+ }
+ }
+ return totalPrice === 0;
};
export const getOptionsFromPrices = (prices: Price[], features: Feature[]) => {
- const featureToOptions: { [key: string]: any } = {};
- for (const price of prices) {
- if (price.config!.type === PriceType.Fixed) {
- continue;
- }
+ const featureToOptions: { [key: string]: any } = {};
+ for (const price of prices) {
+ if (price.config!.type === PriceType.Fixed) {
+ continue;
+ }
- const config = price.config! as UsagePriceConfig;
- // get billing tyoe
- const billingType = getBillingType(price.config!);
- const feature = features.find(
- (f) => f.internal_id === config.internal_feature_id
- );
+ const config = price.config! as UsagePriceConfig;
+ // get billing tyoe
+ const billingType = getBillingType(price.config!);
+ const feature = features.find(
+ (f) => f.internal_id === config.internal_feature_id,
+ );
- if (!feature) {
- continue;
- }
+ if (!feature) {
+ continue;
+ }
- if (billingType === BillingType.UsageInAdvance) {
- if (!featureToOptions[feature.id]) {
- featureToOptions[feature.id] = {
- feature_id: feature.id,
- feature_name: feature.name,
- quantity: 0,
- };
- }
+ if (billingType === BillingType.UsageInAdvance) {
+ if (!featureToOptions[feature.id]) {
+ featureToOptions[feature.id] = {
+ feature_id: feature.id,
+ feature_name: feature.name,
+ quantity: 0,
+ };
+ }
- featureToOptions[feature.id].quantity = 0;
- }
- }
+ featureToOptions[feature.id].quantity = 0;
+ }
+ }
- return Object.values(featureToOptions);
+ return Object.values(featureToOptions);
};
export const checkStripeProductExists = async ({
- db,
- org,
- env,
- product,
- logger,
+ db,
+ org,
+ env,
+ product,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- product: FullProduct;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ product: FullProduct;
+ logger: any;
}) => {
- let createNew = false;
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ let createNew = false;
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- if (!product.processor || !product.processor.id) {
- createNew = true;
- } else {
- try {
- const stripeProduct = await stripeCli.products.retrieve(
- product.processor!.id
- );
+ if (!product.processor || !product.processor.id) {
+ createNew = true;
+ } else {
+ try {
+ const stripeProduct = await stripeCli.products.retrieve(
+ product.processor!.id,
+ );
- if (!stripeProduct.active) {
- await stripeCli.products.update(product.processor!.id, {
- active: true,
- });
- }
- } catch (error) {
- createNew = true;
- }
- }
+ if (!stripeProduct.active) {
+ await stripeCli.products.update(product.processor!.id, {
+ active: true,
+ });
+ }
+ } catch (error) {
+ createNew = true;
+ }
+ }
- if (createNew) {
- logger.info(`Creating new product in Stripe for ${product.name}`);
- const stripeProduct = await stripeCli.products.create({
- name: product.name,
- });
+ if (createNew) {
+ logger.info(`Creating new product in Stripe for ${product.name}`);
+ const stripeProduct = await stripeCli.products.create({
+ name: product.name,
+ });
- await ProductService.updateByInternalId({
- db,
- internalId: product.internal_id,
- update: {
- processor: { id: stripeProduct.id, type: ProcessorType.Stripe },
- },
- });
+ await ProductService.updateByInternalId({
+ db,
+ internalId: product.internal_id,
+ update: {
+ processor: { id: stripeProduct.id, type: ProcessorType.Stripe },
+ },
+ });
- console.log(
- `Updated product ${product.name} with stripe product ${stripeProduct.id}`
- );
+ console.log(
+ `Updated product ${product.name} with stripe product ${stripeProduct.id}`,
+ );
- product.processor = {
- id: stripeProduct.id,
- type: ProcessorType.Stripe,
- };
- }
+ product.processor = {
+ id: stripeProduct.id,
+ type: ProcessorType.Stripe,
+ };
+ }
};
export const getPricesForProduct = (product: FullProduct, prices: Price[]) => {
- return prices.filter((p) => p.internal_product_id === product.internal_id);
+ return prices.filter((p) => p.internal_product_id === product.internal_id);
};
export const attachToInsertParams = (
- attachParams: AttachParams,
- product: FullProduct,
- entityId?: string
+ attachParams: AttachParams,
+ product: FullProduct,
+ entityId?: string,
) => {
- // Get entity
- let { internalEntityId, entityId: attachEntityId } = attachParams;
- if (notNullish(entityId)) {
- const entity = attachParams.customer.entities.find(
- (e) => e.id === entityId
- );
+ // Get entity
+ let { internalEntityId, entityId: attachEntityId } = attachParams;
+ if (notNullish(entityId)) {
+ const entity = attachParams.customer.entities.find(
+ (e) => e.id === entityId,
+ );
- if (entity) {
- internalEntityId = entity.internal_id;
- attachEntityId = entity.id;
- }
- }
+ if (entity) {
+ internalEntityId = entity.internal_id;
+ attachEntityId = entity.id;
+ }
+ }
- return {
- ...attachParams,
- product,
- prices: getPricesForProduct(product, attachParams.prices),
- entitlements: getEntitlementsForProduct(product, attachParams.entitlements),
- entityId: attachEntityId,
- internalEntityId: internalEntityId,
- } as InsertCusProductParams;
+ return {
+ ...attachParams,
+ product,
+ prices: getPricesForProduct(product, attachParams.prices),
+ entitlements: getEntitlementsForProduct(product, attachParams.entitlements),
+ entityId: attachEntityId,
+ internalEntityId: internalEntityId,
+ } as InsertCusProductParams;
};
// COPY PRODUCT
export const copyProduct = async ({
- db,
- product,
- toOrgId,
- toId,
- toName,
- fromEnv,
- toEnv,
- toFeatures,
- fromFeatures,
- org,
- logger,
+ db,
+ product,
+ toOrgId,
+ toId,
+ toName,
+ fromEnv,
+ toEnv,
+ toFeatures,
+ fromFeatures,
+ org,
+ logger,
}: {
- db: DrizzleCli;
- product: FullProduct;
- toOrgId: string;
- fromEnv: AppEnv;
- toEnv: AppEnv;
- toId: string;
- toName: string;
- toFeatures: Feature[];
- fromFeatures: Feature[];
- org: Organization;
- logger: any;
+ db: DrizzleCli;
+ product: FullProduct;
+ toOrgId: string;
+ fromEnv: AppEnv;
+ toEnv: AppEnv;
+ toId: string;
+ toName: string;
+ toFeatures: Feature[];
+ fromFeatures: Feature[];
+ org: Organization;
+ logger: any;
}) => {
- const newProduct = {
- ...product,
- name: toName,
- id: toId,
- internal_id: generateId("prod"),
- org_id: toOrgId,
- env: toEnv,
- processor: null,
- base_variant_id: fromEnv === toEnv ? null : product.base_variant_id,
- };
+ const newProduct = {
+ ...product,
+ name: toName,
+ id: toId,
+ internal_id: generateId("prod"),
+ org_id: toOrgId,
+ env: toEnv,
+ processor: null,
+ base_variant_id: fromEnv === toEnv ? null : product.base_variant_id,
+ };
- const newEntitlements: Entitlement[] = [];
- const newEntIds: Record = {};
+ const newEntitlements: Entitlement[] = [];
+ const newEntIds: Record = {};
- for (const entitlement of product.entitlements) {
- // 1. Get from feature
- const fromFeature = fromFeatures.find(
- (f) => f.internal_id === entitlement.internal_feature_id
- );
+ for (const entitlement of product.entitlements) {
+ // 1. Get from feature
+ const fromFeature = fromFeatures.find(
+ (f) => f.internal_id === entitlement.internal_feature_id,
+ );
- // 2. Get to feature
- const toFeature = toFeatures.find((f) => f.id === fromFeature?.id);
+ // 2. Get to feature
+ const toFeature = toFeatures.find((f) => f.id === fromFeature?.id);
- if (!toFeature) {
- throw new RecaseError({
- message: `Feature ${entitlement.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!toFeature) {
+ throw new RecaseError({
+ message: `Feature ${entitlement.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- const newId = generateId("ent");
- newEntitlements.push(
- EntitlementSchema.parse({
- ...entitlement,
- interval_count: entitlement.interval_count ?? 1,
- id: newId,
- org_id: toOrgId,
- created_at: Date.now(),
- internal_product_id: newProduct.internal_id,
- internal_feature_id: toFeature.internal_id,
- })
- );
+ const newId = generateId("ent");
+ newEntitlements.push(
+ EntitlementSchema.parse({
+ ...entitlement,
+ interval_count: entitlement.interval_count ?? 1,
+ id: newId,
+ org_id: toOrgId,
+ created_at: Date.now(),
+ internal_product_id: newProduct.internal_id,
+ internal_feature_id: toFeature.internal_id,
+ }),
+ );
- newEntIds[entitlement.id!] = newId;
- }
+ newEntIds[entitlement.id!] = newId;
+ }
- const newPrices: Price[] = [];
- for (const price of product.prices) {
- // 1. Copy price
- const newPrice = structuredClone(price);
+ const newPrices: Price[] = [];
+ for (const price of product.prices) {
+ // 1. Copy price
+ const newPrice = structuredClone(price);
- const config = newPrice.config as UsagePriceConfig;
+ const config = newPrice.config as UsagePriceConfig;
- // Clear Stripe IDs
- config.stripe_meter_id = undefined;
- config.stripe_product_id = undefined;
- config.stripe_placeholder_price_id = undefined;
- config.stripe_price_id = undefined;
+ // Clear Stripe IDs
+ config.stripe_meter_id = undefined;
+ config.stripe_product_id = undefined;
+ config.stripe_placeholder_price_id = undefined;
+ config.stripe_price_id = undefined;
- if (config.type === PriceType.Usage) {
- const fromFeature = fromFeatures.find(
- (f) => f.internal_id === config.internal_feature_id
- );
+ if (config.type === PriceType.Usage) {
+ const fromFeature = fromFeatures.find(
+ (f) => f.internal_id === config.internal_feature_id,
+ );
- const toFeature = toFeatures.find((f) => f.id === fromFeature?.id);
+ const toFeature = toFeatures.find((f) => f.id === fromFeature?.id);
- if (!toFeature) {
- throw new RecaseError({
- message: `Feature ${config.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!toFeature) {
+ throw new RecaseError({
+ message: `Feature ${config.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- config.internal_feature_id = toFeature.internal_id!;
- config.feature_id = toFeature.id;
+ config.internal_feature_id = toFeature.internal_id!;
+ config.feature_id = toFeature.id;
- // Update entitlement id
- const entitlementId = newEntIds[price.entitlement_id!];
- if (!entitlementId) {
- throw new RecaseError({
- message: `Failed to swap entitlement id for price ${price.id}`,
- code: ErrCode.InternalError,
- statusCode: 500,
- });
- }
- newPrice.entitlement_id = entitlementId;
- }
+ // Update entitlement id
+ const entitlementId = newEntIds[price.entitlement_id!];
+ if (!entitlementId) {
+ throw new RecaseError({
+ message: `Failed to swap entitlement id for price ${price.id}`,
+ code: ErrCode.InternalError,
+ statusCode: 500,
+ });
+ }
+ newPrice.entitlement_id = entitlementId;
+ }
- newPrices.push(
- PriceSchema.parse({
- ...newPrice,
- id: generateId("pr"),
- created_at: Date.now(),
- org_id: toOrgId,
- internal_product_id: newProduct.internal_id,
- config: config,
- })
- );
- }
+ newPrices.push(
+ PriceSchema.parse({
+ ...newPrice,
+ id: generateId("pr"),
+ created_at: Date.now(),
+ org_id: toOrgId,
+ internal_product_id: newProduct.internal_id,
+ config: config,
+ }),
+ );
+ }
- await ProductService.insert({
- db,
- product: {
- ...ProductSchema.parse(newProduct),
- version: 1,
- },
- });
+ await ProductService.insert({
+ db,
+ product: {
+ ...ProductSchema.parse(newProduct),
+ version: 1,
+ },
+ });
- await EntitlementService.insert({
- db,
- data: newEntitlements,
- });
+ await EntitlementService.insert({
+ db,
+ data: newEntitlements,
+ });
- await PriceService.insert({
- db,
- data: newPrices,
- });
+ await PriceService.insert({
+ db,
+ data: newPrices,
+ });
- if (product.free_trial) {
- await FreeTrialService.insert({
- db,
- data: {
- ...product.free_trial,
- id: generateId("ft"),
- created_at: Date.now(),
- internal_product_id: newProduct.internal_id,
- },
- });
- }
+ if (product.free_trial) {
+ await FreeTrialService.insert({
+ db,
+ data: {
+ ...product.free_trial,
+ id: generateId("ft"),
+ created_at: Date.now(),
+ internal_product_id: newProduct.internal_id,
+ },
+ });
+ }
- // await initProductInStripe({
- // db,
- // org,
- // env: toEnv,
- // logger,
- // product: {
- // ...newProduct,
- // prices: newPrices,
- // entitlements: getEntsWithFeature({
- // ents: newEntitlements,
- // features: toFeatures,
- // }),
- // },
- // });
+ // await initProductInStripe({
+ // db,
+ // org,
+ // env: toEnv,
+ // logger,
+ // product: {
+ // ...newProduct,
+ // prices: newPrices,
+ // entitlements: getEntsWithFeature({
+ // ents: newEntitlements,
+ // features: toFeatures,
+ // }),
+ // },
+ // });
};
export const isOneOff = (prices: Price[]) => {
- return (
- prices.every((p) => p.config?.interval === BillingInterval.OneOff) &&
- prices.some((p) => {
- if (p.config?.type === PriceType.Usage) {
- const config = p.config as UsagePriceConfig;
- return config.usage_tiers.some((t) => t.amount > 0);
- } else {
- const config = p.config as FixedPriceConfig;
- return config.amount > 0;
- }
- })
- );
+ return (
+ prices.every((p) => p.config?.interval === BillingInterval.OneOff) &&
+ prices.some((p) => {
+ if (p.config?.type === PriceType.Usage) {
+ const config = p.config as UsagePriceConfig;
+ return config.usage_tiers.some((t) => t.amount > 0);
+ } else {
+ const config = p.config as FixedPriceConfig;
+ return config.amount > 0;
+ }
+ })
+ );
};
export const itemsAreOneOff = (items: Entitlement[]) => {
- return items.every(
- (item) =>
- item.interval === null ||
- item.interval === undefined ||
- item.interval === EntInterval.Lifetime
- );
+ return items.every(
+ (item) =>
+ item.interval === null ||
+ item.interval === undefined ||
+ item.interval === EntInterval.Lifetime,
+ );
};
export const initProductInStripe = async ({
- db,
- org,
- env,
- logger,
- product,
+ db,
+ org,
+ env,
+ logger,
+ product,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- logger: any;
- product: FullProduct;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ logger: any;
+ product: FullProduct;
}) => {
- if (!isStripeConnected({ org, env })) return;
+ if (!isStripeConnected({ org, env })) return;
- await checkStripeProductExists({
- db,
- org,
- env,
- product,
- logger,
- });
+ await checkStripeProductExists({
+ db,
+ org,
+ env,
+ product,
+ logger,
+ });
- const batchPriceUpdate = [];
- const stripeCli = createStripeCli({
- org,
- env,
- });
- for (const price of product.prices) {
- batchPriceUpdate.push(
- createStripePriceIFNotExist({
- db,
- org,
- stripeCli,
- price,
- entitlements: product.entitlements,
- product: product,
- logger,
- })
- );
- }
+ const batchPriceUpdate = [];
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
+ for (const price of product.prices) {
+ batchPriceUpdate.push(
+ createStripePriceIFNotExist({
+ db,
+ org,
+ stripeCli,
+ price,
+ entitlements: product.entitlements,
+ product: product,
+ logger,
+ }),
+ );
+ }
- await Promise.all(batchPriceUpdate);
+ await Promise.all(batchPriceUpdate);
};
export const searchProductsByStripeId = async ({
- products,
- stripeId,
+ products,
+ stripeId,
}: {
- products: FullProduct[];
- stripeId: string;
+ products: FullProduct[];
+ stripeId: string;
}) => {
- return products.find((p) => p.processor?.id === stripeId);
+ return products.find((p) => p.processor?.id === stripeId);
};
export const getGroupToDefaults = ({
- defaultProds,
+ defaultProds,
}: {
- defaultProds: FullProduct[];
+ defaultProds: FullProduct[];
}) => {
- const groupToDefaults: Record> = {};
+ const groupToDefaults: Record> = {};
- for (const product of defaultProds) {
- if (product.archived || !product.is_default) continue;
- if (!groupToDefaults[product.group]) {
- groupToDefaults[product.group] = {};
- }
+ for (const product of defaultProds) {
+ if (product.archived || !product.is_default) continue;
+ if (!groupToDefaults[product.group]) {
+ groupToDefaults[product.group] = {};
+ }
- if (isDefaultTrialFullProduct({ product })) {
- groupToDefaults[product.group].defaultTrial = product;
- }
+ if (isDefaultTrialFullProduct({ product })) {
+ groupToDefaults[product.group].defaultTrial = product;
+ }
- if (isFreeProduct(product.prices)) {
- groupToDefaults[product.group].free = product;
- }
- }
+ if (isFreeProduct(product.prices)) {
+ groupToDefaults[product.group].free = product;
+ }
+ }
- return groupToDefaults;
+ return groupToDefaults;
};
diff --git a/server/src/internal/products/productUtils/classifyProduct.ts b/server/src/internal/products/productUtils/classifyProduct.ts
index 68ae2479b..863601e0a 100644
--- a/server/src/internal/products/productUtils/classifyProduct.ts
+++ b/server/src/internal/products/productUtils/classifyProduct.ts
@@ -4,63 +4,63 @@ import { isFeatureItem } from "../product-items/productItemUtils/getItemType.js"
import { isFreeProduct } from "../productUtils.js";
export const prodIsAddOn = ({ product }: { product: FullProduct }) => {
- return product.is_add_on;
+ return product.is_add_on;
};
export const oneOffOrAddOn = ({
- product,
- prices,
+ product,
+ prices,
}: {
- product: FullProduct;
- prices?: Price[];
+ product: FullProduct;
+ prices?: Price[];
}) => {
- const isOneOff = prices
- ? pricesOnlyOneOff(prices)
- : pricesOnlyOneOff(product.prices);
+ const isOneOff = prices
+ ? pricesOnlyOneOff(prices)
+ : pricesOnlyOneOff(product.prices);
- return prodIsAddOn({ product }) || isOneOff;
+ return prodIsAddOn({ product }) || isOneOff;
};
export const isMainProduct = ({
- product,
- prices,
+ product,
+ prices,
}: {
- product: FullProduct;
- prices?: Price[];
+ product: FullProduct;
+ prices?: Price[];
}) => {
- return !prodIsAddOn({ product }) && !oneOffOrAddOn({ product, prices });
+ return !prodIsAddOn({ product }) && !oneOffOrAddOn({ product, prices });
};
export const isFreeProductV2 = ({ product }: { product: ProductV2 }) => {
- return product.items.every((item) => isFeatureItem(item));
+ return product.items.every((item) => isFeatureItem(item));
};
export const isDefaultTrial = ({
- product,
- skipDefault = false,
+ product,
+ skipDefault = false,
}: {
- product: ProductV2;
- skipDefault?: boolean;
+ product: ProductV2;
+ skipDefault?: boolean;
}) => {
- return (
- product.free_trial &&
- !product.free_trial?.card_required &&
- (product.is_default || skipDefault) &&
- !isFreeProductV2({ product })
- );
+ return (
+ product.free_trial &&
+ !product.free_trial?.card_required &&
+ (product.is_default || skipDefault) &&
+ !isFreeProductV2({ product })
+ );
};
export const isDefaultTrialFullProduct = ({
- product,
- skipDefault = false,
+ product,
+ skipDefault = false,
}: {
- product: FullProduct;
- skipDefault?: boolean;
+ product: FullProduct;
+ skipDefault?: boolean;
}) => {
- return (
- product.free_trial &&
- !product.free_trial?.card_required &&
- (product.is_default || skipDefault) &&
- !isFreeProduct(product.prices)
- );
+ return (
+ product.free_trial &&
+ !product.free_trial?.card_required &&
+ (product.is_default || skipDefault) &&
+ !isFreeProduct(product.prices)
+ );
};
diff --git a/server/src/internal/products/productUtils/compareProductUtils.ts b/server/src/internal/products/productUtils/compareProductUtils.ts
index be62f8603..773e9e9c4 100644
--- a/server/src/internal/products/productUtils/compareProductUtils.ts
+++ b/server/src/internal/products/productUtils/compareProductUtils.ts
@@ -1,22 +1,22 @@
import {
- ErrCode,
- Feature,
- FullProduct,
- OnDecrease,
- OnIncrease,
- ProductItem,
- ProductV2,
+ ErrCode,
+ Feature,
+ FullProduct,
+ OnDecrease,
+ OnIncrease,
+ ProductItem,
+ ProductV2,
} from "@autumn/shared";
import { mapToProductItems } from "../productV2Utils.js";
import {
- findSimilarItem,
- itemsAreSame,
+ findSimilarItem,
+ itemsAreSame,
} from "../product-items/compareItemUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { freeTrialsAreSame } from "../free-trials/freeTrialUtils.js";
import {
- isFeaturePriceItem,
- isPriceItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "../product-items/productItemUtils/getItemType.js";
import { StatusCodes } from "http-status-codes";
import { itemToPriceOrTiers } from "../product-items/productItemUtils.js";
@@ -24,176 +24,176 @@ import { nullish } from "@/utils/genUtils.js";
import { getResetUsage } from "../product-items/productItemUtils/itemToPriceAndEnt.js";
const sanitizeItems = ({
- items,
- features,
+ items,
+ features,
}: {
- items: ProductItem[];
- features: Feature[];
+ items: ProductItem[];
+ features: Feature[];
}) => {
- return items.map((item) => {
- let priceData = itemToPriceOrTiers({ item });
- const newItem = {
- ...item,
- reset_usage_when_enabled: getResetUsage({
- item,
- feature: features.find((f) => f.id === item.feature_id),
- }),
- ...priceData,
- };
+ return items.map((item) => {
+ let priceData = itemToPriceOrTiers({ item });
+ const newItem = {
+ ...item,
+ reset_usage_when_enabled: getResetUsage({
+ item,
+ feature: features.find((f) => f.id === item.feature_id),
+ }),
+ ...priceData,
+ };
- if (!newItem.config) {
- newItem.config = {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- };
- }
+ if (!newItem.config) {
+ newItem.config = {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ };
+ }
- return newItem;
- });
+ return newItem;
+ });
};
export const productsAreSame = ({
- newProductV1,
- newProductV2,
- curProductV1,
- curProductV2,
- features,
+ newProductV1,
+ newProductV2,
+ curProductV1,
+ curProductV2,
+ features,
}: {
- newProductV1?: FullProduct;
- newProductV2?: ProductV2;
- curProductV1?: FullProduct;
- curProductV2?: ProductV2;
- features: Feature[];
+ newProductV1?: FullProduct;
+ newProductV2?: ProductV2;
+ curProductV1?: FullProduct;
+ curProductV2?: ProductV2;
+ features: Feature[];
}) => {
- if (!newProductV1 && !newProductV2) {
- throw new RecaseError({
- message: "productsAreSame error: product1 not provided",
- code: ErrCode.InvalidRequest,
- });
- }
+ if (!newProductV1 && !newProductV2) {
+ throw new RecaseError({
+ message: "productsAreSame error: product1 not provided",
+ code: ErrCode.InvalidRequest,
+ });
+ }
- if (!curProductV1 && !curProductV2) {
- throw new RecaseError({
- message: "productsAreSame error: product2 not provided",
- code: ErrCode.InvalidRequest,
- });
- }
+ if (!curProductV1 && !curProductV2) {
+ throw new RecaseError({
+ message: "productsAreSame error: product2 not provided",
+ code: ErrCode.InvalidRequest,
+ });
+ }
- let items1 =
- newProductV2?.items ||
- mapToProductItems({
- prices: newProductV1?.prices || [],
- entitlements: newProductV1?.entitlements || [],
- features,
- });
+ let items1 =
+ newProductV2?.items ||
+ mapToProductItems({
+ prices: newProductV1?.prices || [],
+ entitlements: newProductV1?.entitlements || [],
+ features,
+ });
- let items2 =
- curProductV2?.items ||
- mapToProductItems({
- prices: curProductV1?.prices || [],
- entitlements: curProductV1?.entitlements || [],
- features,
- });
+ let items2 =
+ curProductV2?.items ||
+ mapToProductItems({
+ prices: curProductV1?.prices || [],
+ entitlements: curProductV1?.entitlements || [],
+ features,
+ });
- items1 = sanitizeItems({ items: items1, features });
- items2 = sanitizeItems({ items: items2, features });
+ items1 = sanitizeItems({ items: items1, features });
+ items2 = sanitizeItems({ items: items2, features });
- let itemsSame = true;
- let pricesChanged = false;
- const newItems: ProductItem[] = [];
- const removedItems: ProductItem[] = [];
+ let itemsSame = true;
+ let pricesChanged = false;
+ const newItems: ProductItem[] = [];
+ const removedItems: ProductItem[] = [];
- if (items1.length !== items2.length) {
- itemsSame = false;
- }
+ if (items1.length !== items2.length) {
+ itemsSame = false;
+ }
- // Check if any feature's usage limits have changed
- items1.some((item1) => {
- const matchingItem2 = items2?.find(
- (item2) => item2.feature_id === item1.feature_id
- );
- if (!matchingItem2) return false;
+ // Check if any feature's usage limits have changed
+ items1.some((item1) => {
+ const matchingItem2 = items2?.find(
+ (item2) => item2.feature_id === item1.feature_id,
+ );
+ if (!matchingItem2) return false;
- const feature = features.find((f) => f.id === item1.feature_id);
- if (!feature) return false;
+ const feature = features.find((f) => f.id === item1.feature_id);
+ if (!feature) return false;
- return false;
- });
+ return false;
+ });
- if (items1.length !== items2.length) {
- itemsSame = false;
- }
+ if (items1.length !== items2.length) {
+ itemsSame = false;
+ }
- for (const item of items1) {
- let similarItem = findSimilarItem({
- item,
- items: items2,
- });
+ for (const item of items1) {
+ let similarItem = findSimilarItem({
+ item,
+ items: items2,
+ });
- if (!similarItem) {
- if (isFeaturePriceItem(item) || isPriceItem(item)) {
- pricesChanged = true;
- }
+ if (!similarItem) {
+ if (isFeaturePriceItem(item) || isPriceItem(item)) {
+ pricesChanged = true;
+ }
- itemsSame = false;
- newItems.push(item);
+ itemsSame = false;
+ newItems.push(item);
- continue;
- }
+ continue;
+ }
- const { same, pricesChanged: pricesChanged_ } = itemsAreSame({
- item1: item,
- item2: similarItem!,
- features,
- });
+ const { same, pricesChanged: pricesChanged_ } = itemsAreSame({
+ item1: item,
+ item2: similarItem!,
+ features,
+ });
- if (!same) {
- itemsSame = false;
- newItems.push(item);
- }
+ if (!same) {
+ itemsSame = false;
+ newItems.push(item);
+ }
- if (pricesChanged_) {
- pricesChanged = true;
- }
- }
+ if (pricesChanged_) {
+ pricesChanged = true;
+ }
+ }
- for (const item of items2) {
- let similarItem = findSimilarItem({
- item,
- items: items1,
- });
+ for (const item of items2) {
+ let similarItem = findSimilarItem({
+ item,
+ items: items1,
+ });
- if (!similarItem) {
- itemsSame = false;
- if (isFeaturePriceItem(item) || isPriceItem(item)) {
- pricesChanged = true;
- }
+ if (!similarItem) {
+ itemsSame = false;
+ if (isFeaturePriceItem(item) || isPriceItem(item)) {
+ pricesChanged = true;
+ }
- removedItems.push(item);
- }
- }
+ removedItems.push(item);
+ }
+ }
- // Compare free trial
- let freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial;
- let freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial;
+ // Compare free trial
+ let freeTrial1 = curProductV1?.free_trial || curProductV2?.free_trial;
+ let freeTrial2 = newProductV1?.free_trial || newProductV2?.free_trial;
- let freeTrialsSame = freeTrialsAreSame({
- ft1: freeTrial1,
- ft2: freeTrial2,
- });
+ let freeTrialsSame = freeTrialsAreSame({
+ ft1: freeTrial1,
+ ft2: freeTrial2,
+ });
- if (!freeTrialsSame) {
- console.log("Free trials different");
- console.log("Free trial 1:", freeTrial1);
- console.log("Free trial 2:", freeTrial2);
- console.log("--------------------------------");
- }
+ if (!freeTrialsSame) {
+ console.log("Free trials different");
+ console.log("Free trial 1:", freeTrial1);
+ console.log("Free trial 2:", freeTrial2);
+ console.log("--------------------------------");
+ }
- // Compare name
- return {
- itemsSame,
- freeTrialsSame,
- onlyEntsChanged: !pricesChanged,
- newItems,
- removedItems,
- };
+ // Compare name
+ return {
+ itemsSame,
+ freeTrialsSame,
+ onlyEntsChanged: !pricesChanged,
+ newItems,
+ removedItems,
+ };
};
diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts
index 62f9b7788..10762a264 100644
--- a/server/src/internal/products/productUtils/detectProductVariant.ts
+++ b/server/src/internal/products/productUtils/detectProductVariant.ts
@@ -24,98 +24,98 @@ To determine if a product is an interval variant, please follow these guidelines
`;
export const detectBaseVariant = async ({
- db,
- curProduct,
- logger,
+ db,
+ curProduct,
+ logger,
}: {
- db: DrizzleCli;
- curProduct: FullProduct;
- logger: Logger;
+ db: DrizzleCli;
+ curProduct: FullProduct;
+ logger: Logger;
}) => {
- logger.info(`Detecting base variant for ${curProduct.id}`);
- if (!process.env.ANTHROPIC_API_KEY) return;
+ logger.info(`Detecting base variant for ${curProduct.id}`);
+ if (!process.env.ANTHROPIC_API_KEY) return;
- let existingProducts = (await ProductService.listFull({
- db,
- orgId: curProduct.org_id,
- env: curProduct.env,
- excludeEnts: true,
- })) as FullProduct[];
+ let existingProducts = (await ProductService.listFull({
+ db,
+ orgId: curProduct.org_id,
+ env: curProduct.env,
+ excludeEnts: true,
+ })) as FullProduct[];
- // if (product.base_variant_id == baseVariantId) {
- let curPrices = curProduct.prices;
- let intervals = curPrices.map((price) => price.config.interval);
+ // if (product.base_variant_id == baseVariantId) {
+ let curPrices = curProduct.prices;
+ let intervals = curPrices.map((price) => price.config.interval);
- // 1. Return null if add on
- if (curProduct.is_add_on) return null;
+ // 1. Return null if add on
+ if (curProduct.is_add_on) return null;
- // 2. Return null if only one off or monthly price
- const oneOffOrMonthly = [BillingInterval.OneOff, BillingInterval.Month];
- if (intervals.every((i: BillingInterval) => oneOffOrMonthly.includes(i))) {
- logger.info(`Is one off or monthly, skipping`);
- return null;
- }
+ // 2. Return null if only one off or monthly price
+ const oneOffOrMonthly = [BillingInterval.OneOff, BillingInterval.Month];
+ if (intervals.every((i: BillingInterval) => oneOffOrMonthly.includes(i))) {
+ logger.info(`Is one off or monthly, skipping`);
+ return null;
+ }
- const filteredExistingProducts = existingProducts.filter(
- (p) =>
- p.id != curProduct.id &&
- nullish(p.base_variant_id) &&
- !p.is_add_on &&
- p.prices.length > 0 &&
- p.prices.every(
- (price) => price.config.interval == BillingInterval.Month
- ) &&
- p.group == curProduct.group
- );
+ const filteredExistingProducts = existingProducts.filter(
+ (p) =>
+ p.id != curProduct.id &&
+ nullish(p.base_variant_id) &&
+ !p.is_add_on &&
+ p.prices.length > 0 &&
+ p.prices.every(
+ (price) => price.config.interval == BillingInterval.Month,
+ ) &&
+ p.group == curProduct.group,
+ );
- if (filteredExistingProducts.length == 0) {
- logger.info(`No base product to search for`);
- return null;
- }
+ if (filteredExistingProducts.length == 0) {
+ logger.info(`No base product to search for`);
+ return null;
+ }
- const variables = `
+ const variables = `
${JSON.stringify({
- id: curProduct.id,
- name: curProduct.name,
- prices: curPrices,
- })}
+ id: curProduct.id,
+ name: curProduct.name,
+ prices: curPrices,
+ })}
${filteredExistingProducts
- .map((p) =>
- JSON.stringify({
- id: p.id,
- name: p.name,
- prices: p.prices,
- })
- )
- .join("\n")}
+ .map((p) =>
+ JSON.stringify({
+ id: p.id,
+ name: p.name,
+ prices: p.prices,
+ }),
+ )
+ .join("\n")}
`;
- let { object } = await generateObject({
- model: anthropic("claude-3-5-haiku-latest"),
- schema: z.object({ base_variant_id: z.string().nullable() }),
- prompt: `${prompt}\n\n${variables}`,
- });
+ let { object } = await generateObject({
+ model: anthropic("claude-3-5-haiku-latest"),
+ schema: z.object({ base_variant_id: z.string().nullable() }),
+ prompt: `${prompt}\n\n${variables}`,
+ });
- let baseVariantId = object.base_variant_id;
+ let baseVariantId = object.base_variant_id;
- logger.info(
- `llm response for base variant of ${curProduct.id}: ${baseVariantId}`
- );
+ logger.info(
+ `llm response for base variant of ${curProduct.id}: ${baseVariantId}`,
+ );
- if (baseVariantId) {
- await ProductService.updateByInternalId({
- db,
- internalId: curProduct.internal_id,
- update: {
- base_variant_id: baseVariantId,
- },
- });
- }
+ if (baseVariantId) {
+ await ProductService.updateByInternalId({
+ db,
+ internalId: curProduct.internal_id,
+ update: {
+ base_variant_id: baseVariantId,
+ },
+ });
+ }
- return baseVariantId;
+ return baseVariantId;
};
diff --git a/server/src/internal/products/productUtils/productResponseUtils/getAttachScenario.ts b/server/src/internal/products/productUtils/productResponseUtils/getAttachScenario.ts
index 4ddc8fadd..b5d8f7658 100644
--- a/server/src/internal/products/productUtils/productResponseUtils/getAttachScenario.ts
+++ b/server/src/internal/products/productUtils/productResponseUtils/getAttachScenario.ts
@@ -1,66 +1,66 @@
import { cusProductToProduct } from "@autumn/shared";
import { AttachScenario, FullCustomer, FullProduct } from "@autumn/shared";
import {
- isFreeProduct,
- isOneOff,
- isProductUpgrade,
+ isFreeProduct,
+ isOneOff,
+ isProductUpgrade,
} from "../../productUtils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { isCanceled } from "@/internal/customers/cusProducts/cusProductUtils/classifyCusProduct.js";
export const getAttachScenario = ({
- fullCus,
- fullProduct,
+ fullCus,
+ fullProduct,
}: {
- fullCus?: FullCustomer;
- fullProduct: FullProduct;
+ fullCus?: FullCustomer;
+ fullProduct: FullProduct;
}) => {
- if (!fullCus) return AttachScenario.New;
+ if (!fullCus) return AttachScenario.New;
- let { curMainProduct, curScheduledProduct } = getExistingCusProducts({
- product: fullProduct,
- cusProducts: fullCus?.customer_products || [],
- internalEntityId: fullCus?.entity?.internal_id,
- });
+ let { curMainProduct, curScheduledProduct } = getExistingCusProducts({
+ product: fullProduct,
+ cusProducts: fullCus?.customer_products || [],
+ internalEntityId: fullCus?.entity?.internal_id,
+ });
- if (!curMainProduct || fullProduct.is_add_on) return AttachScenario.New;
+ if (!curMainProduct || fullProduct.is_add_on) return AttachScenario.New;
- if (isOneOff(fullProduct.prices)) {
- return AttachScenario.New;
- }
+ if (isOneOff(fullProduct.prices)) {
+ return AttachScenario.New;
+ }
- // 1. If current product is the same as the product, return active
- if (curMainProduct?.product.id == fullProduct.id) {
- if (isCanceled({ cusProduct: curMainProduct })) {
- return AttachScenario.Renew;
- } else return AttachScenario.Active;
- }
+ // 1. If current product is the same as the product, return active
+ if (curMainProduct?.product.id == fullProduct.id) {
+ if (isCanceled({ cusProduct: curMainProduct })) {
+ return AttachScenario.Renew;
+ } else return AttachScenario.Active;
+ }
- if (curScheduledProduct?.product.id == fullProduct.id) {
- return AttachScenario.Scheduled;
- }
+ if (curScheduledProduct?.product.id == fullProduct.id) {
+ return AttachScenario.Scheduled;
+ }
- let curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
+ let curFullProduct = cusProductToProduct({ cusProduct: curMainProduct });
- if (
- isFreeProduct(curFullProduct.prices) &&
- isFreeProduct(fullProduct.prices)
- ) {
- return AttachScenario.New;
- }
+ if (
+ isFreeProduct(curFullProduct.prices) &&
+ isFreeProduct(fullProduct.prices)
+ ) {
+ return AttachScenario.New;
+ }
- let isUpgrade = isProductUpgrade({
- prices1: curFullProduct.prices,
- prices2: fullProduct.prices,
- });
+ let isUpgrade = isProductUpgrade({
+ prices1: curFullProduct.prices,
+ prices2: fullProduct.prices,
+ });
- if (
- !isUpgrade &&
- !isFreeProduct(curFullProduct.prices) &&
- isFreeProduct(fullProduct.prices)
- ) {
- return AttachScenario.Cancel;
- }
+ if (
+ !isUpgrade &&
+ !isFreeProduct(curFullProduct.prices) &&
+ isFreeProduct(fullProduct.prices)
+ ) {
+ return AttachScenario.Cancel;
+ }
- return isUpgrade ? AttachScenario.Upgrade : AttachScenario.Downgrade;
+ return isUpgrade ? AttachScenario.Upgrade : AttachScenario.Downgrade;
};
diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts
index 15ea23722..6a8ef9e89 100644
--- a/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts
+++ b/server/src/internal/products/productUtils/productResponseUtils/getProductItemDisplay.ts
@@ -2,68 +2,68 @@ import RecaseError from "@/utils/errorUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- ErrCode,
- Feature,
- getFeatureName,
- Infinite,
- numberWithCommas,
- Organization,
- ProductItem,
- ProductItemFeatureType,
- ProductItemInterval,
+ ErrCode,
+ Feature,
+ getFeatureName,
+ Infinite,
+ numberWithCommas,
+ Organization,
+ ProductItem,
+ ProductItemFeatureType,
+ ProductItemInterval,
} from "@autumn/shared";
import {
- isFeatureItem,
- isFeaturePriceItem,
- isPriceItem,
+ isFeatureItem,
+ isFeaturePriceItem,
+ isPriceItem,
} from "../../product-items/productItemUtils/getItemType.js";
export const getIntervalString = ({
- interval,
- intervalCount,
- prefix = "per ",
+ interval,
+ intervalCount,
+ prefix = "per ",
}: {
- interval: ProductItemInterval;
- intervalCount?: number | null;
- prefix?: string;
+ interval: ProductItemInterval;
+ intervalCount?: number | null;
+ prefix?: string;
}) => {
- if (!interval) return "";
- if (intervalCount == 1) {
- return `${prefix}${interval}`;
- }
- return `${prefix}${intervalCount} ${interval}s`;
+ if (!interval) return "";
+ if (intervalCount == 1) {
+ return `${prefix}${interval}`;
+ }
+ return `${prefix}${intervalCount} ${interval}s`;
};
export const formatTiers = ({
- item,
- currency,
+ item,
+ currency,
}: {
- item: ProductItem;
- currency?: string | null;
+ item: ProductItem;
+ currency?: string | null;
}) => {
- let tiers = item.tiers;
- if (tiers) {
- if (tiers.length == 1) {
- return formatAmount({
- currency,
- amount: tiers[0].amount,
- maxFractionDigits: 10,
- });
- }
+ let tiers = item.tiers;
+ if (tiers) {
+ if (tiers.length == 1) {
+ return formatAmount({
+ currency,
+ amount: tiers[0].amount,
+ maxFractionDigits: 10,
+ });
+ }
- let firstPrice = tiers[0].amount;
- let lastPrice = tiers[tiers.length - 1].amount;
+ let firstPrice = tiers[0].amount;
+ let lastPrice = tiers[tiers.length - 1].amount;
- return `${formatAmount({
- currency,
- amount: firstPrice,
- maxFractionDigits: 10,
- })} - ${formatAmount({
- currency,
- amount: lastPrice,
- maxFractionDigits: 10,
- })}`;
- }
+ return `${formatAmount({
+ currency,
+ amount: firstPrice,
+ maxFractionDigits: 10,
+ })} - ${formatAmount({
+ currency,
+ amount: lastPrice,
+ maxFractionDigits: 10,
+ })}`;
+ }
};
// export const getIncludedFeatureName = ({
@@ -80,191 +80,191 @@ export const formatTiers = ({
// };
export const getFeatureItemDisplay = ({
- item,
- feature,
+ item,
+ feature,
}: {
- item: ProductItem;
- feature?: Feature;
+ item: ProductItem;
+ feature?: Feature;
}) => {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
- // 1. If feature
- if (item.feature_type == ProductItemFeatureType.Static) {
- return {
- primary_text: getFeatureName({
- feature,
- plural: false,
- capitalize: true,
- }),
- };
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
+ // 1. If feature
+ if (item.feature_type == ProductItemFeatureType.Static) {
+ return {
+ primary_text: getFeatureName({
+ feature,
+ plural: false,
+ capitalize: true,
+ }),
+ };
+ }
- let featureName = getFeatureName({
- feature,
- units: item.included_usage,
- });
+ let featureName = getFeatureName({
+ feature,
+ units: item.included_usage,
+ });
- let includedUsageTxt =
- item.included_usage == Infinite
- ? "Unlimited "
- : nullish(item.included_usage) || item.included_usage == 0
- ? ""
- : `${numberWithCommas(item.included_usage!)} `;
+ let includedUsageTxt =
+ item.included_usage == Infinite
+ ? "Unlimited "
+ : nullish(item.included_usage) || item.included_usage == 0
+ ? ""
+ : `${numberWithCommas(item.included_usage!)} `;
- return {
- primary_text: `${includedUsageTxt}${featureName}`,
- secondary_text: undefined,
- };
+ return {
+ primary_text: `${includedUsageTxt}${featureName}`,
+ secondary_text: undefined,
+ };
};
export const getPriceItemDisplay = ({
- item,
- currency,
+ item,
+ currency,
}: {
- item: ProductItem;
- currency?: string | null;
+ item: ProductItem;
+ currency?: string | null;
}) => {
- let primaryText = formatAmount({
- currency,
- amount: item.price as number,
- });
- let intervalStr = getIntervalString({
- interval: item.interval!,
- intervalCount: item.interval_count,
- });
+ let primaryText = formatAmount({
+ currency,
+ amount: item.price as number,
+ });
+ let intervalStr = getIntervalString({
+ interval: item.interval!,
+ intervalCount: item.interval_count,
+ });
- let secondaryText = intervalStr || undefined;
+ let secondaryText = intervalStr || undefined;
- return {
- primary_text: primaryText,
- secondary_text: secondaryText,
- };
+ return {
+ primary_text: primaryText,
+ secondary_text: secondaryText,
+ };
};
export const getFeaturePriceItemDisplay = ({
- feature,
- item,
- currency,
- isMainPrice = false,
- minifyIncluded = false,
+ feature,
+ item,
+ currency,
+ isMainPrice = false,
+ minifyIncluded = false,
}: {
- feature?: Feature;
- item: ProductItem;
- currency?: string | null;
- isMainPrice?: boolean;
- minifyIncluded?: boolean;
+ feature?: Feature;
+ item: ProductItem;
+ currency?: string | null;
+ isMainPrice?: boolean;
+ minifyIncluded?: boolean;
}) => {
- if (!feature) {
- throw new RecaseError({
- message: `Feature ${item.feature_id} not found`,
- code: ErrCode.FeatureNotFound,
- statusCode: 404,
- });
- }
+ if (!feature) {
+ throw new RecaseError({
+ message: `Feature ${item.feature_id} not found`,
+ code: ErrCode.FeatureNotFound,
+ statusCode: 404,
+ });
+ }
- // 1. Get included usage
- let includedFeatureName = getFeatureName({
- feature,
- units: item.included_usage,
- });
+ // 1. Get included usage
+ let includedFeatureName = getFeatureName({
+ feature,
+ units: item.included_usage,
+ });
- let includedUsage = item.included_usage as number | null;
- let includedUsageStr = "";
- if (notNullish(includedUsage) && includedUsage! > 0) {
- if (minifyIncluded) {
- includedUsageStr = `${numberWithCommas(includedUsage!)} included`;
- } else {
- includedUsageStr = `${numberWithCommas(includedUsage!)} ${includedFeatureName}`;
- }
- }
+ let includedUsage = item.included_usage as number | null;
+ let includedUsageStr = "";
+ if (notNullish(includedUsage) && includedUsage! > 0) {
+ if (minifyIncluded) {
+ includedUsageStr = `${numberWithCommas(includedUsage!)} included`;
+ } else {
+ includedUsageStr = `${numberWithCommas(includedUsage!)} ${includedFeatureName}`;
+ }
+ }
- let priceStr = formatTiers({ item, currency });
- let billingFeatureName = getFeatureName({
- feature,
- units: item.billing_units,
- });
+ let priceStr = formatTiers({ item, currency });
+ let billingFeatureName = getFeatureName({
+ feature,
+ units: item.billing_units,
+ });
- let priceStr2 = "";
- if (item.billing_units && item.billing_units > 1) {
- priceStr2 = `${numberWithCommas(item.billing_units)} ${billingFeatureName}`;
- } else {
- priceStr2 = `${billingFeatureName}`;
- }
+ let priceStr2 = "";
+ if (item.billing_units && item.billing_units > 1) {
+ priceStr2 = `${numberWithCommas(item.billing_units)} ${billingFeatureName}`;
+ } else {
+ priceStr2 = `${billingFeatureName}`;
+ }
- // let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
- let intervalStr = isMainPrice
- ? getIntervalString({
- interval: item.interval!,
- intervalCount: item.interval_count,
- })
- : "";
+ // let intervalStr = isMainPrice && item.interval ? ` per ${item.interval}` : "";
+ let intervalStr = isMainPrice
+ ? getIntervalString({
+ interval: item.interval!,
+ intervalCount: item.interval_count,
+ })
+ : "";
- // console.log("isMainPrice", isMainPrice);
- // console.log("intervalStr", intervalStr);
+ // console.log("isMainPrice", isMainPrice);
+ // console.log("intervalStr", intervalStr);
- if (includedUsageStr) {
- return {
- primary_text: includedUsageStr,
- secondary_text: `then ${priceStr} per ${priceStr2}${intervalStr}`,
- };
- }
+ if (includedUsageStr) {
+ return {
+ primary_text: includedUsageStr,
+ secondary_text: `then ${priceStr} per ${priceStr2}${intervalStr}`,
+ };
+ }
- if (isMainPrice) {
- return {
- primary_text: priceStr + ` per ${priceStr2}`,
- secondary_text: `${intervalStr}`,
- };
- }
+ if (isMainPrice) {
+ return {
+ primary_text: priceStr + ` per ${priceStr2}`,
+ secondary_text: `${intervalStr}`,
+ };
+ }
- // ${intervalStr}
- return {
- primary_text: priceStr + ` per ${priceStr2}`,
- // secondary_text: `per ${priceStr2}${intervalStr}`,
- secondary_text: "",
- };
+ // ${intervalStr}
+ return {
+ primary_text: priceStr + ` per ${priceStr2}`,
+ // secondary_text: `per ${priceStr2}${intervalStr}`,
+ secondary_text: "",
+ };
};
export const getProductItemDisplay = ({
- item,
- features,
- currency = "usd",
- isMainPrice = false,
+ item,
+ features,
+ currency = "usd",
+ isMainPrice = false,
}: {
- item: ProductItem;
- features: Feature[];
- currency?: string | null;
- isMainPrice?: boolean;
+ item: ProductItem;
+ features: Feature[];
+ currency?: string | null;
+ isMainPrice?: boolean;
}) => {
- if (isFeatureItem(item)) {
- return getFeatureItemDisplay({
- item,
- feature: features.find((f) => f.id === item.feature_id),
- });
- }
+ if (isFeatureItem(item)) {
+ return getFeatureItemDisplay({
+ item,
+ feature: features.find((f) => f.id === item.feature_id),
+ });
+ }
- if (isPriceItem(item)) {
- return getPriceItemDisplay({
- item,
- currency,
- });
- }
+ if (isPriceItem(item)) {
+ return getPriceItemDisplay({
+ item,
+ currency,
+ });
+ }
- if (isFeaturePriceItem(item)) {
- return getFeaturePriceItemDisplay({
- item,
- feature: features.find((f) => f.id === item.feature_id),
- currency,
- isMainPrice,
- });
- }
+ if (isFeaturePriceItem(item)) {
+ return getFeaturePriceItemDisplay({
+ item,
+ feature: features.find((f) => f.id === item.feature_id),
+ currency,
+ isMainPrice,
+ });
+ }
- return {
- primary_text: "couldn't detect item type",
- secondary_text: "",
- };
+ return {
+ primary_text: "couldn't detect item type",
+ secondary_text: "",
+ };
};
diff --git a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts
index 4d811a33f..ebed16c7c 100644
--- a/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts
+++ b/server/src/internal/products/productUtils/productResponseUtils/getProductResponse.ts
@@ -1,18 +1,18 @@
import {
- AttachScenario,
- BillingInterval,
- type Feature,
- type FeatureOptions,
- type FreeTrialResponse,
- FreeTrialResponseSchema,
- type FullCustomer,
- type FullProduct,
- type Price,
- type ProductItem,
- ProductItemResponseSchema,
- ProductPropertiesSchema,
- ProductResponseSchema,
- UsageModel,
+ AttachScenario,
+ BillingInterval,
+ type Feature,
+ type FeatureOptions,
+ type FreeTrialResponse,
+ FreeTrialResponseSchema,
+ type FullCustomer,
+ type FullProduct,
+ type Price,
+ type ProductItem,
+ ProductItemResponseSchema,
+ ProductPropertiesSchema,
+ ProductResponseSchema,
+ UsageModel,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js";
@@ -29,192 +29,192 @@ import { getAttachScenario } from "./getAttachScenario.js";
import { getProductItemDisplay } from "./getProductItemDisplay.js";
export const getProductItemResponse = ({
- item,
- features,
- currency,
- withDisplay = true,
- options,
- isMainPrice = false,
+ item,
+ features,
+ currency,
+ withDisplay = true,
+ options,
+ isMainPrice = false,
}: {
- item: ProductItem;
- features: Feature[];
- currency?: string | null;
- withDisplay?: boolean;
- options?: FeatureOptions[];
- isMainPrice?: boolean;
+ item: ProductItem;
+ features: Feature[];
+ currency?: string | null;
+ withDisplay?: boolean;
+ options?: FeatureOptions[];
+ isMainPrice?: boolean;
}) => {
- // 1. Get item type
- const type = getItemType(item);
+ // 1. Get item type
+ const type = getItemType(item);
- // 2. Get display
- const display = getProductItemDisplay({
- item,
- features,
- currency,
- isMainPrice,
- });
+ // 2. Get display
+ const display = getProductItemDisplay({
+ item,
+ features,
+ currency,
+ isMainPrice,
+ });
- const priceData = itemToPriceOrTiers({ item });
+ const priceData = itemToPriceOrTiers({ item });
- let quantity: number | undefined;
- let upcomingQuantity: number | undefined;
+ let quantity: number | undefined;
+ let upcomingQuantity: number | undefined;
- if (item.usage_model === UsageModel.Prepaid && notNullish(options)) {
- const option = options!.find((o) => o.feature_id === item.feature_id);
- quantity = option?.quantity
- ? option?.quantity * (item.billing_units ?? 1)
- : undefined;
+ if (item.usage_model === UsageModel.Prepaid && notNullish(options)) {
+ const option = options!.find((o) => o.feature_id === item.feature_id);
+ quantity = option?.quantity
+ ? option?.quantity * (item.billing_units ?? 1)
+ : undefined;
- upcomingQuantity = option?.upcoming_quantity
- ? option?.upcoming_quantity * (item.billing_units ?? 1)
- : undefined;
- }
+ upcomingQuantity = option?.upcoming_quantity
+ ? option?.upcoming_quantity * (item.billing_units ?? 1)
+ : undefined;
+ }
- const feature = features.find((f) => f.id === item.feature_id);
- return ProductItemResponseSchema.parse({
- type,
- ...item,
- feature: feature ? toAPIFeature({ feature }) : null,
- display: withDisplay ? display : undefined,
- ...priceData,
- quantity,
- next_cycle_quantity: upcomingQuantity,
- });
+ const feature = features.find((f) => f.id === item.feature_id);
+ return ProductItemResponseSchema.parse({
+ type,
+ ...item,
+ feature: feature ? toAPIFeature({ feature }) : null,
+ display: withDisplay ? display : undefined,
+ ...priceData,
+ quantity,
+ next_cycle_quantity: upcomingQuantity,
+ });
};
export const getFreeTrialResponse = async ({
- db,
- product,
- fullCus,
- attachScenario,
+ db,
+ product,
+ fullCus,
+ attachScenario,
}: {
- db?: DrizzleCli;
- product: FullProduct;
- fullCus?: FullCustomer;
- attachScenario: AttachScenario;
+ db?: DrizzleCli;
+ product: FullProduct;
+ fullCus?: FullCustomer;
+ attachScenario: AttachScenario;
}) => {
- if (!db) return product.free_trial;
+ if (!db) return product.free_trial;
- if (product.free_trial && fullCus) {
- let trial = await getFreeTrialAfterFingerprint({
- db,
- freeTrial: product.free_trial,
- fingerprint: fullCus.fingerprint,
- internalCustomerId: fullCus.internal_id,
- multipleAllowed: false,
- productId: product.id,
- });
+ if (product.free_trial && fullCus) {
+ let trial = await getFreeTrialAfterFingerprint({
+ db,
+ freeTrial: product.free_trial,
+ fingerprint: fullCus.fingerprint,
+ internalCustomerId: fullCus.internal_id,
+ multipleAllowed: false,
+ productId: product.id,
+ });
- if (attachScenario === AttachScenario.Downgrade) trial = null;
- return FreeTrialResponseSchema.parse({
- duration: product.free_trial?.duration,
- length: product.free_trial?.length,
- unique_fingerprint: product.free_trial?.unique_fingerprint,
- trial_available: notNullish(trial) ? true : false,
- card_required: product.free_trial?.card_required,
- });
- }
+ if (attachScenario === AttachScenario.Downgrade) trial = null;
+ return FreeTrialResponseSchema.parse({
+ duration: product.free_trial?.duration,
+ length: product.free_trial?.length,
+ unique_fingerprint: product.free_trial?.unique_fingerprint,
+ trial_available: notNullish(trial) ? true : false,
+ card_required: product.free_trial?.card_required,
+ });
+ }
- if (product.free_trial) {
- return FreeTrialResponseSchema.parse({
- duration: product.free_trial?.duration,
- length: product.free_trial?.length,
- unique_fingerprint: product.free_trial?.unique_fingerprint,
- card_required: product.free_trial?.card_required,
- });
- }
+ if (product.free_trial) {
+ return FreeTrialResponseSchema.parse({
+ duration: product.free_trial?.duration,
+ length: product.free_trial?.length,
+ unique_fingerprint: product.free_trial?.unique_fingerprint,
+ card_required: product.free_trial?.card_required,
+ });
+ }
- return null;
+ return null;
};
export const getProductProperties = ({
- product,
- freeTrial,
+ product,
+ freeTrial,
}: {
- product: FullProduct;
- freeTrial?: FreeTrialResponse | null;
+ product: FullProduct;
+ freeTrial?: FreeTrialResponse | null;
}) => {
- const largestInterval = getLargestInterval({
- prices: product.prices,
- excludeOneOff: true,
- });
+ const largestInterval = getLargestInterval({
+ prices: product.prices,
+ excludeOneOff: true,
+ });
- const hasFreeTrial =
- notNullish(freeTrial) && freeTrial?.trial_available !== false;
+ const hasFreeTrial =
+ notNullish(freeTrial) && freeTrial?.trial_available !== false;
- return ProductPropertiesSchema.parse({
- is_free: isFreeProduct(product.prices) || false,
- is_one_off: isOneOff(product.prices) || false,
- interval_group: largestInterval?.interval,
- has_trial: hasFreeTrial,
- updateable: product.prices.some(
- (p: Price) =>
- isPrepaidPrice({ price: p }) &&
- p.config.interval !== BillingInterval.OneOff
- ),
- });
+ return ProductPropertiesSchema.parse({
+ is_free: isFreeProduct(product.prices) || false,
+ is_one_off: isOneOff(product.prices) || false,
+ interval_group: largestInterval?.interval,
+ has_trial: hasFreeTrial,
+ updateable: product.prices.some(
+ (p: Price) =>
+ isPrepaidPrice({ price: p }) &&
+ p.config.interval !== BillingInterval.OneOff,
+ ),
+ });
};
export const getProductResponse = async ({
- product,
- features,
- fullCus,
- currency,
- db,
- withDisplay = true,
- options,
+ product,
+ features,
+ fullCus,
+ currency,
+ db,
+ withDisplay = true,
+ options,
}: {
- product: FullProduct;
- features: Feature[];
- fullCus?: FullCustomer;
- currency?: string | null;
- db?: DrizzleCli;
- withDisplay?: boolean;
- options?: FeatureOptions[];
+ product: FullProduct;
+ features: Feature[];
+ fullCus?: FullCustomer;
+ currency?: string | null;
+ db?: DrizzleCli;
+ withDisplay?: boolean;
+ options?: FeatureOptions[];
}) => {
- // 1. Get items with display
- const rawItems = mapToProductItems({
- prices: product.prices,
- entitlements: product.entitlements,
- features: features,
- });
+ // 1. Get items with display
+ const rawItems = mapToProductItems({
+ prices: product.prices,
+ entitlements: product.entitlements,
+ features: features,
+ });
- // Sort raw items first
- const sortedItems = sortProductItems(rawItems, features);
+ // Sort raw items first
+ const sortedItems = sortProductItems(rawItems, features);
- // Transform sorted items
- const items = sortedItems.map((item, index) => {
- return getProductItemResponse({
- item,
- features,
- currency,
- withDisplay,
- options,
- isMainPrice: index === 0,
- });
- });
+ // Transform sorted items
+ const items = sortedItems.map((item, index) => {
+ return getProductItemResponse({
+ item,
+ features,
+ currency,
+ withDisplay,
+ options,
+ isMainPrice: index === 0,
+ });
+ });
- // 2. Get product properties
- const attachScenario = getAttachScenario({
- fullCus,
- fullProduct: product,
- });
+ // 2. Get product properties
+ const attachScenario = getAttachScenario({
+ fullCus,
+ fullProduct: product,
+ });
- const freeTrial = (await getFreeTrialResponse({
- db: db as DrizzleCli,
- product,
- fullCus,
- attachScenario,
- })) as FreeTrialResponse;
+ const freeTrial = (await getFreeTrialResponse({
+ db: db as DrizzleCli,
+ product,
+ fullCus,
+ attachScenario,
+ })) as FreeTrialResponse;
- return ProductResponseSchema.parse({
- ...product,
- name: product.name || null,
- group: product.group || null,
- items: items,
- free_trial: freeTrial || null,
- scenario: attachScenario,
- properties: getProductProperties({ product, freeTrial }),
- archived: product.archived ? true : undefined,
- });
+ return ProductResponseSchema.parse({
+ ...product,
+ name: product.name || null,
+ group: product.group || null,
+ items: items,
+ free_trial: freeTrial || null,
+ scenario: attachScenario,
+ properties: getProductProperties({ product, freeTrial }),
+ archived: product.archived ? true : undefined,
+ });
};
diff --git a/server/src/internal/products/productUtils/sortProductUtils.ts b/server/src/internal/products/productUtils/sortProductUtils.ts
index 468f3dc38..4302b8ffc 100644
--- a/server/src/internal/products/productUtils/sortProductUtils.ts
+++ b/server/src/internal/products/productUtils/sortProductUtils.ts
@@ -2,37 +2,37 @@ import { FullProduct, Product, ProductV2 } from "@autumn/shared";
import { isProductUpgrade } from "../productUtils.js";
export const sortProductsByPrice = ({
- products,
+ products,
}: {
- products: FullProduct[];
+ products: FullProduct[];
}) => {
- products.sort((a, b) => {
- let isUpgradeA = isProductUpgrade({
- prices1: a.prices,
- prices2: b.prices,
- usageAlwaysUpgrade: false,
- });
+ products.sort((a, b) => {
+ let isUpgradeA = isProductUpgrade({
+ prices1: a.prices,
+ prices2: b.prices,
+ usageAlwaysUpgrade: false,
+ });
- return isUpgradeA ? -1 : 1;
- });
+ return isUpgradeA ? -1 : 1;
+ });
};
export const sortFullProducts = ({ products }: { products: FullProduct[] }) => {
- return products.sort((a, b) => {
- if (a.group !== b.group) {
- return a.group < b.group ? -1 : 1;
- }
+ return products.sort((a, b) => {
+ if (a.group !== b.group) {
+ return a.group < b.group ? -1 : 1;
+ }
- if (a.is_add_on !== b.is_add_on) {
- return a.is_add_on ? 1 : -1;
- }
+ if (a.is_add_on !== b.is_add_on) {
+ return a.is_add_on ? 1 : -1;
+ }
- const isUpgrade = isProductUpgrade({
- prices1: a.prices,
- prices2: b.prices,
- usageAlwaysUpgrade: false,
- });
+ const isUpgrade = isProductUpgrade({
+ prices1: a.prices,
+ prices2: b.prices,
+ usageAlwaysUpgrade: false,
+ });
- return isUpgrade ? -1 : 1;
- });
-};
\ No newline at end of file
+ return isUpgrade ? -1 : 1;
+ });
+};
diff --git a/server/src/internal/products/productV2Utils.ts b/server/src/internal/products/productV2Utils.ts
index 40d8cccc9..4f4b0afbd 100644
--- a/server/src/internal/products/productV2Utils.ts
+++ b/server/src/internal/products/productV2Utils.ts
@@ -1,10 +1,10 @@
import {
- EntitlementWithFeature,
- Feature,
- FullProduct,
- Price,
- ProductItem,
- ProductV2,
+ EntitlementWithFeature,
+ Feature,
+ FullProduct,
+ Price,
+ ProductItem,
+ ProductV2,
} from "@autumn/shared";
import { getEntRelatedPrice } from "./entitlements/entitlementUtils.js";
import { getPriceEntitlement } from "./prices/priceUtils.js";
@@ -12,92 +12,92 @@ import { toProductItem } from "@autumn/shared";
import { getItemFeatureType } from "@autumn/shared";
export const mapToProductItems = ({
- prices,
- entitlements,
- features,
- allowFeatureMatch = false,
+ prices,
+ entitlements,
+ features,
+ allowFeatureMatch = false,
}: {
- prices: Price[];
- entitlements: EntitlementWithFeature[];
- features: Feature[];
- allowFeatureMatch?: boolean;
+ prices: Price[];
+ entitlements: EntitlementWithFeature[];
+ features: Feature[];
+ allowFeatureMatch?: boolean;
}): ProductItem[] => {
- let items: ProductItem[] = [];
+ let items: ProductItem[] = [];
- for (const ent of entitlements) {
- let relatedPrice = getEntRelatedPrice(ent, prices, allowFeatureMatch);
- let item = toProductItem({ ent, price: relatedPrice });
- items.push(item);
- }
+ for (const ent of entitlements) {
+ let relatedPrice = getEntRelatedPrice(ent, prices, allowFeatureMatch);
+ let item = toProductItem({ ent, price: relatedPrice });
+ items.push(item);
+ }
- for (const price of prices) {
- let relatedEnt = getPriceEntitlement(
- price,
- entitlements,
- allowFeatureMatch
- );
+ for (const price of prices) {
+ let relatedEnt = getPriceEntitlement(
+ price,
+ entitlements,
+ allowFeatureMatch,
+ );
- if (!relatedEnt) {
- items.push(toProductItem({ price }));
- }
- }
+ if (!relatedEnt) {
+ items.push(toProductItem({ price }));
+ }
+ }
- for (const item of items) {
- let feature = features.find((f) => f.id == item.feature_id);
- if (feature) {
- item.feature_type = getItemFeatureType({ item, features });
- }
- }
+ for (const item of items) {
+ let feature = features.find((f) => f.id == item.feature_id);
+ if (feature) {
+ item.feature_type = getItemFeatureType({ item, features });
+ }
+ }
- return items;
+ return items;
};
export const mapToProductV2 = ({
- product,
- features,
+ product,
+ features,
}: {
- product: FullProduct;
- features: Feature[];
+ product: FullProduct;
+ features: Feature[];
}): ProductV2 => {
- let items: ProductItem[] = [];
- // console.log("Prices:", product.prices);
- // console.log("Entitlements:", product.entitlements);
+ let items: ProductItem[] = [];
+ // console.log("Prices:", product.prices);
+ // console.log("Entitlements:", product.entitlements);
- for (const ent of product.entitlements) {
- let relatedPrice = getEntRelatedPrice(ent, product.prices);
- items.push(toProductItem({ ent, price: relatedPrice }));
- }
+ for (const ent of product.entitlements) {
+ let relatedPrice = getEntRelatedPrice(ent, product.prices);
+ items.push(toProductItem({ ent, price: relatedPrice }));
+ }
- for (const price of product.prices) {
- let relatedEnt = getPriceEntitlement(price, product.entitlements);
+ for (const price of product.prices) {
+ let relatedEnt = getPriceEntitlement(price, product.entitlements);
- // console.log("Price:", price.id);
- // console.log("Related ent:", relatedEnt);
- if (!relatedEnt) {
- items.push(toProductItem({ price }));
- }
- }
+ // console.log("Price:", price.id);
+ // console.log("Related ent:", relatedEnt);
+ if (!relatedEnt) {
+ items.push(toProductItem({ price }));
+ }
+ }
- for (const item of items) {
- item.feature_type = getItemFeatureType({ item, features });
- }
+ for (const item of items) {
+ item.feature_type = getItemFeatureType({ item, features });
+ }
- let productV2: ProductV2 = {
- internal_id: product.internal_id,
+ let productV2: ProductV2 = {
+ internal_id: product.internal_id,
- id: product.id,
- name: product.name,
- is_add_on: product.is_add_on,
- is_default: product.is_default,
- version: product.version,
- group: product.group,
- free_trial: product.free_trial,
- created_at: product.created_at,
+ id: product.id,
+ name: product.name,
+ is_add_on: product.is_add_on,
+ is_default: product.is_default,
+ version: product.version,
+ group: product.group,
+ free_trial: product.free_trial,
+ created_at: product.created_at,
- items: items,
- stripe_id: product.processor?.id || null,
- archived: product.archived || false,
- };
+ items: items,
+ stripe_id: product.processor?.id || null,
+ archived: product.archived || false,
+ };
- return productV2;
+ return productV2;
};
diff --git a/server/src/internal/rewards/RewardProgramService.ts b/server/src/internal/rewards/RewardProgramService.ts
index 4b5791435..58671485a 100644
--- a/server/src/internal/rewards/RewardProgramService.ts
+++ b/server/src/internal/rewards/RewardProgramService.ts
@@ -1,300 +1,300 @@
import { and, arrayContains, count, eq, inArray, or } from "drizzle-orm";
import RecaseError from "@/utils/errorUtils.js";
import {
- ErrCode,
- Reward,
- RewardProgram,
- rewardPrograms,
- RewardTriggerEvent,
+ ErrCode,
+ Reward,
+ RewardProgram,
+ rewardPrograms,
+ RewardTriggerEvent,
} from "@autumn/shared";
import { ReferralCode } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { referralCodes, rewardRedemptions } from "@autumn/shared";
export class RewardProgramService {
- static async get({
- db,
- idOrInternalId,
- orgId,
- env,
- errorIfNotFound = false,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: string;
- errorIfNotFound?: boolean;
- }) {
- let result = await db.query.rewardPrograms.findFirst({
- where: and(
- or(
- eq(rewardPrograms.id, idOrInternalId),
- eq(rewardPrograms.internal_id, idOrInternalId)
- ),
- eq(rewardPrograms.org_id, orgId),
- eq(rewardPrograms.env, env)
- ),
- });
+ static async get({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ errorIfNotFound = false,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: string;
+ errorIfNotFound?: boolean;
+ }) {
+ let result = await db.query.rewardPrograms.findFirst({
+ where: and(
+ or(
+ eq(rewardPrograms.id, idOrInternalId),
+ eq(rewardPrograms.internal_id, idOrInternalId),
+ ),
+ eq(rewardPrograms.org_id, orgId),
+ eq(rewardPrograms.env, env),
+ ),
+ });
- if (!result) {
- if (errorIfNotFound) {
- throw new RecaseError({
- message: "Reward program not found",
- code: ErrCode.RewardNotFound,
- });
- }
+ if (!result) {
+ if (errorIfNotFound) {
+ throw new RecaseError({
+ message: "Reward program not found",
+ code: ErrCode.RewardNotFound,
+ });
+ }
- return null;
- }
+ return null;
+ }
- return result as RewardProgram;
- }
+ return result as RewardProgram;
+ }
- static async list({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: string;
- }) {
- let result = await db.query.rewardPrograms.findMany({
- where: and(eq(rewardPrograms.org_id, orgId), eq(rewardPrograms.env, env)),
- });
+ static async list({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: string;
+ }) {
+ let result = await db.query.rewardPrograms.findMany({
+ where: and(eq(rewardPrograms.org_id, orgId), eq(rewardPrograms.env, env)),
+ });
- return result as RewardProgram[];
- }
+ return result as RewardProgram[];
+ }
- static async getByProductId({
- db,
- productIds,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- productIds: string[];
- orgId: string;
- env: string;
- }) {
- let result = await db.query.rewardPrograms.findMany({
- where: and(
- eq(rewardPrograms.org_id, orgId),
- eq(rewardPrograms.env, env),
- eq(rewardPrograms.when, RewardTriggerEvent.Checkout),
- arrayContains(rewardPrograms.product_ids, productIds)
- ),
- });
+ static async getByProductId({
+ db,
+ productIds,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ productIds: string[];
+ orgId: string;
+ env: string;
+ }) {
+ let result = await db.query.rewardPrograms.findMany({
+ where: and(
+ eq(rewardPrograms.org_id, orgId),
+ eq(rewardPrograms.env, env),
+ eq(rewardPrograms.when, RewardTriggerEvent.Checkout),
+ arrayContains(rewardPrograms.product_ids, productIds),
+ ),
+ });
- return result as RewardProgram[];
- }
+ return result as RewardProgram[];
+ }
- static async getCodeByCustomerAndRewardProgram({
- db,
- orgId,
- env,
- internalCustomerId,
- internalRewardProgramId,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: string;
- internalCustomerId: string;
- internalRewardProgramId: string;
- }) {
- let result = await db.query.referralCodes.findFirst({
- where: and(
- eq(referralCodes.internal_customer_id, internalCustomerId),
- eq(referralCodes.internal_reward_program_id, internalRewardProgramId),
- eq(referralCodes.org_id, orgId),
- eq(referralCodes.env, env)
- ),
- });
+ static async getCodeByCustomerAndRewardProgram({
+ db,
+ orgId,
+ env,
+ internalCustomerId,
+ internalRewardProgramId,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: string;
+ internalCustomerId: string;
+ internalRewardProgramId: string;
+ }) {
+ let result = await db.query.referralCodes.findFirst({
+ where: and(
+ eq(referralCodes.internal_customer_id, internalCustomerId),
+ eq(referralCodes.internal_reward_program_id, internalRewardProgramId),
+ eq(referralCodes.org_id, orgId),
+ eq(referralCodes.env, env),
+ ),
+ });
- if (!result) {
- return null;
- }
+ if (!result) {
+ return null;
+ }
- return result as ReferralCode;
- }
+ return result as ReferralCode;
+ }
- static async create({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: RewardProgram | RewardProgram[];
- }) {
- let result = await db
- .insert(rewardPrograms)
- .values(data as any)
- .returning();
+ static async create({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: RewardProgram | RewardProgram[];
+ }) {
+ let result = await db
+ .insert(rewardPrograms)
+ .values(data as any)
+ .returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Failed to create reward program",
- code: ErrCode.InsertRewardProgramFailed,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Failed to create reward program",
+ code: ErrCode.InsertRewardProgramFailed,
+ });
+ }
- return result[0] as RewardProgram;
- }
+ return result[0] as RewardProgram;
+ }
- static async delete({
- db,
- idOrInternalId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: string;
- }) {
- let result = await db
- .delete(rewardPrograms)
- .where(
- and(
- or(
- eq(rewardPrograms.id, idOrInternalId),
- eq(rewardPrograms.internal_id, idOrInternalId)
- ),
- eq(rewardPrograms.org_id, orgId),
- eq(rewardPrograms.env, env)
- )
- )
- .returning();
+ static async delete({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: string;
+ }) {
+ let result = await db
+ .delete(rewardPrograms)
+ .where(
+ and(
+ or(
+ eq(rewardPrograms.id, idOrInternalId),
+ eq(rewardPrograms.internal_id, idOrInternalId),
+ ),
+ eq(rewardPrograms.org_id, orgId),
+ eq(rewardPrograms.env, env),
+ ),
+ )
+ .returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Reward program not found",
- code: ErrCode.RewardNotFound,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Reward program not found",
+ code: ErrCode.RewardNotFound,
+ });
+ }
- return result[0] as RewardProgram;
- }
+ return result[0] as RewardProgram;
+ }
- // REFERRAL CODE FUNCTIONS
- static async getReferralCode({
- db,
- orgId,
- env,
- code,
- withRewardProgram = false,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: string;
- code: string;
- withRewardProgram?: boolean;
- }) {
- let result = await db.query.referralCodes.findFirst({
- where: and(
- eq(referralCodes.code, code),
- eq(referralCodes.org_id, orgId),
- eq(referralCodes.env, env)
- ),
- with: withRewardProgram
- ? {
- reward_program: {
- with: {
- reward: true,
- },
- },
- }
- : undefined,
- });
+ // REFERRAL CODE FUNCTIONS
+ static async getReferralCode({
+ db,
+ orgId,
+ env,
+ code,
+ withRewardProgram = false,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: string;
+ code: string;
+ withRewardProgram?: boolean;
+ }) {
+ let result = await db.query.referralCodes.findFirst({
+ where: and(
+ eq(referralCodes.code, code),
+ eq(referralCodes.org_id, orgId),
+ eq(referralCodes.env, env),
+ ),
+ with: withRewardProgram
+ ? {
+ reward_program: {
+ with: {
+ reward: true,
+ },
+ },
+ }
+ : undefined,
+ });
- if (!result) {
- throw new RecaseError({
- message: "Referral code not found",
- code: ErrCode.ReferralCodeNotFound,
- statusCode: 404,
- });
- }
+ if (!result) {
+ throw new RecaseError({
+ message: "Referral code not found",
+ code: ErrCode.ReferralCodeNotFound,
+ statusCode: 404,
+ });
+ }
- return result as ReferralCode & {
- reward_program: RewardProgram & {
- reward: Reward;
- };
- };
- }
+ return result as ReferralCode & {
+ reward_program: RewardProgram & {
+ reward: Reward;
+ };
+ };
+ }
- static async createReferralCode({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: ReferralCode;
- }) {
- let result = await db.insert(referralCodes).values(data).returning();
+ static async createReferralCode({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: ReferralCode;
+ }) {
+ let result = await db.insert(referralCodes).values(data).returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Failed to create referral code",
- code: ErrCode.InsertReferralCodeFailed,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Failed to create referral code",
+ code: ErrCode.InsertReferralCodeFailed,
+ });
+ }
- return result[0] as ReferralCode;
- }
+ return result[0] as ReferralCode;
+ }
- static async getCodeRedemptionCount({
- db,
- referralCodeId,
- }: {
- db: DrizzleCli;
- referralCodeId: string;
- }) {
- let result = await db
- .select({ count: count() })
- .from(rewardRedemptions)
- .where(
- and(
- eq(rewardRedemptions.referral_code_id, referralCodeId),
- eq(rewardRedemptions.triggered, true)
- )
- );
+ static async getCodeRedemptionCount({
+ db,
+ referralCodeId,
+ }: {
+ db: DrizzleCli;
+ referralCodeId: string;
+ }) {
+ let result = await db
+ .select({ count: count() })
+ .from(rewardRedemptions)
+ .where(
+ and(
+ eq(rewardRedemptions.referral_code_id, referralCodeId),
+ eq(rewardRedemptions.triggered, true),
+ ),
+ );
- return result[0].count;
- }
+ return result[0].count;
+ }
- static async update({
- db,
- idOrInternalId,
- orgId,
- env,
- data,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: string;
- data: RewardProgram;
- }) {
- let result = await db
- .update(rewardPrograms)
- .set(data as any)
- .where(
- and(
- or(
- eq(rewardPrograms.id, idOrInternalId),
- eq(rewardPrograms.internal_id, idOrInternalId)
- ),
- eq(rewardPrograms.org_id, orgId),
- eq(rewardPrograms.env, env)
- )
- )
- .returning();
+ static async update({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ data,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: string;
+ data: RewardProgram;
+ }) {
+ let result = await db
+ .update(rewardPrograms)
+ .set(data as any)
+ .where(
+ and(
+ or(
+ eq(rewardPrograms.id, idOrInternalId),
+ eq(rewardPrograms.internal_id, idOrInternalId),
+ ),
+ eq(rewardPrograms.org_id, orgId),
+ eq(rewardPrograms.env, env),
+ ),
+ )
+ .returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: "Reward program not found",
- code: ErrCode.RewardNotFound,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: "Reward program not found",
+ code: ErrCode.RewardNotFound,
+ });
+ }
- return result[0] as RewardProgram;
- }
+ return result[0] as RewardProgram;
+ }
}
diff --git a/server/src/internal/rewards/RewardRedemptionService.ts b/server/src/internal/rewards/RewardRedemptionService.ts
index 24bc9b5f3..c9b03ab07 100644
--- a/server/src/internal/rewards/RewardRedemptionService.ts
+++ b/server/src/internal/rewards/RewardRedemptionService.ts
@@ -1,247 +1,247 @@
import {
- customers,
- ErrCode,
- type RewardRedemption,
- type RewardTriggerEvent,
- referralCodes,
- rewardPrograms,
- rewardRedemptions,
- rewards,
+ customers,
+ ErrCode,
+ type RewardRedemption,
+ type RewardTriggerEvent,
+ referralCodes,
+ rewardPrograms,
+ rewardRedemptions,
+ rewards,
} from "@autumn/shared";
import { and, eq, inArray, or } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
export class RewardRedemptionService {
- static async getById({ db, id }: { db: DrizzleCli; id: string }) {
- const data = await db.query.rewardRedemptions.findFirst({
- where: eq(rewardRedemptions.id, id),
- });
+ static async getById({ db, id }: { db: DrizzleCli; id: string }) {
+ const data = await db.query.rewardRedemptions.findFirst({
+ where: eq(rewardRedemptions.id, id),
+ });
- if (!data) {
- throw new RecaseError({
- code: ErrCode.RewardRedemptionNotFound,
- message: `Reward redemption ${id} not found`,
- statusCode: 404,
- });
- }
+ if (!data) {
+ throw new RecaseError({
+ code: ErrCode.RewardRedemptionNotFound,
+ message: `Reward redemption ${id} not found`,
+ statusCode: 404,
+ });
+ }
- return data;
- }
+ return data;
+ }
- static async getByCustomer({
- db,
- internalCustomerId,
- triggered,
- withReferralCode = false,
- withRewardProgram = false,
- internalRewardProgramId,
- triggerWhen,
- limit,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- triggered?: boolean;
- withReferralCode?: boolean;
- withRewardProgram?: boolean;
- internalRewardProgramId?: string;
- triggerWhen?: RewardTriggerEvent;
- limit?: number;
- }) {
- const data = await db.query.rewardRedemptions.findMany({
- where: and(
- eq(rewardRedemptions.internal_customer_id, internalCustomerId),
- internalRewardProgramId
- ? eq(
- rewardRedemptions.internal_reward_program_id,
- internalRewardProgramId
- )
- : undefined,
- triggered ? eq(rewardRedemptions.triggered, triggered) : undefined
- ),
- with: {
- reward_program: {
- with: {
- reward: true,
- },
- },
- referral_code: true,
- },
- limit: limit ?? 100,
- });
+ static async getByCustomer({
+ db,
+ internalCustomerId,
+ triggered,
+ withReferralCode = false,
+ withRewardProgram = false,
+ internalRewardProgramId,
+ triggerWhen,
+ limit,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ triggered?: boolean;
+ withReferralCode?: boolean;
+ withRewardProgram?: boolean;
+ internalRewardProgramId?: string;
+ triggerWhen?: RewardTriggerEvent;
+ limit?: number;
+ }) {
+ const data = await db.query.rewardRedemptions.findMany({
+ where: and(
+ eq(rewardRedemptions.internal_customer_id, internalCustomerId),
+ internalRewardProgramId
+ ? eq(
+ rewardRedemptions.internal_reward_program_id,
+ internalRewardProgramId,
+ )
+ : undefined,
+ triggered ? eq(rewardRedemptions.triggered, triggered) : undefined,
+ ),
+ with: {
+ reward_program: {
+ with: {
+ reward: true,
+ },
+ },
+ referral_code: true,
+ },
+ limit: limit ?? 100,
+ });
- return data as any;
- }
+ return data as any;
+ }
- static async getByReferrer({
- db,
- internalCustomerId,
- withCustomer = false,
- limit = 100,
- withRewardProgram = false,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- withCustomer?: boolean;
- limit?: number;
- withRewardProgram?: boolean;
- }) {
- let query = db
- .select()
- .from(rewardRedemptions)
- .innerJoin(
- referralCodes,
- eq(rewardRedemptions.referral_code_id, referralCodes.id)
- )
- .innerJoin(
- customers,
- eq(rewardRedemptions.internal_customer_id, customers.internal_id)
- );
+ static async getByReferrer({
+ db,
+ internalCustomerId,
+ withCustomer = false,
+ limit = 100,
+ withRewardProgram = false,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ withCustomer?: boolean;
+ limit?: number;
+ withRewardProgram?: boolean;
+ }) {
+ let query = db
+ .select()
+ .from(rewardRedemptions)
+ .innerJoin(
+ referralCodes,
+ eq(rewardRedemptions.referral_code_id, referralCodes.id),
+ )
+ .innerJoin(
+ customers,
+ eq(rewardRedemptions.internal_customer_id, customers.internal_id),
+ );
- if (withRewardProgram) {
- query = query.innerJoin(
- rewardPrograms,
- eq(
- rewardRedemptions.internal_reward_program_id,
- rewardPrograms.internal_id
- )
- );
- }
- const data = await query
- .where(eq(referralCodes.internal_customer_id, internalCustomerId))
- .limit(limit);
+ if (withRewardProgram) {
+ query = query.innerJoin(
+ rewardPrograms,
+ eq(
+ rewardRedemptions.internal_reward_program_id,
+ rewardPrograms.internal_id,
+ ),
+ );
+ }
+ const data = await query
+ .where(eq(referralCodes.internal_customer_id, internalCustomerId))
+ .limit(limit);
- const processed = data.map((d) => ({
- ...d.reward_redemptions,
- referral_code: d.referral_codes,
- customer: d.customers,
- reward_program: withRewardProgram
- ? (d as any).reward_programs
- : undefined,
- }));
+ const processed = data.map((d) => ({
+ ...d.reward_redemptions,
+ referral_code: d.referral_codes,
+ customer: d.customers,
+ reward_program: withRewardProgram
+ ? (d as any).reward_programs
+ : undefined,
+ }));
- return processed;
- }
+ return processed;
+ }
- static async insert({
- db,
- rewardRedemption,
- }: {
- db: DrizzleCli;
- rewardRedemption: RewardRedemption;
- }) {
- const data = await db
- .insert(rewardRedemptions)
- .values(rewardRedemption)
- .returning();
+ static async insert({
+ db,
+ rewardRedemption,
+ }: {
+ db: DrizzleCli;
+ rewardRedemption: RewardRedemption;
+ }) {
+ const data = await db
+ .insert(rewardRedemptions)
+ .values(rewardRedemption)
+ .returning();
- if (data.length === 0) {
- throw new RecaseError({
- code: ErrCode.InsertRewardRedemptionFailed,
- message: `Failed to insert reward redemption`,
- statusCode: 500,
- });
- }
+ if (data.length === 0) {
+ throw new RecaseError({
+ code: ErrCode.InsertRewardRedemptionFailed,
+ message: `Failed to insert reward redemption`,
+ statusCode: 500,
+ });
+ }
- return data[0] as RewardRedemption;
- }
+ return data[0] as RewardRedemption;
+ }
- static async update({
- db,
- id,
- updates,
- }: {
- db: DrizzleCli;
- id: string;
- updates: any;
- }) {
- const data = await db
- .update(rewardRedemptions)
- .set(updates)
- .where(eq(rewardRedemptions.id, id))
- .returning();
+ static async update({
+ db,
+ id,
+ updates,
+ }: {
+ db: DrizzleCli;
+ id: string;
+ updates: any;
+ }) {
+ const data = await db
+ .update(rewardRedemptions)
+ .set(updates)
+ .where(eq(rewardRedemptions.id, id))
+ .returning();
- if (data.length === 0) {
- throw new RecaseError({
- code: "REWARD_REDEMPTION_NOT_FOUND",
- message: `Reward redemption ${id} not found`,
- });
- }
+ if (data.length === 0) {
+ throw new RecaseError({
+ code: "REWARD_REDEMPTION_NOT_FOUND",
+ message: `Reward redemption ${id} not found`,
+ });
+ }
- return data[0] as RewardRedemption;
- }
+ return data[0] as RewardRedemption;
+ }
- static async getUnappliedRedemptions({
- db,
- internalCustomerId,
- }: {
- db: DrizzleCli;
- internalCustomerId: string;
- }) {
- const data = await db
- .select()
- .from(rewardRedemptions)
- .innerJoin(
- referralCodes,
- eq(rewardRedemptions.referral_code_id, referralCodes.id)
- )
- .innerJoin(
- customers,
- eq(rewardRedemptions.internal_customer_id, customers.internal_id)
- )
- .innerJoin(
- rewardPrograms,
- eq(
- rewardRedemptions.internal_reward_program_id,
- rewardPrograms.internal_id
- )
- )
- .innerJoin(
- rewards,
- eq(rewardPrograms.internal_reward_id, rewards.internal_id)
- )
- .where(
- or(
- and(
- eq(referralCodes.internal_customer_id, internalCustomerId),
- eq(rewardRedemptions.triggered, true),
- eq(rewardRedemptions.applied, false)
- ),
- and(
- eq(rewardRedemptions.internal_customer_id, internalCustomerId),
- eq(rewardRedemptions.triggered, true),
- eq(rewardRedemptions.redeemer_applied, false)
- )
- )
- );
+ static async getUnappliedRedemptions({
+ db,
+ internalCustomerId,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string;
+ }) {
+ const data = await db
+ .select()
+ .from(rewardRedemptions)
+ .innerJoin(
+ referralCodes,
+ eq(rewardRedemptions.referral_code_id, referralCodes.id),
+ )
+ .innerJoin(
+ customers,
+ eq(rewardRedemptions.internal_customer_id, customers.internal_id),
+ )
+ .innerJoin(
+ rewardPrograms,
+ eq(
+ rewardRedemptions.internal_reward_program_id,
+ rewardPrograms.internal_id,
+ ),
+ )
+ .innerJoin(
+ rewards,
+ eq(rewardPrograms.internal_reward_id, rewards.internal_id),
+ )
+ .where(
+ or(
+ and(
+ eq(referralCodes.internal_customer_id, internalCustomerId),
+ eq(rewardRedemptions.triggered, true),
+ eq(rewardRedemptions.applied, false),
+ ),
+ and(
+ eq(rewardRedemptions.internal_customer_id, internalCustomerId),
+ eq(rewardRedemptions.triggered, true),
+ eq(rewardRedemptions.redeemer_applied, false),
+ ),
+ ),
+ );
- if (data.length === 0) return [];
+ if (data.length === 0) return [];
- const processed = data.map((d) => ({
- ...d.reward_redemptions,
- referral_code: d.referral_codes,
- reward_program: {
- ...d.reward_programs,
- reward: d.rewards,
- },
- }));
+ const processed = data.map((d) => ({
+ ...d.reward_redemptions,
+ referral_code: d.referral_codes,
+ reward_program: {
+ ...d.reward_programs,
+ reward: d.rewards,
+ },
+ }));
- return processed;
- }
+ return processed;
+ }
- static async _resetCustomerRedemptions({
- db,
- internalCustomerId,
- }: {
- db: DrizzleCli;
- internalCustomerId: string | string[];
- }) {
- if (!Array.isArray(internalCustomerId))
- internalCustomerId = [internalCustomerId];
- return await db
- .delete(rewardRedemptions)
- .where(
- inArray(rewardRedemptions.internal_customer_id, internalCustomerId)
- );
- }
+ static async _resetCustomerRedemptions({
+ db,
+ internalCustomerId,
+ }: {
+ db: DrizzleCli;
+ internalCustomerId: string | string[];
+ }) {
+ if (!Array.isArray(internalCustomerId))
+ internalCustomerId = [internalCustomerId];
+ return await db
+ .delete(rewardRedemptions)
+ .where(
+ inArray(rewardRedemptions.internal_customer_id, internalCustomerId),
+ );
+ }
}
diff --git a/server/src/internal/rewards/RewardService.ts b/server/src/internal/rewards/RewardService.ts
index ff7e417b3..fe33b2ce2 100644
--- a/server/src/internal/rewards/RewardService.ts
+++ b/server/src/internal/rewards/RewardService.ts
@@ -1,176 +1,176 @@
import {
- type AppEnv,
- ErrCode,
- type Reward,
- rewards,
- RewardType,
+ type AppEnv,
+ ErrCode,
+ type Reward,
+ rewards,
+ RewardType,
} from "@autumn/shared";
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
export class RewardService {
- static async get({
- db,
- idOrInternalId,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- idOrInternalId: string;
- orgId: string;
- env: AppEnv;
- }) {
- const result = await db.query.rewards.findFirst({
- where: and(
- or(
- eq(rewards.id, idOrInternalId),
- eq(rewards.internal_id, idOrInternalId)
- ),
- eq(rewards.org_id, orgId),
- eq(rewards.env, env)
- ),
- });
+ static async get({
+ db,
+ idOrInternalId,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ idOrInternalId: string;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const result = await db.query.rewards.findFirst({
+ where: and(
+ or(
+ eq(rewards.id, idOrInternalId),
+ eq(rewards.internal_id, idOrInternalId),
+ ),
+ eq(rewards.org_id, orgId),
+ eq(rewards.env, env),
+ ),
+ });
- if (!result) {
- return null;
- }
+ if (!result) {
+ return null;
+ }
- return result as Reward;
- }
+ return result as Reward;
+ }
- static async getByIdOrCode({
- db,
- codes,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- codes: string[];
- orgId: string;
- env: AppEnv;
- }) {
- const reward = await db.query.rewards.findMany({
- where: and(
- eq(rewards.org_id, orgId),
- eq(rewards.env, env),
- or(
- inArray(rewards.id, codes),
- ...codes.map(
- (code) => sql`EXISTS (
+ static async getByIdOrCode({
+ db,
+ codes,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ codes: string[];
+ orgId: string;
+ env: AppEnv;
+ }) {
+ const reward = await db.query.rewards.findMany({
+ where: and(
+ eq(rewards.org_id, orgId),
+ eq(rewards.env, env),
+ or(
+ inArray(rewards.id, codes),
+ ...codes.map(
+ (code) => sql`EXISTS (
SELECT 1 FROM unnest("promo_codes") AS elem
WHERE elem->>'code' = ${code}
- )`
- )
- )
- ),
- });
+ )`,
+ ),
+ ),
+ ),
+ });
- return reward as Reward[];
- }
+ return reward as Reward[];
+ }
- static async insert({
- db,
- data,
- }: {
- db: DrizzleCli;
- data: Reward | Reward[];
- }) {
- const results = await db.insert(rewards).values(data as Reward);
- return results as Reward[];
- }
+ static async insert({
+ db,
+ data,
+ }: {
+ db: DrizzleCli;
+ data: Reward | Reward[];
+ }) {
+ const results = await db.insert(rewards).values(data as Reward);
+ return results as Reward[];
+ }
- static async list({
- db,
- orgId,
- env,
- inTypes,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- inTypes?: RewardType[];
- }) {
- const results = await db.query.rewards.findMany({
- where: and(
- eq(rewards.org_id, orgId),
- eq(rewards.env, env),
- inTypes ? inArray(rewards.type, inTypes) : undefined
- ),
- orderBy: [desc(rewards.internal_id)],
- });
+ static async list({
+ db,
+ orgId,
+ env,
+ inTypes,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ inTypes?: RewardType[];
+ }) {
+ const results = await db.query.rewards.findMany({
+ where: and(
+ eq(rewards.org_id, orgId),
+ eq(rewards.env, env),
+ inTypes ? inArray(rewards.type, inTypes) : undefined,
+ ),
+ orderBy: [desc(rewards.internal_id)],
+ });
- return results as Reward[];
- }
+ return results as Reward[];
+ }
- static async delete({
- db,
- internalId,
- env,
- orgId,
- }: {
- db: DrizzleCli;
- internalId: string;
- env: AppEnv;
- orgId: string;
- }) {
- await db
- .delete(rewards)
- .where(
- and(
- eq(rewards.internal_id, internalId),
- eq(rewards.env, env),
- eq(rewards.org_id, orgId)
- )
- );
- }
+ static async delete({
+ db,
+ internalId,
+ env,
+ orgId,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ env: AppEnv;
+ orgId: string;
+ }) {
+ await db
+ .delete(rewards)
+ .where(
+ and(
+ eq(rewards.internal_id, internalId),
+ eq(rewards.env, env),
+ eq(rewards.org_id, orgId),
+ ),
+ );
+ }
- static async update({
- db,
- internalId,
- env,
- orgId,
- update,
- }: {
- db: DrizzleCli;
- internalId: string;
- env: AppEnv;
- orgId: string;
- update: Partial;
- }) {
- const result = await db
- .update(rewards)
- .set(update)
- .where(
- and(
- eq(rewards.internal_id, internalId),
- eq(rewards.env, env),
- eq(rewards.org_id, orgId)
- )
- )
- .returning();
+ static async update({
+ db,
+ internalId,
+ env,
+ orgId,
+ update,
+ }: {
+ db: DrizzleCli;
+ internalId: string;
+ env: AppEnv;
+ orgId: string;
+ update: Partial;
+ }) {
+ const result = await db
+ .update(rewards)
+ .set(update)
+ .where(
+ and(
+ eq(rewards.internal_id, internalId),
+ eq(rewards.env, env),
+ eq(rewards.org_id, orgId),
+ ),
+ )
+ .returning();
- if (result.length === 0) {
- throw new RecaseError({
- message: `Reward ${internalId} not found`,
- code: ErrCode.InvalidRequest,
- });
- }
+ if (result.length === 0) {
+ throw new RecaseError({
+ message: `Reward ${internalId} not found`,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- return result[0] as Reward;
- }
+ return result[0] as Reward;
+ }
- static async deleteByOrgId({
- db,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- }) {
- await db
- .delete(rewards)
- .where(and(eq(rewards.org_id, orgId), eq(rewards.env, env)));
- }
+ static async deleteByOrgId({
+ db,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ }) {
+ await db
+ .delete(rewards)
+ .where(and(eq(rewards.org_id, orgId), eq(rewards.env, env)));
+ }
}
diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts
index 5f6eb0a40..88ca95884 100644
--- a/server/src/internal/rewards/referralUtils.ts
+++ b/server/src/internal/rewards/referralUtils.ts
@@ -1,14 +1,14 @@
import {
- type AppEnv,
- AttachBranch,
- type Customer,
- ErrCode,
- type FullRewardProgram,
- type ReferralCode,
- type Reward,
- RewardProgram,
- RewardReceivedBy,
- type RewardRedemption,
+ type AppEnv,
+ AttachBranch,
+ type Customer,
+ ErrCode,
+ type FullRewardProgram,
+ type ReferralCode,
+ type Reward,
+ RewardProgram,
+ RewardReceivedBy,
+ type RewardRedemption,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type Stripe from "stripe";
@@ -26,145 +26,145 @@ import { RewardProgramService } from "./RewardProgramService.js";
import type { InsertCusProductParams } from "../customers/cusProducts/AttachParams.js";
import { ProductService } from "../products/ProductService.js";
import {
- isFreeProduct,
- isOneOff,
- itemsAreOneOff,
+ isFreeProduct,
+ isOneOff,
+ itemsAreOneOff,
} from "../products/productUtils.js";
import { RewardRedemptionService } from "./RewardRedemptionService.js";
import {
- receivedByRedeemer,
- receivedByReferrer,
- triggerFreePaidProduct,
+ receivedByRedeemer,
+ receivedByReferrer,
+ triggerFreePaidProduct,
} from "./referralUtils/triggerFreePaidProduct.js";
export const ReferralResponseCodes = {
- OwnsProduct: "has_product_already",
- Success: "success",
- Unknown: "unknown",
- NotConfigured: "not_configured",
- InternalError: "internal_error",
+ OwnsProduct: "has_product_already",
+ Success: "success",
+ Unknown: "unknown",
+ NotConfigured: "not_configured",
+ InternalError: "internal_error",
};
export const generateReferralCode = () => {
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- const codeLength = 6;
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ const codeLength = 6;
- let code = "";
+ let code = "";
- for (let i = 0; i < codeLength; i++) {
- code += chars.charAt(Math.floor(Math.random() * chars.length));
- }
+ for (let i = 0; i < codeLength; i++) {
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
- return code;
+ return code;
};
// Trigger reward
export const triggerRedemption = async ({
- db,
- referralCode,
- org,
- env,
- logger,
- reward,
- redemption,
+ db,
+ referralCode,
+ org,
+ env,
+ logger,
+ reward,
+ redemption,
}: {
- db: DrizzleCli;
- org: any;
- env: AppEnv;
- logger: any;
- referralCode: ReferralCode & { reward_program: RewardProgram };
- reward: Reward;
- redemption: RewardRedemption;
+ db: DrizzleCli;
+ org: any;
+ env: AppEnv;
+ logger: any;
+ referralCode: ReferralCode & { reward_program: RewardProgram };
+ reward: Reward;
+ redemption: RewardRedemption;
}) => {
- logger.info(
- `Triggering redemption ${redemption.id} for referral code ${referralCode.code}`
- );
+ logger.info(
+ `Triggering redemption ${redemption.id} for referral code ${referralCode.code}`,
+ );
- const referrer = await CusService.getByInternalId({
- db,
- internalId: referralCode.internal_customer_id,
- });
+ const referrer = await CusService.getByInternalId({
+ db,
+ internalId: referralCode.internal_customer_id,
+ });
- const redeemer = await CusService.getByInternalId({
- db,
- internalId: redemption.internal_customer_id,
- });
+ const redeemer = await CusService.getByInternalId({
+ db,
+ internalId: redemption.internal_customer_id,
+ });
- const rewardProgram = referralCode.reward_program;
+ const rewardProgram = referralCode.reward_program;
- if (!rewardProgram) {
- throw new RecaseError({
- message: `Reward program ${referralCode.internal_reward_program_id} not found`,
- code: ErrCode.RewardProgramNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!rewardProgram) {
+ throw new RecaseError({
+ message: `Reward program ${referralCode.internal_reward_program_id} not found`,
+ code: ErrCode.RewardProgramNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let applied = false;
- let redeemerApplied = false;
- for (let i = 0; i < 2; i++) {
- let customer = i === 0 ? referrer : redeemer;
+ let applied = false;
+ let redeemerApplied = false;
+ for (let i = 0; i < 2; i++) {
+ let customer = i === 0 ? referrer : redeemer;
- if (i === 0 && !receivedByReferrer(rewardProgram.received_by)) {
- continue;
- } else if (i === 1 && !receivedByRedeemer(rewardProgram.received_by)) {
- continue;
- }
+ if (i === 0 && !receivedByReferrer(rewardProgram.received_by)) {
+ continue;
+ } else if (i === 1 && !receivedByRedeemer(rewardProgram.received_by)) {
+ continue;
+ }
- if (!customer) {
- throw new RecaseError({
- message: `Customer ${i === 0 ? "referrer" : "redeemer"} not found`,
- code: ErrCode.CustomerNotFound,
- statusCode: StatusCodes.NOT_FOUND,
- });
- }
+ if (!customer) {
+ throw new RecaseError({
+ message: `Customer ${i === 0 ? "referrer" : "redeemer"} not found`,
+ code: ErrCode.CustomerNotFound,
+ statusCode: StatusCodes.NOT_FOUND,
+ });
+ }
- let stripeCli = createStripeCli({
- org,
- env,
- legacyVersion: true,
- });
+ let stripeCli = createStripeCli({
+ org,
+ env,
+ legacyVersion: true,
+ });
- await createStripeCusIfNotExists({
- db,
- customer: customer,
- org,
- env,
- logger,
- });
+ await createStripeCusIfNotExists({
+ db,
+ customer: customer,
+ org,
+ env,
+ logger,
+ });
- let stripeCusId = customer.processor.id;
- let stripeCus = (await stripeCli.customers.retrieve(
- stripeCusId
- )) as Stripe.Customer;
+ let stripeCusId = customer.processor.id;
+ let stripeCus = (await stripeCli.customers.retrieve(
+ stripeCusId,
+ )) as Stripe.Customer;
- if (!stripeCus.discount) {
- await stripeCli.customers.update(stripeCusId, {
- // @ts-ignore
- coupon: reward.id,
- });
+ if (!stripeCus.discount) {
+ await stripeCli.customers.update(stripeCusId, {
+ // @ts-ignore
+ coupon: reward.id,
+ });
- if (i === 0) {
- applied = true;
- } else {
- redeemerApplied = true;
- }
+ if (i === 0) {
+ applied = true;
+ } else {
+ redeemerApplied = true;
+ }
- logger.info(`Applied coupon to customer in Stripe`);
- }
- }
+ logger.info(`Applied coupon to customer in Stripe`);
+ }
+ }
- let updatedRedemption = await RewardRedemptionService.update({
- db,
- id: redemption.id,
- updates: {
- applied,
- redeemer_applied: redeemerApplied,
- triggered: true,
- },
- });
+ let updatedRedemption = await RewardRedemptionService.update({
+ db,
+ id: redemption.id,
+ updates: {
+ applied,
+ redeemer_applied: redeemerApplied,
+ triggered: true,
+ },
+ });
- logger.info(`Successfully triggered redemption, applied: ${applied}`);
+ logger.info(`Successfully triggered redemption, applied: ${applied}`);
- return updatedRedemption;
+ return updatedRedemption;
};
diff --git a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts
index 1e2350a89..00b2c3f17 100644
--- a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts
+++ b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts
@@ -1,13 +1,13 @@
import {
- AttachBranch,
- type Customer,
- ErrCode,
- type FullProduct,
- type FullRewardProgram,
- type ReferralCode,
- type Reward,
- RewardReceivedBy,
- type RewardRedemption,
+ AttachBranch,
+ type Customer,
+ ErrCode,
+ type FullProduct,
+ type FullRewardProgram,
+ type ReferralCode,
+ type Reward,
+ RewardReceivedBy,
+ type RewardRedemption,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type Stripe from "stripe";
diff --git a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts
index fc10fc6b6..2804630ae 100644
--- a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts
+++ b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts
@@ -1,12 +1,12 @@
import {
- type AppEnv,
- type Customer,
- ErrCode,
- type FullRewardProgram,
- type ReferralCode,
- type Reward,
- RewardReceivedBy,
- type RewardRedemption,
+ type AppEnv,
+ type Customer,
+ ErrCode,
+ type FullRewardProgram,
+ type ReferralCode,
+ type Reward,
+ RewardReceivedBy,
+ type RewardRedemption,
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
diff --git a/server/src/internal/rewards/rewardTriggerUtils.ts b/server/src/internal/rewards/rewardTriggerUtils.ts
index 23508f0ff..ff9270600 100644
--- a/server/src/internal/rewards/rewardTriggerUtils.ts
+++ b/server/src/internal/rewards/rewardTriggerUtils.ts
@@ -2,22 +2,22 @@ import { generateId } from "@/utils/genUtils.js";
import { CreateRewardProgram, RewardProgram } from "@autumn/shared";
export const constructRewardProgram = ({
- rewardProgramData,
- orgId,
- env,
+ rewardProgramData,
+ orgId,
+ env,
}: {
- rewardProgramData: CreateRewardProgram;
- orgId: string;
- env: string;
+ rewardProgramData: CreateRewardProgram;
+ orgId: string;
+ env: string;
}) => {
- let rewardProgram: RewardProgram = {
- ...rewardProgramData,
- internal_id: generateId("rs"),
- unlimited_redemptions: false,
- created_at: Date.now(),
- org_id: orgId,
- env,
- };
+ let rewardProgram: RewardProgram = {
+ ...rewardProgramData,
+ internal_id: generateId("rs"),
+ unlimited_redemptions: false,
+ created_at: Date.now(),
+ org_id: orgId,
+ env,
+ };
- return rewardProgram;
+ return rewardProgram;
};
diff --git a/server/src/internal/rewards/rewardUtils.ts b/server/src/internal/rewards/rewardUtils.ts
index 94e6ba998..bbf8f9770 100644
--- a/server/src/internal/rewards/rewardUtils.ts
+++ b/server/src/internal/rewards/rewardUtils.ts
@@ -1,16 +1,16 @@
import RecaseError from "@/utils/errorUtils.js";
import { generateId, getUnique, nullish } from "@/utils/genUtils.js";
import {
- Reward,
- CreateReward,
- RewardType,
- RewardCategory,
- ErrCode,
- DiscountConfigSchema,
- Price,
- Organization,
- AppEnv,
- Product,
+ Reward,
+ CreateReward,
+ RewardType,
+ RewardCategory,
+ ErrCode,
+ DiscountConfigSchema,
+ Price,
+ Organization,
+ AppEnv,
+ Product,
} from "@autumn/shared";
import { ProductService } from "../products/ProductService.js";
@@ -22,279 +22,279 @@ import { isFixedPrice } from "../products/prices/priceUtils/usagePriceUtils/clas
import { formatPrice } from "../products/prices/priceUtils.js";
export const constructReward = ({
- internalId,
- reward,
- orgId,
- env,
+ internalId,
+ reward,
+ orgId,
+ env,
}: {
- internalId?: string;
- reward: CreateReward;
- orgId: string;
- env: string;
+ internalId?: string;
+ reward: CreateReward;
+ orgId: string;
+ env: string;
}) => {
- if (!reward.id || !reward.name) {
- throw new RecaseError({
- message: "Reward ID and name are required",
- code: ErrCode.InvalidReward,
- });
- }
+ if (!reward.id || !reward.name) {
+ throw new RecaseError({
+ message: "Reward ID and name are required",
+ code: ErrCode.InvalidReward,
+ });
+ }
- if (reward.type === RewardType.FreeProduct && !reward.free_product_id) {
- throw new RecaseError({
- message: "Select a free product",
- code: ErrCode.InvalidReward,
- });
- }
+ if (reward.type === RewardType.FreeProduct && !reward.free_product_id) {
+ throw new RecaseError({
+ message: "Select a free product",
+ code: ErrCode.InvalidReward,
+ });
+ }
- if (getRewardCat(reward as Reward) === RewardCategory.Discount) {
- DiscountConfigSchema.parse(reward.discount_config);
- }
+ if (getRewardCat(reward as Reward) === RewardCategory.Discount) {
+ DiscountConfigSchema.parse(reward.discount_config);
+ }
- let promoCodes = reward.promo_codes.filter((promoCode) => {
- return promoCode.code.length > 0;
- });
+ let promoCodes = reward.promo_codes.filter((promoCode) => {
+ return promoCode.code.length > 0;
+ });
- let configData = {};
- if (reward.type === RewardType.FreeProduct) {
- configData = {
- free_product_id: reward.free_product_id,
- discount_config: null,
- };
- } else if (reward.type === RewardType.PercentageDiscount) {
- configData = {
- discount_config: reward.discount_config,
- free_product_id: null,
- };
- }
+ let configData = {};
+ if (reward.type === RewardType.FreeProduct) {
+ configData = {
+ free_product_id: reward.free_product_id,
+ discount_config: null,
+ };
+ } else if (reward.type === RewardType.PercentageDiscount) {
+ configData = {
+ discount_config: reward.discount_config,
+ free_product_id: null,
+ };
+ }
- let newReward = {
- ...reward,
- ...configData,
- internal_id: internalId || generateId("rew"),
- created_at: Date.now(),
- org_id: orgId,
- env,
- promo_codes: promoCodes,
- };
+ let newReward = {
+ ...reward,
+ ...configData,
+ internal_id: internalId || generateId("rew"),
+ created_at: Date.now(),
+ org_id: orgId,
+ env,
+ promo_codes: promoCodes,
+ };
- return newReward as Reward;
+ return newReward as Reward;
};
export const getRewardCat = (reward: Reward) => {
- if (reward.type === RewardType.FreeProduct) {
- return RewardCategory.FreeProduct;
- }
- return RewardCategory.Discount;
+ if (reward.type === RewardType.FreeProduct) {
+ return RewardCategory.FreeProduct;
+ }
+ return RewardCategory.Discount;
};
export enum CouponType {
- AddInvoiceBalance = "add_invoice_balance",
- AddBillingCredits = "add_billing_credits",
- Standard = "standard",
+ AddInvoiceBalance = "add_invoice_balance",
+ AddBillingCredits = "add_billing_credits",
+ Standard = "standard",
}
export const getCouponType = (reward: Reward) => {
- if (!reward) return null;
+ if (!reward) return null;
- let config = reward.discount_config;
- if (nullish(config)) {
- return null;
- }
+ let config = reward.discount_config;
+ if (nullish(config)) {
+ return null;
+ }
- if (config!.apply_to_all && config!.should_rollover) {
- return CouponType.AddInvoiceBalance;
- } else if (config!.should_rollover) {
- return CouponType.AddBillingCredits;
- }
- return CouponType.Standard;
+ if (config!.apply_to_all && config!.should_rollover) {
+ return CouponType.AddInvoiceBalance;
+ } else if (config!.should_rollover) {
+ return CouponType.AddBillingCredits;
+ }
+ return CouponType.Standard;
};
export const getOriginalCouponId = (couponId: string) => {
- if (!couponId) return null;
- const index = couponId.indexOf("_roll_");
- if (index !== -1) {
- return couponId.substring(0, index);
- }
- return couponId;
+ if (!couponId) return null;
+ const index = couponId.indexOf("_roll_");
+ if (index !== -1) {
+ return couponId.substring(0, index);
+ }
+ return couponId;
};
export const initRewardStripePrices = async ({
- db,
- prices,
- org,
- env,
- logger,
+ db,
+ prices,
+ org,
+ env,
+ logger,
}: {
- db: DrizzleCli;
- prices: (Price & { product: Product })[];
- org: Organization;
- env: AppEnv;
- logger: any;
+ db: DrizzleCli;
+ prices: (Price & { product: Product })[];
+ org: Organization;
+ env: AppEnv;
+ logger: any;
}) => {
- let pricesToInit = prices.map((p: Price) =>
- nullish(p.config.stripe_price_id)
- );
+ let pricesToInit = prices.map((p: Price) =>
+ nullish(p.config.stripe_price_id),
+ );
- if (pricesToInit.length === 0) {
- return;
- }
+ if (pricesToInit.length === 0) {
+ return;
+ }
- let internalProductIds = getUnique(
- prices.map((p: Price) => p.internal_product_id)
- );
- let products = await ProductService.listByInternalIds({
- db,
- internalIds: internalProductIds,
- });
+ let internalProductIds = getUnique(
+ prices.map((p: Price) => p.internal_product_id),
+ );
+ let products = await ProductService.listByInternalIds({
+ db,
+ internalIds: internalProductIds,
+ });
- const batchInit: Promise[] = [];
- for (const product of products) {
- batchInit.push(
- initProductInStripe({
- db,
- product,
- org,
- env,
- logger,
- })
- );
- }
- await Promise.all(batchInit);
+ const batchInit: Promise[] = [];
+ for (const product of products) {
+ batchInit.push(
+ initProductInStripe({
+ db,
+ product,
+ org,
+ env,
+ logger,
+ }),
+ );
+ }
+ await Promise.all(batchInit);
- for (const price of prices) {
- let product = products.find(
- (p) => p.internal_id === price.internal_product_id
- );
+ for (const price of prices) {
+ let product = products.find(
+ (p) => p.internal_id === price.internal_product_id,
+ );
- price.product = product as Product;
- }
- return;
+ price.product = product as Product;
+ }
+ return;
};
export const formatReward = ({ reward }: { reward: Reward }) => {
- if (!reward) return "";
- const discountString =
- reward.type == RewardType.PercentageDiscount
- ? `${reward.discount_config?.discount_value}%`
- : `${reward.discount_config?.discount_value} off`;
+ if (!reward) return "";
+ const discountString =
+ reward.type == RewardType.PercentageDiscount
+ ? `${reward.discount_config?.discount_value}%`
+ : `${reward.discount_config?.discount_value} off`;
- if (reward.discount_config?.apply_to_all) {
- return `${discountString} off all products`;
- } else if (reward.discount_config?.price_ids) {
- return `${discountString} off prices: ${reward.discount_config?.price_ids.join(", ")}`;
- }
+ if (reward.discount_config?.apply_to_all) {
+ return `${discountString} off all products`;
+ } else if (reward.discount_config?.price_ids) {
+ return `${discountString} off prices: ${reward.discount_config?.price_ids.join(", ")}`;
+ }
- return discountString;
+ return discountString;
};
export const getAmountAfterReward = ({
- amount,
- reward,
- subDiscounts,
+ amount,
+ reward,
+ subDiscounts,
}: {
- amount: number;
- reward: Reward;
- subDiscounts: Stripe.Discount[];
+ amount: number;
+ reward: Reward;
+ subDiscounts: Stripe.Discount[];
}) => {
- if (subDiscounts.find((d) => d.coupon?.id === reward.id)) {
- return amount;
- }
+ if (subDiscounts.find((d) => d.coupon?.id === reward.id)) {
+ return amount;
+ }
- if (reward.type === RewardType.PercentageDiscount) {
- const discountValue = new Decimal(
- reward.discount_config?.discount_value ?? 0
- );
- const discountRatio = new Decimal(1).minus(discountValue.div(100));
- return new Decimal(amount).mul(discountRatio).toNumber();
- } else if (reward.type === RewardType.FixedDiscount) {
- const discountAmount = new Decimal(
- reward.discount_config?.discount_value ?? 0
- );
- return new Decimal(amount).minus(discountAmount).toNumber();
- }
- return amount;
+ if (reward.type === RewardType.PercentageDiscount) {
+ const discountValue = new Decimal(
+ reward.discount_config?.discount_value ?? 0,
+ );
+ const discountRatio = new Decimal(1).minus(discountValue.div(100));
+ return new Decimal(amount).mul(discountRatio).toNumber();
+ } else if (reward.type === RewardType.FixedDiscount) {
+ const discountAmount = new Decimal(
+ reward.discount_config?.discount_value ?? 0,
+ );
+ return new Decimal(amount).minus(discountAmount).toNumber();
+ }
+ return amount;
};
export const discountAppliesToPrice = ({
- discount,
- product,
- price,
+ discount,
+ product,
+ price,
}: {
- discount: Stripe.Discount;
- product: Product;
- price: Price;
+ discount: Stripe.Discount;
+ product: Product;
+ price: Price;
}) => {
- const appliesTo = discount.coupon?.applies_to?.products;
+ const appliesTo = discount.coupon?.applies_to?.products;
- if (nullish(appliesTo)) return true;
+ if (nullish(appliesTo)) return true;
- if (isFixedPrice({ price })) {
- return appliesTo!.some(
- (stripeProdId) => stripeProdId === product.processor?.id
- );
- }
+ if (isFixedPrice({ price })) {
+ return appliesTo!.some(
+ (stripeProdId) => stripeProdId === product.processor?.id,
+ );
+ }
- return appliesTo!.some(
- (stripeProdId) => stripeProdId === price.config.stripe_product_id
- );
+ return appliesTo!.some(
+ (stripeProdId) => stripeProdId === price.config.stripe_product_id,
+ );
};
export const getUnusedAmountAfterDiscount = ({
- amount,
- discountAmounts,
- ratio,
+ amount,
+ discountAmounts,
+ ratio,
}: {
- amount: number;
- discountAmounts: any[];
- ratio: number;
+ amount: number;
+ discountAmounts: any[];
+ ratio: number;
}) => {
- let amountAfterDiscount = Math.abs(amount);
+ let amountAfterDiscount = Math.abs(amount);
- for (const discountAmount of discountAmounts) {
- const appliedDiscount = new Decimal(discountAmount.amount || 0)
- .div(100)
- .mul(ratio);
+ for (const discountAmount of discountAmounts) {
+ const appliedDiscount = new Decimal(discountAmount.amount || 0)
+ .div(100)
+ .mul(ratio);
- amountAfterDiscount = new Decimal(amountAfterDiscount)
- .minus(appliedDiscount)
- .toNumber();
- }
- return amountAfterDiscount;
+ amountAfterDiscount = new Decimal(amountAfterDiscount)
+ .minus(appliedDiscount)
+ .toNumber();
+ }
+ return amountAfterDiscount;
};
export const getAmountAfterStripeDiscounts = ({
- price,
- amount,
- product,
- stripeDiscounts,
+ price,
+ amount,
+ product,
+ stripeDiscounts,
}: {
- price: Price;
- product: Product;
- amount: number;
- stripeDiscounts: Stripe.Discount[];
+ price: Price;
+ product: Product;
+ amount: number;
+ stripeDiscounts: Stripe.Discount[];
}) => {
- let amountAfterDiscount = amount;
+ let amountAfterDiscount = amount;
- for (const discount of stripeDiscounts) {
- if (!discountAppliesToPrice({ discount, product, price })) continue;
+ for (const discount of stripeDiscounts) {
+ if (!discountAppliesToPrice({ discount, product, price })) continue;
- console.log(
- `Coupon: ${discount.coupon?.id} applies to price (${formatPrice({ price, product })})`
- );
- const coupon: Stripe.Coupon = discount.coupon;
- if (coupon.percent_off) {
- const ratio = new Decimal(1).minus(
- new Decimal(coupon.percent_off).div(100)
- );
- amountAfterDiscount = new Decimal(amountAfterDiscount)
- .mul(ratio)
- .toNumber();
- } else if (coupon.amount_off) {
- // must do some ratio ting here...
- amountAfterDiscount = new Decimal(amountAfterDiscount)
- .minus(new Decimal(coupon.amount_off).div(100))
- .toNumber();
- }
- }
- return amountAfterDiscount;
+ console.log(
+ `Coupon: ${discount.coupon?.id} applies to price (${formatPrice({ price, product })})`,
+ );
+ const coupon: Stripe.Coupon = discount.coupon;
+ if (coupon.percent_off) {
+ const ratio = new Decimal(1).minus(
+ new Decimal(coupon.percent_off).div(100),
+ );
+ amountAfterDiscount = new Decimal(amountAfterDiscount)
+ .mul(ratio)
+ .toNumber();
+ } else if (coupon.amount_off) {
+ // must do some ratio ting here...
+ amountAfterDiscount = new Decimal(amountAfterDiscount)
+ .minus(new Decimal(coupon.amount_off).div(100))
+ .toNumber();
+ }
+ }
+ return amountAfterDiscount;
};
diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts
index 775108f03..6e0580942 100644
--- a/server/src/internal/rewards/triggerCheckoutReward.ts
+++ b/server/src/internal/rewards/triggerCheckoutReward.ts
@@ -1,10 +1,10 @@
import {
- type ReferralCode,
- type Reward,
- RewardCategory,
- type RewardProgram,
- type RewardRedemption,
- RewardTriggerEvent,
+ type ReferralCode,
+ type Reward,
+ RewardCategory,
+ type RewardProgram,
+ type RewardRedemption,
+ RewardTriggerEvent,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
@@ -14,127 +14,127 @@ import { triggerFreeProduct } from "./referralUtils/triggerFreeProduct.js";
import { triggerRedemption } from "./referralUtils.js";
import { getRewardCat } from "./rewardUtils.js";
export const runTriggerCheckoutReward = async ({
- db,
- payload,
- logger,
+ db,
+ payload,
+ logger,
}: {
- db: DrizzleCli;
- payload: any;
- logger: any;
+ db: DrizzleCli;
+ payload: any;
+ logger: any;
}) => {
- try {
- // Customer redeeming code, product they're buying
- const { customer, product, org, env, subId } = payload;
- const stripeCli = createStripeCli({
- org,
- env,
- });
+ try {
+ // Customer redeeming code, product they're buying
+ const { customer, product, org, env, subId } = payload;
+ const stripeCli = createStripeCli({
+ org,
+ env,
+ });
- // 1. Check if redemption exists
- const redemptions = await RewardRedemptionService.getByCustomer({
- db,
- internalCustomerId: customer.internal_id, // customer that redeemed code
- withRewardProgram: true,
- triggered: false,
- withReferralCode: true,
- triggerWhen: RewardTriggerEvent.Checkout,
- });
+ // 1. Check if redemption exists
+ const redemptions = await RewardRedemptionService.getByCustomer({
+ db,
+ internalCustomerId: customer.internal_id, // customer that redeemed code
+ withRewardProgram: true,
+ triggered: false,
+ withReferralCode: true,
+ triggerWhen: RewardTriggerEvent.Checkout,
+ });
- for (const redemption of redemptions) {
- if (
- !redemption ||
- redemption.reward_program.when !== RewardTriggerEvent.Checkout
- ) {
- console.info(
- "No redemption found or reward program not set to checkout, skipping"
- );
- return;
- }
+ for (const redemption of redemptions) {
+ if (
+ !redemption ||
+ redemption.reward_program.when !== RewardTriggerEvent.Checkout
+ ) {
+ console.info(
+ "No redemption found or reward program not set to checkout, skipping",
+ );
+ return;
+ }
- const { reward_program, referral_code: referralCode } =
- redemption as RewardRedemption & {
- reward_program: RewardProgram & { reward: Reward };
- referral_code: ReferralCode;
- };
- const { reward } = reward_program;
+ const { reward_program, referral_code: referralCode } =
+ redemption as RewardRedemption & {
+ reward_program: RewardProgram & { reward: Reward };
+ referral_code: ReferralCode;
+ };
+ const { reward } = reward_program;
- console.info(`--------------------------------`);
- console.info(`CHECKING FOR CHECKOUT REWARD, ORG: ${org.slug}`);
- console.info(
- `Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_program.id}`
- );
- console.info(`Referral code: ${referralCode.code} (${referralCode.id})`);
- console.info(
- `Products: ${reward_program.product_ids?.join(", ")}, ${reward_program.reward.free_product_id}`
- );
+ console.info(`--------------------------------`);
+ console.info(`CHECKING FOR CHECKOUT REWARD, ORG: ${org.slug}`);
+ console.info(
+ `Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_program.id}`,
+ );
+ console.info(`Referral code: ${referralCode.code} (${referralCode.id})`);
+ console.info(
+ `Products: ${reward_program.product_ids?.join(", ")}, ${reward_program.reward.free_product_id}`,
+ );
- if (!reward_program.product_ids?.includes(product.id)) {
- console.info(
- `Product ${product.name} (${product.id}) not included in referral program, skipping`
- );
- if (reward_program.reward.free_product_id !== product.id) {
- return;
- }
- }
+ if (!reward_program.product_ids?.includes(product.id)) {
+ console.info(
+ `Product ${product.name} (${product.id}) not included in referral program, skipping`,
+ );
+ if (reward_program.reward.free_product_id !== product.id) {
+ return;
+ }
+ }
- // Check for trial
- let hasTrial = false;
- if (subId) {
- const sub = await stripeCli.subscriptions.retrieve(subId);
- // hasTrial = Boolean(sub.trial_end && sub.trial_end > Date.now());
- hasTrial = sub.status === "trialing";
- }
+ // Check for trial
+ let hasTrial = false;
+ if (subId) {
+ const sub = await stripeCli.subscriptions.retrieve(subId);
+ // hasTrial = Boolean(sub.trial_end && sub.trial_end > Date.now());
+ hasTrial = sub.status === "trialing";
+ }
- if (hasTrial) {
- console.info(`Subscription is on trial, not triggering reward`);
- return;
- }
+ if (hasTrial) {
+ console.info(`Subscription is on trial, not triggering reward`);
+ return;
+ }
- // Get redemption count
- const redemptionCount = await RewardProgramService.getCodeRedemptionCount(
- {
- db,
- referralCodeId: referralCode.id,
- }
- );
+ // Get redemption count
+ const redemptionCount = await RewardProgramService.getCodeRedemptionCount(
+ {
+ db,
+ referralCodeId: referralCode.id,
+ },
+ );
- if (redemptionCount >= reward_program.max_redemptions!) {
- console.info(
- `Max redemptions reached, not triggering latest redemption`
- );
- return;
- }
+ if (redemptionCount >= reward_program.max_redemptions!) {
+ console.info(
+ `Max redemptions reached, not triggering latest redemption`,
+ );
+ return;
+ }
- const rewardCat = getRewardCat(reward);
- if (rewardCat === RewardCategory.FreeProduct) {
- await triggerFreeProduct({
- req: undefined,
- db,
- referralCode,
- redeemer: customer,
- rewardProgram: reward_program,
- org,
- env,
- logger,
- redemption,
- });
- } else {
- await triggerRedemption({
- db,
- referralCode: {
- ...referralCode,
- reward_program,
- },
- org,
- env,
- logger,
- reward,
- redemption,
- });
- }
- }
- } catch (error) {
- console.error("Failed to trigger checkout reward");
- console.error(error);
- }
+ const rewardCat = getRewardCat(reward);
+ if (rewardCat === RewardCategory.FreeProduct) {
+ await triggerFreeProduct({
+ req: undefined,
+ db,
+ referralCode,
+ redeemer: customer,
+ rewardProgram: reward_program,
+ org,
+ env,
+ logger,
+ redemption,
+ });
+ } else {
+ await triggerRedemption({
+ db,
+ referralCode: {
+ ...referralCode,
+ reward_program,
+ },
+ org,
+ env,
+ logger,
+ reward,
+ redemption,
+ });
+ }
+ }
+ } catch (error) {
+ console.error("Failed to trigger checkout reward");
+ console.error(error);
+ }
};
diff --git a/server/src/internal/saved-views/ViewsService.ts b/server/src/internal/saved-views/ViewsService.ts
index 2f4484228..121c94594 100644
--- a/server/src/internal/saved-views/ViewsService.ts
+++ b/server/src/internal/saved-views/ViewsService.ts
@@ -7,127 +7,127 @@ import RecaseError from "@/utils/errorUtils.js";
import { ErrCode } from "@autumn/shared";
export class ViewsService {
- static async saveView(req: ExtendedRequest, res: Response) {
- return routeHandler({
- req,
- res,
- action: "save org view",
- handler: async () => {
- const { name, filters } = req.body;
- const orgId = req.org.id;
- const env = req.env;
+ static async saveView(req: ExtendedRequest, res: Response) {
+ return routeHandler({
+ req,
+ res,
+ action: "save org view",
+ handler: async () => {
+ const { name, filters } = req.body;
+ const orgId = req.org.id;
+ const env = req.env;
- if (!name) {
- throw new RecaseError({
- message: "Name is required",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!name) {
+ throw new RecaseError({
+ message: "Name is required",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- if (!filters) {
- throw new RecaseError({
- message: "Please select some filters first",
- code: ErrCode.InvalidRequest,
- statusCode: 400,
- });
- }
+ if (!filters) {
+ throw new RecaseError({
+ message: "Please select some filters first",
+ code: ErrCode.InvalidRequest,
+ statusCode: 400,
+ });
+ }
- const viewId = nanoid(8);
- const view = {
- id: viewId,
- name,
- filters,
- created_at: new Date().toISOString(),
- org_id: orgId,
- };
+ const viewId = nanoid(8);
+ const view = {
+ id: viewId,
+ name,
+ filters,
+ created_at: new Date().toISOString(),
+ org_id: orgId,
+ };
- // Save to Redis with key: saved_views:orgId:env:viewId (org+env scoped, no TTL)
- const key = `saved_views:${orgId}:${env}:${viewId}`;
- await CacheManager.setJson(key, view, "forever"); // No TTL - store forever
+ // Save to Redis with key: saved_views:orgId:env:viewId (org+env scoped, no TTL)
+ const key = `saved_views:${orgId}:${env}:${viewId}`;
+ await CacheManager.setJson(key, view, "forever"); // No TTL - store forever
- // Also save to a list for easy retrieval
- const listKey = `saved_views_list:${orgId}:${env}`;
- const existingViews = (await CacheManager.getJson(listKey)) || [];
- existingViews.push(viewId);
- await CacheManager.setJson(listKey, existingViews, "forever"); // No TTL
+ // Also save to a list for easy retrieval
+ const listKey = `saved_views_list:${orgId}:${env}`;
+ const existingViews = (await CacheManager.getJson(listKey)) || [];
+ existingViews.push(viewId);
+ await CacheManager.setJson(listKey, existingViews, "forever"); // No TTL
- res.status(201).json({
- message: "View saved successfully",
- view: {
- id: viewId,
- name,
- created_at: view.created_at,
- },
- });
- },
- });
- }
+ res.status(201).json({
+ message: "View saved successfully",
+ view: {
+ id: viewId,
+ name,
+ created_at: view.created_at,
+ },
+ });
+ },
+ });
+ }
- static async getViews(req: ExtendedRequest, res: Response) {
- return routeHandler({
- req,
- res,
- action: "get org views",
- handler: async () => {
- const orgId = req.org.id;
- const env = req.env;
+ static async getViews(req: ExtendedRequest, res: Response) {
+ return routeHandler({
+ req,
+ res,
+ action: "get org views",
+ handler: async () => {
+ const orgId = req.org.id;
+ const env = req.env;
- const listKey = `saved_views_list:${orgId}:${env}`;
- const viewIds = (await CacheManager.getJson(listKey)) || [];
+ const listKey = `saved_views_list:${orgId}:${env}`;
+ const viewIds = (await CacheManager.getJson(listKey)) || [];
- const views = [];
- for (const viewId of viewIds) {
- const key = `saved_views:${orgId}:${env}:${viewId}`;
- const view = await CacheManager.getJson(key);
- if (view) {
- views.push({
- id: view.id,
- name: view.name,
- filters: view.filters,
- created_at: view.created_at,
- });
- }
- }
+ const views = [];
+ for (const viewId of viewIds) {
+ const key = `saved_views:${orgId}:${env}:${viewId}`;
+ const view = await CacheManager.getJson(key);
+ if (view) {
+ views.push({
+ id: view.id,
+ name: view.name,
+ filters: view.filters,
+ created_at: view.created_at,
+ });
+ }
+ }
- // Sort by creation date (newest first)
- views.sort(
- (a, b) =>
- new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
- );
+ // Sort by creation date (newest first)
+ views.sort(
+ (a, b) =>
+ new Date(b.created_at).getTime() - new Date(a.created_at).getTime(),
+ );
- res.json({ views });
- },
- });
- }
+ res.json({ views });
+ },
+ });
+ }
- static async deleteView(req: ExtendedRequest, res: Response) {
- return routeHandler({
- req,
- res,
- action: "delete org view",
- handler: async (req: ExtendedRequest, res: Response) => {
- const { viewId } = req.params;
- const orgId = req.org.id;
- const env = req.env;
+ static async deleteView(req: ExtendedRequest, res: Response) {
+ return routeHandler({
+ req,
+ res,
+ action: "delete org view",
+ handler: async (req: ExtendedRequest, res: Response) => {
+ const { viewId } = req.params;
+ const orgId = req.org.id;
+ const env = req.env;
- // Delete from Redis
- const key = `saved_views:${orgId}:${env}:${viewId}`;
- await CacheManager.invalidate({
- action: "",
- value: key.replace(":", ""),
- });
+ // Delete from Redis
+ const key = `saved_views:${orgId}:${env}:${viewId}`;
+ await CacheManager.invalidate({
+ action: "",
+ value: key.replace(":", ""),
+ });
- // Remove from list
- const listKey = `saved_views_list:${orgId}:${env}`;
- const existingViews = (await CacheManager.getJson(listKey)) || [];
- const updatedViews = existingViews.filter(
- (id: string) => id !== viewId
- );
- await CacheManager.setJson(listKey, updatedViews, "forever"); // No TTL
+ // Remove from list
+ const listKey = `saved_views_list:${orgId}:${env}`;
+ const existingViews = (await CacheManager.getJson(listKey)) || [];
+ const updatedViews = existingViews.filter(
+ (id: string) => id !== viewId,
+ );
+ await CacheManager.setJson(listKey, updatedViews, "forever"); // No TTL
- res.json({ message: "View deleted successfully" });
- },
- });
- }
+ res.json({ message: "View deleted successfully" });
+ },
+ });
+ }
}
diff --git a/server/src/internal/saved-views/savedViewsRouter.ts b/server/src/internal/saved-views/savedViewsRouter.ts
index acdbe2e3c..755f930b6 100644
--- a/server/src/internal/saved-views/savedViewsRouter.ts
+++ b/server/src/internal/saved-views/savedViewsRouter.ts
@@ -10,4 +10,4 @@ viewsRouter.post("/save", ViewsService.saveView as any);
viewsRouter.get("/", ViewsService.getViews as any);
// Delete view
-viewsRouter.delete("/:viewId", ViewsService.deleteView as any);
\ No newline at end of file
+viewsRouter.delete("/:viewId", ViewsService.deleteView as any);
diff --git a/server/src/internal/subscriptions/SubService.ts b/server/src/internal/subscriptions/SubService.ts
index e0d782ec7..712ce166d 100644
--- a/server/src/internal/subscriptions/SubService.ts
+++ b/server/src/internal/subscriptions/SubService.ts
@@ -7,173 +7,173 @@ import { and, eq, inArray } from "drizzle-orm";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export class SubService {
- static async createSub({ db, sub }: { db: DrizzleCli; sub: Subscription }) {
- let data = await db.insert(subscriptions).values(sub).returning();
+ static async createSub({ db, sub }: { db: DrizzleCli; sub: Subscription }) {
+ let data = await db.insert(subscriptions).values(sub).returning();
- if (data.length === 0) {
- throw new RecaseError({
- code: ErrCode.InsertSubscriptionFailed,
- message: "Failed to create subscription",
- statusCode: 500,
- });
- }
+ if (data.length === 0) {
+ throw new RecaseError({
+ code: ErrCode.InsertSubscriptionFailed,
+ message: "Failed to create subscription",
+ statusCode: 500,
+ });
+ }
- return data[0] as Subscription;
- }
+ return data[0] as Subscription;
+ }
- static async addUsageFeatures({
- db,
- stripeId,
- scheduleId,
- usageFeatures,
- orgId,
- env,
- }: {
- db: DrizzleCli;
- stripeId?: string;
- scheduleId?: string;
- usageFeatures: string[];
- orgId: string;
- env: AppEnv;
- }) {
- if (!stripeId && !scheduleId) {
- throw new Error("Either stripeId or scheduleId must be provided");
- }
+ static async addUsageFeatures({
+ db,
+ stripeId,
+ scheduleId,
+ usageFeatures,
+ orgId,
+ env,
+ }: {
+ db: DrizzleCli;
+ stripeId?: string;
+ scheduleId?: string;
+ usageFeatures: string[];
+ orgId: string;
+ env: AppEnv;
+ }) {
+ if (!stripeId && !scheduleId) {
+ throw new Error("Either stripeId or scheduleId must be provided");
+ }
- let data = await db
- .select()
- .from(subscriptions)
- .where(
- and(
- stripeId ? eq(subscriptions.stripe_id, stripeId) : undefined,
- scheduleId
- ? eq(subscriptions.stripe_schedule_id, scheduleId)
- : undefined
- )
- );
+ let data = await db
+ .select()
+ .from(subscriptions)
+ .where(
+ and(
+ stripeId ? eq(subscriptions.stripe_id, stripeId) : undefined,
+ scheduleId
+ ? eq(subscriptions.stripe_schedule_id, scheduleId)
+ : undefined,
+ ),
+ );
- if (data.length == 0) {
- return await SubService.createSub({
- db,
- sub: {
- id: generateId("sub"),
- created_at: Date.now(),
- stripe_id: stripeId || null,
- stripe_schedule_id: scheduleId || null,
- usage_features: usageFeatures,
- org_id: orgId,
- env,
- current_period_start: null,
- current_period_end: null,
- },
- });
- }
+ if (data.length == 0) {
+ return await SubService.createSub({
+ db,
+ sub: {
+ id: generateId("sub"),
+ created_at: Date.now(),
+ stripe_id: stripeId || null,
+ stripe_schedule_id: scheduleId || null,
+ usage_features: usageFeatures,
+ org_id: orgId,
+ env,
+ current_period_start: null,
+ current_period_end: null,
+ },
+ });
+ }
- let curSub = data[0];
- let updateResult = await db
- .update(subscriptions)
- .set({
- usage_features: [
- ...new Set([...(curSub.usage_features || []), ...usageFeatures]),
- ],
- })
- .where(eq(subscriptions.id, curSub.id))
- .returning();
+ let curSub = data[0];
+ let updateResult = await db
+ .update(subscriptions)
+ .set({
+ usage_features: [
+ ...new Set([...(curSub.usage_features || []), ...usageFeatures]),
+ ],
+ })
+ .where(eq(subscriptions.id, curSub.id))
+ .returning();
- if (updateResult.length === 0) {
- throw new RecaseError({
- code: ErrCode.UpdateSubscriptionFailed,
- message: "Failed to update subscription",
- statusCode: 500,
- });
- }
+ if (updateResult.length === 0) {
+ throw new RecaseError({
+ code: ErrCode.UpdateSubscriptionFailed,
+ message: "Failed to update subscription",
+ statusCode: 500,
+ });
+ }
- return updateResult[0] as Subscription;
- }
+ return updateResult[0] as Subscription;
+ }
- static async updateFromStripe({
- db,
- stripeSub,
- }: {
- db: DrizzleCli;
- stripeSub: Stripe.Subscription;
- }) {
- const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
- let results = await db
- .update(subscriptions)
- .set({
- current_period_start: start,
- current_period_end: end,
- })
- .where(eq(subscriptions.stripe_id, stripeSub.id))
- .returning();
+ static async updateFromStripe({
+ db,
+ stripeSub,
+ }: {
+ db: DrizzleCli;
+ stripeSub: Stripe.Subscription;
+ }) {
+ const { start, end } = subToPeriodStartEnd({ sub: stripeSub });
+ let results = await db
+ .update(subscriptions)
+ .set({
+ current_period_start: start,
+ current_period_end: end,
+ })
+ .where(eq(subscriptions.stripe_id, stripeSub.id))
+ .returning();
- if (results.length === 0) {
- return null;
- }
+ if (results.length === 0) {
+ return null;
+ }
- return results[0] as Subscription;
- }
+ return results[0] as Subscription;
+ }
- static async getFromScheduleId({
- db,
- scheduleId,
- }: {
- db: DrizzleCli;
- scheduleId: string;
- }) {
- let data = await db
- .select()
- .from(subscriptions)
- .where(eq(subscriptions.stripe_schedule_id, scheduleId));
+ static async getFromScheduleId({
+ db,
+ scheduleId,
+ }: {
+ db: DrizzleCli;
+ scheduleId: string;
+ }) {
+ let data = await db
+ .select()
+ .from(subscriptions)
+ .where(eq(subscriptions.stripe_schedule_id, scheduleId));
- if (data.length === 0) {
- return null;
- }
+ if (data.length === 0) {
+ return null;
+ }
- return data[0] as Subscription;
- }
+ return data[0] as Subscription;
+ }
- static async deleteFromScheduleId({
- db,
- scheduleId,
- }: {
- db: DrizzleCli;
- scheduleId: string;
- }) {
- await db
- .delete(subscriptions)
- .where(eq(subscriptions.stripe_schedule_id, scheduleId));
+ static async deleteFromScheduleId({
+ db,
+ scheduleId,
+ }: {
+ db: DrizzleCli;
+ scheduleId: string;
+ }) {
+ await db
+ .delete(subscriptions)
+ .where(eq(subscriptions.stripe_schedule_id, scheduleId));
- return;
- }
+ return;
+ }
- static async updateFromScheduleId({
- db,
- scheduleId,
- updates,
- }: {
- db: DrizzleCli;
- scheduleId: string;
- updates: any;
- }) {
- let results = await db
- .update(subscriptions)
- .set(updates)
- .where(eq(subscriptions.stripe_schedule_id, scheduleId))
- .returning();
+ static async updateFromScheduleId({
+ db,
+ scheduleId,
+ updates,
+ }: {
+ db: DrizzleCli;
+ scheduleId: string;
+ updates: any;
+ }) {
+ let results = await db
+ .update(subscriptions)
+ .set(updates)
+ .where(eq(subscriptions.stripe_schedule_id, scheduleId))
+ .returning();
- if (results.length === 0) {
- return null;
- }
+ if (results.length === 0) {
+ return null;
+ }
- return results[0] as Subscription;
- }
+ return results[0] as Subscription;
+ }
- static async getInStripeIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
- return (await db
- .select()
- .from(subscriptions)
- .where(inArray(subscriptions.stripe_id, ids))) as Subscription[];
- }
+ static async getInStripeIds({ db, ids }: { db: DrizzleCli; ids: string[] }) {
+ return (await db
+ .select()
+ .from(subscriptions)
+ .where(inArray(subscriptions.stripe_id, ids))) as Subscription[];
+ }
}
diff --git a/server/src/internal/subscriptions/subUtils.ts b/server/src/internal/subscriptions/subUtils.ts
index 44dbcedfc..41eb12cbe 100644
--- a/server/src/internal/subscriptions/subUtils.ts
+++ b/server/src/internal/subscriptions/subUtils.ts
@@ -2,33 +2,33 @@ import { generateId } from "@/utils/genUtils.js";
import { AppEnv, Subscription } from "@autumn/shared";
export const constructSub = ({
- stripeId,
- stripeScheduleId,
- usageFeatures,
- orgId,
- env,
- currentPeriodStart,
- currentPeriodEnd,
+ stripeId,
+ stripeScheduleId,
+ usageFeatures,
+ orgId,
+ env,
+ currentPeriodStart,
+ currentPeriodEnd,
}: {
- stripeId?: string;
- stripeScheduleId?: string;
- usageFeatures: string[];
- orgId: string;
- env: AppEnv;
- currentPeriodStart?: number;
- currentPeriodEnd?: number;
+ stripeId?: string;
+ stripeScheduleId?: string;
+ usageFeatures: string[];
+ orgId: string;
+ env: AppEnv;
+ currentPeriodStart?: number;
+ currentPeriodEnd?: number;
}) => {
- let newSub: Subscription = {
- id: generateId("sub"),
- stripe_id: stripeId || null,
- stripe_schedule_id: stripeScheduleId || null,
- created_at: Date.now(),
- usage_features: usageFeatures,
- org_id: orgId,
- env: env,
- current_period_start: currentPeriodStart || null,
- current_period_end: currentPeriodEnd || null,
- };
+ let newSub: Subscription = {
+ id: generateId("sub"),
+ stripe_id: stripeId || null,
+ stripe_schedule_id: stripeScheduleId || null,
+ created_at: Date.now(),
+ usage_features: usageFeatures,
+ org_id: orgId,
+ env: env,
+ current_period_start: currentPeriodStart || null,
+ current_period_end: currentPeriodEnd || null,
+ };
- return newSub;
+ return newSub;
};
diff --git a/server/src/internal/users/userRouter.ts b/server/src/internal/users/userRouter.ts
index 9b06ba60d..a509585a8 100644
--- a/server/src/internal/users/userRouter.ts
+++ b/server/src/internal/users/userRouter.ts
@@ -3,5 +3,5 @@ import { Router } from "express";
export const userRouter: Router = Router();
userRouter.get("", async (req: any, res) => {
- res.status(200).json({ userId: req.userId });
+ res.status(200).json({ userId: req.userId });
});
diff --git a/server/src/middleware/analyticsMiddleware.ts b/server/src/middleware/analyticsMiddleware.ts
index b7c163630..7d25fdcb7 100644
--- a/server/src/middleware/analyticsMiddleware.ts
+++ b/server/src/middleware/analyticsMiddleware.ts
@@ -1,80 +1,80 @@
const handleResFinish = (req: any, res: any) => {
- let skipUrls = ["/v1/customers/all/search"];
+ let skipUrls = ["/v1/customers/all/search"];
- try {
- if (skipUrls.includes(req.originalUrl)) {
- return;
- }
+ try {
+ if (skipUrls.includes(req.originalUrl)) {
+ return;
+ }
- if (process.env.NODE_ENV !== "development") {
- req.logtail.info(
- `[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
- {
- statusCode: res.statusCode,
- res: res.locals.responseBody,
- }
- );
- }
- } catch (error) {
- console.error("Failed to log response to logtailAll");
- console.error(error);
- }
+ if (process.env.NODE_ENV !== "development") {
+ req.logtail.info(
+ `[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
+ {
+ statusCode: res.statusCode,
+ res: res.locals.responseBody,
+ },
+ );
+ }
+ } catch (error) {
+ console.error("Failed to log response to logtailAll");
+ console.error(error);
+ }
};
const parseCustomerIdFromUrl = (url: string): string | undefined => {
- if (!url.startsWith("/v1")) {
- return undefined;
- }
+ if (!url.startsWith("/v1")) {
+ return undefined;
+ }
- const cleanUrl = url.split("?")[0].replace(/^\/+|\/+$/g, "");
- const segments = cleanUrl.split("/");
- const customersIndex = segments.findIndex(
- (segment) => segment === "customers"
- );
+ const cleanUrl = url.split("?")[0].replace(/^\/+|\/+$/g, "");
+ const segments = cleanUrl.split("/");
+ const customersIndex = segments.findIndex(
+ (segment) => segment === "customers",
+ );
- if (customersIndex !== -1 && segments[customersIndex + 1]) {
- return segments[customersIndex + 1];
- }
+ if (customersIndex !== -1 && segments[customersIndex + 1]) {
+ return segments[customersIndex + 1];
+ }
- return undefined;
+ return undefined;
};
export const analyticsMiddleware = async (req: any, res: any, next: any) => {
- let reqContext = {
- org_id: req.org?.id,
- org_slug: req.org?.slug,
- env: req.env,
- authType: req.authType,
- body: req.body,
- customer_id:
- req?.body?.customer_id || parseCustomerIdFromUrl(req.originalUrl),
- user_id: req.userId || null,
- };
+ let reqContext = {
+ org_id: req.org?.id,
+ org_slug: req.org?.slug,
+ env: req.env,
+ authType: req.authType,
+ body: req.body,
+ customer_id:
+ req?.body?.customer_id || parseCustomerIdFromUrl(req.originalUrl),
+ user_id: req.userId || null,
+ };
- if (req.span) {
- req.span.setAttributes({
- org_id: req.org?.id,
- org_slug: req.org?.slug,
- env: req.env,
- customer_id: reqContext.customer_id,
- });
- }
+ if (req.span) {
+ req.span.setAttributes({
+ org_id: req.org?.id,
+ org_slug: req.org?.slug,
+ env: req.env,
+ customer_id: reqContext.customer_id,
+ });
+ }
- req.logtail = req.logtail.child({
- context: {
- context: reqContext,
- },
- });
+ req.logtail = req.logtail.child({
+ context: {
+ context: reqContext,
+ },
+ });
- // Store JSON response
- let originalJson = res.json;
+ // Store JSON response
+ let originalJson = res.json;
- res.json = function (body: any) {
- res.locals.responseBody = body;
- return originalJson.call(this, body);
- };
+ res.json = function (body: any) {
+ res.locals.responseBody = body;
+ return originalJson.call(this, body);
+ };
- res.on("finish", () => handleResFinish(req, res));
+ res.on("finish", () => handleResFinish(req, res));
- next();
+ next();
};
diff --git a/server/src/middleware/apiAuthMiddleware.ts b/server/src/middleware/apiAuthMiddleware.ts
index e0e067b08..32f2acb9a 100644
--- a/server/src/middleware/apiAuthMiddleware.ts
+++ b/server/src/middleware/apiAuthMiddleware.ts
@@ -10,140 +10,140 @@ import { ExtendedResponse } from "@/utils/models/Request.js";
import { trmnlAuthMiddleware, trmnlExclusions } from "./trmnlAuthMiddleware.js";
const verifyApiVersion = (version: string) => {
- let versionFloat = parseFloat(version);
- let apiVersion = floatToVersion(versionFloat);
+ let versionFloat = parseFloat(version);
+ let apiVersion = floatToVersion(versionFloat);
- if (isNaN(versionFloat) || !apiVersion) {
- throw new RecaseError({
- message: `${version} is not a valid API version`,
- code: ErrCode.InvalidApiVersion,
- statusCode: 400,
- });
- }
+ if (isNaN(versionFloat) || !apiVersion) {
+ throw new RecaseError({
+ message: `${version} is not a valid API version`,
+ code: ErrCode.InvalidApiVersion,
+ statusCode: 400,
+ });
+ }
- return apiVersion;
+ return apiVersion;
};
const maskApiKey = (apiKey: string) => {
- return apiKey.slice(0, 15) + apiKey.slice(15).replace(/./g, "*");
+ return apiKey.slice(0, 15) + apiKey.slice(15).replace(/./g, "*");
};
export const verifySecretKey = async (req: any, res: any, next: any) => {
- const authHeader =
- req.headers["authorization"] || req.headers["Authorization"];
+ const authHeader =
+ req.headers["authorization"] || req.headers["Authorization"];
- const logger = req.logtail;
- const version = req.headers["x-api-version"];
+ const logger = req.logtail;
+ const version = req.headers["x-api-version"];
- if (version) {
- req.apiVersion = verifyApiVersion(version);
- }
+ if (version) {
+ req.apiVersion = verifyApiVersion(version);
+ }
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
- let origin = req.get("origin");
- if (dashboardOrigins.includes(origin)) {
- return withOrgAuth(req, res, next);
- } else {
- throw new RecaseError({
- message: "Secret key not found in Authorization header",
- code: ErrCode.NoSecretKey,
- statusCode: 401,
- });
- }
- }
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ let origin = req.get("origin");
+ if (dashboardOrigins.includes(origin)) {
+ return withOrgAuth(req, res, next);
+ } else {
+ throw new RecaseError({
+ message: "Secret key not found in Authorization header",
+ code: ErrCode.NoSecretKey,
+ statusCode: 401,
+ });
+ }
+ }
- const apiKey = authHeader.split(" ")[1];
+ const apiKey = authHeader.split(" ")[1];
- if (!apiKey.startsWith("am_")) {
- throw new RecaseError({
- message: "Invalid secret key",
- code: ErrCode.InvalidSecretKey,
- statusCode: 401,
- });
- }
+ if (!apiKey.startsWith("am_")) {
+ throw new RecaseError({
+ message: "Invalid secret key",
+ code: ErrCode.InvalidSecretKey,
+ statusCode: 401,
+ });
+ }
- if (apiKey.startsWith("am_pk")) {
- console.log("Verifying publishable key");
- return await verifyBearerPublishableKey(apiKey, req, res, next);
- }
+ if (apiKey.startsWith("am_pk")) {
+ console.log("Verifying publishable key");
+ return await verifyBearerPublishableKey(apiKey, req, res, next);
+ }
- const { valid, data } = await verifyKey({
- db: req.db,
- key: apiKey,
- });
+ const { valid, data } = await verifyKey({
+ db: req.db,
+ key: apiKey,
+ });
- if (!valid || !data) {
- throw new RecaseError({
- message: "Invalid secret key",
- code: ErrCode.InvalidSecretKey,
- statusCode: 401,
- });
- }
+ if (!valid || !data) {
+ throw new RecaseError({
+ message: "Invalid secret key",
+ code: ErrCode.InvalidSecretKey,
+ statusCode: 401,
+ });
+ }
- let { org, features, env, userId } = data;
- req.orgId = org.id;
- req.env = env;
- req.minOrg = {
- id: org.id,
- slug: org.slug,
- };
- req.org = org;
- req.features = features;
- req.authType = AuthType.SecretKey;
- req.userId = userId;
+ let { org, features, env, userId } = data;
+ req.orgId = org.id;
+ req.env = env;
+ req.minOrg = {
+ id: org.id,
+ slug: org.slug,
+ };
+ req.org = org;
+ req.features = features;
+ req.authType = AuthType.SecretKey;
+ req.userId = userId;
- const orgConfig = await req.headers["org-config"];
- if (orgConfig) {
- console.log("Org config found!: ", orgConfig);
- let newConfigFields = JSON.parse(orgConfig);
- try {
- req.org.config = {
- ...org.config,
- ...newConfigFields,
- };
- } catch (error) {}
- }
+ const orgConfig = await req.headers["org-config"];
+ if (orgConfig) {
+ console.log("Org config found!: ", orgConfig);
+ let newConfigFields = JSON.parse(orgConfig);
+ try {
+ req.org.config = {
+ ...org.config,
+ ...newConfigFields,
+ };
+ } catch (error) {}
+ }
- next();
+ next();
};
export const apiAuthMiddleware = async (req: any, res: any, next: any) => {
- const logger = req.logtail;
+ const logger = req.logtail;
- if (trmnlExclusions.includes(req.path)) {
- logger.info(
- `exluding TRMNL from auth middleware for device with ID ${req.headers["x-trmnl-id"] || "unknown"}`
- );
- return await trmnlAuthMiddleware(req, res, next);
- }
+ if (trmnlExclusions.includes(req.path)) {
+ logger.info(
+ `exluding TRMNL from auth middleware for device with ID ${req.headers["x-trmnl-id"] || "unknown"}`,
+ );
+ return await trmnlAuthMiddleware(req, res, next);
+ }
- try {
- await verifySecretKey(req, res, next);
+ try {
+ await verifySecretKey(req, res, next);
- return;
- } catch (error: any) {
- if (error instanceof RecaseError) {
- if (error.code === ErrCode.InvalidSecretKey) {
- let apiKey = req.headers["authorization"]?.split(" ")[1];
- error.message = `Invalid secret key: ${maskApiKey(apiKey)}`;
- }
+ return;
+ } catch (error: any) {
+ if (error instanceof RecaseError) {
+ if (error.code === ErrCode.InvalidSecretKey) {
+ let apiKey = req.headers["authorization"]?.split(" ")[1];
+ error.message = `Invalid secret key: ${maskApiKey(apiKey)}`;
+ }
- logger.warn(`auth warning: ${error.message}`);
+ logger.warn(`auth warning: ${error.message}`);
- res.status(error.statusCode).json({
- message: error.message,
- code: error.code,
- });
- } else {
- logger.error(`auth error: ${error.message}`, {
- error,
- });
- res.status(500).json({
- message: `Failed to verify secret key: ${error.message}`,
- code: ErrCode.InternalError,
- });
- }
+ res.status(error.statusCode).json({
+ message: error.message,
+ code: error.code,
+ });
+ } else {
+ logger.error(`auth error: ${error.message}`, {
+ error,
+ });
+ res.status(500).json({
+ message: `Failed to verify secret key: ${error.message}`,
+ code: ErrCode.InternalError,
+ });
+ }
- return;
- }
+ return;
+ }
};
diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts
index 78ce41526..c9c0704c5 100644
--- a/server/src/middleware/authMiddleware.ts
+++ b/server/src/middleware/authMiddleware.ts
@@ -8,109 +8,109 @@ import { eq, and } from "drizzle-orm";
import { member } from "@autumn/shared";
const getTokenData = async (req: any, res: any) => {
- let token;
+ let token;
- try {
- token = req.headers["authorization"]?.split(" ")[1];
- if (!token) {
- throw new Error("authorization header has no token");
- }
- } catch (error) {
- throw new Error("clerk token not found in request headers / invalid");
- }
+ try {
+ token = req.headers["authorization"]?.split(" ")[1];
+ if (!token) {
+ throw new Error("authorization header has no token");
+ }
+ } catch (error) {
+ throw new Error("clerk token not found in request headers / invalid");
+ }
- let secretKey = process.env.CLERK_SECRET_KEY;
+ let secretKey = process.env.CLERK_SECRET_KEY;
- try {
- let verified = await verifyToken(token, {
- secretKey: secretKey,
- });
+ try {
+ let verified = await verifyToken(token, {
+ secretKey: secretKey,
+ });
- if (!verified) {
- throw new Error("failed to verify clerk token");
- }
+ if (!verified) {
+ throw new Error("failed to verify clerk token");
+ }
- return verified;
- } catch (error: any) {
- throw new Error("error verifying clerk token");
- }
+ return verified;
+ } catch (error: any) {
+ throw new Error("error verifying clerk token");
+ }
};
export const withOrgAuth = async (req: any, res: any, next: NextFunction) => {
- const { logtail: logger } = req;
+ const { logtail: logger } = req;
- try {
- // let tokenData = await getTokenData(req, res);
- const session = await auth.api.getSession({
- headers: fromNodeHeaders(req.headers),
- });
+ try {
+ // let tokenData = await getTokenData(req, res);
+ const session = await auth.api.getSession({
+ headers: fromNodeHeaders(req.headers),
+ });
- if (!session) {
- logger.info(`Unauthorized - no session found (${req.originalUrl})`);
- return res
- .status(401)
- .json({ message: "Unauthorized - no session found" });
- }
+ if (!session) {
+ logger.info(`Unauthorized - no session found (${req.originalUrl})`);
+ return res
+ .status(401)
+ .json({ message: "Unauthorized - no session found" });
+ }
- const orgId = session?.session?.activeOrganizationId;
- const userId = session?.user?.id;
+ const orgId = session?.session?.activeOrganizationId;
+ const userId = session?.user?.id;
- if (!orgId) {
- logger.info(`Unauthorized - no org id found`);
- return res
- .status(401)
- .json({ message: "Unauthorized - no org id found" });
- }
+ if (!orgId) {
+ logger.info(`Unauthorized - no org id found`);
+ return res
+ .status(401)
+ .json({ message: "Unauthorized - no org id found" });
+ }
- if (!userId) {
- logger.info(`Unauthorized - no user id found`);
- return res
- .status(401)
- .json({ message: "Unauthorized - no user id found" });
- }
+ if (!userId) {
+ logger.info(`Unauthorized - no user id found`);
+ return res
+ .status(401)
+ .json({ message: "Unauthorized - no user id found" });
+ }
- let data = await OrgService.getWithFeatures({
- db: req.db,
- orgId: orgId,
- env: req.env,
- });
+ let data = await OrgService.getWithFeatures({
+ db: req.db,
+ orgId: orgId,
+ env: req.env,
+ });
- if (!data) {
- logger.warn(`Org ${orgId} not found in DB`);
- return res
- .status(500)
- .json({ message: "Org not found", code: ErrCode.OrgNotFound });
- }
+ if (!data) {
+ logger.warn(`Org ${orgId} not found in DB`);
+ return res
+ .status(500)
+ .json({ message: "Org not found", code: ErrCode.OrgNotFound });
+ }
- const { org, features } = data;
+ const { org, features } = data;
- req.user = session?.user;
- req.userId = userId;
- req.orgId = orgId;
- req.org = org;
- req.features = features;
- req.authType = AuthType.Dashboard;
+ req.user = session?.user;
+ req.userId = userId;
+ req.orgId = orgId;
+ req.org = org;
+ req.features = features;
+ req.authType = AuthType.Dashboard;
- next();
- } catch (error: any) {
- // console.log(`(warning) clerk auth failed:`, error?.message || error);
- logger.warn(`(warning) withOrgAuth failed:`, error?.message || error);
- res.status(401).json({ message: "Unauthorized" });
- return;
- }
+ next();
+ } catch (error: any) {
+ // console.log(`(warning) clerk auth failed:`, error?.message || error);
+ logger.warn(`(warning) withOrgAuth failed:`, error?.message || error);
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
};
export const withAuth = async (req: any, res: any, next: NextFunction) => {
- const session = await auth.api.getSession({
- headers: fromNodeHeaders(req.headers),
- });
+ const session = await auth.api.getSession({
+ headers: fromNodeHeaders(req.headers),
+ });
- if (!session) {
- res.status(401).json({ message: "Unauthorized" });
- return;
- }
+ if (!session) {
+ res.status(401).json({ message: "Unauthorized" });
+ return;
+ }
- req.userId = session?.user.id;
+ req.userId = session?.user.id;
- next();
+ next();
};
diff --git a/server/src/middleware/pricingMiddleware.ts b/server/src/middleware/pricingMiddleware.ts
index 64cae77b0..b4fd5a9e1 100644
--- a/server/src/middleware/pricingMiddleware.ts
+++ b/server/src/middleware/pricingMiddleware.ts
@@ -3,47 +3,47 @@ import { isEntitled } from "@/external/autumn/autumnUtils.js";
import { handleRequestError } from "@/utils/errorUtils.js";
export const pricingMiddleware = async (req: any, res: any, next: any) => {
- let path = req.url;
- let method = req.method;
+ let path = req.url;
+ let method = req.method;
- let host = req.headers.host;
- if (host.includes("localhost")) {
- next();
- return;
- }
+ let host = req.headers.host;
+ if (host.includes("localhost")) {
+ next();
+ return;
+ }
- try {
- if (path == "/products" && method == "POST") {
- await isEntitled({
- org: req.org,
- env: req.env,
- featureId: FeatureId.Products,
- });
- }
+ try {
+ if (path == "/products" && method == "POST") {
+ await isEntitled({
+ org: req.org,
+ env: req.env,
+ featureId: FeatureId.Products,
+ });
+ }
- next();
+ next();
- if (res.statusCode === 200) {
- if (path == "/products" && method === "POST") {
- console.log("sending product create event");
- await sendProductEvent({
- org: req.org,
- env: req.env,
- incrementBy: 1,
- });
- }
+ if (res.statusCode === 200) {
+ if (path == "/products" && method === "POST") {
+ console.log("sending product create event");
+ await sendProductEvent({
+ org: req.org,
+ env: req.env,
+ incrementBy: 1,
+ });
+ }
- if (path.match(/^\/products\/[^\/]+$/) && method === "DELETE") {
- console.log("sending product delete event");
- await sendProductEvent({
- org: req.org,
- env: req.env,
- incrementBy: -1,
- });
- }
- }
- } catch (error) {
- handleRequestError({ req, error, res, action: "pricingMiddleware" });
- return;
- }
+ if (path.match(/^\/products\/[^\/]+$/) && method === "DELETE") {
+ console.log("sending product delete event");
+ await sendProductEvent({
+ org: req.org,
+ env: req.env,
+ incrementBy: -1,
+ });
+ }
+ }
+ } catch (error) {
+ handleRequestError({ req, error, res, action: "pricingMiddleware" });
+ return;
+ }
};
diff --git a/server/src/middleware/publicAuthMiddleware.ts b/server/src/middleware/publicAuthMiddleware.ts
index c523d7239..3bdcb2296 100644
--- a/server/src/middleware/publicAuthMiddleware.ts
+++ b/server/src/middleware/publicAuthMiddleware.ts
@@ -3,116 +3,116 @@ import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, AuthType, ErrCode } from "@autumn/shared";
const allowedEndpoints = [
- {
- method: "GET",
- path: "/v1/products",
- },
- {
- method: "POST",
- path: "/v1/entitled",
- },
- {
- method: "POST",
- path: "/v1/check",
- },
- {
- method: "POST",
- path: "/v1/attach",
- },
- {
- method: "GET",
- path: "/v1/customers/:customerId",
- },
+ {
+ method: "GET",
+ path: "/v1/products",
+ },
+ {
+ method: "POST",
+ path: "/v1/entitled",
+ },
+ {
+ method: "POST",
+ path: "/v1/check",
+ },
+ {
+ method: "POST",
+ path: "/v1/attach",
+ },
+ {
+ method: "GET",
+ path: "/v1/customers/:customerId",
+ },
];
export interface IsAllowedEndpointProps {
- pattern: string;
- path: string;
- method: string;
+ pattern: string;
+ path: string;
+ method: string;
}
const isAllowedEndpoint = ({
- pattern,
- path,
- method,
+ pattern,
+ path,
+ method,
}: IsAllowedEndpointProps) => {
- // Convert pattern to regex, handling path params like :id
- const matchPath = (pattern: string, path: string) => {
- // Convert pattern to regex, handling path params like :id
- const regexPattern = pattern.replace(/:[^/]+/g, "[^/]+");
- const regex = new RegExp(`^${regexPattern}$`);
- // Remove query params before testing
- const pathWithoutQuery = path.split("?")[0];
- return regex.test(pathWithoutQuery);
- };
+ // Convert pattern to regex, handling path params like :id
+ const matchPath = (pattern: string, path: string) => {
+ // Convert pattern to regex, handling path params like :id
+ const regexPattern = pattern.replace(/:[^/]+/g, "[^/]+");
+ const regex = new RegExp(`^${regexPattern}$`);
+ // Remove query params before testing
+ const pathWithoutQuery = path.split("?")[0];
+ return regex.test(pathWithoutQuery);
+ };
- for (const endpoint of allowedEndpoints) {
- if (endpoint.method === method && matchPath(endpoint.path, path)) {
- return true;
- }
- }
- return false;
+ for (const endpoint of allowedEndpoints) {
+ if (endpoint.method === method && matchPath(endpoint.path, path)) {
+ return true;
+ }
+ }
+ return false;
};
export const verifyBearerPublishableKey = async (
- pkey: string,
- req: any,
- res: any,
- next: any,
+ pkey: string,
+ req: any,
+ res: any,
+ next: any,
) => {
- if (
- !isAllowedEndpoint({
- pattern: req.originalUrl,
- path: req.originalUrl,
- method: req.method,
- })
- ) {
- throw new RecaseError({
- message: `Endpoint ${req.originalUrl} not accessable via publishable key. Please try with a secret key instead.`,
- code: ErrCode.EndpointNotPublic,
- statusCode: 401,
- });
- }
+ if (
+ !isAllowedEndpoint({
+ pattern: req.originalUrl,
+ path: req.originalUrl,
+ method: req.method,
+ })
+ ) {
+ throw new RecaseError({
+ message: `Endpoint ${req.originalUrl} not accessable via publishable key. Please try with a secret key instead.`,
+ code: ErrCode.EndpointNotPublic,
+ statusCode: 401,
+ });
+ }
- if (!pkey.startsWith("am_pk_test") && !pkey.startsWith("am_pk_live")) {
- throw new RecaseError({
- message: "Invalid publishable key",
- code: ErrCode.InvalidPublishableKey,
- statusCode: 401,
- });
- }
+ if (!pkey.startsWith("am_pk_test") && !pkey.startsWith("am_pk_live")) {
+ throw new RecaseError({
+ message: "Invalid publishable key",
+ code: ErrCode.InvalidPublishableKey,
+ statusCode: 401,
+ });
+ }
- let env: AppEnv = pkey.startsWith("am_pk_test")
- ? AppEnv.Sandbox
- : AppEnv.Live;
+ let env: AppEnv = pkey.startsWith("am_pk_test")
+ ? AppEnv.Sandbox
+ : AppEnv.Live;
- const data = await verifyPublicKey({
- db: req.db,
- pkey,
- env,
- });
+ const data = await verifyPublicKey({
+ db: req.db,
+ pkey,
+ env,
+ });
- if (!data) {
- throw new RecaseError({
- message: "Invalid publishable key",
- code: ErrCode.InvalidPublishableKey,
- statusCode: 401,
- });
- }
+ if (!data) {
+ throw new RecaseError({
+ message: "Invalid publishable key",
+ code: ErrCode.InvalidPublishableKey,
+ statusCode: 401,
+ });
+ }
- let { org, features } = data;
+ let { org, features } = data;
- req.minOrg = {
- id: org.id,
- slug: org.slug,
- };
- req.orgId = org.id;
- req.env = env;
- req.isPublic = true;
- req.org = org;
- req.features = features;
- req.authType = AuthType.PublicKey;
+ req.minOrg = {
+ id: org.id,
+ slug: org.slug,
+ };
+ req.orgId = org.id;
+ req.env = env;
+ req.isPublic = true;
+ req.org = org;
+ req.features = features;
+ req.authType = AuthType.PublicKey;
- next();
- return;
+ next();
+ return;
};
diff --git a/server/src/middleware/refreshCacheMiddleware.ts b/server/src/middleware/refreshCacheMiddleware.ts
index 0b3170d74..d8951226b 100644
--- a/server/src/middleware/refreshCacheMiddleware.ts
+++ b/server/src/middleware/refreshCacheMiddleware.ts
@@ -1,124 +1,124 @@
import {
- deleteCusCache,
- refreshCusCache,
+ deleteCusCache,
+ refreshCusCache,
} from "@/internal/customers/cusCache/updateCachedCus.js";
const cusPrefixedUrls = [
- {
- method: "POST",
- url: "/customers/:customer_id",
- type: "delete",
- },
- {
- method: "DELETE",
- url: "/customers/:customer_id",
- type: "delete",
- },
- {
- method: "POST",
- url: "/customers/:customer_id/balances",
- type: "delete",
- },
- {
- method: "POST",
- url: "/customers/:customer_id/entitlements/:customer_entitlement_id",
- type: "delete",
- },
- {
- method: "POST",
- url: "/customers/:customer_id/balances",
- type: "delete",
- },
- {
- method: "POST",
- url: "/customers/:customer_id/entities",
- type: "delete",
- },
- {
- method: "DELETE",
- url: "/customers/:customer_id/entities/:entity_id",
- type: "delete",
- },
- {
- method: "POST",
- url: "/customers/:customer_id/transfer_product",
- type: "delete",
- },
+ {
+ method: "POST",
+ url: "/customers/:customer_id",
+ type: "delete",
+ },
+ {
+ method: "DELETE",
+ url: "/customers/:customer_id",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/customers/:customer_id/balances",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/customers/:customer_id/entitlements/:customer_entitlement_id",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/customers/:customer_id/balances",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/customers/:customer_id/entities",
+ type: "delete",
+ },
+ {
+ method: "DELETE",
+ url: "/customers/:customer_id/entities/:entity_id",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/customers/:customer_id/transfer_product",
+ type: "delete",
+ },
];
const matchesCusPrefixedUrl = (url: string, method: string) => {
- return cusPrefixedUrls.find((urlObj) => {
- // Check if method matches
- if (urlObj.method !== method) {
- return false;
- }
+ return cusPrefixedUrls.find((urlObj) => {
+ // Check if method matches
+ if (urlObj.method !== method) {
+ return false;
+ }
- const regexPattern = urlObj.url
- .replace(/:[^/]+/g, "([^/]+)") // Replace :param with capturing group
- .replace(/\//g, "\\/"); // Escape forward slashes
+ const regexPattern = urlObj.url
+ .replace(/:[^/]+/g, "([^/]+)") // Replace :param with capturing group
+ .replace(/\//g, "\\/"); // Escape forward slashes
- const regex = new RegExp(`^${regexPattern}$`);
- return regex.test(url);
- });
+ const regex = new RegExp(`^${regexPattern}$`);
+ return regex.test(url);
+ });
};
const coreUrls = [
- {
- method: "POST",
- url: "/attach",
- type: "delete",
- },
- {
- method: "POST",
- url: "/cancel",
- type: "delete",
- },
+ {
+ method: "POST",
+ url: "/attach",
+ type: "delete",
+ },
+ {
+ method: "POST",
+ url: "/cancel",
+ type: "delete",
+ },
];
const handleRefreshCache = async (req: any, res: any) => {
- const { logger } = req;
- const pathMatch = matchesCusPrefixedUrl(
- req.originalUrl.replace("/v1", ""),
- req.method
- );
+ const { logger } = req;
+ const pathMatch = matchesCusPrefixedUrl(
+ req.originalUrl.replace("/v1", ""),
+ req.method,
+ );
- if (pathMatch) {
- const customerId = req.params.customer_id || req.params.customerId;
- logger.info(
- `Clearing cache for customer ${customerId}, url: ${req.originalUrl}`
- );
- await deleteCusCache({
- db: req.db,
- customerId,
- org: req.org,
- env: req.env,
- });
- }
+ if (pathMatch) {
+ const customerId = req.params.customer_id || req.params.customerId;
+ logger.info(
+ `Clearing cache for customer ${customerId}, url: ${req.originalUrl}`,
+ );
+ await deleteCusCache({
+ db: req.db,
+ customerId,
+ org: req.org,
+ env: req.env,
+ });
+ }
- const coreMatch = coreUrls.find(
- (urlObj) =>
- urlObj.url === req.originalUrl.replace("/v1", "") &&
- urlObj.method === req.method
- );
+ const coreMatch = coreUrls.find(
+ (urlObj) =>
+ urlObj.url === req.originalUrl.replace("/v1", "") &&
+ urlObj.method === req.method,
+ );
- if (coreMatch && req.body.customer_id) {
- logger.info(`Clearing cache for core url ${req.originalUrl}`);
- await deleteCusCache({
- db: req.db,
- customerId: req.body.customer_id,
- org: req.org,
- env: req.env,
- });
- }
+ if (coreMatch && req.body.customer_id) {
+ logger.info(`Clearing cache for core url ${req.originalUrl}`);
+ await deleteCusCache({
+ db: req.db,
+ customerId: req.body.customer_id,
+ org: req.org,
+ env: req.env,
+ });
+ }
};
export const refreshCacheMiddleware = async (req: any, res: any, next: any) => {
- // Replace res.send...
- const originalSend = res.send;
- res.send = async (body: any) => {
- await handleRefreshCache(req, res);
- await originalSend.call(res, body);
- };
+ // Replace res.send...
+ const originalSend = res.send;
+ res.send = async (body: any) => {
+ await handleRefreshCache(req, res);
+ await originalSend.call(res, body);
+ };
- next();
+ next();
};
diff --git a/server/src/middleware/trmnlAuthMiddleware.ts b/server/src/middleware/trmnlAuthMiddleware.ts
index da365d22e..4e348b96b 100644
--- a/server/src/middleware/trmnlAuthMiddleware.ts
+++ b/server/src/middleware/trmnlAuthMiddleware.ts
@@ -7,95 +7,95 @@ import { initUpstash } from "@/internal/customers/cusCache/upstashUtils.js";
export const trmnlExclusions = ["/trmnl/screen"];
export const trmnlAuthMiddleware = async (
- req: any,
- res: ExtendedResponse,
- next: any
+ req: any,
+ res: ExtendedResponse,
+ next: any,
) => {
- const upstash = await initUpstash();
+ const upstash = await initUpstash();
- if (!upstash) {
- res.status(500).json({
- message: "Upstash not found",
- code: ErrCode.InvalidSecretKey,
- statusCode: 401,
- });
- return;
- }
+ if (!upstash) {
+ res.status(500).json({
+ message: "Upstash not found",
+ code: ErrCode.InvalidSecretKey,
+ statusCode: 401,
+ });
+ return;
+ }
- req.logger.info(
- `received trmnl request, device id: ${req.headers["x-trmnl-id"]}`
- );
+ req.logger.info(
+ `received trmnl request, device id: ${req.headers["x-trmnl-id"]}`,
+ );
- const deviceId = req.headers["x-trmnl-id"];
- if (!deviceId) {
- res.status(401).json({
- message: "Device ID not found",
- code: ErrCode.InvalidSecretKey,
- statusCode: 401,
- });
- return;
- }
+ const deviceId = req.headers["x-trmnl-id"];
+ if (!deviceId) {
+ res.status(401).json({
+ message: "Device ID not found",
+ code: ErrCode.InvalidSecretKey,
+ statusCode: 401,
+ });
+ return;
+ }
- const trmnlConfig = (await upstash!.get(`trmnl:device:${deviceId}`)) as {
- orgId: string;
- hideRevenue: boolean;
- };
+ const trmnlConfig = (await upstash!.get(`trmnl:device:${deviceId}`)) as {
+ orgId: string;
+ hideRevenue: boolean;
+ };
- if (!trmnlConfig) {
- res.status(401).json({
- message: "Device ID invalid",
- code: ErrCode.InvalidSecretKey,
- statusCode: 401,
- });
- return;
- }
+ if (!trmnlConfig) {
+ res.status(401).json({
+ message: "Device ID invalid",
+ code: ErrCode.InvalidSecretKey,
+ statusCode: 401,
+ });
+ return;
+ }
- req.logger.info(`trmnl config: ${JSON.stringify(trmnlConfig)}`);
+ req.logger.info(`trmnl config: ${JSON.stringify(trmnlConfig)}`);
- req.env = req.headers["env"] || AppEnv.Live;
- const features = await FeatureService.list({
- db: req.db,
- orgId: trmnlConfig.orgId as string,
- env: req.env,
- });
+ req.env = req.headers["env"] || AppEnv.Live;
+ const features = await FeatureService.list({
+ db: req.db,
+ orgId: trmnlConfig.orgId as string,
+ env: req.env,
+ });
- req.org = {
- id: trmnlConfig.orgId,
- env: req.env,
- hideRevenue: trmnlConfig.hideRevenue,
- };
- req.features = features;
+ req.org = {
+ id: trmnlConfig.orgId,
+ env: req.env,
+ hideRevenue: trmnlConfig.hideRevenue,
+ };
+ req.features = features;
- // const logger = req.logtail;
+ // const logger = req.logtail;
- // const file = await readFile({ bucket: "private", path: "trmnl.json" });
- // const fileString = await file.text();
- // const fileJson = JSON.parse(fileString);
+ // const file = await readFile({ bucket: "private", path: "trmnl.json" });
+ // const fileString = await file.text();
+ // const fileJson = JSON.parse(fileString);
- // let trmnlId = req.headers["x-trmnl-id"];
- // if (!trmnlId)
- // return res.status(401).json({
- // message: "Trmnl ID not found",
- // code: ErrCode.InvalidSecretKey,
- // statusCode: 401,
- // });
+ // let trmnlId = req.headers["x-trmnl-id"];
+ // if (!trmnlId)
+ // return res.status(401).json({
+ // message: "Trmnl ID not found",
+ // code: ErrCode.InvalidSecretKey,
+ // statusCode: 401,
+ // });
- // if (!fileJson[trmnlId]) {
- // return res.status(401).json({
- // message: "Trmnl ID not found",
- // code: ErrCode.InvalidSecretKey,
- // statusCode: 401,
- // });
- // }
+ // if (!fileJson[trmnlId]) {
+ // return res.status(401).json({
+ // message: "Trmnl ID not found",
+ // code: ErrCode.InvalidSecretKey,
+ // statusCode: 401,
+ // });
+ // }
- // req.env = req.headers["env"] || AppEnv.Live;
- // const features = await FeatureService.list({
- // db: req.db,
- // orgId: fileJson[trmnlId],
- // env: req.env,
- // });
+ // req.env = req.headers["env"] || AppEnv.Live;
+ // const features = await FeatureService.list({
+ // db: req.db,
+ // orgId: fileJson[trmnlId],
+ // env: req.env,
+ // });
- // req.features = features;
+ // req.features = features;
- next();
+ next();
};
diff --git a/server/src/queue/JobName.ts b/server/src/queue/JobName.ts
index 213dad18c..4bf1d35ad 100644
--- a/server/src/queue/JobName.ts
+++ b/server/src/queue/JobName.ts
@@ -1,14 +1,14 @@
export enum JobName {
- UpdateBalance = "update-balance",
- UpdateUsage = "update-usage",
+ UpdateBalance = "update-balance",
+ UpdateUsage = "update-usage",
- Migration = "migration",
- RewardMigration = "reward-migration",
+ Migration = "migration",
+ RewardMigration = "reward-migration",
- TriggerCheckoutReward = "trigger-checkout-reward",
- GenerateFeatureDisplay = "generate-feature-display",
- DetectBaseVariant = "detect-base-variant",
+ TriggerCheckoutReward = "trigger-checkout-reward",
+ GenerateFeatureDisplay = "generate-feature-display",
+ DetectBaseVariant = "detect-base-variant",
- HandleProductsUpdated = "handle-products-updated",
- HandleCustomerCreated = "handle-customer-created",
+ HandleProductsUpdated = "handle-products-updated",
+ HandleCustomerCreated = "handle-customer-created",
}
diff --git a/server/src/queue/QueueManager.ts b/server/src/queue/QueueManager.ts
index b2f402ba4..35edd0989 100644
--- a/server/src/queue/QueueManager.ts
+++ b/server/src/queue/QueueManager.ts
@@ -7,156 +7,156 @@ const BACKUP_REDIS_URL = process.env.REDIS_BACKUP_URL || process.env.REDIS_URL;
const MAIN_REDIS_URL = process.env.REDIS_URL;
export class QueueManager {
- private static instance: QueueManager;
- private queue: Queue | null = null;
- private backupQueue: Queue | null = null;
+ private static instance: QueueManager;
+ private queue: Queue | null = null;
+ private backupQueue: Queue | null = null;
- private mainConnection: Redis | null = null;
- private backupConnection: Redis | null = null;
+ private mainConnection: Redis | null = null;
+ private backupConnection: Redis | null = null;
- private constructor() {
- this.initializePromise = this.initQueue();
- }
+ private constructor() {
+ this.initializePromise = this.initQueue();
+ }
- private initializePromise: Promise;
- public static async getInstance(): Promise {
- if (!QueueManager.instance) {
- QueueManager.instance = new QueueManager();
- }
- // Wait for initialization to complete
- await QueueManager.instance.initializePromise;
- return QueueManager.instance;
- }
+ private initializePromise: Promise;
+ public static async getInstance(): Promise {
+ if (!QueueManager.instance) {
+ QueueManager.instance = new QueueManager();
+ }
+ // Wait for initialization to complete
+ await QueueManager.instance.initializePromise;
+ return QueueManager.instance;
+ }
- // 1. Create main redis connection
- private async pingRedis({
- useBackup,
- keepConnection = false,
- }: {
- useBackup: boolean;
- keepConnection?: boolean;
- }) {
- const redisUrl = useBackup ? BACKUP_REDIS_URL : MAIN_REDIS_URL;
+ // 1. Create main redis connection
+ private async pingRedis({
+ useBackup,
+ keepConnection = false,
+ }: {
+ useBackup: boolean;
+ keepConnection?: boolean;
+ }) {
+ const redisUrl = useBackup ? BACKUP_REDIS_URL : MAIN_REDIS_URL;
- const connection = new Redis(redisUrl!, {
- retryStrategy: (times) => {
- return 5000;
- },
- });
+ const connection = new Redis(redisUrl!, {
+ retryStrategy: () => {
+ return 5000;
+ },
+ });
- connection.on("error", (error) => {
- console.log(
- `Redis connection error (${useBackup ? "backup" : "main"}): ${
- error.message
- }`
- );
+ connection.on("error", (error) => {
+ console.log(
+ `Redis connection error (${useBackup ? "backup" : "main"}): ${
+ error.message
+ }`,
+ );
- if (!keepConnection) {
- process.exit(1);
- }
- });
+ if (!keepConnection) {
+ process.exit(1);
+ }
+ });
- // Check if connection is live...
- await connection.ping();
+ // Check if connection is live...
+ await connection.ping();
- if (!keepConnection) {
- await connection.quit();
- }
- return connection;
- }
+ if (!keepConnection) {
+ await connection.quit();
+ }
+ return connection;
+ }
- private async createConnections() {
- console.log("2. Creating redis connections (for workers...)");
+ private async createConnections() {
+ console.log("2. Creating redis connections (for workers...)");
- this.mainConnection = await this.pingRedis({
- useBackup: false,
- keepConnection: true,
- });
- this.backupConnection = await this.pingRedis({
- useBackup: true,
- keepConnection: true,
- });
- }
+ this.mainConnection = await this.pingRedis({
+ useBackup: false,
+ keepConnection: true,
+ });
+ this.backupConnection = await this.pingRedis({
+ useBackup: true,
+ keepConnection: true,
+ });
+ }
- private async initQueue() {
- console.log("Initializing Queue Manager...");
- console.group();
- // 1. Create redis connections
- console.log("1. Pinging main & backup redis");
- this.mainConnection = await this.pingRedis({ useBackup: false });
- this.backupConnection = await this.pingRedis({ useBackup: true });
+ private async initQueue() {
+ console.log("Initializing Queue Manager...");
+ console.group();
+ // 1. Create redis connections
+ console.log("1. Pinging main & backup redis");
+ this.mainConnection = await this.pingRedis({ useBackup: false });
+ this.backupConnection = await this.pingRedis({ useBackup: true });
- await this.createConnections();
- // 2. Initialize main and backup queues
- console.log("2. Initializing main & backup queues");
- const mainQueue = new Queue("autumn", {
- connection: {
- url: MAIN_REDIS_URL,
- enableOfflineQueue: false,
- retryStrategy: (times: number) => {
- return 5000;
- },
- },
- });
+ await this.createConnections();
+ // 2. Initialize main and backup queues
+ console.log("2. Initializing main & backup queues");
+ const mainQueue = new Queue("autumn", {
+ connection: {
+ url: MAIN_REDIS_URL,
+ enableOfflineQueue: false,
+ retryStrategy: () => {
+ return 5000;
+ },
+ },
+ });
- const backupQueue = new Queue("autumn", {
- connection: {
- url: BACKUP_REDIS_URL,
- enableOfflineQueue: false,
- },
- });
+ const backupQueue = new Queue("autumn", {
+ connection: {
+ url: BACKUP_REDIS_URL,
+ enableOfflineQueue: false,
+ },
+ });
- // Set up error handling for the queue
- mainQueue.on("error", async (error: any) => {
- console.error("QUEUE ERROR:", error.message);
- if (error.code !== "ECONNREFUSED") {
- }
- });
+ // Set up error handling for the queue
+ mainQueue.on("error", async (error: any) => {
+ console.error("QUEUE ERROR:", error.message);
+ if (error.code !== "ECONNREFUSED") {
+ }
+ });
- backupQueue.on("error", async (error: any) => {
- console.error("BACKUP QUEUE ERROR:", error.message);
- if (error.code !== "ECONNREFUSED") {
- }
- });
+ backupQueue.on("error", async (error: any) => {
+ console.error("BACKUP QUEUE ERROR:", error.message);
+ if (error.code !== "ECONNREFUSED") {
+ }
+ });
- this.queue = mainQueue;
- this.backupQueue = backupQueue;
- console.groupEnd();
- }
+ this.queue = mainQueue;
+ this.backupQueue = backupQueue;
+ console.groupEnd();
+ }
- // Create workers
+ // Create workers
- public static async getQueue({
- useBackup,
- }: {
- useBackup: boolean;
- }): Promise {
- const queueManager = await QueueManager.getInstance();
- if (!queueManager.queue || !queueManager.backupQueue) {
- throw new Error("Queue not initialized");
- }
+ public static async getQueue({
+ useBackup,
+ }: {
+ useBackup: boolean;
+ }): Promise {
+ const queueManager = await QueueManager.getInstance();
+ if (!queueManager.queue || !queueManager.backupQueue) {
+ throw new Error("Queue not initialized");
+ }
- return useBackup ? queueManager.backupQueue : queueManager.queue;
- }
+ return useBackup ? queueManager.backupQueue : queueManager.queue;
+ }
- public static async getConnection({
- useBackup,
- }: {
- useBackup: boolean;
- }): Promise {
- const queueManager = await QueueManager.getInstance();
- if (!queueManager.mainConnection || !queueManager.backupConnection) {
- throw new Error("Connection not initialized");
- }
- return useBackup
- ? queueManager.backupConnection
- : queueManager.mainConnection;
- }
+ public static async getConnection({
+ useBackup,
+ }: {
+ useBackup: boolean;
+ }): Promise {
+ const queueManager = await QueueManager.getInstance();
+ if (!queueManager.mainConnection || !queueManager.backupConnection) {
+ throw new Error("Connection not initialized");
+ }
+ return useBackup
+ ? queueManager.backupConnection
+ : queueManager.mainConnection;
+ }
- public getBackupConnection(): Redis {
- if (!this.backupConnection) {
- throw new Error("Backup connection not initialized");
- }
- return this.backupConnection;
- }
+ public getBackupConnection(): Redis {
+ if (!this.backupConnection) {
+ throw new Error("Backup connection not initialized");
+ }
+ return this.backupConnection;
+ }
}
diff --git a/server/src/queue/lockUtils.ts b/server/src/queue/lockUtils.ts
index 745e1d2ed..2061a264f 100644
--- a/server/src/queue/lockUtils.ts
+++ b/server/src/queue/lockUtils.ts
@@ -2,68 +2,68 @@ import { Job, Queue } from "bullmq";
import { QueueManager } from "./QueueManager.js";
export const getRedisConnection = ({
- useBackup = false,
+ useBackup = false,
}: {
- useBackup?: boolean;
+ useBackup?: boolean;
}) => {
- let redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
+ let redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
- if (useBackup) {
- redisUrl = process.env.REDIS_BACKUP_URL || "redis://localhost:6379";
- }
+ if (useBackup) {
+ redisUrl = process.env.REDIS_BACKUP_URL || "redis://localhost:6379";
+ }
- return {
- connection: {
- url: redisUrl,
- // enableOfflineQueue: false,
- },
- };
+ return {
+ connection: {
+ url: redisUrl,
+ // enableOfflineQueue: false,
+ },
+ };
};
export async function getLock({
- lockKey,
- queue,
- job,
- useBackup = false,
+ lockKey,
+ queue,
+ job,
+ useBackup = false,
}: {
- lockKey: string;
- queue: Queue;
- job: Job;
- useBackup?: boolean;
+ lockKey: string;
+ queue: Queue;
+ job: Job;
+ useBackup?: boolean;
}) {
- if (!(await acquireLock({ lockKey, useBackup }))) {
- await queue.add(job.name, job.data, {
- delay: 1000,
- });
- return false;
- }
+ if (!(await acquireLock({ lockKey, useBackup }))) {
+ await queue.add(job.name, job.data, {
+ delay: 1000,
+ });
+ return false;
+ }
- return true;
+ return true;
}
export async function acquireLock({
- lockKey,
- timeout = 30000,
- useBackup = false,
+ lockKey,
+ timeout = 30000,
+ useBackup = false,
}: {
- lockKey: string;
- timeout?: number;
- useBackup?: boolean;
+ lockKey: string;
+ timeout?: number;
+ useBackup?: boolean;
}): Promise {
- // const redis = getRedisClient({ useBackup });
- const redis = await QueueManager.getConnection({ useBackup });
+ // const redis = getRedisClient({ useBackup });
+ const redis = await QueueManager.getConnection({ useBackup });
- const acquired = await redis.set(lockKey, "1", "PX", timeout, "NX");
- return acquired === "OK";
+ const acquired = await redis.set(lockKey, "1", "PX", timeout, "NX");
+ return acquired === "OK";
}
export async function releaseLock({
- lockKey,
- useBackup,
+ lockKey,
+ useBackup,
}: {
- lockKey: string;
- useBackup: boolean;
+ lockKey: string;
+ useBackup: boolean;
}): Promise {
- const redis = await QueueManager.getConnection({ useBackup });
- await redis.del(lockKey);
+ const redis = await QueueManager.getConnection({ useBackup });
+ await redis.del(lockKey);
}
diff --git a/server/src/queue/queueUtils.ts b/server/src/queue/queueUtils.ts
index 0438010a2..a1691993a 100644
--- a/server/src/queue/queueUtils.ts
+++ b/server/src/queue/queueUtils.ts
@@ -4,41 +4,41 @@ import { JobName } from "./JobName.js";
import { QueueManager } from "./QueueManager.js";
export interface Payloads {
- [JobName.RewardMigration]: {
- oldPrices: Price[];
- productId: string;
- // newPrices: Price[];
- // product: FullProduct;
- orgId: string;
- env: AppEnv;
- };
- [key: string]: any;
+ [JobName.RewardMigration]: {
+ oldPrices: Price[];
+ productId: string;
+ // newPrices: Price[];
+ // product: FullProduct;
+ orgId: string;
+ env: AppEnv;
+ };
+ [key: string]: any;
}
export const addTaskToQueue = async ({
- jobName,
- payload,
+ jobName,
+ payload,
}: {
- jobName: T;
- payload: Payloads[T];
+ jobName: T;
+ payload: Payloads[T];
}) => {
- try {
- const queue = await QueueManager.getQueue({ useBackup: false });
- await queue.add(jobName as string, payload);
- } catch (error: any) {
- try {
- console.log(`Adding ${jobName} to backup queue`);
- const backupQueue = await QueueManager.getQueue({ useBackup: true });
- await backupQueue.add(jobName as string, payload);
- } catch (error: any) {
- throw new RecaseError({
- message: `Failed to add ${jobName} to queue (backup)`,
- code: "EVENT_QUEUE_ERROR",
- statusCode: 500,
- data: {
- message: error.message,
- },
- });
- }
- }
+ try {
+ const queue = await QueueManager.getQueue({ useBackup: false });
+ await queue.add(jobName as string, payload);
+ } catch (error: any) {
+ try {
+ console.log(`Adding ${jobName} to backup queue`);
+ const backupQueue = await QueueManager.getQueue({ useBackup: true });
+ await backupQueue.add(jobName as string, payload);
+ } catch (error: any) {
+ throw new RecaseError({
+ message: `Failed to add ${jobName} to queue (backup)`,
+ code: "EVENT_QUEUE_ERROR",
+ statusCode: 500,
+ data: {
+ message: error.message,
+ },
+ });
+ }
+ }
};
diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts
index cf59b314e..6b950873b 100644
--- a/server/src/queue/workersInit.ts
+++ b/server/src/queue/workersInit.ts
@@ -19,225 +19,225 @@ import { runRewardMigrationTask } from "@/internal/migrations/runRewardMigration
const NUM_WORKERS = 10;
const actionHandlers = [
- JobName.HandleProductsUpdated,
- JobName.HandleCustomerCreated,
+ JobName.HandleProductsUpdated,
+ JobName.HandleCustomerCreated,
];
const { db } = initDrizzle({ maxConnections: 10 });
const initWorker = ({
- id,
- queue,
- useBackup,
- db,
+ id,
+ queue,
+ useBackup,
+ db,
}: {
- id: number;
- queue: Queue;
- useBackup: boolean;
- db: DrizzleCli;
+ id: number;
+ queue: Queue;
+ useBackup: boolean;
+ db: DrizzleCli;
}) => {
- const worker = new Worker(
- "autumn",
- async (job: Job) => {
- const logtail = logger.child({
- context: {
- worker: {
- task: job.name,
- data: job.data,
- jobId: generateId("job"),
- workerId: id,
- },
- },
- });
+ const worker = new Worker(
+ "autumn",
+ async (job: Job) => {
+ const logtail = logger.child({
+ context: {
+ worker: {
+ task: job.name,
+ data: job.data,
+ jobId: generateId("job"),
+ workerId: id,
+ },
+ },
+ });
- try {
- if (job.name === JobName.DetectBaseVariant) {
- await detectBaseVariant({
- db,
- curProduct: job.data.curProduct,
- logger: logtail as Logger,
- });
- return;
- }
+ try {
+ if (job.name === JobName.DetectBaseVariant) {
+ await detectBaseVariant({
+ db,
+ curProduct: job.data.curProduct,
+ logger: logtail as Logger,
+ });
+ return;
+ }
- if (job.name === JobName.GenerateFeatureDisplay) {
- await runSaveFeatureDisplayTask({
- db,
- feature: job.data.feature,
- logger: logtail,
- });
- return;
- }
+ if (job.name === JobName.GenerateFeatureDisplay) {
+ await runSaveFeatureDisplayTask({
+ db,
+ feature: job.data.feature,
+ logger: logtail,
+ });
+ return;
+ }
- if (job.name === JobName.Migration) {
- await runMigrationTask({
- db,
- payload: job.data,
- logger: logtail,
- });
- return;
- }
+ if (job.name === JobName.Migration) {
+ await runMigrationTask({
+ db,
+ payload: job.data,
+ logger: logtail,
+ });
+ return;
+ }
- if (actionHandlers.includes(job.name as JobName)) {
- await runActionHandlerTask({
- queue,
- job,
- logger: logtail,
- db,
- useBackup,
- });
- return;
- }
+ if (actionHandlers.includes(job.name as JobName)) {
+ await runActionHandlerTask({
+ queue,
+ job,
+ logger: logtail,
+ db,
+ useBackup,
+ });
+ return;
+ }
- if (job.name === JobName.RewardMigration) {
- await runRewardMigrationTask({
- db,
- payload: job.data,
- logger: logtail,
- });
- }
- } catch (error: any) {
- logtail.error(`Failed to process bullmq job: ${job.name}`, {
- jobName: job.name,
- error: {
- message: error.message,
- stack: error.stack,
- },
- });
- }
+ if (job.name === JobName.RewardMigration) {
+ await runRewardMigrationTask({
+ db,
+ payload: job.data,
+ logger: logtail,
+ });
+ }
+ } catch (error: any) {
+ logtail.error(`Failed to process bullmq job: ${job.name}`, {
+ jobName: job.name,
+ error: {
+ message: error.message,
+ stack: error.stack,
+ },
+ });
+ }
- // TRIGGER CHECKOUT REWARD
- if (job.name === JobName.TriggerCheckoutReward) {
- const lockKey = `reward_trigger:${job.data.customer?.internal_id}`;
- if (
- !(await acquireLock({
- lockKey,
- timeout: 10000,
- useBackup,
- }))
- ) {
- await queue.add(job.name, job.data, {
- delay: 1000,
- });
- return;
- }
+ // TRIGGER CHECKOUT REWARD
+ if (job.name === JobName.TriggerCheckoutReward) {
+ const lockKey = `reward_trigger:${job.data.customer?.internal_id}`;
+ if (
+ !(await acquireLock({
+ lockKey,
+ timeout: 10000,
+ useBackup,
+ }))
+ ) {
+ await queue.add(job.name, job.data, {
+ delay: 1000,
+ });
+ return;
+ }
- try {
- await runTriggerCheckoutReward({
- db,
- payload: job.data,
- logger: logtail,
- });
- } catch (error) {
- console.error("Error processing job:", error);
- } finally {
- await releaseLock({ lockKey, useBackup });
- }
+ try {
+ await runTriggerCheckoutReward({
+ db,
+ payload: job.data,
+ logger: logtail,
+ });
+ } catch (error) {
+ console.error("Error processing job:", error);
+ } finally {
+ await releaseLock({ lockKey, useBackup });
+ }
- return;
- }
+ return;
+ }
- // EVENT HANDLERS
- const { internalCustomerId } = job.data; // customerId is internal customer id
+ // EVENT HANDLERS
+ const { internalCustomerId } = job.data; // customerId is internal customer id
- while (
- !(await acquireLock({
- lockKey: `event:${internalCustomerId}`,
- timeout: 10000,
- useBackup,
- }))
- ) {
- await queue.add(job.name, job.data, {
- delay: 200,
- });
- return;
- }
+ while (
+ !(await acquireLock({
+ lockKey: `event:${internalCustomerId}`,
+ timeout: 10000,
+ useBackup,
+ }))
+ ) {
+ await queue.add(job.name, job.data, {
+ delay: 200,
+ });
+ return;
+ }
- try {
- if (job.name === JobName.UpdateBalance) {
- await runUpdateBalanceTask({
- payload: job.data,
- logger: logtail,
- db,
- });
- } else if (job.name === JobName.UpdateUsage) {
- await runUpdateUsageTask({
- payload: job.data,
- logger: logtail,
- db,
- });
- }
- } catch (error) {
- console.error("Error processing job:", error);
- } finally {
- await releaseLock({
- lockKey: `event:${internalCustomerId}`,
- useBackup,
- });
- }
- },
- {
- ...getRedisConnection({ useBackup }),
- concurrency: 1,
- removeOnComplete: {
- count: 0,
- },
- removeOnFail: {
- count: 0,
- },
- drainDelay: 1000,
- maxStalledCount: 0,
- }
- );
+ try {
+ if (job.name === JobName.UpdateBalance) {
+ await runUpdateBalanceTask({
+ payload: job.data,
+ logger: logtail,
+ db,
+ });
+ } else if (job.name === JobName.UpdateUsage) {
+ await runUpdateUsageTask({
+ payload: job.data,
+ logger: logtail,
+ db,
+ });
+ }
+ } catch (error) {
+ console.error("Error processing job:", error);
+ } finally {
+ await releaseLock({
+ lockKey: `event:${internalCustomerId}`,
+ useBackup,
+ });
+ }
+ },
+ {
+ ...getRedisConnection({ useBackup }),
+ concurrency: 1,
+ removeOnComplete: {
+ count: 0,
+ },
+ removeOnFail: {
+ count: 0,
+ },
+ drainDelay: 1000,
+ maxStalledCount: 0,
+ },
+ );
- worker.on("ready", () => {
- console.log(`Worker ${id} ready (${useBackup ? "BACKUP" : "MAIN"})`);
- });
+ worker.on("ready", () => {
+ console.log(`Worker ${id} ready (${useBackup ? "BACKUP" : "MAIN"})`);
+ });
- worker.on("stalled", (jobId: string) => {
- console.log(`Worker ${id} stalled (${useBackup ? "BACKUP" : "MAIN"})`);
- console.log("JOB ID:", jobId);
- });
+ worker.on("stalled", (jobId: string) => {
+ console.log(`Worker ${id} stalled (${useBackup ? "BACKUP" : "MAIN"})`);
+ console.log("JOB ID:", jobId);
+ });
- worker.on("error", async (error: any) => {
- if (error.code !== "ECONNREFUSED") {
- console.log("WORKER ERROR:", error.message);
- }
- });
+ worker.on("error", async (error: any) => {
+ if (error.code !== "ECONNREFUSED") {
+ console.log("WORKER ERROR:", error.message);
+ }
+ });
- worker.on("failed", (_, error) => {
- console.log("WORKER FAILED:", error.message);
- });
+ worker.on("failed", (_, error) => {
+ console.log("WORKER FAILED:", error.message);
+ });
};
export const initWorkers = async () => {
- const workers = [];
+ const workers = [];
- const mainQueue = await QueueManager.getQueue({ useBackup: false });
- const backupQueue = await QueueManager.getQueue({ useBackup: true });
- await CacheManager.getInstance();
+ const mainQueue = await QueueManager.getQueue({ useBackup: false });
+ const backupQueue = await QueueManager.getQueue({ useBackup: true });
+ await CacheManager.getInstance();
- for (let i = 0; i < NUM_WORKERS; i++) {
- workers.push(
- initWorker({
- id: i,
- queue: mainQueue,
- useBackup: false,
- db,
- })
- );
- workers.push(
- initWorker({
- id: i,
- queue: backupQueue,
- useBackup: true,
+ for (let i = 0; i < NUM_WORKERS; i++) {
+ workers.push(
+ initWorker({
+ id: i,
+ queue: mainQueue,
+ useBackup: false,
+ db,
+ }),
+ );
+ workers.push(
+ initWorker({
+ id: i,
+ queue: backupQueue,
+ useBackup: true,
- db,
- })
- );
- }
+ db,
+ }),
+ );
+ }
- // Get stalled jobs
+ // Get stalled jobs
- return workers;
+ return workers;
};
diff --git a/server/src/trigger/adjustAllowance.ts b/server/src/trigger/adjustAllowance.ts
index afab5331a..66e1277c9 100644
--- a/server/src/trigger/adjustAllowance.ts
+++ b/server/src/trigger/adjustAllowance.ts
@@ -1,18 +1,18 @@
import {
- Entitlement,
- ErrCode,
- FullCusEntWithFullCusProduct,
- FullCusEntWithProduct,
- Price,
+ Entitlement,
+ ErrCode,
+ FullCusEntWithFullCusProduct,
+ FullCusEntWithProduct,
+ Price,
} from "@autumn/shared";
import {
- AppEnv,
- BillingType,
- Customer,
- Feature,
- FullCustomerPrice,
- Organization,
- UsagePriceConfig,
+ AppEnv,
+ BillingType,
+ Customer,
+ Feature,
+ FullCustomerPrice,
+ Organization,
+ UsagePriceConfig,
} from "@autumn/shared";
import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
@@ -29,145 +29,145 @@ import RecaseError from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";
export const getUsageFromBalance = ({
- ent,
- price,
- balance,
+ ent,
+ price,
+ balance,
}: {
- ent: Entitlement;
- price: Price;
- balance: number;
+ ent: Entitlement;
+ price: Price;
+ balance: number;
}) => {
- let config = price.config as UsagePriceConfig;
- let billingUnits = config.billing_units || 1;
+ let config = price.config as UsagePriceConfig;
+ let billingUnits = config.billing_units || 1;
- // Should get overage...
- let overage = -Math.min(0, balance);
- let roundedOverage = new Decimal(overage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
+ // Should get overage...
+ let overage = -Math.min(0, balance);
+ let roundedOverage = new Decimal(overage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
- let usage = new Decimal(ent.allowance!).sub(balance).toNumber();
+ let usage = new Decimal(ent.allowance!).sub(balance).toNumber();
- let roundedUsage = usage;
- if (overage > 0) {
- roundedUsage = new Decimal(usage)
- .div(billingUnits)
- .ceil()
- .mul(billingUnits)
- .toNumber();
- }
+ let roundedUsage = usage;
+ if (overage > 0) {
+ roundedUsage = new Decimal(usage)
+ .div(billingUnits)
+ .ceil()
+ .mul(billingUnits)
+ .toNumber();
+ }
- return { usage, roundedUsage, overage, roundedOverage };
+ return { usage, roundedUsage, overage, roundedOverage };
};
export const adjustAllowance = async ({
- db,
- env,
- org,
- affectedFeature,
- cusEnt,
- cusPrices,
- customer,
- originalBalance,
- newBalance,
- logger,
- errorIfIncomplete = false,
- // deduction,
- // product,
- // fromEntities = false,
+ db,
+ env,
+ org,
+ affectedFeature,
+ cusEnt,
+ cusPrices,
+ customer,
+ originalBalance,
+ newBalance,
+ logger,
+ errorIfIncomplete = false,
+ // deduction,
+ // product,
+ // fromEntities = false,
}: {
- db: DrizzleCli;
- env: AppEnv;
- affectedFeature: Feature;
- org: Organization;
- cusEnt: FullCusEntWithFullCusProduct;
- cusPrices: FullCustomerPrice[];
- customer: Customer;
- originalBalance: number;
- newBalance: number;
- logger: any;
- errorIfIncomplete?: boolean;
+ db: DrizzleCli;
+ env: AppEnv;
+ affectedFeature: Feature;
+ org: Organization;
+ cusEnt: FullCusEntWithFullCusProduct;
+ cusPrices: FullCustomerPrice[];
+ customer: Customer;
+ originalBalance: number;
+ newBalance: number;
+ logger: any;
+ errorIfIncomplete?: boolean;
}) => {
- let cusPrice = getRelatedCusPrice(cusEnt, cusPrices);
- let billingType = cusPrice ? getBillingType(cusPrice.price.config!) : null;
- let cusProduct = cusEnt.customer_product;
+ let cusPrice = getRelatedCusPrice(cusEnt, cusPrices);
+ let billingType = cusPrice ? getBillingType(cusPrice.price.config!) : null;
+ let cusProduct = cusEnt.customer_product;
- // TODO: TRACK
+ // TODO: TRACK
- if (
- !cusProduct ||
- !cusPrice ||
- billingType !== BillingType.InArrearProrated ||
- originalBalance == newBalance
- ) {
- return { newReplaceables: [], invoice: null, deletedReplaceables: null };
- }
+ if (
+ !cusProduct ||
+ !cusPrice ||
+ billingType !== BillingType.InArrearProrated ||
+ originalBalance == newBalance
+ ) {
+ return { newReplaceables: [], invoice: null, deletedReplaceables: null };
+ }
- let ent = cusEnt.entitlement;
- if (ent.usage_limit && newBalance < ent.allowance! - (ent.usage_limit || 0)) {
- throw new RecaseError({
- message: `Balance exceeds usage limit of ${cusEnt.entitlement.usage_limit}`,
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ let ent = cusEnt.entitlement;
+ if (ent.usage_limit && newBalance < ent.allowance! - (ent.usage_limit || 0)) {
+ throw new RecaseError({
+ message: `Balance exceeds usage limit of ${cusEnt.entitlement.usage_limit}`,
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- logger.info(`--------------------------------`);
- logger.info(`Updating arrear prorated usage: ${affectedFeature.name}`);
- logger.info(`Customer: ${customer.name}, Org: ${org.slug}`);
+ logger.info(`--------------------------------`);
+ logger.info(`Updating arrear prorated usage: ${affectedFeature.name}`);
+ logger.info(`Customer: ${customer.name}, Org: ${org.slug}`);
- let stripeCli = createStripeCli({ org, env });
- let sub = await getUsageBasedSub({
- db,
- stripeCli,
- subIds: cusProduct.subscription_ids!,
- feature: affectedFeature,
- });
+ let stripeCli = createStripeCli({ org, env });
+ let sub = await getUsageBasedSub({
+ db,
+ stripeCli,
+ subIds: cusProduct.subscription_ids!,
+ feature: affectedFeature,
+ });
- if (!sub) {
- logger.error("adjustAllowance: no usage-based sub found");
- return { newReplaceables: null, invoice: null, deletedReplaceables: null };
- }
+ if (!sub) {
+ logger.error("adjustAllowance: no usage-based sub found");
+ return { newReplaceables: null, invoice: null, deletedReplaceables: null };
+ }
- let subItem = findStripeItemForPrice({
- price: cusPrice.price,
- stripeItems: sub.items.data,
- });
+ let subItem = findStripeItemForPrice({
+ price: cusPrice.price,
+ stripeItems: sub.items.data,
+ });
- if (!subItem) {
- logger.error("adjustAllowance: no sub item found");
- return { newReplaceables: null, invoice: null, deletedReplaceables: null };
- }
+ if (!subItem) {
+ logger.error("adjustAllowance: no sub item found");
+ return { newReplaceables: null, invoice: null, deletedReplaceables: null };
+ }
- let isUpgrade = newBalance < originalBalance;
+ let isUpgrade = newBalance < originalBalance;
- if (isUpgrade) {
- return await handleProratedUpgrade({
- db,
- stripeCli,
- cusEnt,
- cusPrice,
- sub,
- subItem: subItem as Stripe.SubscriptionItem,
- newBalance,
- prevBalance: originalBalance,
- org,
- logger,
- });
- } else {
- return await handleProratedDowngrade({
- db,
- org,
- stripeCli,
- cusEnt,
- cusPrice,
- sub,
- subItem: subItem as Stripe.SubscriptionItem,
- newBalance,
- prevBalance: originalBalance,
- logger,
- });
- }
+ if (isUpgrade) {
+ return await handleProratedUpgrade({
+ db,
+ stripeCli,
+ cusEnt,
+ cusPrice,
+ sub,
+ subItem: subItem as Stripe.SubscriptionItem,
+ newBalance,
+ prevBalance: originalBalance,
+ org,
+ logger,
+ });
+ } else {
+ return await handleProratedDowngrade({
+ db,
+ org,
+ stripeCli,
+ cusEnt,
+ cusPrice,
+ sub,
+ subItem: subItem as Stripe.SubscriptionItem,
+ newBalance,
+ prevBalance: originalBalance,
+ logger,
+ });
+ }
};
diff --git a/server/src/trigger/arrearProratedUsage/createUpgradeProrationInvoice.ts b/server/src/trigger/arrearProratedUsage/createUpgradeProrationInvoice.ts
index e519e3467..bb3b3d535 100644
--- a/server/src/trigger/arrearProratedUsage/createUpgradeProrationInvoice.ts
+++ b/server/src/trigger/arrearProratedUsage/createUpgradeProrationInvoice.ts
@@ -1,11 +1,11 @@
import {
- Feature,
- FullCustomerPrice,
- OnIncrease,
- Organization,
- Price,
- Product,
- UsagePriceConfig,
+ Feature,
+ FullCustomerPrice,
+ OnIncrease,
+ Organization,
+ Price,
+ Product,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import { getFeatureInvoiceDescription } from "@autumn/shared";
@@ -16,153 +16,153 @@ import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import {
- shouldProrate,
- shouldBillNow,
+ shouldProrate,
+ shouldBillNow,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
export const getUpgradeProrationInvoiceItem = ({
- prevPrice,
- newPrice,
- now,
- feature,
- newRoundedUsage,
- price,
- org,
- onIncrease,
- product,
- stripeSub,
- subItem,
+ prevPrice,
+ newPrice,
+ now,
+ feature,
+ newRoundedUsage,
+ price,
+ org,
+ onIncrease,
+ product,
+ stripeSub,
+ subItem,
}: {
- prevPrice: number;
- newPrice: number;
- now: number;
- feature: Feature;
- newRoundedUsage: number;
- price: Price;
- org: Organization;
- onIncrease: OnIncrease;
- product: Product;
- stripeSub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
+ prevPrice: number;
+ newPrice: number;
+ now: number;
+ feature: Feature;
+ newRoundedUsage: number;
+ price: Price;
+ org: Organization;
+ onIncrease: OnIncrease;
+ product: Product;
+ stripeSub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
}) => {
- const billingUnits = (price.config as UsagePriceConfig).billing_units;
- let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
- let invoiceDescription = getFeatureInvoiceDescription({
- feature,
- usage: newRoundedUsage,
- billingUnits,
- prodName: product.name,
- });
+ const billingUnits = (price.config as UsagePriceConfig).billing_units;
+ let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
+ let invoiceDescription = getFeatureInvoiceDescription({
+ feature,
+ usage: newRoundedUsage,
+ billingUnits,
+ prodName: product.name,
+ });
- console.log("Invoice amount: ", invoiceAmount);
- console.log("Invoice description:", invoiceDescription);
+ console.log("Invoice amount: ", invoiceAmount);
+ console.log("Invoice description:", invoiceDescription);
- if (shouldProrate(onIncrease)) {
- invoiceAmount = calculateProrationAmount({
- periodStart: subItem.current_period_start * 1000,
- periodEnd: subItem.current_period_end * 1000,
- now,
- amount: invoiceAmount,
- });
+ if (shouldProrate(onIncrease)) {
+ invoiceAmount = calculateProrationAmount({
+ periodStart: subItem.current_period_start * 1000,
+ periodEnd: subItem.current_period_end * 1000,
+ now,
+ amount: invoiceAmount,
+ });
- let start = formatUnixToDate(now);
- let end = formatUnixToDate(subItem.current_period_end * 1000);
- invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
- }
+ let start = formatUnixToDate(now);
+ let end = formatUnixToDate(subItem.current_period_end * 1000);
+ invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
+ }
- let invoiceItem = constructStripeInvoiceItem({
- product,
- amount: invoiceAmount,
- org,
- price: price,
- description: invoiceDescription,
- stripeSubId: stripeSub.id,
- stripeCustomerId: stripeSub.customer as string,
- periodStart: Math.floor(now / 1000),
- periodEnd: Math.floor(subItem.current_period_end * 1000),
- });
+ let invoiceItem = constructStripeInvoiceItem({
+ product,
+ amount: invoiceAmount,
+ org,
+ price: price,
+ description: invoiceDescription,
+ stripeSubId: stripeSub.id,
+ stripeCustomerId: stripeSub.customer as string,
+ periodStart: Math.floor(now / 1000),
+ periodEnd: Math.floor(subItem.current_period_end * 1000),
+ });
- return invoiceItem;
+ return invoiceItem;
};
export const createUpgradeProrationInvoice = async ({
- org,
- cusPrice,
- stripeCli,
- sub,
- subItem,
- newPrice,
- prevPrice,
- newRoundedUsage,
- feature,
- product,
- config,
- onIncrease,
- logger,
+ org,
+ cusPrice,
+ stripeCli,
+ sub,
+ subItem,
+ newPrice,
+ prevPrice,
+ newRoundedUsage,
+ feature,
+ product,
+ config,
+ onIncrease,
+ logger,
}: {
- org: Organization;
- cusPrice: FullCustomerPrice;
- stripeCli: Stripe;
- sub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
- newPrice: number;
- prevPrice: number;
- newRoundedUsage: number;
- feature: Feature;
- product: Product;
- config: UsagePriceConfig;
- onIncrease: OnIncrease;
- logger: any;
+ org: Organization;
+ cusPrice: FullCustomerPrice;
+ stripeCli: Stripe;
+ sub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
+ newPrice: number;
+ prevPrice: number;
+ newRoundedUsage: number;
+ feature: Feature;
+ product: Product;
+ config: UsagePriceConfig;
+ onIncrease: OnIncrease;
+ logger: any;
}) => {
- let now = await getStripeNow({ stripeCli, stripeSub: sub });
+ let now = await getStripeNow({ stripeCli, stripeSub: sub });
- const paymentMethod = await getCusPaymentMethod({
- stripeCli,
- stripeId: sub.customer as string,
- });
+ const paymentMethod = await getCusPaymentMethod({
+ stripeCli,
+ stripeId: sub.customer as string,
+ });
- let invoiceItem = getUpgradeProrationInvoiceItem({
- prevPrice,
- newPrice,
- now,
- feature,
- newRoundedUsage,
- price: cusPrice.price,
- org,
- onIncrease,
- product,
- stripeSub: sub,
- subItem,
- });
+ let invoiceItem = getUpgradeProrationInvoiceItem({
+ prevPrice,
+ newPrice,
+ now,
+ feature,
+ newRoundedUsage,
+ price: cusPrice.price,
+ org,
+ onIncrease,
+ product,
+ stripeSub: sub,
+ subItem,
+ });
- let invoiceAmount =
- invoiceItem?.amount || invoiceItem?.price_data?.unit_amount || 0;
+ let invoiceAmount =
+ invoiceItem?.amount || invoiceItem?.price_data?.unit_amount || 0;
- let invoiceDescription = invoiceItem?.description || "";
+ let invoiceDescription = invoiceItem?.description || "";
- if (invoiceAmount == 0) return;
+ if (invoiceAmount == 0) return;
- logger.info(
- `🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`
- );
+ logger.info(
+ `🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`,
+ );
- await stripeCli.invoiceItems.create(invoiceItem);
+ await stripeCli.invoiceItems.create(invoiceItem);
- if (shouldBillNow(onIncrease)) {
- const { invoice: finalInvoice } = await createAndFinalizeInvoice({
- stripeCli,
- paymentMethod,
- stripeCusId: sub.customer as string,
- stripeSubId: sub.id,
- logger,
- });
+ if (shouldBillNow(onIncrease)) {
+ const { invoice: finalInvoice } = await createAndFinalizeInvoice({
+ stripeCli,
+ paymentMethod,
+ stripeCusId: sub.customer as string,
+ stripeSubId: sub.id,
+ logger,
+ });
- logger.info(`Paid for invoice ${finalInvoice?.id}`);
- return finalInvoice;
- }
+ logger.info(`Paid for invoice ${finalInvoice?.id}`);
+ return finalInvoice;
+ }
- return null;
+ return null;
};
diff --git a/server/src/trigger/arrearProratedUsage/handleCreateReplaceables.ts b/server/src/trigger/arrearProratedUsage/handleCreateReplaceables.ts
index a46acc200..b853838ff 100644
--- a/server/src/trigger/arrearProratedUsage/handleCreateReplaceables.ts
+++ b/server/src/trigger/arrearProratedUsage/handleCreateReplaceables.ts
@@ -3,9 +3,9 @@ import { findLinkedCusEnts } from "@/internal/customers/cusProducts/cusEnts/cusE
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import { generateId } from "@/utils/genUtils.js";
import {
- FullCusEntWithFullCusProduct,
- FullCusEntWithProduct,
- InsertReplaceableSchema,
+ FullCusEntWithFullCusProduct,
+ FullCusEntWithProduct,
+ InsertReplaceableSchema,
} from "@autumn/shared";
// export const handleCreateReplaceables = async ({
diff --git a/server/src/trigger/arrearProratedUsage/handleProratedDowngrade.ts b/server/src/trigger/arrearProratedUsage/handleProratedDowngrade.ts
index a302e0d9c..8626fb861 100644
--- a/server/src/trigger/arrearProratedUsage/handleProratedDowngrade.ts
+++ b/server/src/trigger/arrearProratedUsage/handleProratedDowngrade.ts
@@ -1,16 +1,16 @@
import {
- FullCusEntWithFullCusProduct,
- FullCustomerPrice,
- InsertReplaceable,
- OnDecrease,
+ FullCusEntWithFullCusProduct,
+ FullCustomerPrice,
+ InsertReplaceable,
+ OnDecrease,
} from "@autumn/shared";
import Stripe from "stripe";
import {
- Feature,
- Organization,
- Product,
- UsagePriceConfig,
+ Feature,
+ Organization,
+ Product,
+ UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import { getFeatureInvoiceDescription } from "@autumn/shared";
@@ -26,210 +26,210 @@ import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtil
import { getReplaceables } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.js";
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
import {
- shouldBillNow,
- shouldProrate,
+ shouldBillNow,
+ shouldProrate,
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
export const createDowngradeProrationInvoice = async ({
- org,
- cusPrice,
- stripeCli,
- sub,
- subItem,
- newPrice,
- prevPrice,
- newRoundedUsage,
- feature,
- product,
- onDecrease,
- logger,
+ org,
+ cusPrice,
+ stripeCli,
+ sub,
+ subItem,
+ newPrice,
+ prevPrice,
+ newRoundedUsage,
+ feature,
+ product,
+ onDecrease,
+ logger,
}: {
- org: Organization;
- cusPrice: FullCustomerPrice;
- stripeCli: Stripe;
- sub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
- newPrice: number;
- prevPrice: number;
- newRoundedUsage: number;
- feature: Feature;
- product: Product;
- onDecrease: OnDecrease;
- logger: any;
+ org: Organization;
+ cusPrice: FullCustomerPrice;
+ stripeCli: Stripe;
+ sub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
+ newPrice: number;
+ prevPrice: number;
+ newRoundedUsage: number;
+ feature: Feature;
+ product: Product;
+ onDecrease: OnDecrease;
+ logger: any;
}) => {
- const config = cusPrice.price.config as UsagePriceConfig;
+ const config = cusPrice.price.config as UsagePriceConfig;
- let now = await getStripeNow({ stripeCli, stripeSub: sub });
- let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
+ let now = await getStripeNow({ stripeCli, stripeSub: sub });
+ let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
- logger.info(`Prev price: ${prevPrice}, New price: ${newPrice}`);
- logger.info(`Invoice amount: ${invoiceAmount}`);
+ logger.info(`Prev price: ${prevPrice}, New price: ${newPrice}`);
+ logger.info(`Invoice amount: ${invoiceAmount}`);
- let invoiceDescription = getFeatureInvoiceDescription({
- feature,
- usage: newRoundedUsage,
- billingUnits: config.billing_units,
- prodName: product.name,
- });
+ let invoiceDescription = getFeatureInvoiceDescription({
+ feature,
+ usage: newRoundedUsage,
+ billingUnits: config.billing_units,
+ prodName: product.name,
+ });
- invoiceAmount = calculateProrationAmount({
- periodStart: subItem.current_period_start * 1000,
- periodEnd: subItem.current_period_end * 1000,
- now,
- amount: invoiceAmount,
- allowNegative: true,
- });
+ invoiceAmount = calculateProrationAmount({
+ periodStart: subItem.current_period_start * 1000,
+ periodEnd: subItem.current_period_end * 1000,
+ now,
+ amount: invoiceAmount,
+ allowNegative: true,
+ });
- let start = formatUnixToDate(now);
- let end = formatUnixToDate(subItem.current_period_end * 1000);
- invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
+ let start = formatUnixToDate(now);
+ let end = formatUnixToDate(subItem.current_period_end * 1000);
+ invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
- if (invoiceAmount == 0) return;
+ if (invoiceAmount == 0) return;
- logger.info(
- `🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`
- );
+ logger.info(
+ `🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`,
+ );
- const invoiceItem = constructStripeInvoiceItem({
- product,
- amount: invoiceAmount,
- org,
- price: cusPrice.price,
- description: invoiceDescription,
- stripeSubId: sub.id,
- stripeCustomerId: sub.customer as string,
- periodStart: Math.floor(now / 1000),
- periodEnd: Math.floor(subItem.current_period_end * 1000),
- });
+ const invoiceItem = constructStripeInvoiceItem({
+ product,
+ amount: invoiceAmount,
+ org,
+ price: cusPrice.price,
+ description: invoiceDescription,
+ stripeSubId: sub.id,
+ stripeCustomerId: sub.customer as string,
+ periodStart: Math.floor(now / 1000),
+ periodEnd: Math.floor(subItem.current_period_end * 1000),
+ });
- await stripeCli.invoiceItems.create(invoiceItem);
- let invoice = null;
+ await stripeCli.invoiceItems.create(invoiceItem);
+ let invoice = null;
- if (shouldBillNow(onDecrease)) {
- const { invoice: finalInvoice } = await createAndFinalizeInvoice({
- stripeCli,
- paymentMethod: null,
- stripeCusId: sub.customer as string,
- stripeSubId: sub.id,
- logger,
- });
+ if (shouldBillNow(onDecrease)) {
+ const { invoice: finalInvoice } = await createAndFinalizeInvoice({
+ stripeCli,
+ paymentMethod: null,
+ stripeCusId: sub.customer as string,
+ stripeSubId: sub.id,
+ logger,
+ });
- invoice = finalInvoice;
- }
+ invoice = finalInvoice;
+ }
- return invoice;
+ return invoice;
};
export const handleProratedDowngrade = async ({
- db,
- org,
- stripeCli,
- cusEnt,
- cusPrice,
- sub,
- subItem,
- newBalance,
- prevBalance,
- logger,
+ db,
+ org,
+ stripeCli,
+ cusEnt,
+ cusPrice,
+ sub,
+ subItem,
+ newBalance,
+ prevBalance,
+ logger,
}: {
- db: DrizzleCli;
- org: Organization;
- stripeCli: Stripe;
- cusEnt: FullCusEntWithFullCusProduct;
- cusPrice: FullCustomerPrice;
- sub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
- newBalance: number;
- prevBalance: number;
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ stripeCli: Stripe;
+ cusEnt: FullCusEntWithFullCusProduct;
+ cusPrice: FullCustomerPrice;
+ sub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
+ newBalance: number;
+ prevBalance: number;
+ logger: any;
}) => {
- logger.info(`Handling quantity decrease`);
+ logger.info(`Handling quantity decrease`);
- const { overage: prevOverage, usage: prevUsage } = getUsageFromBalance({
- ent: cusEnt.entitlement,
- price: cusPrice.price,
- balance: prevBalance,
- });
+ const { overage: prevOverage, usage: prevUsage } = getUsageFromBalance({
+ ent: cusEnt.entitlement,
+ price: cusPrice.price,
+ balance: prevBalance,
+ });
- const { overage: newOverage, usage: newUsage } = getUsageFromBalance({
- ent: cusEnt.entitlement,
- price: cusPrice.price,
- balance: newBalance,
- });
+ const { overage: newOverage, usage: newUsage } = getUsageFromBalance({
+ ent: cusEnt.entitlement,
+ price: cusPrice.price,
+ balance: newBalance,
+ });
- let onDecrease =
- cusPrice.price.proration_config?.on_decrease ||
- OnDecrease.ProrateImmediately;
+ let onDecrease =
+ cusPrice.price.proration_config?.on_decrease ||
+ OnDecrease.ProrateImmediately;
- const feature = cusEnt.entitlement.feature;
- const product = cusEnt.customer_product.product;
+ const feature = cusEnt.entitlement.feature;
+ const product = cusEnt.customer_product.product;
- let invoice = null;
- let newReplaceables: InsertReplaceable[] = [];
+ let invoice = null;
+ let newReplaceables: InsertReplaceable[] = [];
- if (onDecrease == OnDecrease.NoProrations) {
- } else if (shouldProrate(onDecrease)) {
- let prevPrice = priceToInvoiceAmount({
- price: cusPrice.price,
- overage: roundUsage({
- usage: prevOverage,
- price: cusPrice.price,
- }),
- });
+ if (onDecrease == OnDecrease.NoProrations) {
+ } else if (shouldProrate(onDecrease)) {
+ let prevPrice = priceToInvoiceAmount({
+ price: cusPrice.price,
+ overage: roundUsage({
+ usage: prevOverage,
+ price: cusPrice.price,
+ }),
+ });
- let newPrice = priceToInvoiceAmount({
- price: cusPrice.price,
- overage: roundUsage({
- usage: newOverage,
- price: cusPrice.price,
- }),
- });
+ let newPrice = priceToInvoiceAmount({
+ price: cusPrice.price,
+ overage: roundUsage({
+ usage: newOverage,
+ price: cusPrice.price,
+ }),
+ });
- invoice = await createDowngradeProrationInvoice({
- org,
- cusPrice,
- stripeCli,
- sub,
- subItem,
- newPrice,
- prevPrice,
- newRoundedUsage: roundUsage({
- usage: newUsage,
- price: cusPrice.price,
- }),
- feature,
- product,
- onDecrease,
- logger,
- });
- } else {
- if (prevOverage > 0) {
- newReplaceables = getReplaceables({
- cusEnt,
- prevOverage: prevUsage,
- newOverage: newUsage,
- });
+ invoice = await createDowngradeProrationInvoice({
+ org,
+ cusPrice,
+ stripeCli,
+ sub,
+ subItem,
+ newPrice,
+ prevPrice,
+ newRoundedUsage: roundUsage({
+ usage: newUsage,
+ price: cusPrice.price,
+ }),
+ feature,
+ product,
+ onDecrease,
+ logger,
+ });
+ } else {
+ if (prevOverage > 0) {
+ newReplaceables = getReplaceables({
+ cusEnt,
+ prevOverage: prevUsage,
+ newOverage: newUsage,
+ });
- await RepService.insert({
- db,
- data: newReplaceables,
- });
- }
- }
+ await RepService.insert({
+ db,
+ data: newReplaceables,
+ });
+ }
+ }
- let numDeletedReplaceables = cusEnt.replaceables.filter(
- (r) => r.delete_next_cycle
- ).length;
- let newQuantity = newUsage - numDeletedReplaceables;
+ let numDeletedReplaceables = cusEnt.replaceables.filter(
+ (r) => r.delete_next_cycle,
+ ).length;
+ let newQuantity = newUsage - numDeletedReplaceables;
- await stripeCli.subscriptionItems.update(subItem.id, {
- quantity: roundUsage({
- usage: newQuantity,
- price: cusPrice.price,
- }),
- proration_behavior: "none",
- });
- logger.info(`Updated sub item quantity to ${newUsage}`);
+ await stripeCli.subscriptionItems.update(subItem.id, {
+ quantity: roundUsage({
+ usage: newQuantity,
+ price: cusPrice.price,
+ }),
+ proration_behavior: "none",
+ });
+ logger.info(`Updated sub item quantity to ${newUsage}`);
- return { invoice, newReplaceables, deletedReplaceables: null };
+ return { invoice, newReplaceables, deletedReplaceables: null };
};
diff --git a/server/src/trigger/arrearProratedUsage/handleProratedUpgrade.ts b/server/src/trigger/arrearProratedUsage/handleProratedUpgrade.ts
index c2b20538c..74421b71a 100644
--- a/server/src/trigger/arrearProratedUsage/handleProratedUpgrade.ts
+++ b/server/src/trigger/arrearProratedUsage/handleProratedUpgrade.ts
@@ -2,14 +2,14 @@ import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/pric
import { shouldCreateInvoiceItem } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
import {
- Entitlement,
- FullCusEntWithFullCusProduct,
- FullCusEntWithProduct,
- FullCustomerPrice,
- OnIncrease,
- Organization,
- Price,
- UsagePriceConfig,
+ Entitlement,
+ FullCusEntWithFullCusProduct,
+ FullCusEntWithProduct,
+ FullCustomerPrice,
+ OnIncrease,
+ Organization,
+ Price,
+ UsagePriceConfig,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -20,167 +20,167 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
interface UsageValues {
- prevRoundedUsage: number;
- newRoundedUsage: number;
- prevRoundedOverage: number;
- newRoundedOverage: number;
+ prevRoundedUsage: number;
+ newRoundedUsage: number;
+ prevRoundedOverage: number;
+ newRoundedOverage: number;
}
export const getPrevAndNewPriceForUpgrade = ({
- ent,
- // numReplaceables,
- price,
- newBalance,
- prevBalance,
- logger,
+ ent,
+ // numReplaceables,
+ price,
+ newBalance,
+ prevBalance,
+ logger,
}: {
- ent: Entitlement;
- // numReplaceables: number;
- price: Price;
- newBalance: number;
- prevBalance: number;
- logger: any;
+ ent: Entitlement;
+ // numReplaceables: number;
+ price: Price;
+ newBalance: number;
+ prevBalance: number;
+ logger: any;
}) => {
- const { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
- ent,
- price,
- balance: prevBalance,
- });
+ const { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: prevBalance,
+ });
- const { usage: newUsage, overage: newOverage } = getUsageFromBalance({
- ent,
- price,
- balance: newBalance,
- });
+ const { usage: newUsage, overage: newOverage } = getUsageFromBalance({
+ ent,
+ price,
+ balance: newBalance,
+ });
- let prevPrice = priceToInvoiceAmount({
- price,
- overage: roundUsage({
- // usage: prevUsage,
- usage: prevOverage,
- price,
- }),
- });
+ let prevPrice = priceToInvoiceAmount({
+ price,
+ overage: roundUsage({
+ // usage: prevUsage,
+ usage: prevOverage,
+ price,
+ }),
+ });
- let newPrice = priceToInvoiceAmount({
- price,
- overage: roundUsage({
- // usage: newUsage,
- usage: newOverage,
- price,
- }),
- });
+ let newPrice = priceToInvoiceAmount({
+ price,
+ overage: roundUsage({
+ // usage: newUsage,
+ usage: newOverage,
+ price,
+ }),
+ });
- return {
- // prevOverage,
- // newOverage,
- newUsage,
- // prevUsage,
- prevPrice,
- newPrice,
- };
+ return {
+ // prevOverage,
+ // newOverage,
+ newUsage,
+ // prevUsage,
+ prevPrice,
+ newPrice,
+ };
};
export function getReps({
- cusEnt,
- prevBalance,
- newBalance,
+ cusEnt,
+ prevBalance,
+ newBalance,
}: {
- cusEnt: FullCusEntWithFullCusProduct;
- prevBalance: number;
- newBalance: number;
+ cusEnt: FullCusEntWithFullCusProduct;
+ prevBalance: number;
+ newBalance: number;
}) {
- let usageDiff = prevBalance - newBalance;
- let reps = cusEnt.replaceables.slice(0, usageDiff);
- return reps;
+ let usageDiff = prevBalance - newBalance;
+ let reps = cusEnt.replaceables.slice(0, usageDiff);
+ return reps;
}
export const handleProratedUpgrade = async ({
- db,
- stripeCli,
- cusEnt,
- org,
- cusPrice,
- sub,
- subItem,
- newBalance,
- prevBalance,
- logger,
+ db,
+ stripeCli,
+ cusEnt,
+ org,
+ cusPrice,
+ sub,
+ subItem,
+ newBalance,
+ prevBalance,
+ logger,
}: {
- db: DrizzleCli;
- stripeCli: Stripe;
- org: Organization;
- cusEnt: FullCusEntWithFullCusProduct;
- cusPrice: FullCustomerPrice;
- sub: Stripe.Subscription;
- subItem: Stripe.SubscriptionItem;
- newBalance: number;
- prevBalance: number;
- logger: any;
+ db: DrizzleCli;
+ stripeCli: Stripe;
+ org: Organization;
+ cusEnt: FullCusEntWithFullCusProduct;
+ cusPrice: FullCustomerPrice;
+ sub: Stripe.Subscription;
+ subItem: Stripe.SubscriptionItem;
+ newBalance: number;
+ prevBalance: number;
+ logger: any;
}) => {
- logger.info(`Handling quantity increase`);
+ logger.info(`Handling quantity increase`);
- // 1. Get num reps to use
- let reps = getReps({
- cusEnt,
- prevBalance,
- newBalance,
- });
- newBalance = newBalance + reps.length; // Increase new balance by number of reps
+ // 1. Get num reps to use
+ let reps = getReps({
+ cusEnt,
+ prevBalance,
+ newBalance,
+ });
+ newBalance = newBalance + reps.length; // Increase new balance by number of reps
- let { prevPrice, newPrice, newUsage } = getPrevAndNewPriceForUpgrade({
- ent: cusEnt.entitlement,
- price: cusPrice.price,
- newBalance,
- prevBalance,
- logger,
- });
+ let { prevPrice, newPrice, newUsage } = getPrevAndNewPriceForUpgrade({
+ ent: cusEnt.entitlement,
+ price: cusPrice.price,
+ newBalance,
+ prevBalance,
+ logger,
+ });
- const config = cusPrice.price.config as UsagePriceConfig;
- const product = cusEnt.customer_product.product;
- const feature = cusEnt.entitlement.feature;
+ const config = cusPrice.price.config as UsagePriceConfig;
+ const product = cusEnt.customer_product.product;
+ const feature = cusEnt.entitlement.feature;
- let onIncrease =
- cusPrice.price.proration_config?.on_increase ||
- OnIncrease.ProrateImmediately;
+ let onIncrease =
+ cusPrice.price.proration_config?.on_increase ||
+ OnIncrease.ProrateImmediately;
- const newRoundedUsage = roundUsage({
- usage: newUsage,
- price: cusPrice.price,
- });
+ const newRoundedUsage = roundUsage({
+ usage: newUsage,
+ price: cusPrice.price,
+ });
- let invoice = null;
- if (shouldCreateInvoiceItem(onIncrease)) {
- invoice = await createUpgradeProrationInvoice({
- org,
- cusPrice,
- stripeCli,
- sub,
- subItem,
- newPrice,
- prevPrice,
- newRoundedUsage,
- feature,
- product,
- config,
- onIncrease,
- logger,
- });
- }
+ let invoice = null;
+ if (shouldCreateInvoiceItem(onIncrease)) {
+ invoice = await createUpgradeProrationInvoice({
+ org,
+ cusPrice,
+ stripeCli,
+ sub,
+ subItem,
+ newPrice,
+ prevPrice,
+ newRoundedUsage,
+ feature,
+ product,
+ config,
+ onIncrease,
+ logger,
+ });
+ }
- let deleted = await RepService.deleteInIds({
- db,
- ids: reps.map((r) => r.id),
- });
+ let deleted = await RepService.deleteInIds({
+ db,
+ ids: reps.map((r) => r.id),
+ });
- await stripeCli.subscriptionItems.update(subItem.id, {
- quantity: roundUsage({
- usage: newUsage,
- price: cusPrice.price,
- }),
- proration_behavior: "none",
- });
+ await stripeCli.subscriptionItems.update(subItem.id, {
+ quantity: roundUsage({
+ usage: newUsage,
+ price: cusPrice.price,
+ }),
+ proration_behavior: "none",
+ });
- logger.info(`Updated sub item ${subItem.id} to quantity: ${newRoundedUsage}`);
- return { deletedReplaceables: deleted, invoice, newReplaceables: [] };
+ logger.info(`Updated sub item ${subItem.id} to quantity: ${newRoundedUsage}`);
+ return { deletedReplaceables: deleted, invoice, newReplaceables: [] };
};
diff --git a/server/src/trigger/deductUtils.ts b/server/src/trigger/deductUtils.ts
index 1019ccdd8..8e9242a51 100644
--- a/server/src/trigger/deductUtils.ts
+++ b/server/src/trigger/deductUtils.ts
@@ -1,10 +1,10 @@
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- Feature,
- Event,
- FeatureType,
- FullCustomerEntitlement,
- Entitlement,
+ Feature,
+ Event,
+ FeatureType,
+ FullCustomerEntitlement,
+ Entitlement,
} from "@autumn/shared";
import { AggregateType } from "@autumn/shared";
@@ -13,133 +13,133 @@ import { Decimal } from "decimal.js";
const DEFAULT_VALUE = 1;
export const getMeteredDeduction = (meteredFeature: Feature, event: Event) => {
- let config = meteredFeature.config;
- let aggregate = config.aggregate;
+ let config = meteredFeature.config;
+ let aggregate = config.aggregate;
- if (aggregate.type == AggregateType.Count) {
- return 1;
- }
+ if (aggregate.type == AggregateType.Count) {
+ return 1;
+ }
- let value = notNullish(event.value)
- ? event.value
- : notNullish(event.properties?.value)
- ? event.properties?.value
- : DEFAULT_VALUE;
+ let value = notNullish(event.value)
+ ? event.value
+ : notNullish(event.properties?.value)
+ ? event.properties?.value
+ : DEFAULT_VALUE;
- let floatVal = parseFloat(value);
- if (isNaN(floatVal)) {
- return 0;
- }
+ let floatVal = parseFloat(value);
+ if (isNaN(floatVal)) {
+ return 0;
+ }
- if (
- meteredFeature.type == FeatureType.CreditSystem ||
- aggregate.type == AggregateType.Sum
- ) {
- return value;
- }
+ if (
+ meteredFeature.type == FeatureType.CreditSystem ||
+ aggregate.type == AggregateType.Sum
+ ) {
+ return value;
+ }
- return 0;
+ return 0;
};
export const getCreditSystemDeduction = ({
- meteredFeatures,
- creditSystem,
- event,
+ meteredFeatures,
+ creditSystem,
+ event,
}: {
- meteredFeatures: Feature[];
- creditSystem: Feature;
- event: Event;
+ meteredFeatures: Feature[];
+ creditSystem: Feature;
+ event: Event;
}) => {
- let creditsUpdate = 0;
- let meteredFeatureIds = meteredFeatures.map((feature) => feature.id);
+ let creditsUpdate = 0;
+ let meteredFeatureIds = meteredFeatures.map((feature) => feature.id);
- for (const schema of creditSystem.config.schema) {
- if (meteredFeatureIds.includes(schema.metered_feature_id)) {
- let meteredFeature = meteredFeatures.find(
- (feature) => feature.id === schema.metered_feature_id,
- );
+ for (const schema of creditSystem.config.schema) {
+ if (meteredFeatureIds.includes(schema.metered_feature_id)) {
+ let meteredFeature = meteredFeatures.find(
+ (feature) => feature.id === schema.metered_feature_id,
+ );
- if (!meteredFeature) {
- continue;
- }
+ if (!meteredFeature) {
+ continue;
+ }
- let meteredDeduction = getMeteredDeduction(meteredFeature, event);
+ let meteredDeduction = getMeteredDeduction(meteredFeature, event);
- let meteredDeductionDecimal = new Decimal(meteredDeduction);
- let featureAmountDecimal = new Decimal(schema.feature_amount);
- let creditAmountDecimal = new Decimal(schema.credit_amount);
- creditsUpdate += meteredDeductionDecimal
- .div(featureAmountDecimal)
- .mul(creditAmountDecimal)
- .toNumber();
- }
- }
+ let meteredDeductionDecimal = new Decimal(meteredDeduction);
+ let featureAmountDecimal = new Decimal(schema.feature_amount);
+ let creditAmountDecimal = new Decimal(schema.credit_amount);
+ creditsUpdate += meteredDeductionDecimal
+ .div(featureAmountDecimal)
+ .mul(creditAmountDecimal)
+ .toNumber();
+ }
+ }
- return creditsUpdate;
+ return creditsUpdate;
};
// Deduct allowance
export const performDeduction = ({
- cusEntBalance,
- toDeduct,
- allowNegativeBalance = false,
- ent,
- resetBalance,
- blockUsageLimit = true,
+ cusEntBalance,
+ toDeduct,
+ allowNegativeBalance = false,
+ ent,
+ resetBalance,
+ blockUsageLimit = true,
}: {
- cusEntBalance: Decimal;
- toDeduct: number;
- allowNegativeBalance?: boolean;
- ent: Entitlement;
- resetBalance: number;
- blockUsageLimit?: boolean;
+ cusEntBalance: Decimal;
+ toDeduct: number;
+ allowNegativeBalance?: boolean;
+ ent: Entitlement;
+ resetBalance: number;
+ blockUsageLimit?: boolean;
}) => {
- // Either deduct from balance or entity balance
- if (allowNegativeBalance) {
- let usageLimit = ent.usage_limit;
- let minBalance = usageLimit
- ? new Decimal(resetBalance).minus(usageLimit).toNumber()
- : undefined;
- let newBalance = cusEntBalance.minus(toDeduct).toNumber();
+ // Either deduct from balance or entity balance
+ if (allowNegativeBalance) {
+ let usageLimit = ent.usage_limit;
+ let minBalance = usageLimit
+ ? new Decimal(resetBalance).minus(usageLimit).toNumber()
+ : undefined;
+ let newBalance = cusEntBalance.minus(toDeduct).toNumber();
- if (
- blockUsageLimit &&
- minBalance &&
- new Decimal(newBalance).lt(minBalance)
- ) {
- newBalance = minBalance;
- let deducted = new Decimal(cusEntBalance).minus(minBalance).toNumber();
- let toDeduct_ = new Decimal(toDeduct).minus(deducted).toNumber();
- return { newBalance, deducted, toDeduct: toDeduct_ };
- } else {
- let deducted = toDeduct;
- let toDeduct_ = 0;
- return { newBalance, deducted, toDeduct: toDeduct_ };
- }
- }
+ if (
+ blockUsageLimit &&
+ minBalance &&
+ new Decimal(newBalance).lt(minBalance)
+ ) {
+ newBalance = minBalance;
+ let deducted = new Decimal(cusEntBalance).minus(minBalance).toNumber();
+ let toDeduct_ = new Decimal(toDeduct).minus(deducted).toNumber();
+ return { newBalance, deducted, toDeduct: toDeduct_ };
+ } else {
+ let deducted = toDeduct;
+ let toDeduct_ = 0;
+ return { newBalance, deducted, toDeduct: toDeduct_ };
+ }
+ }
- if (cusEntBalance.lte(0) && toDeduct > 0) {
- return { newBalance: cusEntBalance.toNumber(), deducted: 0, toDeduct };
- }
+ if (cusEntBalance.lte(0) && toDeduct > 0) {
+ return { newBalance: cusEntBalance.toNumber(), deducted: 0, toDeduct };
+ }
- // If toDeduct is negative, add to balance and set toDeduct to 0
- let newBalance, deducted;
- if (toDeduct < 0) {
- newBalance = cusEntBalance.minus(toDeduct).toNumber();
- deducted = toDeduct;
- toDeduct = 0;
- }
+ // If toDeduct is negative, add to balance and set toDeduct to 0
+ let newBalance, deducted;
+ if (toDeduct < 0) {
+ newBalance = cusEntBalance.minus(toDeduct).toNumber();
+ deducted = toDeduct;
+ toDeduct = 0;
+ }
- // If cusEnt has less balance to deduct than 0, deduct the balance and set balance to 0
- else if (cusEntBalance.minus(toDeduct).lt(0)) {
- toDeduct = new Decimal(toDeduct).minus(cusEntBalance).toNumber(); // toDeduct = toDeduct - cusEntBalance
- deducted = cusEntBalance.toNumber(); // deducted = cusEntBalance
- newBalance = 0; // newBalance = 0
- } else {
- newBalance = cusEntBalance.minus(toDeduct).toNumber();
- deducted = toDeduct;
- toDeduct = 0;
- }
+ // If cusEnt has less balance to deduct than 0, deduct the balance and set balance to 0
+ else if (cusEntBalance.minus(toDeduct).lt(0)) {
+ toDeduct = new Decimal(toDeduct).minus(cusEntBalance).toNumber(); // toDeduct = toDeduct - cusEntBalance
+ deducted = cusEntBalance.toNumber(); // deducted = cusEntBalance
+ newBalance = 0; // newBalance = 0
+ } else {
+ newBalance = cusEntBalance.minus(toDeduct).toNumber();
+ deducted = toDeduct;
+ toDeduct = 0;
+ }
- return { newBalance, deducted, toDeduct };
+ return { newBalance, deducted, toDeduct };
};
diff --git a/server/src/trigger/handleThresholdReached.ts b/server/src/trigger/handleThresholdReached.ts
index f4b50e7a8..35f1f587e 100644
--- a/server/src/trigger/handleThresholdReached.ts
+++ b/server/src/trigger/handleThresholdReached.ts
@@ -6,278 +6,278 @@ import { getV2CheckResponse } from "@/internal/api/entitled/checkUtils/getV2Chec
import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js";
import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js";
import {
- FullCusEntWithFullCusProduct,
- Feature,
- FullCustomer,
- Organization,
- AppEnv,
- FullCusProduct,
- APIVersion,
- WebhookEventType,
+ FullCusEntWithFullCusProduct,
+ Feature,
+ FullCustomer,
+ Organization,
+ AppEnv,
+ FullCusProduct,
+ APIVersion,
+ WebhookEventType,
} from "@autumn/shared";
export const mergeNewCusEntsIntoCusProducts = ({
- cusProducts,
- newCusEnts,
+ cusProducts,
+ newCusEnts,
}: {
- cusProducts: FullCusProduct[];
- newCusEnts: FullCusEntWithFullCusProduct[];
+ cusProducts: FullCusProduct[];
+ newCusEnts: FullCusEntWithFullCusProduct[];
}) => {
- for (const cusProduct of cusProducts) {
- for (let i = 0; i < cusProduct.customer_entitlements.length; i++) {
- let correspondingCusEnt = newCusEnts.find(
- (cusEnt) => cusEnt.id == cusProduct.customer_entitlements[i].id
- );
+ for (const cusProduct of cusProducts) {
+ for (let i = 0; i < cusProduct.customer_entitlements.length; i++) {
+ let correspondingCusEnt = newCusEnts.find(
+ (cusEnt) => cusEnt.id == cusProduct.customer_entitlements[i].id,
+ );
- if (correspondingCusEnt) {
- const { customer_product, ...rest } = correspondingCusEnt;
- cusProduct.customer_entitlements[i] = rest;
- }
- }
- }
+ if (correspondingCusEnt) {
+ const { customer_product, ...rest } = correspondingCusEnt;
+ cusProduct.customer_entitlements[i] = rest;
+ }
+ }
+ }
- return cusProducts;
+ return cusProducts;
};
export const sendSvixThresholdReachedEvent = async ({
- db,
- org,
- env,
- features,
- logger,
- feature,
- fullCus,
- thresholdType,
+ db,
+ org,
+ env,
+ features,
+ logger,
+ feature,
+ fullCus,
+ thresholdType,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- features: Feature[];
- logger: any;
- feature: Feature;
- fullCus: FullCustomer;
- thresholdType: "limit_reached" | "allowance_used";
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ features: Feature[];
+ logger: any;
+ feature: Feature;
+ fullCus: FullCustomer;
+ thresholdType: "limit_reached" | "allowance_used";
}) => {
- const cusDetails = await getCustomerDetails({
- db,
- customer: fullCus,
- org,
- env,
- features,
- logger,
- cusProducts: fullCus.customer_products,
- expand: [],
- });
+ const cusDetails = await getCustomerDetails({
+ db,
+ customer: fullCus,
+ org,
+ env,
+ features,
+ logger,
+ cusProducts: fullCus.customer_products,
+ expand: [],
+ });
- if (fullCus.entity) {
- await getSingleEntityResponse({
- org,
- env,
- features,
- fullCus,
- entity: fullCus.entity,
- entityId: fullCus.entity.id,
- });
- }
+ if (fullCus.entity) {
+ await getSingleEntityResponse({
+ org,
+ env,
+ features,
+ fullCus,
+ entity: fullCus.entity,
+ entityId: fullCus.entity.id,
+ });
+ }
- await sendSvixEvent({
- org: org,
- env: env,
- eventType: WebhookEventType.CustomerThresholdReached,
- data: {
- threshold_type: thresholdType,
- customer: cusDetails,
- feature: toAPIFeature({ feature }),
- },
- });
+ await sendSvixEvent({
+ org: org,
+ env: env,
+ eventType: WebhookEventType.CustomerThresholdReached,
+ data: {
+ threshold_type: thresholdType,
+ customer: cusDetails,
+ feature: toAPIFeature({ feature }),
+ },
+ });
- logger.info(`Sent Svix event for threshold reached (type: ${thresholdType})`);
- return;
+ logger.info(`Sent Svix event for threshold reached (type: ${thresholdType})`);
+ return;
};
export const handleAllowanceUsed = async ({
- db,
- org,
- env,
- features,
- logger,
- cusEnts,
- newCusEnts,
- feature,
- fullCus,
+ db,
+ org,
+ env,
+ features,
+ logger,
+ cusEnts,
+ newCusEnts,
+ feature,
+ fullCus,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- cusEnts: FullCusEntWithFullCusProduct[];
- newCusEnts: FullCusEntWithFullCusProduct[];
- feature: Feature;
- fullCus: FullCustomer;
- features: Feature[];
- logger: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ cusEnts: FullCusEntWithFullCusProduct[];
+ newCusEnts: FullCusEntWithFullCusProduct[];
+ feature: Feature;
+ fullCus: FullCustomer;
+ features: Feature[];
+ logger: any;
}) => {
- // Allowance used...
- // Make sure overage allowed is false
- const oldCusEnts = structuredClone(cusEnts);
- for (const cusEnt of oldCusEnts) {
- cusEnt.usage_allowed = false;
- }
+ // Allowance used...
+ // Make sure overage allowed is false
+ const oldCusEnts = structuredClone(cusEnts);
+ for (const cusEnt of oldCusEnts) {
+ cusEnt.usage_allowed = false;
+ }
- const clonedNewCusEnts = structuredClone(newCusEnts);
- for (const cusEnt of clonedNewCusEnts) {
- cusEnt.usage_allowed = false;
- }
+ const clonedNewCusEnts = structuredClone(newCusEnts);
+ for (const cusEnt of clonedNewCusEnts) {
+ cusEnt.usage_allowed = false;
+ }
- const prevCheckResponse = await getV2CheckResponse({
- fullCus,
- cusEnts: oldCusEnts,
- creditSystems: [],
- feature,
- org,
- cusProducts: fullCus.customer_products,
- apiVersion: APIVersion.v1_2,
- });
+ const prevCheckResponse = await getV2CheckResponse({
+ fullCus,
+ cusEnts: oldCusEnts,
+ creditSystems: [],
+ feature,
+ org,
+ cusProducts: fullCus.customer_products,
+ apiVersion: APIVersion.v1_2,
+ });
- const v2CheckResponse = await getV2CheckResponse({
- fullCus,
- cusEnts: clonedNewCusEnts,
- creditSystems: [],
- feature,
- org,
- cusProducts: fullCus.customer_products,
- apiVersion: APIVersion.v1_2,
- });
+ const v2CheckResponse = await getV2CheckResponse({
+ fullCus,
+ cusEnts: clonedNewCusEnts,
+ creditSystems: [],
+ feature,
+ org,
+ cusProducts: fullCus.customer_products,
+ apiVersion: APIVersion.v1_2,
+ });
- // console.log(`Handling allowance used for feature: ${feature.id}`);
- // console.log(
- // `Prev: allowed (${prevCheckResponse.allowed}), balance (${prevCheckResponse.balance})`
- // );
- // console.log(
- // `Current: allowed (${v2CheckResponse.allowed}), balance (${v2CheckResponse.balance})`
- // );
+ // console.log(`Handling allowance used for feature: ${feature.id}`);
+ // console.log(
+ // `Prev: allowed (${prevCheckResponse.allowed}), balance (${prevCheckResponse.balance})`
+ // );
+ // console.log(
+ // `Current: allowed (${v2CheckResponse.allowed}), balance (${v2CheckResponse.balance})`
+ // );
- if (prevCheckResponse.allowed === true && v2CheckResponse.allowed === false) {
- await sendSvixThresholdReachedEvent({
- db,
- org,
- env,
- features,
- logger,
- feature,
- fullCus,
- thresholdType: "allowance_used",
- });
- }
+ if (prevCheckResponse.allowed === true && v2CheckResponse.allowed === false) {
+ await sendSvixThresholdReachedEvent({
+ db,
+ org,
+ env,
+ features,
+ logger,
+ feature,
+ fullCus,
+ thresholdType: "allowance_used",
+ });
+ }
};
export const handleThresholdReached = async ({
- db,
- feature,
- cusEnts,
- newCusEnts,
- fullCus,
- org,
- env,
- features,
- logger,
+ db,
+ feature,
+ cusEnts,
+ newCusEnts,
+ fullCus,
+ org,
+ env,
+ features,
+ logger,
}: {
- db: DrizzleCli;
- feature: Feature;
- cusEnts: FullCusEntWithFullCusProduct[];
- newCusEnts: FullCusEntWithFullCusProduct[];
+ db: DrizzleCli;
+ feature: Feature;
+ cusEnts: FullCusEntWithFullCusProduct[];
+ newCusEnts: FullCusEntWithFullCusProduct[];
- fullCus: FullCustomer;
- org: Organization;
- env: AppEnv;
- features: Feature[];
- logger: any;
+ fullCus: FullCustomer;
+ org: Organization;
+ env: AppEnv;
+ features: Feature[];
+ logger: any;
}) => {
- try {
- const newCusProducts = mergeNewCusEntsIntoCusProducts({
- cusProducts: fullCus.customer_products,
- newCusEnts: newCusEnts,
- });
+ try {
+ const newCusProducts = mergeNewCusEntsIntoCusProducts({
+ cusProducts: fullCus.customer_products,
+ newCusEnts: newCusEnts,
+ });
- fullCus.customer_products = newCusProducts;
+ fullCus.customer_products = newCusProducts;
- const prevCheckResponse = await getV2CheckResponse({
- fullCus,
- cusEnts: cusEnts,
- creditSystems: [],
- feature,
- org,
- cusProducts: fullCus.customer_products,
- apiVersion: APIVersion.v1_2,
- });
+ const prevCheckResponse = await getV2CheckResponse({
+ fullCus,
+ cusEnts: cusEnts,
+ creditSystems: [],
+ feature,
+ org,
+ cusProducts: fullCus.customer_products,
+ apiVersion: APIVersion.v1_2,
+ });
- const v2CheckResponse = await getV2CheckResponse({
- fullCus,
- cusEnts: newCusEnts,
- creditSystems: [],
- feature,
- org,
- cusProducts: newCusProducts,
- apiVersion: APIVersion.v1_2,
- });
+ const v2CheckResponse = await getV2CheckResponse({
+ fullCus,
+ cusEnts: newCusEnts,
+ creditSystems: [],
+ feature,
+ org,
+ cusProducts: newCusProducts,
+ apiVersion: APIVersion.v1_2,
+ });
- if (
- prevCheckResponse.allowed === true &&
- v2CheckResponse.allowed === false
- ) {
- const cusDetails = await getCustomerDetails({
- db,
- customer: fullCus,
- org,
- env,
- features,
- logger,
- cusProducts: newCusProducts,
- expand: [],
- });
+ if (
+ prevCheckResponse.allowed === true &&
+ v2CheckResponse.allowed === false
+ ) {
+ const cusDetails = await getCustomerDetails({
+ db,
+ customer: fullCus,
+ org,
+ env,
+ features,
+ logger,
+ cusProducts: newCusProducts,
+ expand: [],
+ });
- if (fullCus.entity) {
- await getSingleEntityResponse({
- org,
- env,
- features,
- entity: fullCus.entity,
- fullCus,
- entityId: fullCus.entity.id,
- });
- }
+ if (fullCus.entity) {
+ await getSingleEntityResponse({
+ org,
+ env,
+ features,
+ entity: fullCus.entity,
+ fullCus,
+ entityId: fullCus.entity.id,
+ });
+ }
- await sendSvixEvent({
- org: org,
- env: env,
- eventType: WebhookEventType.CustomerThresholdReached,
- data: {
- threshold_type: "limit_reached",
- customer: cusDetails,
- feature: toAPIFeature({ feature }),
- },
- });
+ await sendSvixEvent({
+ org: org,
+ env: env,
+ eventType: WebhookEventType.CustomerThresholdReached,
+ data: {
+ threshold_type: "limit_reached",
+ customer: cusDetails,
+ feature: toAPIFeature({ feature }),
+ },
+ });
- logger.info(
- "Sent Svix event for threshold reached (type: limit_reached)"
- );
- return;
- }
- await handleAllowanceUsed({
- db,
- org,
- env,
- features,
- logger,
- cusEnts,
- newCusEnts,
- feature,
- fullCus,
- });
- return;
- } catch (error: any) {
- logger.error("Failed to handle threshold reached", {
- error,
- message: error?.message,
- });
- }
+ logger.info(
+ "Sent Svix event for threshold reached (type: limit_reached)",
+ );
+ return;
+ }
+ await handleAllowanceUsed({
+ db,
+ org,
+ env,
+ features,
+ logger,
+ cusEnts,
+ newCusEnts,
+ feature,
+ fullCus,
+ });
+ return;
+ } catch (error: any) {
+ logger.error("Failed to handle threshold reached", {
+ error,
+ message: error?.message,
+ });
+ }
};
diff --git a/server/src/trigger/updateBalanceTask.ts b/server/src/trigger/updateBalanceTask.ts
index 04ee93ac0..366b0820f 100644
--- a/server/src/trigger/updateBalanceTask.ts
+++ b/server/src/trigger/updateBalanceTask.ts
@@ -1,16 +1,16 @@
import {
- AllowanceType,
- AppEnv,
- CusProductStatus,
- Entity,
- Event,
- Feature,
- FullCustomerEntitlement,
- FullCustomerPrice,
- Organization,
- FullCusEntWithFullCusProduct,
- BillingType,
- FeatureUsageType,
+ AllowanceType,
+ AppEnv,
+ CusProductStatus,
+ Entity,
+ Event,
+ Feature,
+ FullCustomerEntitlement,
+ FullCustomerPrice,
+ Organization,
+ FullCusEntWithFullCusProduct,
+ BillingType,
+ FeatureUsageType,
} from "@autumn/shared";
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
import { Customer, FeatureType } from "@autumn/shared";
@@ -18,29 +18,29 @@ import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js"
import { Decimal } from "decimal.js";
import { adjustAllowance } from "./adjustAllowance.js";
import {
- getMeteredDeduction,
- getCreditSystemDeduction,
- performDeduction,
+ getMeteredDeduction,
+ getCreditSystemDeduction,
+ performDeduction,
} from "./deductUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- creditSystemContainsFeature,
- featureToCreditSystem,
+ creditSystemContainsFeature,
+ featureToCreditSystem,
} from "@/internal/features/creditSystemUtils.js";
import {
- getCusEntMasterBalance,
- getRelatedCusPrice,
- getResetBalance,
- getTotalNegativeBalance,
+ getCusEntMasterBalance,
+ getRelatedCusPrice,
+ getResetBalance,
+ getTotalNegativeBalance,
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { entityFeatureIdExists } from "@/internal/api/entities/entityUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import {
- getBillingType,
- getEntOptions,
+ getBillingType,
+ getEntOptions,
} from "@/internal/products/prices/priceUtils.js";
import { deductFromCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
@@ -49,730 +49,730 @@ import { handleThresholdReached } from "./handleThresholdReached.js";
// Decimal.set({ precision: 12 }); // 12 DP precision
export type DeductParams = {
- db: DrizzleCli;
- env: AppEnv;
- org: Organization;
- cusPrices: FullCustomerPrice[];
- customer: Customer;
- properties: any;
- feature: Feature;
- entity?: Entity;
+ db: DrizzleCli;
+ env: AppEnv;
+ org: Organization;
+ cusPrices: FullCustomerPrice[];
+ customer: Customer;
+ properties: any;
+ feature: Feature;
+ entity?: Entity;
};
export type RolloverDeductParams = {
- db: DrizzleCli;
- env: AppEnv;
- feature: Feature;
- entity?: Entity;
+ db: DrizzleCli;
+ env: AppEnv;
+ feature: Feature;
+ entity?: Entity;
};
// 2. Get deductions for each feature
const getFeatureDeductions = ({
- cusEnts,
- event,
- features,
+ cusEnts,
+ event,
+ features,
}: {
- cusEnts: FullCustomerEntitlement[];
- event: Event;
- features: Feature[];
+ cusEnts: FullCustomerEntitlement[];
+ event: Event;
+ features: Feature[];
}) => {
- const meteredFeatures = features.filter(
- (feature) => feature.type === FeatureType.Metered
- );
- const featureDeductions = [];
- for (const feature of features) {
- let deduction;
- if (feature.type === FeatureType.Metered) {
- deduction = getMeteredDeduction(feature, event);
- } else if (feature.type === FeatureType.CreditSystem) {
- deduction = getCreditSystemDeduction({
- meteredFeatures: meteredFeatures,
- creditSystem: feature,
- event,
- });
- }
+ const meteredFeatures = features.filter(
+ (feature) => feature.type === FeatureType.Metered,
+ );
+ const featureDeductions = [];
+ for (const feature of features) {
+ let deduction;
+ if (feature.type === FeatureType.Metered) {
+ deduction = getMeteredDeduction(feature, event);
+ } else if (feature.type === FeatureType.CreditSystem) {
+ deduction = getCreditSystemDeduction({
+ meteredFeatures: meteredFeatures,
+ creditSystem: feature,
+ event,
+ });
+ }
- // Check if unlimited exists
- let unlimitedExists = cusEnts.some(
- (cusEnt) =>
- cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
- cusEnt.entitlement.internal_feature_id == feature.internal_id
- );
+ // Check if unlimited exists
+ let unlimitedExists = cusEnts.some(
+ (cusEnt) =>
+ cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
+ cusEnt.entitlement.internal_feature_id == feature.internal_id,
+ );
- if (unlimitedExists || !deduction) {
- continue;
- }
+ if (unlimitedExists || !deduction) {
+ continue;
+ }
- featureDeductions.push({
- feature,
- deduction,
- });
- }
+ featureDeductions.push({
+ feature,
+ deduction,
+ });
+ }
- featureDeductions.sort((a, b) => {
- if (
- a.feature.type === FeatureType.CreditSystem &&
- b.feature.type !== FeatureType.CreditSystem
- ) {
- return 1;
- }
+ featureDeductions.sort((a, b) => {
+ if (
+ a.feature.type === FeatureType.CreditSystem &&
+ b.feature.type !== FeatureType.CreditSystem
+ ) {
+ return 1;
+ }
- if (
- a.feature.type !== FeatureType.CreditSystem &&
- b.feature.type === FeatureType.CreditSystem
- ) {
- return -1;
- }
+ if (
+ a.feature.type !== FeatureType.CreditSystem &&
+ b.feature.type === FeatureType.CreditSystem
+ ) {
+ return -1;
+ }
- return a.feature.id.localeCompare(b.feature.id);
- });
+ return a.feature.id.localeCompare(b.feature.id);
+ });
- return featureDeductions;
+ return featureDeductions;
};
export const logBalanceUpdate = ({
- timeTaken,
- customer,
- features,
- cusEnts,
- featureDeductions,
- properties,
- entityId,
- org,
+ timeTaken,
+ customer,
+ features,
+ cusEnts,
+ featureDeductions,
+ properties,
+ entityId,
+ org,
}: {
- timeTaken: string;
- customer: Customer;
- features: Feature[];
- cusEnts: FullCustomerEntitlement[];
- featureDeductions: any;
- properties: any;
- entityId?: string | null;
- org: Organization;
+ timeTaken: string;
+ customer: Customer;
+ features: Feature[];
+ cusEnts: FullCustomerEntitlement[];
+ featureDeductions: any;
+ properties: any;
+ entityId?: string | null;
+ org: Organization;
}) => {
- console.log(
- ` - Customer: ${customer.id} (${customer.env}) | Org: ${
- org.slug
- } | Features: ${features.map((f) => f.id).join(", ")}`
- );
- console.log(" - Properties:", properties);
- console.log(
- " - CusEnts:",
- cusEnts.map((cusEnt: any) => {
- let balanceStr = cusEnt.balance;
+ console.log(
+ ` - Customer: ${customer.id} (${customer.env}) | Org: ${
+ org.slug
+ } | Features: ${features.map((f) => f.id).join(", ")}`,
+ );
+ console.log(" - Properties:", properties);
+ console.log(
+ " - CusEnts:",
+ cusEnts.map((cusEnt: any) => {
+ let balanceStr = cusEnt.balance;
- if (notNullish(cusEnt.entitlement.entity_feature_id)) {
- console.log(
- ` - Entity feature ID found for feature: ${cusEnt.feature_id}`
- );
+ if (notNullish(cusEnt.entitlement.entity_feature_id)) {
+ console.log(
+ ` - Entity feature ID found for feature: ${cusEnt.feature_id}`,
+ );
- if (notNullish(entityId)) {
- balanceStr = `${cusEnt.entities?.[entityId!]?.balance} [${entityId}]`;
- } else {
- balanceStr = `${
- getCusEntMasterBalance({
- cusEnt,
- entities: cusEnt.customer_product?.entities,
- }).balance
- } [Master]`;
- }
- }
- try {
- if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
- balanceStr = "Unlimited";
- }
- } catch (error) {
- balanceStr = "failed_to_get_balance";
- }
+ if (notNullish(entityId)) {
+ balanceStr = `${cusEnt.entities?.[entityId!]?.balance} [${entityId}]`;
+ } else {
+ balanceStr = `${
+ getCusEntMasterBalance({
+ cusEnt,
+ entities: cusEnt.customer_product?.entities,
+ }).balance
+ } [Master]`;
+ }
+ }
+ try {
+ if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
+ balanceStr = "Unlimited";
+ }
+ } catch (error) {
+ balanceStr = "failed_to_get_balance";
+ }
- return `${cusEnt.feature_id} - ${balanceStr} (${
- cusEnt.customer_product ? cusEnt.customer_product.product_id : ""
- })`;
- }),
- "| Deductions:",
- featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`)
- );
+ return `${cusEnt.feature_id} - ${balanceStr} (${
+ cusEnt.customer_product ? cusEnt.customer_product.product_id : ""
+ })`;
+ }),
+ "| Deductions:",
+ featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`),
+ );
};
export const performDeductionOnCusEnt = ({
- cusEnt,
- toDeduct,
- entityId,
- allowNegativeBalance = false,
- addAdjustment = false,
- setZeroAdjustment = false,
- blockUsageLimit = true,
+ cusEnt,
+ toDeduct,
+ entityId,
+ allowNegativeBalance = false,
+ addAdjustment = false,
+ setZeroAdjustment = false,
+ blockUsageLimit = true,
}: {
- cusEnt: FullCusEntWithFullCusProduct;
- toDeduct: number;
- entityId?: string | null;
- allowNegativeBalance?: boolean;
- addAdjustment?: boolean;
- setZeroAdjustment?: boolean;
- blockUsageLimit?: boolean;
+ cusEnt: FullCusEntWithFullCusProduct;
+ toDeduct: number;
+ entityId?: string | null;
+ allowNegativeBalance?: boolean;
+ addAdjustment?: boolean;
+ setZeroAdjustment?: boolean;
+ blockUsageLimit?: boolean;
}) => {
- let newEntities = structuredClone(cusEnt.entities);
- let newBalance = structuredClone(cusEnt.balance);
- let newAdjustment = structuredClone(cusEnt.adjustment);
- let deducted = 0;
+ let newEntities = structuredClone(cusEnt.entities);
+ let newBalance = structuredClone(cusEnt.balance);
+ let newAdjustment = structuredClone(cusEnt.adjustment);
+ let deducted = 0;
- let cusProduct = cusEnt.customer_product;
- let options = notNullish(cusProduct)
- ? getEntOptions(cusProduct.options, cusEnt.entitlement)
- : undefined;
- let cusPrice = notNullish(cusProduct)
- ? getRelatedCusPrice(cusEnt, cusProduct.customer_prices)
- : undefined;
- let resetBalance = notNullish(cusProduct)
- ? getResetBalance({
- options,
- relatedPrice: cusPrice?.price,
- entitlement: cusEnt.entitlement,
- })
- : cusEnt.entitlement.allowance || 0;
+ let cusProduct = cusEnt.customer_product;
+ let options = notNullish(cusProduct)
+ ? getEntOptions(cusProduct.options, cusEnt.entitlement)
+ : undefined;
+ let cusPrice = notNullish(cusProduct)
+ ? getRelatedCusPrice(cusEnt, cusProduct.customer_prices)
+ : undefined;
+ let resetBalance = notNullish(cusProduct)
+ ? getResetBalance({
+ options,
+ relatedPrice: cusPrice?.price,
+ entitlement: cusEnt.entitlement,
+ })
+ : cusEnt.entitlement.allowance || 0;
- if (entityFeatureIdExists({ cusEnt })) {
- if (nullish(entityId)) {
- // 1. If no entity ID, deduct from all
- newEntities = structuredClone(cusEnt.entities);
- if (!newEntities) {
- newEntities = {};
- }
- let toDeductCursor = toDeduct;
- for (const entityId in cusEnt.entities) {
- if (toDeductCursor == 0) {
- break;
- }
+ if (entityFeatureIdExists({ cusEnt })) {
+ if (nullish(entityId)) {
+ // 1. If no entity ID, deduct from all
+ newEntities = structuredClone(cusEnt.entities);
+ if (!newEntities) {
+ newEntities = {};
+ }
+ let toDeductCursor = toDeduct;
+ for (const entityId in cusEnt.entities) {
+ if (toDeductCursor == 0) {
+ break;
+ }
- let entityBalance = cusEnt.entities[entityId].balance;
+ let entityBalance = cusEnt.entities[entityId].balance;
- let {
- newBalance: newEntityBalance,
- deducted: newDeducted,
- toDeduct: newToDeduct,
- } = performDeduction({
- cusEntBalance: new Decimal(entityBalance),
- toDeduct: toDeductCursor,
- allowNegativeBalance,
- ent: cusEnt.entitlement,
- resetBalance,
- blockUsageLimit,
- });
+ let {
+ newBalance: newEntityBalance,
+ deducted: newDeducted,
+ toDeduct: newToDeduct,
+ } = performDeduction({
+ cusEntBalance: new Decimal(entityBalance),
+ toDeduct: toDeductCursor,
+ allowNegativeBalance,
+ ent: cusEnt.entitlement,
+ resetBalance,
+ blockUsageLimit,
+ });
- newEntities[entityId].balance = newEntityBalance!;
+ newEntities[entityId].balance = newEntityBalance!;
- if (addAdjustment) {
- let adjustment = newEntities![entityId!]!.adjustment || 0;
- newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
- }
+ if (addAdjustment) {
+ let adjustment = newEntities![entityId!]!.adjustment || 0;
+ newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
+ }
- if (setZeroAdjustment) {
- newEntities![entityId!]!.adjustment = 0;
- }
+ if (setZeroAdjustment) {
+ newEntities![entityId!]!.adjustment = 0;
+ }
- toDeductCursor = newToDeduct!;
- deducted += newDeducted!;
- }
+ toDeductCursor = newToDeduct!;
+ deducted += newDeducted!;
+ }
- toDeduct = toDeductCursor;
- } else {
- // 2. If entity ID, deduct from that entity
- let currentEntityBalance = cusEnt.entities?.[entityId!]?.balance;
+ toDeduct = toDeductCursor;
+ } else {
+ // 2. If entity ID, deduct from that entity
+ let currentEntityBalance = cusEnt.entities?.[entityId!]?.balance;
- let {
- newBalance: newEntityBalance,
- deducted: newDeducted,
- toDeduct: newToDeduct,
- } = performDeduction({
- cusEntBalance: new Decimal(currentEntityBalance!),
- toDeduct,
- allowNegativeBalance,
- ent: cusEnt.entitlement,
- resetBalance,
- blockUsageLimit,
- });
+ let {
+ newBalance: newEntityBalance,
+ deducted: newDeducted,
+ toDeduct: newToDeduct,
+ } = performDeduction({
+ cusEntBalance: new Decimal(currentEntityBalance!),
+ toDeduct,
+ allowNegativeBalance,
+ ent: cusEnt.entitlement,
+ resetBalance,
+ blockUsageLimit,
+ });
- newEntities![entityId!]!.balance = newEntityBalance!;
+ newEntities![entityId!]!.balance = newEntityBalance!;
- if (addAdjustment) {
- let adjustment = newEntities![entityId!]!.adjustment || 0;
- newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
- }
+ if (addAdjustment) {
+ let adjustment = newEntities![entityId!]!.adjustment || 0;
+ newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
+ }
- if (setZeroAdjustment) {
- newEntities![entityId!]!.adjustment = 0;
- }
+ if (setZeroAdjustment) {
+ newEntities![entityId!]!.adjustment = 0;
+ }
- toDeduct = newToDeduct!;
- deducted += newDeducted!;
- }
- } else {
- let {
- newBalance: newBalance_,
- deducted: deducted_,
- toDeduct: newToDeduct_,
- } = performDeduction({
- cusEntBalance: new Decimal(cusEnt.balance!),
- toDeduct,
- allowNegativeBalance,
- ent: cusEnt.entitlement,
- resetBalance,
- blockUsageLimit,
- });
+ toDeduct = newToDeduct!;
+ deducted += newDeducted!;
+ }
+ } else {
+ let {
+ newBalance: newBalance_,
+ deducted: deducted_,
+ toDeduct: newToDeduct_,
+ } = performDeduction({
+ cusEntBalance: new Decimal(cusEnt.balance!),
+ toDeduct,
+ allowNegativeBalance,
+ ent: cusEnt.entitlement,
+ resetBalance,
+ blockUsageLimit,
+ });
- newBalance = newBalance_;
- deducted = deducted_;
- toDeduct = newToDeduct_;
+ newBalance = newBalance_;
+ deducted = deducted_;
+ toDeduct = newToDeduct_;
- if (addAdjustment) {
- let adjustment = cusEnt.adjustment || 0;
- newAdjustment = adjustment - deducted!;
- }
- }
- return { newBalance, newEntities, deducted, toDeduct, newAdjustment };
+ if (addAdjustment) {
+ let adjustment = cusEnt.adjustment || 0;
+ newAdjustment = adjustment - deducted!;
+ }
+ }
+ return { newBalance, newEntities, deducted, toDeduct, newAdjustment };
};
export const deductAllowanceFromCusEnt = async ({
- toDeduct,
- deductParams,
- cusEnt,
- featureDeductions,
- willDeductCredits = false,
- setZeroAdjustment = false,
+ toDeduct,
+ deductParams,
+ cusEnt,
+ featureDeductions,
+ willDeductCredits = false,
+ setZeroAdjustment = false,
}: {
- toDeduct: number;
- deductParams: DeductParams;
- cusEnt: FullCusEntWithFullCusProduct;
- featureDeductions: any;
- willDeductCredits?: boolean;
- setZeroAdjustment?: boolean;
+ toDeduct: number;
+ deductParams: DeductParams;
+ cusEnt: FullCusEntWithFullCusProduct;
+ featureDeductions: any;
+ willDeductCredits?: boolean;
+ setZeroAdjustment?: boolean;
}) => {
- const { db, feature, env, org, cusPrices, customer, entity } = deductParams;
+ const { db, feature, env, org, cusPrices, customer, entity } = deductParams;
- if (toDeduct == 0) {
- }
+ if (toDeduct == 0) {
+ }
- if (
- entity &&
- entityFeatureIdExists({ cusEnt }) &&
- cusEnt.entitlement.entity_feature_id !== entity.feature_id
- )
- return toDeduct;
+ if (
+ entity &&
+ entityFeatureIdExists({ cusEnt }) &&
+ cusEnt.entitlement.entity_feature_id !== entity.feature_id
+ )
+ return toDeduct;
- let {
- newBalance,
- newEntities,
- deducted,
- toDeduct: newToDeduct,
- } = performDeductionOnCusEnt({
- cusEnt,
- toDeduct,
- entityId: entity?.id,
- allowNegativeBalance: false,
- setZeroAdjustment,
- });
+ let {
+ newBalance,
+ newEntities,
+ deducted,
+ toDeduct: newToDeduct,
+ } = performDeductionOnCusEnt({
+ cusEnt,
+ toDeduct,
+ entityId: entity?.id,
+ allowNegativeBalance: false,
+ setZeroAdjustment,
+ });
- let originalGrpBalance = getTotalNegativeBalance({
- cusEnt,
- balance: cusEnt.balance!,
- entities: cusEnt.entities!,
- });
+ let originalGrpBalance = getTotalNegativeBalance({
+ cusEnt,
+ balance: cusEnt.balance!,
+ entities: cusEnt.entities!,
+ });
- let newGrpBalance = getTotalNegativeBalance({
- cusEnt,
- balance: newBalance!,
- entities: newEntities!,
- });
+ let newGrpBalance = getTotalNegativeBalance({
+ cusEnt,
+ balance: newBalance!,
+ entities: newEntities!,
+ });
- let updates: any = {
- balance: newBalance,
- entities: newEntities,
- };
- if (setZeroAdjustment) {
- updates.adjustment = 0;
- }
+ let updates: any = {
+ balance: newBalance,
+ entities: newEntities,
+ };
+ if (setZeroAdjustment) {
+ updates.adjustment = 0;
+ }
- const { newReplaceables, deletedReplaceables } = await adjustAllowance({
- db,
- env,
- org,
- cusPrices: cusPrices as any,
- customer,
- affectedFeature: feature,
- cusEnt: cusEnt as any,
- originalBalance: originalGrpBalance,
- newBalance: newGrpBalance,
- logger: console,
- });
+ const { newReplaceables, deletedReplaceables } = await adjustAllowance({
+ db,
+ env,
+ org,
+ cusPrices: cusPrices as any,
+ customer,
+ affectedFeature: feature,
+ cusEnt: cusEnt as any,
+ originalBalance: originalGrpBalance,
+ newBalance: newGrpBalance,
+ logger: console,
+ });
- if (newReplaceables && newReplaceables.length > 0) {
- updates.balance = newBalance! - newReplaceables.length;
- } else if (deletedReplaceables && deletedReplaceables.length > 0) {
- updates.balance = newBalance! + deletedReplaceables.length;
- }
+ if (newReplaceables && newReplaceables.length > 0) {
+ updates.balance = newBalance! - newReplaceables.length;
+ } else if (deletedReplaceables && deletedReplaceables.length > 0) {
+ updates.balance = newBalance! + deletedReplaceables.length;
+ }
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates,
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates,
+ });
- // Deduct credit amounts too
- if (feature.type === FeatureType.Metered && willDeductCredits) {
- for (let i = 0; i < featureDeductions.length; i++) {
- let { feature: creditSystem, deduction } = featureDeductions[i];
+ // Deduct credit amounts too
+ if (feature.type === FeatureType.Metered && willDeductCredits) {
+ for (let i = 0; i < featureDeductions.length; i++) {
+ let { feature: creditSystem, deduction } = featureDeductions[i];
- if (
- creditSystem.type === FeatureType.CreditSystem &&
- creditSystemContainsFeature({
- creditSystem: creditSystem,
- meteredFeatureId: feature.id!,
- })
- ) {
- // toDeduct -= deduction;
- let creditAmount = featureToCreditSystem({
- featureId: feature.id!,
- creditSystem: creditSystem,
- amount: deducted,
- });
- let newDeduction = new Decimal(deduction)
- .minus(creditAmount)
- .toNumber();
+ if (
+ creditSystem.type === FeatureType.CreditSystem &&
+ creditSystemContainsFeature({
+ creditSystem: creditSystem,
+ meteredFeatureId: feature.id!,
+ })
+ ) {
+ // toDeduct -= deduction;
+ let creditAmount = featureToCreditSystem({
+ featureId: feature.id!,
+ creditSystem: creditSystem,
+ amount: deducted,
+ });
+ let newDeduction = new Decimal(deduction)
+ .minus(creditAmount)
+ .toNumber();
- featureDeductions[i].deduction = newDeduction;
- }
- }
- }
+ featureDeductions[i].deduction = newDeduction;
+ }
+ }
+ }
- cusEnt.balance = newBalance;
- cusEnt.entities = newEntities;
+ cusEnt.balance = newBalance;
+ cusEnt.entities = newEntities;
- return newToDeduct;
+ return newToDeduct;
};
export const deductFromUsageBasedCusEnt = async ({
- toDeduct,
- deductParams,
- cusEnts,
- setZeroAdjustment = false,
+ toDeduct,
+ deductParams,
+ cusEnts,
+ setZeroAdjustment = false,
}: {
- toDeduct: number;
- deductParams: DeductParams;
- cusEnts: FullCusEntWithFullCusProduct[];
- setZeroAdjustment?: boolean;
+ toDeduct: number;
+ deductParams: DeductParams;
+ cusEnts: FullCusEntWithFullCusProduct[];
+ setZeroAdjustment?: boolean;
}) => {
- const { db, feature, env, org, cusPrices, customer, entity } = deductParams;
+ const { db, feature, env, org, cusPrices, customer, entity } = deductParams;
- // Deduct from usage-based price
- let usageBasedEnt = findCusEnt({
- cusEnts,
- feature,
- entity,
- onlyUsageAllowed: true,
- }) as FullCusEntWithFullCusProduct;
+ // Deduct from usage-based price
+ let usageBasedEnt = findCusEnt({
+ cusEnts,
+ feature,
+ entity,
+ onlyUsageAllowed: true,
+ }) as FullCusEntWithFullCusProduct;
- console.log(
- "Cus ents:",
- cusEnts.map(
- (ce) =>
- `Feature: ${ce.entitlement.feature_id}, Balance: ${ce.balance}, Usage Allowed: ${ce.usage_allowed}`
- )
- );
+ console.log(
+ "Cus ents:",
+ cusEnts.map(
+ (ce) =>
+ `Feature: ${ce.entitlement.feature_id}, Balance: ${ce.balance}, Usage Allowed: ${ce.usage_allowed}`,
+ ),
+ );
- if (
- !usageBasedEnt &&
- feature.config?.usage_type == FeatureUsageType.Continuous
- ) {
- console.log(`FALLING BACK TO REGULAR CUS ENT, FEATURE: ${feature.id}`);
- usageBasedEnt = findCusEnt({
- cusEnts,
- feature,
- entity,
- }) as FullCusEntWithFullCusProduct; // fallback to regular cus ent if allowed...
- }
+ if (
+ !usageBasedEnt &&
+ feature.config?.usage_type == FeatureUsageType.Continuous
+ ) {
+ console.log(`FALLING BACK TO REGULAR CUS ENT, FEATURE: ${feature.id}`);
+ usageBasedEnt = findCusEnt({
+ cusEnts,
+ feature,
+ entity,
+ }) as FullCusEntWithFullCusProduct; // fallback to regular cus ent if allowed...
+ }
- if (!usageBasedEnt) {
- console.log(
- ` - Feature ${feature.id}, To deduct: ${toDeduct} -> no usage-based entitlement found`
- );
- return;
- }
+ if (!usageBasedEnt) {
+ console.log(
+ ` - Feature ${feature.id}, To deduct: ${toDeduct} -> no usage-based entitlement found`,
+ );
+ return;
+ }
- let cusPrice = getRelatedCusPrice(usageBasedEnt, cusPrices);
- let billingType = cusPrice?.price
- ? getBillingType(cusPrice?.price.config!)
- : undefined;
- let blockUsageLimit =
- billingType === BillingType.InArrearProrated ? false : true;
+ let cusPrice = getRelatedCusPrice(usageBasedEnt, cusPrices);
+ let billingType = cusPrice?.price
+ ? getBillingType(cusPrice?.price.config!)
+ : undefined;
+ let blockUsageLimit =
+ billingType === BillingType.InArrearProrated ? false : true;
- let { newBalance, newEntities, deducted } = performDeductionOnCusEnt({
- cusEnt: usageBasedEnt,
- toDeduct,
- allowNegativeBalance: true,
- setZeroAdjustment,
- entityId: entity?.id,
- blockUsageLimit,
- });
+ let { newBalance, newEntities, deducted } = performDeductionOnCusEnt({
+ cusEnt: usageBasedEnt,
+ toDeduct,
+ allowNegativeBalance: true,
+ setZeroAdjustment,
+ entityId: entity?.id,
+ blockUsageLimit,
+ });
- let oldGrpBalance = getTotalNegativeBalance({
- cusEnt: usageBasedEnt,
- balance: usageBasedEnt.balance!,
- entities: usageBasedEnt.entities!,
- });
+ let oldGrpBalance = getTotalNegativeBalance({
+ cusEnt: usageBasedEnt,
+ balance: usageBasedEnt.balance!,
+ entities: usageBasedEnt.entities!,
+ });
- let newGrpBalance = getTotalNegativeBalance({
- cusEnt: usageBasedEnt,
- balance: newBalance!,
- entities: newEntities!,
- });
+ let newGrpBalance = getTotalNegativeBalance({
+ cusEnt: usageBasedEnt,
+ balance: newBalance!,
+ entities: newEntities!,
+ });
- // Update usageBasedEnt in place with the deduction results
- usageBasedEnt.balance = newBalance;
- usageBasedEnt.entities = newEntities;
- if (setZeroAdjustment) {
- usageBasedEnt.adjustment = 0;
- }
+ // Update usageBasedEnt in place with the deduction results
+ usageBasedEnt.balance = newBalance;
+ usageBasedEnt.entities = newEntities;
+ if (setZeroAdjustment) {
+ usageBasedEnt.adjustment = 0;
+ }
- let updates: any = {
- balance: newBalance,
- entities: newEntities,
- };
- if (setZeroAdjustment) {
- updates.adjustment = 0;
- }
+ let updates: any = {
+ balance: newBalance,
+ entities: newEntities,
+ };
+ if (setZeroAdjustment) {
+ updates.adjustment = 0;
+ }
- const { newReplaceables, deletedReplaceables } = await adjustAllowance({
- db,
- env,
- affectedFeature: feature,
- org,
- cusEnt: usageBasedEnt as any,
- cusPrices: cusPrices as any,
- customer,
- originalBalance: oldGrpBalance,
- newBalance: newGrpBalance,
- logger: console,
- });
+ const { newReplaceables, deletedReplaceables } = await adjustAllowance({
+ db,
+ env,
+ affectedFeature: feature,
+ org,
+ cusEnt: usageBasedEnt as any,
+ cusPrices: cusPrices as any,
+ customer,
+ originalBalance: oldGrpBalance,
+ newBalance: newGrpBalance,
+ logger: console,
+ });
- if (newReplaceables && newReplaceables.length > 0) {
- const finalBalance = newBalance! - newReplaceables.length;
- updates.balance = finalBalance;
- usageBasedEnt.balance = finalBalance;
- } else if (deletedReplaceables && deletedReplaceables.length > 0) {
- const finalBalance = newBalance! + deletedReplaceables.length;
- updates.balance = finalBalance;
- usageBasedEnt.balance = finalBalance;
- }
+ if (newReplaceables && newReplaceables.length > 0) {
+ const finalBalance = newBalance! - newReplaceables.length;
+ updates.balance = finalBalance;
+ usageBasedEnt.balance = finalBalance;
+ } else if (deletedReplaceables && deletedReplaceables.length > 0) {
+ const finalBalance = newBalance! + deletedReplaceables.length;
+ updates.balance = finalBalance;
+ usageBasedEnt.balance = finalBalance;
+ }
- await CusEntService.update({
- db,
- id: usageBasedEnt!.id,
- updates,
- });
+ await CusEntService.update({
+ db,
+ id: usageBasedEnt!.id,
+ updates,
+ });
- console.log("Usage based cus ent balance", usageBasedEnt.balance);
+ console.log("Usage based cus ent balance", usageBasedEnt.balance);
};
// Main function to update customer balance
export const updateCustomerBalance = async ({
- db,
- customerId,
- entityId,
- event,
- features,
- org,
- env,
- logger,
- allFeatures,
+ db,
+ customerId,
+ entityId,
+ event,
+ features,
+ org,
+ env,
+ logger,
+ allFeatures,
}: {
- db: DrizzleCli;
- customerId: string;
- entityId: string;
- event: Event;
- features: Feature[];
- org: Organization;
- env: AppEnv;
- logger: any;
- allFeatures: Feature[];
+ db: DrizzleCli;
+ customerId: string;
+ entityId: string;
+ event: Event;
+ features: Feature[];
+ org: Organization;
+ env: AppEnv;
+ logger: any;
+ allFeatures: Feature[];
}) => {
- const startTime = performance.now();
- console.log("REVERSE DEDUCTION ORDER", org.config.reverse_deduction_order);
- const customer = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- entityId,
- withSubs: true,
- });
+ const startTime = performance.now();
+ console.log("REVERSE DEDUCTION ORDER", org.config.reverse_deduction_order);
+ const customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ entityId,
+ withSubs: true,
+ });
- const { cusEnts, cusPrices } = await getCusEntsInFeatures({
- customer,
- internalFeatureIds: features.map((f) => f.internal_id!),
- logger,
- reverseOrder: org.config.reverse_deduction_order,
- });
+ const { cusEnts, cusPrices } = await getCusEntsInFeatures({
+ customer,
+ internalFeatureIds: features.map((f) => f.internal_id!),
+ logger,
+ reverseOrder: org.config.reverse_deduction_order,
+ });
- const endTime = performance.now();
+ const endTime = performance.now();
- // 1. Get deductions for each feature
- const featureDeductions = getFeatureDeductions({
- cusEnts,
- event,
- features,
- });
+ // 1. Get deductions for each feature
+ const featureDeductions = getFeatureDeductions({
+ cusEnts,
+ event,
+ features,
+ });
- logBalanceUpdate({
- timeTaken: (endTime - startTime).toFixed(2),
- customer,
- features,
- cusEnts,
- featureDeductions,
- properties: event.properties,
- org,
- entityId: event.entity_id,
- });
+ logBalanceUpdate({
+ timeTaken: (endTime - startTime).toFixed(2),
+ customer,
+ features,
+ cusEnts,
+ featureDeductions,
+ properties: event.properties,
+ org,
+ entityId: event.entity_id,
+ });
- // 3. Return if no customer entitlements or features found
- if (cusEnts.length === 0 || features.length === 0) {
- console.log(" - No customer entitlements or features found");
- return;
- }
+ // 3. Return if no customer entitlements or features found
+ if (cusEnts.length === 0 || features.length === 0) {
+ console.log(" - No customer entitlements or features found");
+ return;
+ }
- // 4. Perform deductions and update customer balance
- for (const obj of featureDeductions) {
- let { feature, deduction: toDeduct } = obj;
+ // 4. Perform deductions and update customer balance
+ for (const obj of featureDeductions) {
+ let { feature, deduction: toDeduct } = obj;
- const originalCusEnts = structuredClone(cusEnts);
+ const originalCusEnts = structuredClone(cusEnts);
- for (const cusEnt of cusEnts) {
- if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
- continue;
- }
+ for (const cusEnt of cusEnts) {
+ if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
+ continue;
+ }
- toDeduct = await deductFromCusRollovers({
- toDeduct,
- cusEnt,
- deductParams: {
- db,
- feature,
- env,
- entity: customer.entity ? customer.entity : undefined,
- },
- });
+ toDeduct = await deductFromCusRollovers({
+ toDeduct,
+ cusEnt,
+ deductParams: {
+ db,
+ feature,
+ env,
+ entity: customer.entity ? customer.entity : undefined,
+ },
+ });
- if (toDeduct == 0) continue;
+ if (toDeduct == 0) continue;
- toDeduct = await deductAllowanceFromCusEnt({
- toDeduct,
- cusEnt,
- deductParams: {
- db,
- feature,
- env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties: event.properties,
- entity: customer.entity,
- },
- featureDeductions,
- willDeductCredits: true,
- });
- }
+ toDeduct = await deductAllowanceFromCusEnt({
+ toDeduct,
+ cusEnt,
+ deductParams: {
+ db,
+ feature,
+ env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties: event.properties,
+ entity: customer.entity,
+ },
+ featureDeductions,
+ willDeductCredits: true,
+ });
+ }
- if (toDeduct !== 0) {
- await deductFromUsageBasedCusEnt({
- toDeduct,
- cusEnts,
- deductParams: {
- db,
- feature,
- env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties: event.properties,
- entity: customer.entity,
- },
- });
- }
+ if (toDeduct !== 0) {
+ await deductFromUsageBasedCusEnt({
+ toDeduct,
+ cusEnts,
+ deductParams: {
+ db,
+ feature,
+ env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties: event.properties,
+ entity: customer.entity,
+ },
+ });
+ }
- handleThresholdReached({
- org,
- env,
- features: allFeatures,
- db,
- feature,
- cusEnts: originalCusEnts,
- newCusEnts: cusEnts,
- fullCus: customer,
- logger,
- });
- }
+ handleThresholdReached({
+ org,
+ env,
+ features: allFeatures,
+ db,
+ feature,
+ cusEnts: originalCusEnts,
+ newCusEnts: cusEnts,
+ fullCus: customer,
+ logger,
+ });
+ }
- return cusEnts;
+ return cusEnts;
};
// MAIN FUNCTION
export const runUpdateBalanceTask = async ({
- payload,
- logger,
- db,
+ payload,
+ logger,
+ db,
}: {
- payload: any;
- logger: any;
- db: DrizzleCli;
+ payload: any;
+ logger: any;
+ db: DrizzleCli;
}) => {
- try {
- // 1. Update customer balance
- const { customerId, features, event, org, env, entityId, allFeatures } =
- payload;
+ try {
+ // 1. Update customer balance
+ const { customerId, features, event, org, env, entityId, allFeatures } =
+ payload;
- console.log("--------------------------------");
- console.log(
- `UPDATING BALANCE FOR CUSTOMER (${customerId}), ORG: ${org.slug}`
- );
+ console.log("--------------------------------");
+ console.log(
+ `UPDATING BALANCE FOR CUSTOMER (${customerId}), ORG: ${org.slug}`,
+ );
- const cusEnts: any = await updateCustomerBalance({
- db,
- customerId,
- features,
- event,
- org,
- env,
- logger,
- entityId,
- allFeatures,
- });
+ const cusEnts: any = await updateCustomerBalance({
+ db,
+ customerId,
+ features,
+ event,
+ org,
+ env,
+ logger,
+ entityId,
+ allFeatures,
+ });
- // console.time("refreshCusCache");
- await refreshCusCache({
- db,
- customerId,
- org,
- env,
- entityId,
- });
- // console.timeEnd("refreshCusCache");
+ // console.time("refreshCusCache");
+ await refreshCusCache({
+ db,
+ customerId,
+ org,
+ env,
+ entityId,
+ });
+ // console.timeEnd("refreshCusCache");
- if (!cusEnts || cusEnts.length === 0) {
- return;
- }
- console.log(" ✅ Customer balance updated");
- } catch (error) {
- if (logger) {
- logger.use((log: any) => {
- return {
- ...log,
- data: payload,
- };
- });
+ if (!cusEnts || cusEnts.length === 0) {
+ return;
+ }
+ console.log(" ✅ Customer balance updated");
+ } catch (error) {
+ if (logger) {
+ logger.use((log: any) => {
+ return {
+ ...log,
+ data: payload,
+ };
+ });
- logger.error(`ERROR UPDATING BALANCE`);
- logger.error(error);
- } else {
- console.log(error);
- }
- }
+ logger.error(`ERROR UPDATING BALANCE`);
+ logger.error(error);
+ } else {
+ console.log(error);
+ }
+ }
};
diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts
index afccf7d92..c70520c5a 100644
--- a/server/src/trigger/updateUsageTask.ts
+++ b/server/src/trigger/updateUsageTask.ts
@@ -1,12 +1,12 @@
import {
- AllowanceType,
- AppEnv,
- CusProductStatus,
- Customer,
- Feature,
- FeatureType,
- FullCustomerEntitlement,
- Organization,
+ AllowanceType,
+ AppEnv,
+ CusProductStatus,
+ Customer,
+ Feature,
+ FeatureType,
+ FullCustomerEntitlement,
+ Organization,
} from "@autumn/shared";
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
@@ -15,8 +15,8 @@ import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusE
import { Decimal } from "decimal.js";
import {
- deductAllowanceFromCusEnt,
- deductFromUsageBasedCusEnt,
+ deductAllowanceFromCusEnt,
+ deductFromUsageBasedCusEnt,
} from "./updateBalanceTask.js";
import { CusService } from "@/internal/customers/CusService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -26,348 +26,348 @@ import { handleThresholdReached } from "./handleThresholdReached.js";
// 2. Get deductions for each feature
const getFeatureDeductions = ({
- cusEnts,
- value,
- features,
- shouldSet,
+ cusEnts,
+ value,
+ features,
+ shouldSet,
}: {
- cusEnts: FullCustomerEntitlement[];
- value: number;
- features: Feature[];
- shouldSet: boolean;
+ cusEnts: FullCustomerEntitlement[];
+ value: number;
+ features: Feature[];
+ shouldSet: boolean;
}) => {
- let meteredFeature =
- features.find((f) => f.type === FeatureType.Metered) || features[0];
+ let meteredFeature =
+ features.find((f) => f.type === FeatureType.Metered) || features[0];
- const featureDeductions = [];
- for (const feature of features) {
- let newValue = value;
- let unlimitedExists = cusEnts.some(
- (cusEnt) =>
- cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
- cusEnt.entitlement.internal_feature_id == feature.internal_id
- );
+ const featureDeductions = [];
+ for (const feature of features) {
+ let newValue = value;
+ let unlimitedExists = cusEnts.some(
+ (cusEnt) =>
+ cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
+ cusEnt.entitlement.internal_feature_id == feature.internal_id,
+ );
- if (unlimitedExists) {
- continue;
- }
+ if (unlimitedExists) {
+ continue;
+ }
- if (feature.type === FeatureType.CreditSystem) {
- newValue = featureToCreditSystem({
- featureId: meteredFeature.id,
- creditSystem: feature,
- amount: value,
- });
- }
+ if (feature.type === FeatureType.CreditSystem) {
+ newValue = featureToCreditSystem({
+ featureId: meteredFeature.id,
+ creditSystem: feature,
+ amount: value,
+ });
+ }
- // If it's set
- let deduction = newValue;
+ // If it's set
+ let deduction = newValue;
- if (shouldSet) {
- let totalAllowance = cusEnts.reduce((acc, curr) => {
- return acc + (curr.entitlement.allowance || 0);
- }, 0);
+ if (shouldSet) {
+ let totalAllowance = cusEnts.reduce((acc, curr) => {
+ return acc + (curr.entitlement.allowance || 0);
+ }, 0);
- let targetBalance = new Decimal(totalAllowance).sub(value).toNumber();
+ let targetBalance = new Decimal(totalAllowance).sub(value).toNumber();
- let totalBalance = getFeatureBalance({
- cusEnts,
- internalFeatureId: feature.internal_id!,
- })!;
+ let totalBalance = getFeatureBalance({
+ cusEnts,
+ internalFeatureId: feature.internal_id!,
+ })!;
- deduction = new Decimal(totalBalance).sub(targetBalance).toNumber();
- }
+ deduction = new Decimal(totalBalance).sub(targetBalance).toNumber();
+ }
- if (deduction == 0) {
- console.log(` - Skipping feature ${feature.id} -- deduction is 0`);
- continue;
- }
+ if (deduction == 0) {
+ console.log(` - Skipping feature ${feature.id} -- deduction is 0`);
+ continue;
+ }
- featureDeductions.push({
- feature,
- deduction,
- });
- }
+ featureDeductions.push({
+ feature,
+ deduction,
+ });
+ }
- featureDeductions.sort((a, b) => {
- if (
- a.feature.type === FeatureType.CreditSystem &&
- b.feature.type !== FeatureType.CreditSystem
- ) {
- return 1;
- }
+ featureDeductions.sort((a, b) => {
+ if (
+ a.feature.type === FeatureType.CreditSystem &&
+ b.feature.type !== FeatureType.CreditSystem
+ ) {
+ return 1;
+ }
- if (
- a.feature.type !== FeatureType.CreditSystem &&
- b.feature.type === FeatureType.CreditSystem
- ) {
- return -1;
- }
+ if (
+ a.feature.type !== FeatureType.CreditSystem &&
+ b.feature.type === FeatureType.CreditSystem
+ ) {
+ return -1;
+ }
- return a.feature.id.localeCompare(b.feature.id);
- });
+ return a.feature.id.localeCompare(b.feature.id);
+ });
- return featureDeductions;
+ return featureDeductions;
};
const logUsageUpdate = ({
- customer,
- features,
- cusEnts,
- featureDeductions,
- org,
- setUsage,
- entityId,
+ customer,
+ features,
+ cusEnts,
+ featureDeductions,
+ org,
+ setUsage,
+ entityId,
}: {
- customer: Customer;
- features: Feature[];
- cusEnts: FullCustomerEntitlement[];
- featureDeductions: any;
- org: Organization;
- setUsage: boolean;
- entityId?: string;
+ customer: Customer;
+ features: Feature[];
+ cusEnts: FullCustomerEntitlement[];
+ featureDeductions: any;
+ org: Organization;
+ setUsage: boolean;
+ entityId?: string;
}) => {
- console.log(
- ` - Customer: ${customer.id} (${customer.env}) | Org: ${
- org.slug
- } | Features: ${features.map((f) => f.id).join(", ")} | Set Usage: ${
- setUsage ? "true" : "false"
- }`
- );
+ console.log(
+ ` - Customer: ${customer.id} (${customer.env}) | Org: ${
+ org.slug
+ } | Features: ${features.map((f) => f.id).join(", ")} | Set Usage: ${
+ setUsage ? "true" : "false"
+ }`,
+ );
- console.log(
- " - CusEnts:",
- cusEnts.map((cusEnt: any) => {
- let balanceStr = cusEnt.balance;
- try {
- if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
- balanceStr = "Unlimited";
- }
- } catch (error) {
- balanceStr = "failed_to_get_balance";
- }
+ console.log(
+ " - CusEnts:",
+ cusEnts.map((cusEnt: any) => {
+ let balanceStr = cusEnt.balance;
+ try {
+ if (cusEnt.entitlement.allowance_type === AllowanceType.Unlimited) {
+ balanceStr = "Unlimited";
+ }
+ } catch (error) {
+ balanceStr = "failed_to_get_balance";
+ }
- if (entityId && cusEnt.entities) {
- balanceStr = `${cusEnt.entities?.[entityId!]?.balance} [${entityId}]`;
- }
+ if (entityId && cusEnt.entities) {
+ balanceStr = `${cusEnt.entities?.[entityId!]?.balance} [${entityId}]`;
+ }
- return `${cusEnt.feature_id} - ${balanceStr} (${
- cusEnt.customer_product ? cusEnt.customer_product.product_id : ""
- })`;
- }),
- "| Deductions:",
- featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`)
- );
+ return `${cusEnt.feature_id} - ${balanceStr} (${
+ cusEnt.customer_product ? cusEnt.customer_product.product_id : ""
+ })`;
+ }),
+ "| Deductions:",
+ featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`),
+ );
};
// Main function to update customer balance
export const updateUsage = async ({
- db,
- customerId,
- features,
- org,
- env,
- value,
- properties,
- setUsage,
- logger,
- entityId,
- allFeatures,
+ db,
+ customerId,
+ features,
+ org,
+ env,
+ value,
+ properties,
+ setUsage,
+ logger,
+ entityId,
+ allFeatures,
}: {
- db: DrizzleCli;
- customerId: string;
- features: Feature[];
- org: Organization;
- env: AppEnv;
- value: number;
- properties: any;
- setUsage: boolean;
- logger: any;
- entityId?: string;
- allFeatures: Feature[];
+ db: DrizzleCli;
+ customerId: string;
+ features: Feature[];
+ org: Organization;
+ env: AppEnv;
+ value: number;
+ properties: any;
+ setUsage: boolean;
+ logger: any;
+ entityId?: string;
+ allFeatures: Feature[];
}) => {
- const customer = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
- entityId,
- withSubs: true,
- });
+ const customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
+ entityId,
+ withSubs: true,
+ });
- const { cusEnts, cusPrices } = await getCusEntsInFeatures({
- customer,
- internalFeatureIds: features.map((f) => f.internal_id!),
- logger,
- reverseOrder: org.config?.reverse_deduction_order,
- });
+ const { cusEnts, cusPrices } = await getCusEntsInFeatures({
+ customer,
+ internalFeatureIds: features.map((f) => f.internal_id!),
+ logger,
+ reverseOrder: org.config?.reverse_deduction_order,
+ });
- // 1. Get deductions for each feature
- const featureDeductions = getFeatureDeductions({
- cusEnts,
- value,
- shouldSet: setUsage,
- features,
- });
+ // 1. Get deductions for each feature
+ const featureDeductions = getFeatureDeductions({
+ cusEnts,
+ value,
+ shouldSet: setUsage,
+ features,
+ });
- logUsageUpdate({
- customer,
- features,
- cusEnts,
- featureDeductions,
- org,
- setUsage,
- entityId,
- });
+ logUsageUpdate({
+ customer,
+ features,
+ cusEnts,
+ featureDeductions,
+ org,
+ setUsage,
+ entityId,
+ });
- // 3. Return if no customer entitlements or features found
- if (cusEnts.length === 0 || features.length === 0) {
- console.log(" - No customer entitlements or features found");
- return;
- }
+ // 3. Return if no customer entitlements or features found
+ if (cusEnts.length === 0 || features.length === 0) {
+ console.log(" - No customer entitlements or features found");
+ return;
+ }
- const originalCusEnts = structuredClone(cusEnts);
- for (const obj of featureDeductions) {
- let { feature, deduction: toDeduct } = obj;
+ const originalCusEnts = structuredClone(cusEnts);
+ for (const obj of featureDeductions) {
+ let { feature, deduction: toDeduct } = obj;
- for (const cusEnt of cusEnts) {
- if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
- continue;
- }
+ for (const cusEnt of cusEnts) {
+ if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
+ continue;
+ }
- toDeduct = await deductFromCusRollovers({
- toDeduct,
- cusEnt,
- deductParams: {
- db,
- feature,
- env,
- entity: customer.entity ? customer.entity : undefined,
- },
- });
+ toDeduct = await deductFromCusRollovers({
+ toDeduct,
+ cusEnt,
+ deductParams: {
+ db,
+ feature,
+ env,
+ entity: customer.entity ? customer.entity : undefined,
+ },
+ });
- if (toDeduct == 0) continue;
+ if (toDeduct == 0) continue;
- toDeduct = await deductAllowanceFromCusEnt({
- toDeduct,
- cusEnt,
- deductParams: {
- db,
- feature,
- env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties,
- entity: customer.entity,
- },
- featureDeductions,
- willDeductCredits: true,
- setZeroAdjustment: true,
- });
- }
+ toDeduct = await deductAllowanceFromCusEnt({
+ toDeduct,
+ cusEnt,
+ deductParams: {
+ db,
+ feature,
+ env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties,
+ entity: customer.entity,
+ },
+ featureDeductions,
+ willDeductCredits: true,
+ setZeroAdjustment: true,
+ });
+ }
- if (toDeduct !== 0) {
- await deductFromUsageBasedCusEnt({
- toDeduct,
- cusEnts,
- deductParams: {
- db,
- feature,
- env,
- org,
- cusPrices: cusPrices as any[],
- customer,
- properties,
- entity: customer.entity,
- },
- setZeroAdjustment: true,
- });
- }
+ if (toDeduct !== 0) {
+ await deductFromUsageBasedCusEnt({
+ toDeduct,
+ cusEnts,
+ deductParams: {
+ db,
+ feature,
+ env,
+ org,
+ cusPrices: cusPrices as any[],
+ customer,
+ properties,
+ entity: customer.entity,
+ },
+ setZeroAdjustment: true,
+ });
+ }
- handleThresholdReached({
- org,
- env,
- features: allFeatures,
- db,
+ handleThresholdReached({
+ org,
+ env,
+ features: allFeatures,
+ db,
- feature,
- cusEnts: originalCusEnts,
- newCusEnts: cusEnts,
- fullCus: customer,
- logger,
- });
- }
+ feature,
+ cusEnts: originalCusEnts,
+ newCusEnts: cusEnts,
+ fullCus: customer,
+ logger,
+ });
+ }
- return cusEnts;
+ return cusEnts;
};
// MAIN FUNCTION
export const runUpdateUsageTask = async ({
- payload,
- logger,
- db,
- throwError = false,
+ payload,
+ logger,
+ db,
+ throwError = false,
}: {
- payload: any;
- logger: any;
- db: DrizzleCli;
- throwError?: boolean;
+ payload: any;
+ logger: any;
+ db: DrizzleCli;
+ throwError?: boolean;
}) => {
- try {
- // 1. Update customer balance
- const {
- internalCustomerId,
- customerId,
- eventId,
- features,
- value,
- set_usage,
- properties,
- org,
- env,
- entityId,
- allFeatures,
- } = payload;
+ try {
+ // 1. Update customer balance
+ const {
+ internalCustomerId,
+ customerId,
+ eventId,
+ features,
+ value,
+ set_usage,
+ properties,
+ org,
+ env,
+ entityId,
+ allFeatures,
+ } = payload;
- console.log("--------------------------------");
- console.log(
- `HANDLING USAGE TASK FOR CUSTOMER (${customerId}), ORG: ${org.slug}, EVENT ID: ${eventId}`
- );
+ console.log("--------------------------------");
+ console.log(
+ `HANDLING USAGE TASK FOR CUSTOMER (${customerId}), ORG: ${org.slug}, EVENT ID: ${eventId}`,
+ );
- const cusEnts: any = await updateUsage({
- db,
- customerId,
- features,
- value,
- properties,
- org,
- env,
- setUsage: set_usage,
- logger,
- entityId,
- allFeatures,
- });
+ const cusEnts: any = await updateUsage({
+ db,
+ customerId,
+ features,
+ value,
+ properties,
+ org,
+ env,
+ setUsage: set_usage,
+ logger,
+ entityId,
+ allFeatures,
+ });
- await refreshCusCache({
- db,
- customerId,
- entityId,
- org,
- env,
- });
+ await refreshCusCache({
+ db,
+ customerId,
+ entityId,
+ org,
+ env,
+ });
- if (!cusEnts || cusEnts.length === 0) {
- return;
- }
- console.log(" ✅ Customer balance updated");
- } catch (error) {
- logger.error(`ERROR UPDATING USAGE`);
- logger.error(error);
+ if (!cusEnts || cusEnts.length === 0) {
+ return;
+ }
+ console.log(" ✅ Customer balance updated");
+ } catch (error) {
+ logger.error(`ERROR UPDATING USAGE`);
+ logger.error(error);
- if (throwError) {
- throw error;
- }
- }
+ if (throwError) {
+ throw error;
+ }
+ }
};
diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts
index 6b033f666..334064c46 100644
--- a/server/src/utils/auth.ts
+++ b/server/src/utils/auth.ts
@@ -17,116 +17,116 @@ import { eq } from "drizzle-orm";
import { logger } from "@/external/logtail/logtailUtils.js";
export const auth = betterAuth({
- telemetry: {
- enabled: false,
- },
+ telemetry: {
+ enabled: false,
+ },
- database: drizzleAdapter(db, {
- provider: "pg", // or "mysql", "sqlite"
- }),
+ database: drizzleAdapter(db, {
+ provider: "pg", // or "mysql", "sqlite"
+ }),
- databaseHooks: {
- user: {
- create: {
- after: async (user) => {
- await createLoopsContact(user);
- await sendOnboardingEmail({
- name: user.name,
- email: user.email,
- });
- },
- },
- },
- session: {
- create: {
- before: beforeSessionCreated,
- },
- },
- },
- user: {
- deleteUser: {
- enabled: true,
+ databaseHooks: {
+ user: {
+ create: {
+ after: async (user) => {
+ await createLoopsContact(user);
+ await sendOnboardingEmail({
+ name: user.name,
+ email: user.email,
+ });
+ },
+ },
+ },
+ session: {
+ create: {
+ before: beforeSessionCreated,
+ },
+ },
+ },
+ user: {
+ deleteUser: {
+ enabled: true,
- sendDeleteAccountVerification: async ({ user, url, token }) => {
- console.log("Delete account verification", { url, token });
- },
- },
- },
- trustedOrigins: [
- "http://localhost:3000",
- "https://app.useautumn.com",
- "https://staging.useautumn.com",
- "https://*.useautumn.com",
- // process.env.CLIENT_URL!,
- ],
- emailAndPassword: {
- enabled: true,
- disableSignUp: false,
- requireEmailVerification: true,
- minPasswordLength: 8,
- maxPasswordLength: 128,
- autoSignIn: true,
- resetPasswordTokenExpiresIn: 3600, // 1 hour
- },
+ sendDeleteAccountVerification: async ({ user, url, token }) => {
+ console.log("Delete account verification", { url, token });
+ },
+ },
+ },
+ trustedOrigins: [
+ "http://localhost:3000",
+ "https://app.useautumn.com",
+ "https://staging.useautumn.com",
+ "https://*.useautumn.com",
+ // process.env.CLIENT_URL!,
+ ],
+ emailAndPassword: {
+ enabled: true,
+ disableSignUp: false,
+ requireEmailVerification: true,
+ minPasswordLength: 8,
+ maxPasswordLength: 128,
+ autoSignIn: true,
+ resetPasswordTokenExpiresIn: 3600, // 1 hour
+ },
- socialProviders: {
- google: {
- clientId: process.env.GOOGLE_CLIENT_ID!,
- clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
- },
- },
- plugins: [
- emailOTP({
- async sendVerificationOTP({ email, otp, type }) {
- // Implement the sendVerificationOTP method to send the OTP to the user's email address
+ socialProviders: {
+ google: {
+ clientId: process.env.GOOGLE_CLIENT_ID!,
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
+ },
+ },
+ plugins: [
+ emailOTP({
+ async sendVerificationOTP({ email, otp, type }) {
+ // Implement the sendVerificationOTP method to send the OTP to the user's email address
- await sendOTPEmail({
- email,
- otp,
- });
- },
- }),
- admin({
- adminUserIds: ADMIN_USER_IDs,
- impersonationSessionDuration: 1000 * 60 * 60 * 24, // 1 days
- }),
+ await sendOTPEmail({
+ email,
+ otp,
+ });
+ },
+ }),
+ admin({
+ adminUserIds: ADMIN_USER_IDs,
+ impersonationSessionDuration: 1000 * 60 * 60 * 24, // 1 days
+ }),
- organization({
- async sendInvitationEmail(data) {
- const inviteLink = `${process.env.CLIENT_URL}/accept?id=${data.id}`;
- await sendInvitationEmail({
- email: data.email,
- orgName: data.organization.name,
- inviteLink,
- });
+ organization({
+ async sendInvitationEmail(data) {
+ const inviteLink = `${process.env.CLIENT_URL}/accept?id=${data.id}`;
+ await sendInvitationEmail({
+ email: data.email,
+ orgName: data.organization.name,
+ inviteLink,
+ });
- try {
- // Update invite to expire in 7 days
- await db
- .update(invitation)
- .set({
- expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
- })
- .where(eq(invitation.id, data.id));
- } catch (error) {
- logger.error("Error updating invite expiration date:", { error });
- }
- },
- schema: {
- organization: {
- modelName: "organizations",
- fields: {
- createdAt: "createdAt",
- },
- },
- },
+ try {
+ // Update invite to expire in 7 days
+ await db
+ .update(invitation)
+ .set({
+ expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
+ })
+ .where(eq(invitation.id, data.id));
+ } catch (error) {
+ logger.error("Error updating invite expiration date:", { error });
+ }
+ },
+ schema: {
+ organization: {
+ modelName: "organizations",
+ fields: {
+ createdAt: "createdAt",
+ },
+ },
+ },
- organizationCreation: {
- disabled: false,
- afterCreate: async ({ organization }) => {
- await afterOrgCreated({ org: organization as any });
- },
- },
- }),
- ],
+ organizationCreation: {
+ disabled: false,
+ afterCreate: async ({ organization }) => {
+ await afterOrgCreated({ org: organization as any });
+ },
+ },
+ }),
+ ],
});
diff --git a/server/src/utils/authUtils.ts b/server/src/utils/authUtils.ts
index 9bcd4fb17..819956686 100644
--- a/server/src/utils/authUtils.ts
+++ b/server/src/utils/authUtils.ts
@@ -1,8 +1,8 @@
export const parseAuthHeader = (req: any) => {
- let authHeader = req.headers["Authorization"] || req.headers["authorization"];
- if (!authHeader || !authHeader.startsWith("Bearer ")) {
- return null;
- }
- let bearerToken = authHeader.split(" ")[1];
- return bearerToken;
+ let authHeader = req.headers["Authorization"] || req.headers["authorization"];
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
+ return null;
+ }
+ let bearerToken = authHeader.split(" ")[1];
+ return bearerToken;
};
diff --git a/server/src/utils/authUtils/afterOrgCreated.ts b/server/src/utils/authUtils/afterOrgCreated.ts
index bcc251446..4f3a1a546 100644
--- a/server/src/utils/authUtils/afterOrgCreated.ts
+++ b/server/src/utils/authUtils/afterOrgCreated.ts
@@ -7,77 +7,77 @@ import { logger } from "@/external/logtail/logtailUtils.js";
import { Organization } from "better-auth/plugins";
export const initOrgSvixApps = async ({
- id,
- slug,
+ id,
+ slug,
}: {
- id: string;
- slug: string;
+ id: string;
+ slug: string;
}) => {
- const batchCreate = [];
- batchCreate.push(
- createSvixApp({
- name: `${slug}_${AppEnv.Sandbox}`,
- orgId: id,
- env: AppEnv.Sandbox,
- }),
- );
- batchCreate.push(
- createSvixApp({
- name: `${slug}_${AppEnv.Live}`,
- orgId: id,
- env: AppEnv.Live,
- }),
- );
+ const batchCreate = [];
+ batchCreate.push(
+ createSvixApp({
+ name: `${slug}_${AppEnv.Sandbox}`,
+ orgId: id,
+ env: AppEnv.Sandbox,
+ }),
+ );
+ batchCreate.push(
+ createSvixApp({
+ name: `${slug}_${AppEnv.Live}`,
+ orgId: id,
+ env: AppEnv.Live,
+ }),
+ );
- const [sandboxApp, liveApp] = await Promise.all(batchCreate);
+ const [sandboxApp, liveApp] = await Promise.all(batchCreate);
- return { sandboxApp, liveApp };
+ return { sandboxApp, liveApp };
};
export const afterOrgCreated = async ({ org }: { org: Organization }) => {
- logger.info(`Org created: ${org.id} (${org.slug})`);
+ logger.info(`Org created: ${org.id} (${org.slug})`);
- const { id, slug, createdAt } = org;
+ const { id, slug, createdAt } = org;
- try {
- await OrgService.update({
- db,
- orgId: id,
- updates: {
- created_at: createdAt.getTime(),
- },
- });
+ try {
+ await OrgService.update({
+ db,
+ orgId: id,
+ updates: {
+ created_at: createdAt.getTime(),
+ },
+ });
- // 1. Create svix webhoooks
- const { sandboxApp, liveApp } = await initOrgSvixApps({
- slug,
- id,
- });
+ // 1. Create svix webhoooks
+ const { sandboxApp, liveApp } = await initOrgSvixApps({
+ slug,
+ id,
+ });
- await OrgService.update({
- db,
- orgId: id,
- updates: {
- svix_config: {
- sandbox_app_id: sandboxApp?.id,
- live_app_id: liveApp?.id,
- },
- test_pkey: generatePublishableKey(AppEnv.Sandbox),
- live_pkey: generatePublishableKey(AppEnv.Live),
- },
- });
+ await OrgService.update({
+ db,
+ orgId: id,
+ updates: {
+ svix_config: {
+ sandbox_app_id: sandboxApp?.id,
+ live_app_id: liveApp?.id,
+ },
+ test_pkey: generatePublishableKey(AppEnv.Sandbox),
+ live_pkey: generatePublishableKey(AppEnv.Live),
+ },
+ });
- logger.info(`Initialized resources for org ${id} (${slug})`);
- } catch (error: any) {
- if (error?.data && error.data.code == "23505") {
- logger.error(
- `Org ${id} already exists in Supabase -- skipping creationg`,
- );
- return;
- }
- logger.error(
- `Failed to insert org. Code: ${error.code}, message: ${error.message}`,
- );
- return;
- }
+ logger.info(`Initialized resources for org ${id} (${slug})`);
+ } catch (error: any) {
+ if (error?.data && error.data.code == "23505") {
+ logger.error(
+ `Org ${id} already exists in Supabase -- skipping creationg`,
+ );
+ return;
+ }
+ logger.error(
+ `Failed to insert org. Code: ${error.code}, message: ${error.message}`,
+ );
+ return;
+ }
};
diff --git a/server/src/utils/authUtils/beforeSessionCreated.ts b/server/src/utils/authUtils/beforeSessionCreated.ts
index ef6ed19e9..45730beea 100644
--- a/server/src/utils/authUtils/beforeSessionCreated.ts
+++ b/server/src/utils/authUtils/beforeSessionCreated.ts
@@ -5,33 +5,33 @@ import { eq, desc } from "drizzle-orm";
import { createDefaultOrg } from "@/utils/authUtils/createDefaultOrg.js";
export const beforeSessionCreated = async (session: Session) => {
- try {
- console.log(`Running beforeSessionCreated for user ${session.userId}`);
+ try {
+ console.log(`Running beforeSessionCreated for user ${session.userId}`);
- let membership = await db.query.member.findFirst({
- where: eq(member.userId, session.userId),
- });
+ let membership = await db.query.member.findFirst({
+ where: eq(member.userId, session.userId),
+ });
- if (membership) {
- console.log(
- "Returning session with active org ID:",
- membership.organizationId,
- );
- return {
- data: {
- ...session,
- activeOrganizationId: membership.organizationId,
- },
- };
- }
+ if (membership) {
+ console.log(
+ "Returning session with active org ID:",
+ membership.organizationId,
+ );
+ return {
+ data: {
+ ...session,
+ activeOrganizationId: membership.organizationId,
+ },
+ };
+ }
- const orgId = await createDefaultOrg({ session });
+ const orgId = await createDefaultOrg({ session });
- return {
- data: {
- ...session,
- activeOrganizationId: orgId,
- },
- };
- } catch (error) {}
+ return {
+ data: {
+ ...session,
+ activeOrganizationId: orgId,
+ },
+ };
+ } catch (error) {}
};
diff --git a/server/src/utils/authUtils/createDefaultOrg.ts b/server/src/utils/authUtils/createDefaultOrg.ts
index 8179a1344..fcad35102 100644
--- a/server/src/utils/authUtils/createDefaultOrg.ts
+++ b/server/src/utils/authUtils/createDefaultOrg.ts
@@ -7,67 +7,67 @@ import { slugify } from "@/utils/genUtils.js";
import { Organization } from "better-auth/plugins/organization";
export const createDefaultOrg = async ({
- session,
+ session,
}: {
- session: Session;
+ session: Session;
}): Promise => {
- try {
- const user = await db.query.user.findFirst({
- where: eq(userTable.id, session.userId),
- });
+ try {
+ const user = await db.query.user.findFirst({
+ where: eq(userTable.id, session.userId),
+ });
- const invites = await db
- .select()
- .from(invitation)
- .where(
- and(
- eq(invitation.email, user?.email || ""),
- eq(invitation.status, "pending"),
- gt(invitation.expiresAt, new Date()),
- ),
- );
+ const invites = await db
+ .select()
+ .from(invitation)
+ .where(
+ and(
+ eq(invitation.email, user?.email || ""),
+ eq(invitation.status, "pending"),
+ gt(invitation.expiresAt, new Date()),
+ ),
+ );
- if (invites.length > 0) {
- console.log(
- `Accepting invite for user ${session.userId} to org ${invites[0].organizationId}`,
- );
+ if (invites.length > 0) {
+ console.log(
+ `Accepting invite for user ${session.userId} to org ${invites[0].organizationId}`,
+ );
- await auth.api.addMember({
- body: {
- userId: session.userId,
- role: invites[0].role as any,
- organizationId: invites[0].organizationId,
- },
- });
+ await auth.api.addMember({
+ body: {
+ userId: session.userId,
+ role: invites[0].role as any,
+ organizationId: invites[0].organizationId,
+ },
+ });
- await db
- .update(invitation)
- .set({ status: "accepted" })
- .where(eq(invitation.id, invites[0].id));
+ await db
+ .update(invitation)
+ .set({ status: "accepted" })
+ .where(eq(invitation.id, invites[0].id));
- console.log(
- `Invite ${invites[0].id} accepted for user ${session.userId}`,
- );
+ console.log(
+ `Invite ${invites[0].id} accepted for user ${session.userId}`,
+ );
- return invites[0].organizationId as any;
- }
+ return invites[0].organizationId as any;
+ }
- let userName = user?.name;
- if (!userName) {
- userName = user?.email?.split("@")[0] || "org";
- }
+ let userName = user?.name;
+ if (!userName) {
+ userName = user?.email?.split("@")[0] || "org";
+ }
- const res = await auth.api.createOrganization({
- body: {
- name: `${userName}'s Org`,
- slug: `${slugify(userName)}_${Math.floor(10000000 + Math.random() * 90000000)}`,
- userId: session.userId,
- },
- });
+ const res = await auth.api.createOrganization({
+ body: {
+ name: `${userName}'s Org`,
+ slug: `${slugify(userName)}_${Math.floor(10000000 + Math.random() * 90000000)}`,
+ userId: session.userId,
+ },
+ });
- return res?.id as any;
- } catch (error) {
- console.error("Error creating org", error);
- return undefined;
- }
+ return res?.id as any;
+ } catch (error) {
+ console.error("Error creating org", error);
+ return undefined;
+ }
};
diff --git a/server/src/utils/checkUtils/checkCustomerCorrect.ts b/server/src/utils/checkUtils/checkCustomerCorrect.ts
index a1da3a8e9..b56588e5a 100644
--- a/server/src/utils/checkUtils/checkCustomerCorrect.ts
+++ b/server/src/utils/checkUtils/checkCustomerCorrect.ts
@@ -1,9 +1,9 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- AppEnv,
- CusProductStatus,
- FullCustomer,
- Organization,
+ AppEnv,
+ CusProductStatus,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -13,22 +13,22 @@ import { getUniqueUpcomingSchedulePairs } from "@/internal/customers/cusProducts
import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js";
import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js";
import {
- cusProductInPhase,
- logPhaseItems,
- similarUnix,
+ cusProductInPhase,
+ logPhaseItems,
+ similarUnix,
} from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { ACTIVE_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js";
import {
- cusProductToPrices,
- cusProductToEnts,
- cusProductToProduct,
+ cusProductToPrices,
+ cusProductToEnts,
+ cusProductToProduct,
} from "@autumn/shared";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import {
- getPriceEntitlement,
- getPriceOptions,
- formatPrice,
+ getPriceEntitlement,
+ getPriceOptions,
+ formatPrice,
} from "@/internal/products/prices/priceUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { defaultApiVersion } from "tests/constants.js";
@@ -36,509 +36,509 @@ import { formatUnixToDateTime, nullish } from "../genUtils.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import assert from "assert";
import {
- isFixedPrice,
- isOneOffPrice,
+ isFixedPrice,
+ isOneOffPrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
const compareActualItems = async ({
- actualItems,
- expectedItems,
- type,
- fullCus,
- db,
- phaseStartsAt,
+ actualItems,
+ expectedItems,
+ type,
+ fullCus,
+ db,
+ phaseStartsAt,
}: {
- actualItems: any[];
- expectedItems: any[];
- type: "sub" | "schedule";
- fullCus: FullCustomer;
- phaseStartsAt?: number;
- db: DrizzleCli;
+ actualItems: any[];
+ expectedItems: any[];
+ type: "sub" | "schedule";
+ fullCus: FullCustomer;
+ phaseStartsAt?: number;
+ db: DrizzleCli;
}) => {
- for (const expectedItem of expectedItems) {
- let actualItem = actualItems.find((item: any) => {
- if (item.price === (expectedItem as any).price) return true;
+ for (const expectedItem of expectedItems) {
+ let actualItem = actualItems.find((item: any) => {
+ if (item.price === (expectedItem as any).price) return true;
- // If only one item, allow matching by stripe prod id
+ // If only one item, allow matching by stripe prod id
- // If prices match, allow item.stripeProdId to match...
+ // If prices match, allow item.stripeProdId to match...
- // if (item.stripeProdId == (expectedItem as any).stripeProdId) return true;
+ // if (item.stripeProdId == (expectedItem as any).stripeProdId) return true;
- return false;
- });
+ return false;
+ });
- if (isFixedPrice({ price: expectedItem.autumnPrice }) && !actualItem) {
- actualItem = actualItems.find((item: any) => {
- return item.stripeProdId === expectedItem.stripeProdId;
- });
- }
+ if (isFixedPrice({ price: expectedItem.autumnPrice }) && !actualItem) {
+ actualItem = actualItems.find((item: any) => {
+ return item.stripeProdId === expectedItem.stripeProdId;
+ });
+ }
- if (!actualItem) {
- // Search for price by stripe id
- const price = await PriceService.getByStripeId({
- db,
- stripePriceId: expectedItem.price,
- });
+ if (!actualItem) {
+ // Search for price by stripe id
+ const price = await PriceService.getByStripeId({
+ db,
+ stripePriceId: expectedItem.price,
+ });
- const { autumnPrice, ...rest } = expectedItem;
- console.log(`(${type}) Missing item:`, rest);
- // if (price) {
- // console.log(`Autumn price:`, `${price.id} - ${formatPrice({ price })}`);
- // }
+ const { autumnPrice, ...rest } = expectedItem;
+ console.log(`(${type}) Missing item:`, rest);
+ // if (price) {
+ // console.log(`Autumn price:`, `${price.id} - ${formatPrice({ price })}`);
+ // }
- // Actual items
- console.log(`(${type}) Actual items (${actualItems.length}):`);
- await logPhaseItems({
- db,
- items: actualItems,
- });
+ // Actual items
+ console.log(`(${type}) Actual items (${actualItems.length}):`);
+ await logPhaseItems({
+ db,
+ items: actualItems,
+ });
- console.log(`(${type}) Expected items (${expectedItems.length}):`);
- await logPhaseItems({
- db,
- items: expectedItems,
- });
- }
+ console.log(`(${type}) Expected items (${expectedItems.length}):`);
+ await logPhaseItems({
+ db,
+ items: expectedItems,
+ });
+ }
- assert(!!actualItem, `actual item should exist`);
+ assert(!!actualItem, `actual item should exist`);
- // team manager...
- if (actualItem.price !== "price_1RGod7JvAPTxxzlIEbN6ZnW1") {
- if (actualItem?.quantity !== (expectedItem as any).quantity) {
- if (phaseStartsAt) {
- console.log(
- `Phase starts at: ${formatUnixToDateTime(phaseStartsAt)}`
- );
- }
+ // team manager...
+ if (actualItem.price !== "price_1RGod7JvAPTxxzlIEbN6ZnW1") {
+ if (actualItem?.quantity !== (expectedItem as any).quantity) {
+ if (phaseStartsAt) {
+ console.log(
+ `Phase starts at: ${formatUnixToDateTime(phaseStartsAt)}`,
+ );
+ }
- console.log("Actual items:");
- await logPhaseItems({
- db,
- items: actualItems,
- });
+ console.log("Actual items:");
+ await logPhaseItems({
+ db,
+ items: actualItems,
+ });
- console.log("Expected items:");
- await logPhaseItems({
- db,
- items: expectedItems,
- });
+ console.log("Expected items:");
+ await logPhaseItems({
+ db,
+ items: expectedItems,
+ });
- console.log(
- `Item quantity mismatch: ${actualItem?.quantity} !== ${expectedItem.quantity}`
- );
+ console.log(
+ `Item quantity mismatch: ${actualItem?.quantity} !== ${expectedItem.quantity}`,
+ );
- const price = await PriceService.getByStripeId({
- db,
- stripePriceId: expectedItem.price,
- });
- if (price) {
- console.log(
- `Autumn price:`,
- `${price?.product.name} - ${formatPrice({ price })}`
- );
- }
+ const price = await PriceService.getByStripeId({
+ db,
+ stripePriceId: expectedItem.price,
+ });
+ if (price) {
+ console.log(
+ `Autumn price:`,
+ `${price?.product.name} - ${formatPrice({ price })}`,
+ );
+ }
- console.log("--------------------------------");
- }
+ console.log("--------------------------------");
+ }
- assert(
- actualItem?.quantity === (expectedItem as any).quantity,
- `actual items quantity should be equals to ${expectedItem.quantity}`
- );
- }
- }
+ assert(
+ actualItem?.quantity === (expectedItem as any).quantity,
+ `actual items quantity should be equals to ${expectedItem.quantity}`,
+ );
+ }
+ }
- if (actualItems.length !== expectedItems.length) {
- console.log("Actual items:");
- await logPhaseItems({
- db,
- items: actualItems,
- });
+ if (actualItems.length !== expectedItems.length) {
+ console.log("Actual items:");
+ await logPhaseItems({
+ db,
+ items: actualItems,
+ });
- console.log("Expected items:");
- await logPhaseItems({
- db,
- items: expectedItems,
- });
- }
+ console.log("Expected items:");
+ await logPhaseItems({
+ db,
+ items: expectedItems,
+ });
+ }
- assert(
- actualItems.length === expectedItems.length,
- `actual items length should be equals to expected items length`
- );
+ assert(
+ actualItems.length === expectedItems.length,
+ `actual items length should be equals to expected items length`,
+ );
};
// If all cus products are free, then should have no sub
const checkAllFreeProducts = async ({
- db,
- fullCus,
- subs,
+ db,
+ fullCus,
+ subs,
}: {
- db: DrizzleCli;
- fullCus: FullCustomer;
- subs: Stripe.Subscription[];
+ db: DrizzleCli;
+ fullCus: FullCustomer;
+ subs: Stripe.Subscription[];
}) => {
- const cusProducts = fullCus.customer_products;
- const allFreeOrOneOff = cusProducts.every((cp) => {
- const product = cusProductToProduct({ cusProduct: cp });
- return isFreeProduct(product.prices) || isOneOff(product.prices);
- });
+ const cusProducts = fullCus.customer_products;
+ const allFreeOrOneOff = cusProducts.every((cp) => {
+ const product = cusProductToProduct({ cusProduct: cp });
+ return isFreeProduct(product.prices) || isOneOff(product.prices);
+ });
- if (allFreeOrOneOff) {
- // Make sure no subs exist for this customer
- const sub = subs.find(
- (sub) =>
- sub.customer === fullCus.processor?.id &&
- (sub.status == "active" || sub.status == "past_due")
- );
+ if (allFreeOrOneOff) {
+ // Make sure no subs exist for this customer
+ const sub = subs.find(
+ (sub) =>
+ sub.customer === fullCus.processor?.id &&
+ (sub.status == "active" || sub.status == "past_due"),
+ );
- if (fullCus.org_id == "6bWdIqEuRHBrReXbTb30l9beMFVZ3Ts3") return true;
+ if (fullCus.org_id == "6bWdIqEuRHBrReXbTb30l9beMFVZ3Ts3") return true;
- assert(
- !sub,
- `no sub should exist for this customer (${fullCus.email}, ${fullCus.id})`
- );
- return true;
- }
+ assert(
+ !sub,
+ `no sub should exist for this customer (${fullCus.email}, ${fullCus.id})`,
+ );
+ return true;
+ }
- return false;
+ return false;
};
export const checkCusSubCorrect = async ({
- db,
- fullCus,
- subs,
- schedules,
- org,
- env,
+ db,
+ fullCus,
+ subs,
+ schedules,
+ org,
+ env,
}: {
- db: DrizzleCli;
- fullCus: FullCustomer;
- subs: Stripe.Subscription[];
- schedules: Stripe.SubscriptionSchedule[];
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ fullCus: FullCustomer;
+ subs: Stripe.Subscription[];
+ schedules: Stripe.SubscriptionSchedule[];
+ org: Organization;
+ env: AppEnv;
}) => {
- const allFree = await checkAllFreeProducts({
- db,
- fullCus,
- subs,
- });
- if (allFree) return;
+ const allFree = await checkAllFreeProducts({
+ db,
+ fullCus,
+ subs,
+ });
+ if (allFree) return;
- // 1. Only 1 sub ID available
- let cusProducts = fullCus.customer_products;
- const subIds = cusProductToSubIds({ cusProducts });
+ // 1. Only 1 sub ID available
+ let cusProducts = fullCus.customer_products;
+ const subIds = cusProductToSubIds({ cusProducts });
- const subId = subIds[0];
+ const subId = subIds[0];
- assert(
- subIds.length === 1,
- `should only have 1 sub ID available, instead got ${subIds.join(", ")}`
- );
+ assert(
+ subIds.length === 1,
+ `should only have 1 sub ID available, instead got ${subIds.join(", ")}`,
+ );
- cusProducts = cusProducts.filter((cp) =>
- cp.subscription_ids?.includes(subId!)
- );
+ cusProducts = cusProducts.filter((cp) =>
+ cp.subscription_ids?.includes(subId!),
+ );
- // Get the items that should be in the sub
- const supposedSubItems = [];
+ // Get the items that should be in the sub
+ const supposedSubItems = [];
- const scheduleUnixes = getUniqueUpcomingSchedulePairs({
- cusProducts,
- now: Date.now(),
- });
+ const scheduleUnixes = getUniqueUpcomingSchedulePairs({
+ cusProducts,
+ now: Date.now(),
+ });
- const supposedPhases: any[] = scheduleUnixes.map((unix) => {
- return {
- start_date: unix, // milliseconds
- items: [],
- };
- });
+ const supposedPhases: any[] = scheduleUnixes.map((unix) => {
+ return {
+ start_date: unix, // milliseconds
+ items: [],
+ };
+ });
- // console.log(`\n\nChecking sub correct`);
- let printCusProduct = false;
- if (printCusProduct) {
- console.log(`\n\nChecking sub correct`);
- }
+ // console.log(`\n\nChecking sub correct`);
+ let printCusProduct = false;
+ if (printCusProduct) {
+ console.log(`\n\nChecking sub correct`);
+ }
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const ents = cusProductToEnts({ cusProduct });
- const product = cusProductToProduct({ cusProduct });
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const ents = cusProductToEnts({ cusProduct });
+ const product = cusProductToProduct({ cusProduct });
- // Add to schedules
- const scheduleIndexes: number[] = [];
- const apiVersion = cusProduct.api_version || defaultApiVersion;
+ // Add to schedules
+ const scheduleIndexes: number[] = [];
+ const apiVersion = cusProduct.api_version || defaultApiVersion;
- if (isFreeProduct(product.prices)) {
- assert(
- cusProduct.subscription_ids?.length === 0,
- "free product should have no subs"
- );
- continue;
- }
+ if (isFreeProduct(product.prices)) {
+ assert(
+ cusProduct.subscription_ids?.length === 0,
+ "free product should have no subs",
+ );
+ continue;
+ }
- if (printCusProduct) {
- console.log(
- `Cus product: ${cusProduct.product.name}, Status: ${cusProduct.status}, Entity ID: ${cusProduct.entity_id}`
- );
- console.log(`Starts at: ${formatUnixToDateTime(cusProduct.starts_at)}`);
- }
+ if (printCusProduct) {
+ console.log(
+ `Cus product: ${cusProduct.product.name}, Status: ${cusProduct.status}, Entity ID: ${cusProduct.entity_id}`,
+ );
+ console.log(`Starts at: ${formatUnixToDateTime(cusProduct.starts_at)}`);
+ }
- scheduleUnixes.forEach((unix, index) => {
- if (
- cusProduct.status === CusProductStatus.Scheduled &&
- cusProductInPhase({ phaseStartMillis: unix, cusProduct })
- ) {
- return scheduleIndexes.push(index);
- }
+ scheduleUnixes.forEach((unix, index) => {
+ if (
+ cusProduct.status === CusProductStatus.Scheduled &&
+ cusProductInPhase({ phaseStartMillis: unix, cusProduct })
+ ) {
+ return scheduleIndexes.push(index);
+ }
- if (cusProduct.status === CusProductStatus.Scheduled) return;
+ if (cusProduct.status === CusProductStatus.Scheduled) return;
- if (cusProduct.product.is_add_on) {
- // 1. If it's canceled
- if (cusProduct.canceled && (cusProduct.ended_at || 0) > unix) {
- return scheduleIndexes.push(index);
- } else if (!cusProduct.canceled) {
- return scheduleIndexes.push(index);
- }
+ if (cusProduct.product.is_add_on) {
+ // 1. If it's canceled
+ if (cusProduct.canceled && (cusProduct.ended_at || 0) > unix) {
+ return scheduleIndexes.push(index);
+ } else if (!cusProduct.canceled) {
+ return scheduleIndexes.push(index);
+ }
- return;
- }
+ return;
+ }
- // 2. If main product, check that schedule is AFTER this phase
- const curScheduledProduct = cusProducts.find(
- (cp) =>
- cp.product.group === product.group &&
- cp.status === CusProductStatus.Scheduled &&
- (cp.internal_entity_id
- ? cp.internal_entity_id == cusProduct.internal_entity_id
- : nullish(cp.internal_entity_id))
- );
+ // 2. If main product, check that schedule is AFTER this phase
+ const curScheduledProduct = cusProducts.find(
+ (cp) =>
+ cp.product.group === product.group &&
+ cp.status === CusProductStatus.Scheduled &&
+ (cp.internal_entity_id
+ ? cp.internal_entity_id == cusProduct.internal_entity_id
+ : nullish(cp.internal_entity_id)),
+ );
- if (!curScheduledProduct) return scheduleIndexes.push(index);
+ if (!curScheduledProduct) return scheduleIndexes.push(index);
- // If scheduled product NOT in phase, add main product to schedule
- if (
- !cusProductInPhase({
- phaseStartMillis: unix,
- cusProduct: curScheduledProduct,
- })
- ) {
- scheduleIndexes.push(index);
- }
- });
+ // If scheduled product NOT in phase, add main product to schedule
+ if (
+ !cusProductInPhase({
+ phaseStartMillis: unix,
+ cusProduct: curScheduledProduct,
+ })
+ ) {
+ scheduleIndexes.push(index);
+ }
+ });
- if (printCusProduct) {
- console.log(`Schedule indexes:`, scheduleIndexes);
- console.log("--------------------------------");
- }
+ if (printCusProduct) {
+ console.log(`Schedule indexes:`, scheduleIndexes);
+ console.log("--------------------------------");
+ }
- // const hasScheduledProduct =
- cusProduct.status !== CusProductStatus.Scheduled &&
- !cusProduct.product.is_add_on &&
- cusProducts.some(
- (cp) =>
- cp.product.group === product.group &&
- ACTIVE_STATUSES.includes(cp.status)
- );
+ // const hasScheduledProduct =
+ cusProduct.status !== CusProductStatus.Scheduled &&
+ !cusProduct.product.is_add_on &&
+ cusProducts.some(
+ (cp) =>
+ cp.product.group === product.group &&
+ ACTIVE_STATUSES.includes(cp.status),
+ );
- const addToSub = cusProduct.status !== CusProductStatus.Scheduled;
+ const addToSub = cusProduct.status !== CusProductStatus.Scheduled;
- for (const price of prices) {
- if (isOneOffPrice({ price })) continue;
+ for (const price of prices) {
+ if (isOneOffPrice({ price })) continue;
- const relatedEnt = getPriceEntitlement(price, ents);
- const options = getPriceOptions(price, cusProduct.options);
- let existingUsage = getExistingUsageFromCusProducts({
- entitlement: relatedEnt,
- cusProducts,
- entities: fullCus.entities,
- carryExistingUsages: true,
- internalEntityId: cusProduct.internal_entity_id || undefined,
- });
+ const relatedEnt = getPriceEntitlement(price, ents);
+ const options = getPriceOptions(price, cusProduct.options);
+ let existingUsage = getExistingUsageFromCusProducts({
+ entitlement: relatedEnt,
+ cusProducts,
+ entities: fullCus.entities,
+ carryExistingUsages: true,
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ });
- const res = priceToStripeItem({
- price,
- relatedEnt,
- product,
- org,
- options,
- existingUsage,
- withEntity: !!cusProduct.internal_entity_id,
- isCheckout: false,
- apiVersion,
- productOptions: cusProduct.quantity
- ? {
- product_id: product.id,
- quantity: Number(cusProduct.quantity || 1),
- }
- : undefined,
- });
+ const res = priceToStripeItem({
+ price,
+ relatedEnt,
+ product,
+ org,
+ options,
+ existingUsage,
+ withEntity: !!cusProduct.internal_entity_id,
+ isCheckout: false,
+ apiVersion,
+ productOptions: cusProduct.quantity
+ ? {
+ product_id: product.id,
+ quantity: Number(cusProduct.quantity || 1),
+ }
+ : undefined,
+ });
- if (res?.lineItem && nullish(res.lineItem.quantity)) {
- res.lineItem.quantity = 0;
- }
+ if (res?.lineItem && nullish(res.lineItem.quantity)) {
+ res.lineItem.quantity = 0;
+ }
- // console.log("API VERSION:", apiVersion);
- // console.log("LINE ITEM:", res?.lineItem);
- if (options?.upcoming_quantity && res?.lineItem) {
- res.lineItem.quantity = options.upcoming_quantity;
- }
+ // console.log("API VERSION:", apiVersion);
+ // console.log("LINE ITEM:", res?.lineItem);
+ if (options?.upcoming_quantity && res?.lineItem) {
+ res.lineItem.quantity = options.upcoming_quantity;
+ }
- const lineItem: any = res?.lineItem;
- if (lineItem && res?.lineItem) {
- lineItem.quantity = Math.max(lineItem.quantity, 0);
- if (addToSub) {
- const existingIndex = supposedSubItems.findIndex(
- (si: any) => si.price === lineItem.price
- );
+ const lineItem: any = res?.lineItem;
+ if (lineItem && res?.lineItem) {
+ lineItem.quantity = Math.max(lineItem.quantity, 0);
+ if (addToSub) {
+ const existingIndex = supposedSubItems.findIndex(
+ (si: any) => si.price === lineItem.price,
+ );
- if (existingIndex !== -1) {
- // @ts-ignore
- supposedSubItems[existingIndex].quantity += lineItem.quantity;
- } else {
- supposedSubItems.push({
- ...res.lineItem,
- priceStr: `${product.id}-${formatPrice({ price })}`,
- stripeProdId: product.processor?.id,
- autumnPrice: price,
- });
- }
- }
+ if (existingIndex !== -1) {
+ // @ts-ignore
+ supposedSubItems[existingIndex].quantity += lineItem.quantity;
+ } else {
+ supposedSubItems.push({
+ ...res.lineItem,
+ priceStr: `${product.id}-${formatPrice({ price })}`,
+ stripeProdId: product.processor?.id,
+ autumnPrice: price,
+ });
+ }
+ }
- for (const scheduleIndex of scheduleIndexes) {
- const phase = supposedPhases[scheduleIndex];
- const existingIndex = phase.items.findIndex(
- (item: any) => item.price === lineItem.price
- );
+ for (const scheduleIndex of scheduleIndexes) {
+ const phase = supposedPhases[scheduleIndex];
+ const existingIndex = phase.items.findIndex(
+ (item: any) => item.price === lineItem.price,
+ );
- if (existingIndex !== -1) {
- phase.items[existingIndex].quantity += lineItem.quantity!;
- } else {
- phase.items.push({
- price: lineItem.price,
- quantity: lineItem.quantity!,
- });
- }
- }
- }
- }
- }
+ if (existingIndex !== -1) {
+ phase.items[existingIndex].quantity += lineItem.quantity!;
+ } else {
+ phase.items.push({
+ price: lineItem.price,
+ quantity: lineItem.quantity!,
+ });
+ }
+ }
+ }
+ }
+ }
- const sub = subs.find((sub) => sub.id === subId);
- assert(!!sub, `Sub ${subId} should exist`);
+ const sub = subs.find((sub) => sub.id === subId);
+ assert(!!sub, `Sub ${subId} should exist`);
- const actualItems = sub!.items.data.map((item: any) => ({
- price: item.price.id,
- quantity: item.quantity || 0,
- stripeProdId: item.price.product,
- }));
+ const actualItems = sub!.items.data.map((item: any) => ({
+ price: item.price.id,
+ quantity: item.quantity || 0,
+ stripeProdId: item.price.product,
+ }));
- // console.log("Actual items:");
- // await logPhaseItems({
- // db,
- // items: actualItems,
- // });
- // console.log("Expected items:");
- // await logPhaseItems({
- // db,
- // items: actualItems,
- // });
+ // console.log("Actual items:");
+ // await logPhaseItems({
+ // db,
+ // items: actualItems,
+ // });
+ // console.log("Expected items:");
+ // await logPhaseItems({
+ // db,
+ // items: actualItems,
+ // });
- await compareActualItems({
- actualItems,
- expectedItems: supposedSubItems,
- type: "sub",
- fullCus,
- db,
- });
+ await compareActualItems({
+ actualItems,
+ expectedItems: supposedSubItems,
+ type: "sub",
+ fullCus,
+ db,
+ });
- // Should be canceled
- const cusSubShouldBeCanceled = cusProducts.every((cp) => {
- if (cp.subscription_ids?.includes(subId!)) {
- // 1. Get scheduled product
- const { curScheduledProduct } = getExistingCusProducts({
- cusProducts,
- product: cp.product,
- internalEntityId: cp.internal_entity_id,
- });
+ // Should be canceled
+ const cusSubShouldBeCanceled = cusProducts.every((cp) => {
+ if (cp.subscription_ids?.includes(subId!)) {
+ // 1. Get scheduled product
+ const { curScheduledProduct } = getExistingCusProducts({
+ cusProducts,
+ product: cp.product,
+ internalEntityId: cp.internal_entity_id,
+ });
- if (curScheduledProduct) {
- const scheduledProduct = cusProductToProduct({
- cusProduct: curScheduledProduct,
- });
- if (!isFreeProduct(scheduledProduct.prices)) {
- return false;
- }
- }
+ if (curScheduledProduct) {
+ const scheduledProduct = cusProductToProduct({
+ cusProduct: curScheduledProduct,
+ });
+ if (!isFreeProduct(scheduledProduct.prices)) {
+ return false;
+ }
+ }
- return cp.canceled;
- }
+ return cp.canceled;
+ }
- return true;
- });
+ return true;
+ });
- const finalShouldBeCanceled = cusSubShouldBeCanceled;
+ const finalShouldBeCanceled = cusSubShouldBeCanceled;
- if (finalShouldBeCanceled) {
- assert(!sub!.schedule, `sub ${subId} should NOT have a schedule`);
- assert(subIsCanceled({ sub: sub! }), `sub ${subId} should be canceled`);
- return;
- }
+ if (finalShouldBeCanceled) {
+ assert(!sub!.schedule, `sub ${subId} should NOT have a schedule`);
+ assert(subIsCanceled({ sub: sub! }), `sub ${subId} should be canceled`);
+ return;
+ }
- const schedule =
- supposedPhases.length > 0
- ? schedules.find((s) => s.id === sub!.schedule)
- : null;
+ const schedule =
+ supposedPhases.length > 0
+ ? schedules.find((s) => s.id === sub!.schedule)
+ : null;
- // console.log("--------------------------------");
- // console.log("Supposed phases:");
- // await logPhases({
- // phases: supposedPhases,
- // db,
- // });
+ // console.log("--------------------------------");
+ // console.log("Supposed phases:");
+ // await logPhases({
+ // phases: supposedPhases,
+ // db,
+ // });
- // console.log("--------------------------------");
- // console.log("Actual phases:");
+ // console.log("--------------------------------");
+ // console.log("Actual phases:");
- // await logPhases({
- // phases: (schedule?.phases as any) || [],
- // db,
- // });
+ // await logPhases({
+ // phases: (schedule?.phases as any) || [],
+ // db,
+ // });
- for (let i = 0; i < supposedPhases.length; i++) {
- const supposedPhase = supposedPhases[i];
+ for (let i = 0; i < supposedPhases.length; i++) {
+ const supposedPhase = supposedPhases[i];
- if (supposedPhase.items.length === 0) continue;
+ if (supposedPhase.items.length === 0) continue;
- const actualPhase = schedule?.phases?.[i + 1];
- expect(schedule?.phases.length).toBeGreaterThan(i + 1);
+ const actualPhase = schedule?.phases?.[i + 1];
+ expect(schedule?.phases.length).toBeGreaterThan(i + 1);
- expect(
- similarUnix({
- unix1: supposedPhase.start_date,
- unix2: actualPhase!.start_date * 1000,
- })
- ).toBe(true);
+ expect(
+ similarUnix({
+ unix1: supposedPhase.start_date,
+ unix2: actualPhase!.start_date * 1000,
+ }),
+ ).toBe(true);
- const actualItems =
- actualPhase?.items.map((item) => ({
- price: (item.price as Stripe.Price).id,
- quantity: item.quantity,
- })) || [];
+ const actualItems =
+ actualPhase?.items.map((item) => ({
+ price: (item.price as Stripe.Price).id,
+ quantity: item.quantity,
+ })) || [];
- await compareActualItems({
- actualItems,
- expectedItems: supposedPhase.items,
- type: "schedule",
- fullCus,
- db,
- phaseStartsAt: supposedPhase.start_date,
- });
- }
+ await compareActualItems({
+ actualItems,
+ expectedItems: supposedPhase.items,
+ type: "schedule",
+ fullCus,
+ db,
+ phaseStartsAt: supposedPhase.start_date,
+ });
+ }
- assert(!sub!.cancel_at, `sub ${subId} should not be canceled`);
+ assert(!sub!.cancel_at, `sub ${subId} should not be canceled`);
};
diff --git a/server/src/utils/colorUtils.ts b/server/src/utils/colorUtils.ts
index 2b19eeb5e..2151d144c 100644
--- a/server/src/utils/colorUtils.ts
+++ b/server/src/utils/colorUtils.ts
@@ -1,5 +1,5 @@
import chalk from "chalk";
export const colorize = (text: string, chalkColor: any) => {
- return `${chalkColor}${text}${chalk.reset}`;
+ return `${chalkColor}${text}${chalk.reset}`;
};
diff --git a/server/src/utils/constants.ts b/server/src/utils/constants.ts
index 493aebd25..1ff42dc33 100644
--- a/server/src/utils/constants.ts
+++ b/server/src/utils/constants.ts
@@ -4,22 +4,22 @@ import { CusProductStatus } from "@autumn/shared";
export const BREAK_API_VERSION = 0.2;
export const getActiveCusProductStatuses = () => [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
];
export const ADMIN_USER_IDs =
- process.env.ENV == "production" || process.env.NODE_ENV == "production"
- ? [
- "user_2tMgAiPsQzX8JTHjZZh9m0VdvUv", // a
- "user_2sB3tBXsnVVLlTKliQIqvvM2xfB", // j
- "ZsDswIXyOGMP9y1V1At4dAZNaiggClSs", // t
- ]
- : ["user_2rypooIKyMQx81vMS8FFGx24UHU"];
+ process.env.ENV == "production" || process.env.NODE_ENV == "production"
+ ? [
+ "user_2tMgAiPsQzX8JTHjZZh9m0VdvUv", // a
+ "user_2sB3tBXsnVVLlTKliQIqvvM2xfB", // j
+ "ZsDswIXyOGMP9y1V1At4dAZNaiggClSs", // t
+ ]
+ : ["user_2rypooIKyMQx81vMS8FFGx24UHU"];
export const dashboardOrigins = [
- "http://localhost:3000",
- "https://app.useautumn.com",
- "https://staging.useautumn.com",
- process.env.CLIENT_URL!,
+ "http://localhost:3000",
+ "https://app.useautumn.com",
+ "https://staging.useautumn.com",
+ process.env.CLIENT_URL!,
];
diff --git a/server/src/utils/encryptUtils.ts b/server/src/utils/encryptUtils.ts
index 72cf0f06f..f0a6f744b 100644
--- a/server/src/utils/encryptUtils.ts
+++ b/server/src/utils/encryptUtils.ts
@@ -3,53 +3,53 @@ import crypto from "crypto";
import KSUID from "ksuid";
const getKey = () => {
- return crypto
- .createHash("sha512")
- .update(process.env.ENCRYPTION_PASSWORD!)
- .digest("hex")
- .substring(0, 32);
+ return crypto
+ .createHash("sha512")
+ .update(process.env.ENCRYPTION_PASSWORD!)
+ .digest("hex")
+ .substring(0, 32);
};
export function encryptData(data: string) {
- const key = getKey();
- const iv = crypto.randomBytes(16);
- const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
+ const key = getKey();
+ const iv = crypto.randomBytes(16);
+ const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
- const encrypted = Buffer.concat([
- cipher.update(data, "utf8"),
- cipher.final(),
- ]);
+ const encrypted = Buffer.concat([
+ cipher.update(data, "utf8"),
+ cipher.final(),
+ ]);
- // Combine IV and encrypted data
- const result = Buffer.concat([iv, encrypted]);
- return result.toString("base64");
+ // Combine IV and encrypted data
+ const result = Buffer.concat([iv, encrypted]);
+ return result.toString("base64");
}
export function decryptData(encryptedData: string) {
- const buffer = Buffer.from(encryptedData, "base64");
+ const buffer = Buffer.from(encryptedData, "base64");
- // Extract IV and encrypted data
- const iv = buffer.slice(0, 16);
- const encrypted = buffer.slice(16);
+ // Extract IV and encrypted data
+ const iv = buffer.slice(0, 16);
+ const encrypted = buffer.slice(16);
- const key = getKey();
- const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
+ const key = getKey();
+ const decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
- const decrypted = Buffer.concat([
- decipher.update(encrypted),
- decipher.final(),
- ]);
+ const decrypted = Buffer.concat([
+ decipher.update(encrypted),
+ decipher.final(),
+ ]);
- return decrypted.toString("utf8");
+ return decrypted.toString("utf8");
}
export const generatePublishableKey = (env: string) => {
- // let rand = crypto
- // .randomBytes(75) // 75 bytes will give ~100 characters in base64
- // .toString("base64")
- // .replace(/[+/=]/g, "") // Remove non-URL safe characters
- // .slice(0, 100); // Ensure exactly 100 characters
+ // let rand = crypto
+ // .randomBytes(75) // 75 bytes will give ~100 characters in base64
+ // .toString("base64")
+ // .replace(/[+/=]/g, "") // Remove non-URL safe characters
+ // .slice(0, 100); // Ensure exactly 100 characters
- let envString = env === AppEnv.Sandbox ? "test_" : "live_";
- return `am_pk_${envString}${KSUID.randomSync().string}`;
+ let envString = env === AppEnv.Sandbox ? "test_" : "live_";
+ return `am_pk_${envString}${KSUID.randomSync().string}`;
};
diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts
index 461f146f2..11deb9864 100644
--- a/server/src/utils/errorUtils.ts
+++ b/server/src/utils/errorUtils.ts
@@ -5,218 +5,218 @@ import Stripe from "stripe";
import { ZodError } from "zod";
export const isPaymentDeclined = (error: any) => {
- return (
- error instanceof RecaseError && error.code === ErrCode.StripeCardDeclined
- );
+ return (
+ error instanceof RecaseError && error.code === ErrCode.StripeCardDeclined
+ );
};
export default class RecaseError extends Error {
- code: string;
- data: any;
- statusCode: number;
+ code: string;
+ data: any;
+ statusCode: number;
- constructor({
- message,
- code,
- data,
- statusCode = 400,
- }: {
- message: string;
- code: string;
- data?: any;
- statusCode?: number;
- }) {
- super(message);
- this.name = "RecaseError";
- this.code = code;
- this.data = data;
- this.statusCode = statusCode;
- }
+ constructor({
+ message,
+ code,
+ data,
+ statusCode = 400,
+ }: {
+ message: string;
+ code: string;
+ data?: any;
+ statusCode?: number;
+ }) {
+ super(message);
+ this.name = "RecaseError";
+ this.code = code;
+ this.data = data;
+ this.statusCode = statusCode;
+ }
- print(logger: any) {
- logger.warn(`Code: ${chalk.yellow(this.code)}`);
- logger.warn(`Message: ${chalk.yellow(this.message)}`);
+ print(logger: any) {
+ logger.warn(`Code: ${chalk.yellow(this.code)}`);
+ logger.warn(`Message: ${chalk.yellow(this.message)}`);
- if (this.data) {
- logger.warn(`Data:`);
- logger.warn(this.data);
- } else {
- logger.warn("No data");
- }
- }
+ if (this.data) {
+ logger.warn(`Data:`);
+ logger.warn(this.data);
+ } else {
+ logger.warn("No data");
+ }
+ }
}
export function formatZodError(error: ZodError): string {
- return error.errors
- .map((err) =>
- err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message
- )
- .join(", ");
+ return error.errors
+ .map((err) =>
+ err.path.length ? `${err.path.join(".")}: ${err.message}` : err.message,
+ )
+ .join(", ");
}
const getJsonBody = (body: any) => {
- if (Buffer.isBuffer(body)) {
- try {
- return JSON.parse(body.toString());
- } catch (e) {
- return `[Invalid JSON] Raw body: ${body.toString()}`;
- }
- }
- return body;
+ if (Buffer.isBuffer(body)) {
+ try {
+ return JSON.parse(body.toString());
+ } catch (e) {
+ return `[Invalid JSON] Raw body: ${body.toString()}`;
+ }
+ }
+ return body;
};
const logRequestBody = (logger: any, req: any, level: "warn" | "error") => {
- if (
- req.body &&
- typeof req.body === "object" &&
- Object.keys(req.body).length > 0
- ) {
- logger[level]("Request body:");
- logger[level](getJsonBody(req.body));
- }
+ if (
+ req.body &&
+ typeof req.body === "object" &&
+ Object.keys(req.body).length > 0
+ ) {
+ logger[level]("Request body:");
+ logger[level](getJsonBody(req.body));
+ }
};
const logReqUrl = (logger: any, req: any, level: "warn" | "error") => {
- if (req.originalUrl.includes("/webhooks/stripe")) {
- logger[level](`Stripe webhook: ${req.originalUrl}`);
- let body = req.body;
- try {
- body = Buffer.isBuffer(req.body)
- ? JSON.parse(req.body.toString())
- : req.body;
+ if (req.originalUrl.includes("/webhooks/stripe")) {
+ logger[level](`Stripe webhook: ${req.originalUrl}`);
+ let body = req.body;
+ try {
+ body = Buffer.isBuffer(req.body)
+ ? JSON.parse(req.body.toString())
+ : req.body;
- logger[level](`Event type: ${body.type}, ID: ${body.id}`);
- } catch (error) {
- logger[level](`Invalid JSON body`);
- }
- } else {
- logger[level](`${req.method} ${req.originalUrl}`);
- }
+ logger[level](`Event type: ${body.type}, ID: ${body.id}`);
+ } catch (error) {
+ logger[level](`Invalid JSON body`);
+ }
+ } else {
+ logger[level](`${req.method} ${req.originalUrl}`);
+ }
};
export const handleRequestError = ({
- error,
- req,
- res,
- action,
+ error,
+ req,
+ res,
+ action,
}: {
- error: any;
- req: any;
- res: any;
- action: string;
+ error: any;
+ req: any;
+ res: any;
+ action: string;
}) => {
- try {
- const logger = req.logtail;
- if (error instanceof RecaseError) {
- logger.warn(
- `RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`,
- {
- error: error.data ?? error,
- }
- );
+ try {
+ const logger = req.logtail;
+ if (error instanceof RecaseError) {
+ logger.warn(
+ `RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`,
+ {
+ error: error.data ?? error,
+ },
+ );
- res.status(error.statusCode).json({
- message: error.message,
- code: error.code,
- env: req.env,
- });
- return;
- }
+ res.status(error.statusCode).json({
+ message: error.message,
+ code: error.code,
+ env: req.env,
+ });
+ return;
+ }
- if (error instanceof Stripe.errors.StripeError) {
- let curStack;
- try {
- throw new Error("test");
- } catch (e: any) {
- curStack = e.stack;
- }
+ if (error instanceof Stripe.errors.StripeError) {
+ let curStack;
+ try {
+ throw new Error("test");
+ } catch (e: any) {
+ curStack = e.stack;
+ }
- const { raw, headers, ...rest } = error;
- logger.error(
- `STRIPE ERROR (${req.org?.slug || "unknown"}): ${error.message}`,
- {
- error: {
- ...rest,
- stack: curStack,
- },
- }
- );
+ const { raw, headers, ...rest } = error;
+ logger.error(
+ `STRIPE ERROR (${req.org?.slug || "unknown"}): ${error.message}`,
+ {
+ error: {
+ ...rest,
+ stack: curStack,
+ },
+ },
+ );
- res.status(400).json({
- message: `(Stripe Error) ${error.message}`,
- code: `stripe_error`,
- });
- } else if (error instanceof ZodError) {
- logger.error(
- `ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`
- );
+ res.status(400).json({
+ message: `(Stripe Error) ${error.message}`,
+ code: `stripe_error`,
+ });
+ } else if (error instanceof ZodError) {
+ logger.error(
+ `ZOD ERROR (${req.org?.slug || "unknown"}): ${formatZodError(error)}`,
+ );
- res.status(400).json({
- message: formatZodError(error),
- code: ErrCode.InvalidInputs,
- });
- } else {
- logger.error(
- `UNKNOWN ERROR (${req.org?.slug || "unknown"}): ${error.message}, ${error.stack}`,
- {
- error: {
- stack: error.stack,
- message: error.message,
- },
- }
- );
+ res.status(400).json({
+ message: formatZodError(error),
+ code: ErrCode.InvalidInputs,
+ });
+ } else {
+ logger.error(
+ `UNKNOWN ERROR (${req.org?.slug || "unknown"}): ${error.message}, ${error.stack}`,
+ {
+ error: {
+ stack: error.stack,
+ message: error.message,
+ },
+ },
+ );
- res.status(500).json({
- message: error.message || "Unknown error",
- code: error.code || "unknown_error",
- });
- }
- } catch (error) {
- console.log("Failed to log error / warning");
- console.log(`Request: ${req.originalUrl}`);
- console.log(`Body: ${req.body}`);
- console.log(`Log Error: ${error}`);
- }
+ res.status(500).json({
+ message: error.message || "Unknown error",
+ code: error.code || "unknown_error",
+ });
+ }
+ } catch (error) {
+ console.log("Failed to log error / warning");
+ console.log(`Request: ${req.originalUrl}`);
+ console.log(`Body: ${req.body}`);
+ console.log(`Log Error: ${error}`);
+ }
};
export const handleFrontendReqError = ({
- error,
- req,
- res,
- action,
+ error,
+ req,
+ res,
+ action,
}: {
- error: any;
- req: any;
- res: any;
- action: string;
+ error: any;
+ req: any;
+ res: any;
+ action: string;
}) => {
- try {
- const logger = req.logger;
- if (
- error instanceof RecaseError &&
- error.statusCode == StatusCodes.NOT_FOUND
- ) {
- // Temporarily disable logger to prevent thread-stream crashes
- console.log(`(frontend) ${req.method} ${req.originalUrl}: not found`);
- res.status(404).json({
- message: error.message,
- code: error.code,
- });
- return;
- }
+ try {
+ const logger = req.logger;
+ if (
+ error instanceof RecaseError &&
+ error.statusCode == StatusCodes.NOT_FOUND
+ ) {
+ // Temporarily disable logger to prevent thread-stream crashes
+ console.log(`(frontend) ${req.method} ${req.originalUrl}: not found`);
+ res.status(404).json({
+ message: error.message,
+ code: error.code,
+ });
+ return;
+ }
- logger.error(
- `(frontend) ${req.method} ${req.originalUrl}: ${error.message}`,
- {
- error,
- }
- );
+ logger.error(
+ `(frontend) ${req.method} ${req.originalUrl}: ${error.message}`,
+ {
+ error,
+ },
+ );
- res.status(400).json({
- message: error.message || "Unknown error",
- code: error.code || "unknown_error",
- });
- } catch (error) {
- console.log("Failed to log error / warning");
- }
+ res.status(400).json({
+ message: error.message || "Unknown error",
+ code: error.code || "unknown_error",
+ });
+ } catch (error) {
+ console.log("Failed to log error / warning");
+ }
};
diff --git a/server/src/utils/formatUtils.ts b/server/src/utils/formatUtils.ts
index e57c3e774..5ce185ab8 100644
--- a/server/src/utils/formatUtils.ts
+++ b/server/src/utils/formatUtils.ts
@@ -1,22 +1,22 @@
import { Organization } from "@autumn/shared";
export const formatAmount = ({
- org,
- currency,
- amount,
- maxFractionDigits = 2,
- minFractionDigits = 0,
+ org,
+ currency,
+ amount,
+ maxFractionDigits = 2,
+ minFractionDigits = 0,
}: {
- org?: Organization;
- currency?: string | null;
- amount: number;
- maxFractionDigits?: number;
- minFractionDigits?: number;
+ org?: Organization;
+ currency?: string | null;
+ amount: number;
+ maxFractionDigits?: number;
+ minFractionDigits?: number;
}) => {
- return new Intl.NumberFormat(undefined, {
- style: "currency",
- currency: currency || org?.default_currency || "USD",
- minimumFractionDigits: minFractionDigits || 0,
- maximumFractionDigits: maxFractionDigits || 2,
- }).format(amount);
+ return new Intl.NumberFormat(undefined, {
+ style: "currency",
+ currency: currency || org?.default_currency || "USD",
+ minimumFractionDigits: minFractionDigits || 0,
+ maximumFractionDigits: maxFractionDigits || 2,
+ }).format(amount);
};
diff --git a/server/src/utils/genUtils.ts b/server/src/utils/genUtils.ts
index 636e98af2..56eceb9a9 100644
--- a/server/src/utils/genUtils.ts
+++ b/server/src/utils/genUtils.ts
@@ -5,133 +5,133 @@ import { ErrCode } from "@/errors/errCodes.js";
import { UTCDate } from "@date-fns/utc";
export const generateId = (prefix: string) => {
- if (!prefix) {
- return KSUID.randomSync().string;
- } else {
- return `${prefix}_${KSUID.randomSync().string}`;
- }
+ if (!prefix) {
+ return KSUID.randomSync().string;
+ } else {
+ return `${prefix}_${KSUID.randomSync().string}`;
+ }
};
export const compareObjects = (obj1: any, obj2: any) => {
- for (const key in obj1) {
- if (nullish(obj1[key]) && nullish(obj2[key])) {
- continue;
- }
+ for (const key in obj1) {
+ if (nullish(obj1[key]) && nullish(obj2[key])) {
+ continue;
+ }
- if (nullish(obj1[key]) || nullish(obj2[key])) {
- return false;
- }
+ if (nullish(obj1[key]) || nullish(obj2[key])) {
+ return false;
+ }
- if (obj1[key] !== obj2[key]) {
- console.log("Key", key);
- console.log("Obj1", obj1[key]);
- console.log("Obj2", obj2[key]);
- return false;
- }
- }
- return true;
+ if (obj1[key] !== obj2[key]) {
+ console.log("Key", key);
+ console.log("Obj1", obj1[key]);
+ console.log("Obj2", obj2[key]);
+ return false;
+ }
+ }
+ return true;
};
export const keyToTitle = (key: string) => {
- return key
- .replace(/[-_]/g, " ")
- .replace(/\b\w/g, (char) => char.toUpperCase());
+ return key
+ .replace(/[-_]/g, " ")
+ .replace(/\b\w/g, (char) => char.toUpperCase());
};
export const notNullOrUndefined = (value: any) => {
- return value !== null && value !== undefined;
+ return value !== null && value !== undefined;
};
export const nullOrUndefined = (value: any) => {
- return value === null || value === undefined;
+ return value === null || value === undefined;
};
export const nullish = (value: any) => {
- return value === null || value === undefined;
+ return value === null || value === undefined;
};
export const notNullish = (value: any) => {
- return !nullish(value);
+ return !nullish(value);
};
export const formatUnixToDateTime = (
- unixDate?: number | null,
- withTimezone?: boolean
+ unixDate?: number | null,
+ withTimezone?: boolean,
) => {
- if (!unixDate) {
- return "undefined unix date";
- }
- return format(
- new Date(unixDate),
- withTimezone ? "dd MMM yyyy HH:mm:ss z" : "dd MMM yyyy HH:mm:ss"
- );
+ if (!unixDate) {
+ return "undefined unix date";
+ }
+ return format(
+ new Date(unixDate),
+ withTimezone ? "dd MMM yyyy HH:mm:ss z" : "dd MMM yyyy HH:mm:ss",
+ );
};
export const formatUnixToUTCDateTime = (
- unixDate?: number | null,
- withTimezone?: boolean
+ unixDate?: number | null,
+ withTimezone?: boolean,
) => {
- if (!unixDate) {
- return "undefined unix date";
- }
- return format(
- new UTCDate(unixDate),
- withTimezone ? "dd MMM yyyy HH:mm:ss z" : "dd MMM yyyy HH:mm:ss"
- );
+ if (!unixDate) {
+ return "undefined unix date";
+ }
+ return format(
+ new UTCDate(unixDate),
+ withTimezone ? "dd MMM yyyy HH:mm:ss z" : "dd MMM yyyy HH:mm:ss",
+ );
};
export const formatUnixToDate = (unixDate?: number) => {
- if (!unixDate) {
- return null;
- }
- return format(new UTCDate(unixDate), "d MMM yyyy");
+ if (!unixDate) {
+ return null;
+ }
+ return format(new UTCDate(unixDate), "d MMM yyyy");
};
export const timeout = (ms: number) => {
- return new Promise((resolve) => setTimeout(resolve, ms));
+ return new Promise((resolve) => setTimeout(resolve, ms));
};
export const validateId = (type: string, id: string) => {
- if (!id.match(/^[a-zA-Z0-9_-]+$/)) {
- throw new RecaseError({
- message: `${type} ID can only contain alphanumeric characters, underscores, and hyphens`,
- code: ErrCode.InvalidId,
- statusCode: 400,
- });
- }
+ if (!id.match(/^[a-zA-Z0-9_-]+$/)) {
+ throw new RecaseError({
+ message: `${type} ID can only contain alphanumeric characters, underscores, and hyphens`,
+ code: ErrCode.InvalidId,
+ statusCode: 400,
+ });
+ }
};
function stringToSnakeCase(str: string): string {
- return str
- .replace(/([a-z])([A-Z])/g, "$1_$2")
- .replace(/[-\s]+/g, "_")
- .toLowerCase();
+ return str
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
+ .replace(/[-\s]+/g, "_")
+ .toLowerCase();
}
export const toSnakeCase = (obj: any): any => {
- if (Array.isArray(obj)) {
- return obj.map(toSnakeCase);
- } else if (obj !== null && typeof obj === "object") {
- return Object.fromEntries(
- Object.entries(obj).map(([key, value]) => [
- stringToSnakeCase(key),
- toSnakeCase(value),
- ])
- );
- }
- return obj;
+ if (Array.isArray(obj)) {
+ return obj.map(toSnakeCase);
+ } else if (obj !== null && typeof obj === "object") {
+ return Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [
+ stringToSnakeCase(key),
+ toSnakeCase(value),
+ ]),
+ );
+ }
+ return obj;
};
export const slugify = (
- text: string,
- type: "underscore" | "dash" = "underscore"
+ text: string,
+ type: "underscore" | "dash" = "underscore",
) => {
- return text
- .toLowerCase()
- .replace(/ /g, type == "underscore" ? "_" : "-")
- .replace(/[^\w\s-]/g, "");
+ return text
+ .toLowerCase()
+ .replace(/ /g, type == "underscore" ? "_" : "-")
+ .replace(/[^\w\s-]/g, "");
};
export const getUnique = (vals: string[]) => {
- return Array.from(new Set(vals));
+ return Array.from(new Set(vals));
};
diff --git a/server/src/utils/importUtils/addProductFromSubs.ts b/server/src/utils/importUtils/addProductFromSubs.ts
index 7aa3e4bd5..406d94d88 100644
--- a/server/src/utils/importUtils/addProductFromSubs.ts
+++ b/server/src/utils/importUtils/addProductFromSubs.ts
@@ -5,13 +5,13 @@ import { isFreeProduct } from "@/internal/products/productUtils.js";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { constructSub } from "@/internal/subscriptions/subUtils.js";
import {
- FullCustomer,
- FullProduct,
- Price,
- EntitlementWithFeature,
- CusProductStatus,
- UsagePriceConfig,
- BillingInterval,
+ FullCustomer,
+ FullProduct,
+ Price,
+ EntitlementWithFeature,
+ CusProductStatus,
+ UsagePriceConfig,
+ BillingInterval,
} from "@autumn/shared";
import { notNullish } from "../genUtils.js";
@@ -23,147 +23,149 @@ import { PriceService } from "@/internal/products/prices/PriceService.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const addProductFromSubs = async ({
- req,
- autumnCus,
- autumnProduct,
- sub,
- prices,
- entitlements,
- force = false,
- isCustom = false,
- anchorToUnix,
+ req,
+ autumnCus,
+ autumnProduct,
+ sub,
+ prices,
+ entitlements,
+ force = false,
+ isCustom = false,
+ anchorToUnix,
}: {
- req: ExtendedRequest;
- autumnCus: FullCustomer;
- autumnProduct: FullProduct;
- sub?: Stripe.Subscription;
- prices?: Price[];
- entitlements?: EntitlementWithFeature[];
- force?: boolean;
- isCustom?: boolean;
- anchorToUnix?: number;
+ req: ExtendedRequest;
+ autumnCus: FullCustomer;
+ autumnProduct: FullProduct;
+ sub?: Stripe.Subscription;
+ prices?: Price[];
+ entitlements?: EntitlementWithFeature[];
+ force?: boolean;
+ isCustom?: boolean;
+ anchorToUnix?: number;
}) => {
- const { db, logger, org, env } = req;
+ const { db, logger, org, env } = req;
- const cusProducts = autumnCus.customer_products;
- const entity = autumnCus.entity;
+ const cusProducts = autumnCus.customer_products;
+ const entity = autumnCus.entity;
- let mainCusProduct = cusProducts.find(
- (cp) =>
- !cp.product.is_add_on &&
- cp.product_id == autumnProduct.id &&
- (notNullish(entity) ? cp.internal_entity_id == entity!.internal_id : true)
- );
+ let mainCusProduct = cusProducts.find(
+ (cp) =>
+ !cp.product.is_add_on &&
+ cp.product_id == autumnProduct.id &&
+ (notNullish(entity)
+ ? cp.internal_entity_id == entity!.internal_id
+ : true),
+ );
- if (mainCusProduct && !force) {
- let prices = mainCusProduct.customer_prices.map((cp) => cp.price);
- // let isFree = isFreeProduct(prices);
+ if (mainCusProduct && !force) {
+ let prices = mainCusProduct.customer_prices.map((cp) => cp.price);
+ // let isFree = isFreeProduct(prices);
- if (mainCusProduct) {
- logger.info(
- `Customer ${
- autumnCus.id || autumnCus.email
- } already has non-free free product: ${
- mainCusProduct.product.name
- }, skipping...`
- );
- return mainCusProduct;
- }
- }
+ if (mainCusProduct) {
+ logger.info(
+ `Customer ${
+ autumnCus.id || autumnCus.email
+ } already has non-free free product: ${
+ mainCusProduct.product.name
+ }, skipping...`,
+ );
+ return mainCusProduct;
+ }
+ }
- // Handle if trialing
- let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
+ // Handle if trialing
+ let trialEndsAt = sub?.trial_end ? sub.trial_end * 1000 : null;
- // throw new Error("test");
+ // throw new Error("test");
- // 1. Insert custom prices...
- let customPrices = prices?.filter((p) => p.is_custom);
- if (customPrices && customPrices.length > 0) {
- await PriceService.upsert({
- db,
- data: customPrices,
- });
- }
+ // 1. Insert custom prices...
+ let customPrices = prices?.filter((p) => p.is_custom);
+ if (customPrices && customPrices.length > 0) {
+ await PriceService.upsert({
+ db,
+ data: customPrices,
+ });
+ }
- const { start, end } = subToPeriodStartEnd({
- sub,
- });
+ const { start, end } = subToPeriodStartEnd({
+ sub,
+ });
- let newCusProduct = await createFullCusProduct({
- db,
- attachParams: {
- replaceables: [],
- customer: autumnCus,
- product: autumnProduct,
- org,
- prices: notNullish(prices) ? prices! : autumnProduct.prices,
- entitlements: notNullish(entitlements)
- ? entitlements!
- : autumnProduct.entitlements,
- freeTrial: autumnProduct.free_trial || null,
- optionsList: [],
- entities: [],
- cusProducts: cusProducts,
- features: [],
- internalEntityId: entity?.internal_id,
- entityId: entity?.id,
- isCustom: isCustom,
- },
- logger,
- trialEndsAt: trialEndsAt || undefined,
- subscriptionIds: sub ? [sub.id] : [],
- anchorToUnix: anchorToUnix || end,
+ let newCusProduct = await createFullCusProduct({
+ db,
+ attachParams: {
+ replaceables: [],
+ customer: autumnCus,
+ product: autumnProduct,
+ org,
+ prices: notNullish(prices) ? prices! : autumnProduct.prices,
+ entitlements: notNullish(entitlements)
+ ? entitlements!
+ : autumnProduct.entitlements,
+ freeTrial: autumnProduct.free_trial || null,
+ optionsList: [],
+ entities: [],
+ cusProducts: cusProducts,
+ features: [],
+ internalEntityId: entity?.internal_id,
+ entityId: entity?.id,
+ isCustom: isCustom,
+ },
+ logger,
+ trialEndsAt: trialEndsAt || undefined,
+ subscriptionIds: sub ? [sub.id] : [],
+ anchorToUnix: anchorToUnix || end,
- subscriptionStatus: sub?.status
- ? (stripeToAutumnSubStatus(sub?.status) as CusProductStatus)
- : undefined,
+ subscriptionStatus: sub?.status
+ ? (stripeToAutumnSubStatus(sub?.status) as CusProductStatus)
+ : undefined,
- canceledAt: sub?.canceled_at ? sub.canceled_at * 1000 : null,
+ canceledAt: sub?.canceled_at ? sub.canceled_at * 1000 : null,
- createdAt: sub?.created ? sub.created * 1000 : null,
- sendWebhook: false,
- });
+ createdAt: sub?.created ? sub.created * 1000 : null,
+ sendWebhook: false,
+ });
- logger.info(
- `Added product ${autumnProduct.name} to customer ${autumnCus.name}`
- );
+ logger.info(
+ `Added product ${autumnProduct.name} to customer ${autumnCus.name}`,
+ );
- if (sub) {
- // Create sub
- let usageFeatures = autumnProduct.prices
- .filter((p) => isUsagePrice({ price: p }))
- .map((p) => (p.config as UsagePriceConfig).internal_feature_id);
+ if (sub) {
+ // Create sub
+ let usageFeatures = autumnProduct.prices
+ .filter((p) => isUsagePrice({ price: p }))
+ .map((p) => (p.config as UsagePriceConfig).internal_feature_id);
- let subFromDb = await SubService.getInStripeIds({
- db,
- ids: [sub.id],
- });
+ let subFromDb = await SubService.getInStripeIds({
+ db,
+ ids: [sub.id],
+ });
- let subInterval = subToAutumnInterval(sub);
+ let subInterval = subToAutumnInterval(sub);
- if (subFromDb.length === 0) {
- await SubService.createSub({
- db,
- sub: constructSub({
- stripeId: sub.id,
- usageFeatures:
- subInterval.interval == BillingInterval.Month ? usageFeatures : [],
- orgId: org.id,
- env,
- currentPeriodStart: start,
- currentPeriodEnd: end,
- }),
- });
- logger.info(`Created sub ${sub.id} in DB`);
- } else {
- logger.info(`Sub ${sub.id} already exists in DB`);
- }
- }
+ if (subFromDb.length === 0) {
+ await SubService.createSub({
+ db,
+ sub: constructSub({
+ stripeId: sub.id,
+ usageFeatures:
+ subInterval.interval == BillingInterval.Month ? usageFeatures : [],
+ orgId: org.id,
+ env,
+ currentPeriodStart: start,
+ currentPeriodEnd: end,
+ }),
+ });
+ logger.info(`Created sub ${sub.id} in DB`);
+ } else {
+ logger.info(`Sub ${sub.id} already exists in DB`);
+ }
+ }
- autumnCus.customer_products = [
- ...(autumnCus.customer_products || []),
- newCusProduct!,
- ];
+ autumnCus.customer_products = [
+ ...(autumnCus.customer_products || []),
+ newCusProduct!,
+ ];
- return newCusProduct;
+ return newCusProduct;
};
diff --git a/server/src/utils/importUtils/addUsagePricesToSub.ts b/server/src/utils/importUtils/addUsagePricesToSub.ts
index d485b213b..44dac2c53 100644
--- a/server/src/utils/importUtils/addUsagePricesToSub.ts
+++ b/server/src/utils/importUtils/addUsagePricesToSub.ts
@@ -1,11 +1,11 @@
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { filterByBillingType } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import {
- BillingType,
- FullProduct,
- UsagePriceConfig,
- FullCusProduct,
- BillingInterval,
+ BillingType,
+ FullProduct,
+ UsagePriceConfig,
+ FullCusProduct,
+ BillingInterval,
} from "@autumn/shared";
import Stripe from "stripe";
import { ExtendedRequest } from "../models/Request.js";
@@ -15,112 +15,112 @@ import { priceToFeature } from "@/internal/products/prices/priceUtils/convertPri
import { logger } from "@/external/logtail/logtailUtils.js";
export const addContUsePricesToSub = async ({
- stripe,
- sub,
- autumnProduct,
- quantity,
- logger,
+ stripe,
+ sub,
+ autumnProduct,
+ quantity,
+ logger,
}: {
- stripe: Stripe;
- sub: Stripe.Subscription;
- autumnProduct: FullProduct;
- quantity: number;
- logger: any;
+ stripe: Stripe;
+ sub: Stripe.Subscription;
+ autumnProduct: FullProduct;
+ quantity: number;
+ logger: any;
}) => {
- let usagePrices = filterByBillingType({
- prices: autumnProduct.prices,
- billingType: BillingType.InArrearProrated,
- });
+ let usagePrices = filterByBillingType({
+ prices: autumnProduct.prices,
+ billingType: BillingType.InArrearProrated,
+ });
- usagePrices = usagePrices.filter(
- (p) => p.config.interval !== BillingInterval.OneOff
- );
+ usagePrices = usagePrices.filter(
+ (p) => p.config.interval !== BillingInterval.OneOff,
+ );
- logger.info(`Adding ${usagePrices.length} cont use prices to sub`);
+ logger.info(`Adding ${usagePrices.length} cont use prices to sub`);
- for (const usagePrice of usagePrices) {
- const config = usagePrice.config as UsagePriceConfig;
- const latestSub = await stripe.subscriptions.retrieve(sub.id);
- let subItem = findStripeItemForPrice({
- price: usagePrice,
- stripeItems: latestSub.items.data,
- });
+ for (const usagePrice of usagePrices) {
+ const config = usagePrice.config as UsagePriceConfig;
+ const latestSub = await stripe.subscriptions.retrieve(sub.id);
+ let subItem = findStripeItemForPrice({
+ price: usagePrice,
+ stripeItems: latestSub.items.data,
+ });
- if (subItem) {
- logger.info(`Sub already has price for ${config.feature_id}`);
- continue;
- }
+ if (subItem) {
+ logger.info(`Sub already has price for ${config.feature_id}`);
+ continue;
+ }
- const newSubItem = await stripe.subscriptionItems.create({
- subscription: sub.id,
- price: usagePrice.config.stripe_price_id!,
- proration_behavior: "none",
- quantity,
- });
+ const newSubItem = await stripe.subscriptionItems.create({
+ subscription: sub.id,
+ price: usagePrice.config.stripe_price_id!,
+ proration_behavior: "none",
+ quantity,
+ });
- // logger.info(`New sub item:`, {
- // newSubItem,
- // });
+ // logger.info(`New sub item:`, {
+ // newSubItem,
+ // });
- logger.info(`Successfully added ${config.feature_id} to sub`);
- }
+ logger.info(`Successfully added ${config.feature_id} to sub`);
+ }
};
export const addUsagePricesToSub = async ({
- req,
- stripeCli,
- stripeSub,
- cusProduct,
+ req,
+ stripeCli,
+ stripeSub,
+ cusProduct,
}: {
- req: ExtendedRequest;
- stripeCli: Stripe;
- stripeSub: Stripe.Subscription;
- cusProduct: FullCusProduct;
+ req: ExtendedRequest;
+ stripeCli: Stripe;
+ stripeSub: Stripe.Subscription;
+ cusProduct: FullCusProduct;
}) => {
- const { features } = req;
- const prices = cusProductToPrices({
- cusProduct,
- billingType: BillingType.UsageInArrear,
- });
+ const { features } = req;
+ const prices = cusProductToPrices({
+ cusProduct,
+ billingType: BillingType.UsageInArrear,
+ });
- for (const price of prices) {
- const feature = priceToFeature({ price, features: features });
- const usageBasedSub = await getUsageBasedSub({
- stripeCli,
- subIds: cusProduct.subscription_ids || [],
- feature: feature!,
- db: req.db,
- });
+ for (const price of prices) {
+ const feature = priceToFeature({ price, features: features });
+ const usageBasedSub = await getUsageBasedSub({
+ stripeCli,
+ subIds: cusProduct.subscription_ids || [],
+ feature: feature!,
+ db: req.db,
+ });
- let subItem = findStripeItemForPrice({
- price,
- stripeItems: stripeSub.items.data,
- });
+ let subItem = findStripeItemForPrice({
+ price,
+ stripeItems: stripeSub.items.data,
+ });
- let config = price.config as UsagePriceConfig;
- if (subItem) {
- logger.info(`Sub already has price for ${config.feature_id}`);
- continue;
- }
+ let config = price.config as UsagePriceConfig;
+ if (subItem) {
+ logger.info(`Sub already has price for ${config.feature_id}`);
+ continue;
+ }
- logger.info(`Adding ${config.feature_id} to sub ${stripeSub.id}`);
- let stripePrice = await stripeCli.prices.retrieve(
- price.config.stripe_price_id!
- );
+ logger.info(`Adding ${config.feature_id} to sub ${stripeSub.id}`);
+ let stripePrice = await stripeCli.prices.retrieve(
+ price.config.stripe_price_id!,
+ );
- if (stripePrice.recurring?.usage_type !== "metered") {
- logger.info(
- `Skipping ${config.feature_id} because it's not a metered price`
- );
- continue;
- }
+ if (stripePrice.recurring?.usage_type !== "metered") {
+ logger.info(
+ `Skipping ${config.feature_id} because it's not a metered price`,
+ );
+ continue;
+ }
- await stripeCli.subscriptionItems.create({
- subscription: stripeSub.id,
- price: price.config.stripe_price_id!,
- proration_behavior: "none",
- });
+ await stripeCli.subscriptionItems.create({
+ subscription: stripeSub.id,
+ price: price.config.stripe_price_id!,
+ proration_behavior: "none",
+ });
- logger.info(`Successfully added ${config.feature_id} to sub`);
- }
+ logger.info(`Successfully added ${config.feature_id} to sub`);
+ }
};
diff --git a/server/src/utils/importUtils/importUtils.ts b/server/src/utils/importUtils/importUtils.ts
index f7b981516..6a11e4048 100644
--- a/server/src/utils/importUtils/importUtils.ts
+++ b/server/src/utils/importUtils/importUtils.ts
@@ -5,23 +5,23 @@ import { FullProduct } from "@autumn/shared";
// Scenario 1: Replace base price with new base price
export const replaceBasePrice = async ({
- subItems,
- autumnProduct,
- basePrice,
+ subItems,
+ autumnProduct,
+ basePrice,
}: {
- subItems: Stripe.SubscriptionItem[];
- autumnProduct: FullProduct;
- basePrice?: number;
+ subItems: Stripe.SubscriptionItem[];
+ autumnProduct: FullProduct;
+ basePrice?: number;
}) => {
- let prices = autumnProduct.prices.filter((p) => !isFixedPrice({ price: p }));
+ let prices = autumnProduct.prices.filter((p) => !isFixedPrice({ price: p }));
- // Get first sub item
- const subItem = subItems[0];
- const customPrice = subItemToFixedPrice({
- subItem,
- product: autumnProduct,
- basePrice,
- });
+ // Get first sub item
+ const subItem = subItems[0];
+ const customPrice = subItemToFixedPrice({
+ subItem,
+ product: autumnProduct,
+ basePrice,
+ });
- return [customPrice, ...prices];
+ return [customPrice, ...prices];
};
diff --git a/server/src/utils/importUtils/updateUsages.ts b/server/src/utils/importUtils/updateUsages.ts
index 68e4a6981..6d90d7455 100644
--- a/server/src/utils/importUtils/updateUsages.ts
+++ b/server/src/utils/importUtils/updateUsages.ts
@@ -5,33 +5,33 @@ import { cusProductsToCusEnts } from "@autumn/shared";
import { FullCustomer } from "@autumn/shared";
export const updateUsages = async ({
- featureId,
- usage,
- fullCus,
- db,
+ featureId,
+ usage,
+ fullCus,
+ db,
}: {
- featureId: string;
- usage: number;
- fullCus: FullCustomer;
- db: DrizzleCli;
+ featureId: string;
+ usage: number;
+ fullCus: FullCustomer;
+ db: DrizzleCli;
}) => {
- let cusEnts = cusProductsToCusEnts({
- cusProducts: fullCus.customer_products,
- inStatuses: RELEVANT_STATUSES,
- featureId,
- });
- if (cusEnts.length === 0) {
- throw new Error(`No cus ent for ${featureId}`);
- }
+ let cusEnts = cusProductsToCusEnts({
+ cusProducts: fullCus.customer_products,
+ inStatuses: RELEVANT_STATUSES,
+ featureId,
+ });
+ if (cusEnts.length === 0) {
+ throw new Error(`No cus ent for ${featureId}`);
+ }
- let cusEnt = cusEnts[0];
- let newBalance = cusEnt.balance! - usage;
+ let cusEnt = cusEnts[0];
+ let newBalance = cusEnt.balance! - usage;
- await CusEntService.update({
- db,
- id: cusEnt.id,
- updates: {
- balance: newBalance,
- },
- });
+ await CusEntService.update({
+ db,
+ id: cusEnt.id,
+ updates: {
+ balance: newBalance,
+ },
+ });
};
diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts
index 02f327aea..f7c92b9d6 100644
--- a/server/src/utils/initUtils.ts
+++ b/server/src/utils/initUtils.ts
@@ -2,55 +2,59 @@ import { logger } from "@/external/logtail/logtailUtils.js";
import "dotenv/config";
export const checkEnvVars = () => {
- if (!process.env.DATABASE_URL) {
- console.error(`DATABASE_URL is not set`);
- process.exit(1);
- }
+ if (!process.env.DATABASE_URL) {
+ console.error(`DATABASE_URL is not set`);
+ process.exit(1);
+ }
- if (!process.env.ENCRYPTION_IV || !process.env.ENCRYPTION_PASSWORD) {
- console.error(
- `ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`,
- );
- process.exit(1);
- }
+ if (!process.env.ENCRYPTION_IV || !process.env.ENCRYPTION_PASSWORD) {
+ console.error(
+ `ENCRYPTION_IV or ENCRYPTION_PASSWORD is not set (used for Stripe key encryption)`,
+ );
+ process.exit(1);
+ }
- if (!process.env.REDIS_URL) {
- console.error(`REDIS_URL is not set`);
- process.exit(1);
- }
+ if (!process.env.REDIS_URL) {
+ console.error(`REDIS_URL is not set`);
+ process.exit(1);
+ }
- if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) {
- console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
- process.exit(1);
- }
+ if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) {
+ console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
+ process.exit(1);
+ }
- if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
- logger.warn(
- "RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)",
- );
- }
+ if (!process.env.RESEND_API_KEY || !process.env.RESEND_DOMAIN) {
+ logger.warn(
+ "RESEND_API_KEY or RESEND_DOMAIN is not set (use terminal for sign in OTP)",
+ );
+ }
- if (
- !process.env.LOGTAIL_SOURCE_TOKEN ||
- !process.env.LOGTAIL_INGESTING_HOST
- ) {
- logger.warn("LOGTAIL ENV VARs not found, skipping logtail");
- }
+ if (
+ !process.env.LOGTAIL_SOURCE_TOKEN ||
+ !process.env.LOGTAIL_INGESTING_HOST
+ ) {
+ logger.warn("LOGTAIL ENV VARs not found, skipping logtail");
+ }
- if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
- logger.warn(
- `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, some actions will be skipped`,
- );
- }
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) {
+ logger.warn(
+ `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, some actions will be skipped`,
+ );
+ }
- if (!process.env.CLICKHOUSE_URL || !process.env.CLICKHOUSE_USERNAME || !process.env.CLICKHOUSE_PASSWORD) {
- logger.warn(
- `CLICKHOUSE_URL or CLICKHOUSE_USERNAME or CLICKHOUSE_PASSWORD is not set, some actions will be skipped`,
- );
- }
+ if (
+ !process.env.CLICKHOUSE_URL ||
+ !process.env.CLICKHOUSE_USERNAME ||
+ !process.env.CLICKHOUSE_PASSWORD
+ ) {
+ logger.warn(
+ `CLICKHOUSE_URL or CLICKHOUSE_USERNAME or CLICKHOUSE_PASSWORD is not set, some actions will be skipped`,
+ );
+ }
- if (!process.env.SVIX_API_KEY) {
- logger.warn(`SVIX_API_KEY is not set, some actions will be skipped`);
- return;
- }
+ if (!process.env.SVIX_API_KEY) {
+ logger.warn(`SVIX_API_KEY is not set, some actions will be skipped`);
+ return;
+ }
};
diff --git a/server/src/utils/models/ItemSet.ts b/server/src/utils/models/ItemSet.ts
index 4fbf55886..613acbf48 100644
--- a/server/src/utils/models/ItemSet.ts
+++ b/server/src/utils/models/ItemSet.ts
@@ -14,11 +14,11 @@ import { z } from "zod";
// export type ItemSet = z.infer;
export type ItemSet = {
- subItems: (Stripe.SubscriptionUpdateParams.Item & {
- autumnPrice?: Price;
- })[];
- invoiceItems: Stripe.SubscriptionUpdateParams.AddInvoiceItem[];
- usageFeatures: string[];
+ subItems: (Stripe.SubscriptionUpdateParams.Item & {
+ autumnPrice?: Price;
+ })[];
+ invoiceItems: Stripe.SubscriptionUpdateParams.AddInvoiceItem[];
+ usageFeatures: string[];
};
// export const ItemSetSchema2 = z.object({
diff --git a/server/src/utils/models/Request.ts b/server/src/utils/models/Request.ts
index 53913ac12..4939c1042 100644
--- a/server/src/utils/models/Request.ts
+++ b/server/src/utils/models/Request.ts
@@ -1,8 +1,8 @@
import { AppEnv, AuthType, Feature, Organization } from "@autumn/shared";
import { Logtail } from "@logtail/node";
import type {
- Request as ExpressRequest,
- Response as ExpressResponse,
+ Request as ExpressRequest,
+ Response as ExpressResponse,
} from "express";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -10,33 +10,33 @@ import { PostHog } from "posthog-node";
import { ClickHouseClient } from "@clickhouse/client";
export interface ExtendedRequest extends ExpressRequest {
- orgId: string;
- env: AppEnv;
- org: Organization;
- features: Feature[];
- db: DrizzleCli;
- logtail: Logtail;
- logger: any;
- clickhouseClient: ClickHouseClient;
+ orgId: string;
+ env: AppEnv;
+ org: Organization;
+ features: Feature[];
+ db: DrizzleCli;
+ logtail: Logtail;
+ logger: any;
+ clickhouseClient: ClickHouseClient;
- id?: string;
- userId?: string;
- isPublic?: boolean;
- authType?: AuthType;
+ id?: string;
+ userId?: string;
+ isPublic?: boolean;
+ authType?: AuthType;
- posthog?: PostHog;
- apiVersion?: number;
+ posthog?: PostHog;
+ apiVersion?: number;
- timestamp?: number;
+ timestamp?: number;
}
export interface ActionRequest {
- id: string;
- authType: AuthType;
- method: string;
- path: string;
- body: any;
- timestamp: number;
+ id: string;
+ authType: AuthType;
+ method: string;
+ path: string;
+ body: any;
+ timestamp: number;
}
export interface ExtendedResponse extends ExpressResponse {}
diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts
index 2d05cb0fd..02d7525bb 100644
--- a/server/src/utils/routerUtils.ts
+++ b/server/src/utils/routerUtils.ts
@@ -1,7 +1,7 @@
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
import RecaseError, {
- formatZodError,
- handleRequestError,
+ formatZodError,
+ handleRequestError,
} from "./errorUtils.js";
import { ZodAny, ZodError, ZodObject } from "zod";
import { StatusCodes } from "http-status-codes";
@@ -15,94 +15,94 @@ import qs from "qs";
* Parses query parameters with proper type coercion for validation
*/
const parseQueryForValidation = (query: any): any => {
- // Re-parse the query string with qs to handle arrays and nested objects properly
- const queryString = new URLSearchParams(query).toString();
- const parsed = qs.parse(queryString, {
- comma: true, // Parse comma-separated values as arrays
- arrayLimit: 100,
- parseArrays: true,
- });
+ // Re-parse the query string with qs to handle arrays and nested objects properly
+ const queryString = new URLSearchParams(query).toString();
+ const parsed = qs.parse(queryString, {
+ comma: true, // Parse comma-separated values as arrays
+ arrayLimit: 100,
+ parseArrays: true,
+ });
- // Type coercion for common cases
- const coerceValue = (value: any): any => {
- if (Array.isArray(value)) {
- return value.map(coerceValue);
- }
+ // Type coercion for common cases
+ const coerceValue = (value: any): any => {
+ if (Array.isArray(value)) {
+ return value.map(coerceValue);
+ }
- if (typeof value === "string") {
- // Try to parse as number
- if (!isNaN(Number(value)) && value.trim() !== "") {
- return Number(value);
- }
- // Parse booleans
- if (value === "true") return true;
- if (value === "false") return false;
- }
+ if (typeof value === "string") {
+ // Try to parse as number
+ if (!isNaN(Number(value)) && value.trim() !== "") {
+ return Number(value);
+ }
+ // Parse booleans
+ if (value === "true") return true;
+ if (value === "false") return false;
+ }
- return value;
- };
+ return value;
+ };
- const result: any = {};
- for (const [key, value] of Object.entries(parsed)) {
- result[key] = coerceValue(value);
- }
+ const result: any = {};
+ for (const [key, value] of Object.entries(parsed)) {
+ result[key] = coerceValue(value);
+ }
- return result;
+ return result;
};
/**
* Formats Zod validation errors into user-friendly messages
*/
const formatZodValidationError = (error: ZodError): string => {
- const fieldErrors = error.issues.map((issue) => {
- const path = issue.path.length > 0 ? issue.path.join(".") : "root";
+ const fieldErrors = error.issues.map((issue) => {
+ const path = issue.path.length > 0 ? issue.path.join(".") : "root";
- // Handle different types of validation errors with more descriptive messages
- switch (issue.code) {
- case "invalid_type":
- if (issue.received === "undefined") {
- return `${path} is required`;
- }
- return `${path} must be a ${issue.expected}, received ${issue.received}`;
+ // Handle different types of validation errors with more descriptive messages
+ switch (issue.code) {
+ case "invalid_type":
+ if (issue.received === "undefined") {
+ return `${path} is required`;
+ }
+ return `${path} must be a ${issue.expected}, received ${issue.received}`;
- case "too_small":
- if (issue.type === "number") {
- return `${path} must be at least ${issue.minimum}`;
- }
- if (issue.type === "string") {
- return `${path} must be at least ${issue.minimum} characters`;
- }
- if (issue.type === "array") {
- return `${path} must contain at least ${issue.minimum} items`;
- }
- return `${path} is too small`;
+ case "too_small":
+ if (issue.type === "number") {
+ return `${path} must be at least ${issue.minimum}`;
+ }
+ if (issue.type === "string") {
+ return `${path} must be at least ${issue.minimum} characters`;
+ }
+ if (issue.type === "array") {
+ return `${path} must contain at least ${issue.minimum} items`;
+ }
+ return `${path} is too small`;
- case "too_big":
- if (issue.type === "number") {
- return `${path} must be at most ${issue.maximum}`;
- }
- if (issue.type === "string") {
- return `${path} must be at most ${issue.maximum} characters`;
- }
- if (issue.type === "array") {
- return `${path} must contain at most ${issue.maximum} items`;
- }
- return `${path} is too large`;
+ case "too_big":
+ if (issue.type === "number") {
+ return `${path} must be at most ${issue.maximum}`;
+ }
+ if (issue.type === "string") {
+ return `${path} must be at most ${issue.maximum} characters`;
+ }
+ if (issue.type === "array") {
+ return `${path} must contain at most ${issue.maximum} items`;
+ }
+ return `${path} is too large`;
- case "invalid_enum_value":
- return `${path} must be one of: ${issue.options?.join(", ") || "valid options"}`;
+ case "invalid_enum_value":
+ return `${path} must be one of: ${issue.options?.join(", ") || "valid options"}`;
- case "custom":
- return issue.message || `${path} is invalid`;
+ case "custom":
+ return issue.message || `${path} is invalid`;
- default:
- return issue.message || `${path} is invalid`;
- }
- });
+ default:
+ return issue.message || `${path} is invalid`;
+ }
+ });
- // Remove duplicates and join with semicolons for multiple errors
- const uniqueErrors = [...new Set(fieldErrors)];
- return uniqueErrors.join("; ");
+ // Remove duplicates and join with semicolons for multiple errors
+ const uniqueErrors = [...new Set(fieldErrors)];
+ return uniqueErrors.join("; ");
};
/**
@@ -129,209 +129,209 @@ const formatZodValidationError = (error: ZodError): string => {
*/
export const routeHandler = async ({
- req,
- res,
- action,
- handler,
- validator,
- queryValidator,
- loader,
- withSpan = false,
+ req,
+ res,
+ action,
+ handler,
+ validator,
+ queryValidator,
+ loader,
+ withSpan = false,
}: {
- req: any;
- res: any;
- action: string;
- handler: (req: any, res: any, load: TLoad, query?: any) => Promise;
- validator?:
- | ((req: any, res: any) => Promise)
- | ZodAny
- | ZodObject;
- queryValidator?:
- | ((req: any, res: any) => Promise)
- | ZodAny
- | ZodObject;
- withSpan?: boolean;
+ req: any;
+ res: any;
+ action: string;
+ handler: (req: any, res: any, load: TLoad, query?: any) => Promise;
+ validator?:
+ | ((req: any, res: any) => Promise)
+ | ZodAny
+ | ZodObject;
+ queryValidator?:
+ | ((req: any, res: any) => Promise)
+ | ZodAny
+ | ZodObject;
+ withSpan?: boolean;
} & (TLoad extends undefined
- ? { loader?: never }
- : {
- loader: ({
- body,
- query,
- req,
- }: {
- body: any;
- query: any;
- req: ExtendedRequest;
- }) => Promise;
- })) => {
- try {
- let load: TLoad | undefined;
+ ? { loader?: never }
+ : {
+ loader: ({
+ body,
+ query,
+ req,
+ }: {
+ body: any;
+ query: any;
+ req: ExtendedRequest;
+ }) => Promise;
+ })) => {
+ try {
+ let load: TLoad | undefined;
- if (!withSpan) {
- if (typeof validator === "function") {
- await validator(req, res);
- } else if (
- validator instanceof ZodAny ||
- validator instanceof ZodObject
- ) {
- const parseResult = validator.safeParse(req.body);
- if (!parseResult.success) {
- const errorMsg = formatZodValidationError(parseResult.error);
- throw new RecaseError({
- message: errorMsg,
- code: ErrCode.InvalidRequest,
- });
- }
- }
+ if (!withSpan) {
+ if (typeof validator === "function") {
+ await validator(req, res);
+ } else if (
+ validator instanceof ZodAny ||
+ validator instanceof ZodObject
+ ) {
+ const parseResult = validator.safeParse(req.body);
+ if (!parseResult.success) {
+ const errorMsg = formatZodValidationError(parseResult.error);
+ throw new RecaseError({
+ message: errorMsg,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ }
- if (typeof queryValidator === "function") {
- await queryValidator(req, res);
- } else if (
- queryValidator instanceof ZodAny ||
- queryValidator instanceof ZodObject
- ) {
- const parsedQuery = parseQueryForValidation(req.query);
- const parseResult = queryValidator.safeParse(parsedQuery);
- if (!parseResult.success) {
- const errorMsg = formatZodValidationError(parseResult.error);
- throw new RecaseError({
- message: errorMsg,
- code: ErrCode.InvalidRequest,
- });
- }
- // Update req.query with parsed values for use in loader
- req.query = parseResult.data;
- }
- if (loader) {
- load = await loader({
- body: req.body,
- query: req.query,
- req: req as ExtendedRequest,
- });
- }
- await handler(req, res, load as TLoad, req.query);
- } else {
- await withSpanTracer({
- name: action ?? "unknown",
- attributes: {
- org: req.org?.id,
- env: req.env,
- },
- fn: async () => {
- if (typeof validator === "function") {
- await validator(req, res);
- } else if (
- validator instanceof ZodAny ||
- validator instanceof ZodObject
- ) {
- const parseResult = validator.safeParse(req.body);
- if (!parseResult.success) {
- const errorMsg = formatZodValidationError(parseResult.error);
- throw new RecaseError({
- message: errorMsg,
- code: ErrCode.InvalidRequest,
- });
- }
- }
+ if (typeof queryValidator === "function") {
+ await queryValidator(req, res);
+ } else if (
+ queryValidator instanceof ZodAny ||
+ queryValidator instanceof ZodObject
+ ) {
+ const parsedQuery = parseQueryForValidation(req.query);
+ const parseResult = queryValidator.safeParse(parsedQuery);
+ if (!parseResult.success) {
+ const errorMsg = formatZodValidationError(parseResult.error);
+ throw new RecaseError({
+ message: errorMsg,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ // Update req.query with parsed values for use in loader
+ req.query = parseResult.data;
+ }
+ if (loader) {
+ load = await loader({
+ body: req.body,
+ query: req.query,
+ req: req as ExtendedRequest,
+ });
+ }
+ await handler(req, res, load as TLoad, req.query);
+ } else {
+ await withSpanTracer({
+ name: action ?? "unknown",
+ attributes: {
+ org: req.org?.id,
+ env: req.env,
+ },
+ fn: async () => {
+ if (typeof validator === "function") {
+ await validator(req, res);
+ } else if (
+ validator instanceof ZodAny ||
+ validator instanceof ZodObject
+ ) {
+ const parseResult = validator.safeParse(req.body);
+ if (!parseResult.success) {
+ const errorMsg = formatZodValidationError(parseResult.error);
+ throw new RecaseError({
+ message: errorMsg,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ }
- if (typeof queryValidator === "function") {
- await queryValidator(req, res);
- } else if (
- queryValidator instanceof ZodAny ||
- queryValidator instanceof ZodObject
- ) {
- const parsedQuery = parseQueryForValidation(req.query);
- const parseResult = queryValidator.safeParse(parsedQuery);
- if (!parseResult.success) {
- const errorMsg = formatZodValidationError(parseResult.error);
- throw new RecaseError({
- message: errorMsg,
- code: ErrCode.InvalidRequest,
- });
- }
- // Update req.query with parsed values for use in loader
- req.query = parseResult.data;
- }
- if (loader) {
- load = await loader({
- body: req.body,
- query: req.query,
- req: req as ExtendedRequest,
- });
- }
- await handler(req, res, load as TLoad, req.query);
- },
- });
- }
- } catch (error) {
- try {
- if (error instanceof RecaseError) {
- if (error.code === ErrCode.EntityNotFound) {
- req.logtail.warn(
- `${error.message}, org: ${req.org?.slug || req.orgId}`
- );
- return res.status(404).json({
- message: error.message,
- code: error.code,
- });
- }
- }
- } catch (error) {}
+ if (typeof queryValidator === "function") {
+ await queryValidator(req, res);
+ } else if (
+ queryValidator instanceof ZodAny ||
+ queryValidator instanceof ZodObject
+ ) {
+ const parsedQuery = parseQueryForValidation(req.query);
+ const parseResult = queryValidator.safeParse(parsedQuery);
+ if (!parseResult.success) {
+ const errorMsg = formatZodValidationError(parseResult.error);
+ throw new RecaseError({
+ message: errorMsg,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ // Update req.query with parsed values for use in loader
+ req.query = parseResult.data;
+ }
+ if (loader) {
+ load = await loader({
+ body: req.body,
+ query: req.query,
+ req: req as ExtendedRequest,
+ });
+ }
+ await handler(req, res, load as TLoad, req.query);
+ },
+ });
+ }
+ } catch (error) {
+ try {
+ if (error instanceof RecaseError) {
+ if (error.code === ErrCode.EntityNotFound) {
+ req.logtail.warn(
+ `${error.message}, org: ${req.org?.slug || req.orgId}`,
+ );
+ return res.status(404).json({
+ message: error.message,
+ code: error.code,
+ });
+ }
+ }
+ } catch (error) {}
- let originalUrl = req.originalUrl;
- if (error instanceof Stripe.errors.StripeError) {
- if (
- originalUrl.includes("/exchange") &&
- error.message.includes("Invalid API Key provided")
- ) {
- req.logtail.warn(`Exchange router, invalid API Key provided`);
+ let originalUrl = req.originalUrl;
+ if (error instanceof Stripe.errors.StripeError) {
+ if (
+ originalUrl.includes("/exchange") &&
+ error.message.includes("Invalid API Key provided")
+ ) {
+ req.logtail.warn(`Exchange router, invalid API Key provided`);
- return res.status(400).json({
- message: error.message,
- code: ErrCode.InvalidRequest,
- });
- }
+ return res.status(400).json({
+ message: error.message,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- if (
- originalUrl.includes("/billing_portal") &&
- error.message.includes("Provide a configuration or create your default")
- ) {
- req.logtail.warn(`Billing portal config error, org: ${req.org?.slug}`);
- return res.status(404).json({
- message: error.message,
- code: ErrCode.InvalidRequest,
- });
- }
+ if (
+ originalUrl.includes("/billing_portal") &&
+ error.message.includes("Provide a configuration or create your default")
+ ) {
+ req.logtail.warn(`Billing portal config error, org: ${req.org?.slug}`);
+ return res.status(404).json({
+ message: error.message,
+ code: ErrCode.InvalidRequest,
+ });
+ }
- if (
- originalUrl.includes("/billing_portal") &&
- error.message.includes(
- "Invalid URL: An explicit scheme (such as https)"
- )
- ) {
- req.logtail.warn(
- `Billing portal return_url error, org: ${req.org?.slug}, return_url: ${req.body.return_url}`
- );
- return res.status(400).json({
- message: error.message,
- code: ErrCode.InvalidRequest,
- });
- }
- }
+ if (
+ originalUrl.includes("/billing_portal") &&
+ error.message.includes(
+ "Invalid URL: An explicit scheme (such as https)",
+ )
+ ) {
+ req.logtail.warn(
+ `Billing portal return_url error, org: ${req.org?.slug}, return_url: ${req.body.return_url}`,
+ );
+ return res.status(400).json({
+ message: error.message,
+ code: ErrCode.InvalidRequest,
+ });
+ }
+ }
- if (error instanceof ZodError && req.originalUrl.includes("/attach")) {
- error = new RecaseError({
- message: formatZodError(error),
- code: ErrCode.InvalidInputs,
- statusCode: StatusCodes.BAD_REQUEST,
- });
- }
+ if (error instanceof ZodError && req.originalUrl.includes("/attach")) {
+ error = new RecaseError({
+ message: formatZodError(error),
+ code: ErrCode.InvalidInputs,
+ statusCode: StatusCodes.BAD_REQUEST,
+ });
+ }
- handleRequestError({
- error,
- req,
- res,
- action,
- });
- }
+ handleRequestError({
+ error,
+ req,
+ res,
+ action,
+ });
+ }
};
diff --git a/server/src/utils/scriptUtils/clearOrg.ts b/server/src/utils/scriptUtils/clearOrg.ts
index 999d6c360..7514fc32b 100644
--- a/server/src/utils/scriptUtils/clearOrg.ts
+++ b/server/src/utils/scriptUtils/clearOrg.ts
@@ -1,77 +1,77 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- AppEnv,
- customers,
- features,
- Organization,
- products,
+ AppEnv,
+ customers,
+ features,
+ Organization,
+ products,
} from "@autumn/shared";
import { and, eq, inArray } from "drizzle-orm";
export const clearCustomersInBatches = async ({
- db,
- org,
- batchSize = 450,
+ db,
+ org,
+ batchSize = 450,
}: {
- db: DrizzleCli;
- org: Organization;
- batchSize?: number;
+ db: DrizzleCli;
+ org: Organization;
+ batchSize?: number;
}) => {
- let deletedCount = 0;
+ 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);
+ 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
- }
+ if (customerBatch.length === 0) {
+ break; // No more customers to delete
+ }
- // Delete the batch
- const customerIds = customerBatch
- .map((c) => c.internalId)
- .filter((id) => id !== null);
+ // Delete the batch
+ const customerIds = customerBatch
+ .map((c) => c.internalId)
+ .filter((id) => id !== null);
- console.log("Deleting customers:", customerIds);
+ console.log("Deleting customers:", customerIds);
- await db
- .delete(customers)
- .where(inArray(customers.internal_id, customerIds));
+ await db
+ .delete(customers)
+ .where(inArray(customers.internal_id, customerIds));
- deletedCount += customerBatch.length;
- console.log(
- `Deleted ${customerBatch.length} customers (total: ${deletedCount})`
- );
- }
+ deletedCount += customerBatch.length;
+ console.log(
+ `Deleted ${customerBatch.length} customers (total: ${deletedCount})`,
+ );
+ }
- return deletedCount;
+ return deletedCount;
};
export const clearOrg = async ({
- db,
- org,
+ db,
+ org,
}: {
- db: DrizzleCli;
- org: Organization;
+ db: DrizzleCli;
+ org: Organization;
}) => {
- const deletedCount = await clearCustomersInBatches({ db, org });
- console.log(`Cleared ${deletedCount} customers`);
+ 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)));
+ await db
+ .delete(products)
+ .where(and(eq(products.org_id, org.id), eq(products.env, AppEnv.Sandbox)));
- console.log("Cleared products");
+ console.log("Cleared products");
- await db
- .delete(features)
- .where(and(eq(features.org_id, org.id), eq(features.env, AppEnv.Sandbox)));
+ await db
+ .delete(features)
+ .where(and(eq(features.org_id, org.id), eq(features.env, AppEnv.Sandbox)));
- console.log("Cleared features");
+ console.log("Cleared features");
};
diff --git a/server/src/utils/scriptUtils/constructItem.ts b/server/src/utils/scriptUtils/constructItem.ts
index b86870e52..66436a76f 100644
--- a/server/src/utils/scriptUtils/constructItem.ts
+++ b/server/src/utils/scriptUtils/constructItem.ts
@@ -1,184 +1,184 @@
import {
- LimitedItem,
- OnDecrease,
- OnIncrease,
- ProductItem,
- ProductItemConfig,
- ProductItemInterval,
- RolloverConfig,
- UsageModel,
+ LimitedItem,
+ OnDecrease,
+ OnIncrease,
+ ProductItem,
+ ProductItemConfig,
+ ProductItemInterval,
+ RolloverConfig,
+ UsageModel,
} from "@autumn/shared";
export const constructFeatureItem = ({
- featureId,
- includedUsage = 150,
- interval = ProductItemInterval.Month,
- intervalCount = 1,
- entityFeatureId,
- isBoolean = false,
- rolloverConfig,
+ featureId,
+ includedUsage = 150,
+ interval = ProductItemInterval.Month,
+ intervalCount = 1,
+ entityFeatureId,
+ isBoolean = false,
+ rolloverConfig,
}: {
- featureId: string;
- includedUsage?: number;
- interval?: ProductItemInterval | null;
- intervalCount?: number;
- entityFeatureId?: string;
- isBoolean?: boolean;
- rolloverConfig?: RolloverConfig;
+ featureId: string;
+ includedUsage?: number;
+ interval?: ProductItemInterval | null;
+ intervalCount?: number;
+ entityFeatureId?: string;
+ isBoolean?: boolean;
+ rolloverConfig?: RolloverConfig;
}) => {
- if (isBoolean) {
- return {
- feature_id: featureId,
- entity_feature_id: entityFeatureId,
- };
- }
- let item: LimitedItem = {
- feature_id: featureId,
- included_usage: includedUsage,
- entity_feature_id: entityFeatureId,
- interval: interval,
- interval_count: intervalCount,
- };
+ if (isBoolean) {
+ return {
+ feature_id: featureId,
+ entity_feature_id: entityFeatureId,
+ };
+ }
+ let item: LimitedItem = {
+ feature_id: featureId,
+ included_usage: includedUsage,
+ entity_feature_id: entityFeatureId,
+ interval: interval,
+ interval_count: intervalCount,
+ };
- if (rolloverConfig) {
- item.config = {
- rollover: rolloverConfig,
- };
- }
+ if (rolloverConfig) {
+ item.config = {
+ rollover: rolloverConfig,
+ };
+ }
- return item;
+ return item;
};
export const constructPrepaidItem = ({
- featureId,
- price = 9,
- tiers,
- billingUnits = 100,
- includedUsage = 0,
- isOneOff = false,
- config = {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
- rolloverConfig,
- usageLimit,
- intervalCount = 1,
+ featureId,
+ price = 9,
+ tiers,
+ billingUnits = 100,
+ includedUsage = 0,
+ isOneOff = false,
+ config = {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
+ rolloverConfig,
+ usageLimit,
+ intervalCount = 1,
}: {
- featureId: string;
- price?: number;
- tiers?: { amount: number; to: number }[];
- billingUnits?: number;
- includedUsage?: number;
- isOneOff?: boolean;
- config?: ProductItemConfig;
- rolloverConfig?: RolloverConfig;
- usageLimit?: number;
- intervalCount?: number;
+ featureId: string;
+ price?: number;
+ tiers?: { amount: number; to: number }[];
+ billingUnits?: number;
+ includedUsage?: number;
+ isOneOff?: boolean;
+ config?: ProductItemConfig;
+ rolloverConfig?: RolloverConfig;
+ usageLimit?: number;
+ intervalCount?: number;
}) => {
- let item: ProductItem = {
- feature_id: featureId,
- usage_model: UsageModel.Prepaid,
+ let item: ProductItem = {
+ feature_id: featureId,
+ usage_model: UsageModel.Prepaid,
- price: price,
- tiers: tiers,
- billing_units: billingUnits || 100,
- interval: isOneOff ? null : ProductItemInterval.Month,
- interval_count: intervalCount,
- included_usage: includedUsage,
+ price: price,
+ tiers: tiers,
+ billing_units: billingUnits || 100,
+ interval: isOneOff ? null : ProductItemInterval.Month,
+ interval_count: intervalCount,
+ included_usage: includedUsage,
- config: {
- ...config,
- ...(rolloverConfig ? { rollover: rolloverConfig } : {}),
- },
- usage_limit: usageLimit,
- };
+ config: {
+ ...config,
+ ...(rolloverConfig ? { rollover: rolloverConfig } : {}),
+ },
+ usage_limit: usageLimit,
+ };
- return item;
+ return item;
};
export const constructArrearItem = ({
- featureId,
- includedUsage = 10000,
- price = 0.1,
- billingUnits = 1000,
- config = {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
- entityFeatureId,
- usageLimit,
- intervalCount = 1,
+ featureId,
+ includedUsage = 10000,
+ price = 0.1,
+ billingUnits = 1000,
+ config = {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
+ entityFeatureId,
+ usageLimit,
+ intervalCount = 1,
}: {
- featureId: string;
- includedUsage?: number;
- price?: number;
- billingUnits?: number;
- config?: ProductItemConfig;
- entityFeatureId?: string;
- usageLimit?: number;
- intervalCount?: number;
+ featureId: string;
+ includedUsage?: number;
+ price?: number;
+ billingUnits?: number;
+ config?: ProductItemConfig;
+ entityFeatureId?: string;
+ usageLimit?: number;
+ intervalCount?: number;
}) => {
- let item: ProductItem = {
- feature_id: featureId,
- usage_model: UsageModel.PayPerUse,
- included_usage: includedUsage,
- price: price,
- billing_units: billingUnits,
- interval: ProductItemInterval.Month,
- interval_count: intervalCount,
- reset_usage_when_enabled: true,
- config,
- entity_feature_id: entityFeatureId,
- usage_limit: usageLimit,
- };
+ let item: ProductItem = {
+ feature_id: featureId,
+ usage_model: UsageModel.PayPerUse,
+ included_usage: includedUsage,
+ price: price,
+ billing_units: billingUnits,
+ interval: ProductItemInterval.Month,
+ interval_count: intervalCount,
+ reset_usage_when_enabled: true,
+ config,
+ entity_feature_id: entityFeatureId,
+ usage_limit: usageLimit,
+ };
- return item;
+ return item;
};
export const constructArrearProratedItem = ({
- featureId,
- pricePerUnit = 10,
- includedUsage = 1,
- config = {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
- usageLimit,
- rolloverConfig,
+ featureId,
+ pricePerUnit = 10,
+ includedUsage = 1,
+ config = {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ usageLimit,
+ rolloverConfig,
}: {
- featureId: string;
- pricePerUnit?: number;
- includedUsage?: number;
- config?: ProductItemConfig;
- usageLimit?: number;
- rolloverConfig?: RolloverConfig;
+ featureId: string;
+ pricePerUnit?: number;
+ includedUsage?: number;
+ config?: ProductItemConfig;
+ usageLimit?: number;
+ rolloverConfig?: RolloverConfig;
}) => {
- let item: ProductItem = {
- feature_id: featureId,
- usage_model: UsageModel.PayPerUse,
- included_usage: includedUsage,
- price: pricePerUnit,
- billing_units: 1,
- interval: ProductItemInterval.Month,
- config: {
- ...config,
- ...(rolloverConfig ? { rollover: rolloverConfig } : {}),
- },
- usage_limit: usageLimit,
- };
+ let item: ProductItem = {
+ feature_id: featureId,
+ usage_model: UsageModel.PayPerUse,
+ included_usage: includedUsage,
+ price: pricePerUnit,
+ billing_units: 1,
+ interval: ProductItemInterval.Month,
+ config: {
+ ...config,
+ ...(rolloverConfig ? { rollover: rolloverConfig } : {}),
+ },
+ usage_limit: usageLimit,
+ };
- return item;
+ return item;
};
export const constructFixedPrice = ({
- price,
- interval = ProductItemInterval.Month,
+ price,
+ interval = ProductItemInterval.Month,
}: {
- price: number;
- interval?: ProductItemInterval;
+ price: number;
+ interval?: ProductItemInterval;
}) => {
- return {
- price,
- interval,
- };
+ return {
+ price,
+ interval,
+ };
};
diff --git a/server/src/utils/scriptUtils/createTestProducts.ts b/server/src/utils/scriptUtils/createTestProducts.ts
index 44fb41afd..0c4217ae0 100644
--- a/server/src/utils/scriptUtils/createTestProducts.ts
+++ b/server/src/utils/scriptUtils/createTestProducts.ts
@@ -1,202 +1,202 @@
import {
- constructBooleanFeature,
- constructMeteredFeature,
+ constructBooleanFeature,
+ constructMeteredFeature,
} from "@/internal/features/utils/constructFeatureUtils.js";
import {
- AppEnv,
- BillingInterval,
- CouponDurationType,
- CreateFreeTrial,
- CreateFreeTrialSchema,
- CreateReward,
- FeatureUsageType,
- FreeTrial,
- FreeTrialDuration,
- ProductItem,
- ProductV2,
- RewardType,
+ AppEnv,
+ BillingInterval,
+ CouponDurationType,
+ CreateFreeTrial,
+ CreateFreeTrialSchema,
+ CreateReward,
+ FeatureUsageType,
+ FreeTrial,
+ FreeTrialDuration,
+ ProductItem,
+ ProductV2,
+ RewardType,
} from "@autumn/shared";
import { keyToTitle } from "../genUtils.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
export enum TestFeatureType {
- Boolean = "boolean",
- SingleUse = "single_use",
- ContinuousUse = "continuous_use",
+ Boolean = "boolean",
+ SingleUse = "single_use",
+ ContinuousUse = "continuous_use",
}
export const initFeature = ({
- id,
- orgId,
- type,
+ id,
+ orgId,
+ type,
}: {
- id: string;
- orgId: string;
- type: TestFeatureType;
+ id: string;
+ orgId: string;
+ type: TestFeatureType;
}) => {
- if (type === TestFeatureType.Boolean) {
- return constructBooleanFeature({
- featureId: id,
- orgId,
- env: AppEnv.Sandbox,
- });
- } else if (type === TestFeatureType.SingleUse) {
- return constructMeteredFeature({
- featureId: id,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Single,
- });
- } else {
- return constructMeteredFeature({
- featureId: id,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Continuous,
- });
- }
+ if (type === TestFeatureType.Boolean) {
+ return constructBooleanFeature({
+ featureId: id,
+ orgId,
+ env: AppEnv.Sandbox,
+ });
+ } else if (type === TestFeatureType.SingleUse) {
+ return constructMeteredFeature({
+ featureId: id,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Single,
+ });
+ } else {
+ return constructMeteredFeature({
+ featureId: id,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Continuous,
+ });
+ }
};
export const constructRawProduct = ({
- id,
- isAddOn = false,
- items,
+ id,
+ isAddOn = false,
+ items,
}: {
- id: string;
- isAddOn?: boolean;
- items: ProductItem[];
+ id: string;
+ isAddOn?: boolean;
+ items: ProductItem[];
}) => {
- return {
- id,
- name: keyToTitle(id),
- items,
- is_add_on: isAddOn,
- is_default: false,
- version: 1,
- group: "",
- created_at: Date.now(),
- };
+ return {
+ id,
+ name: keyToTitle(id),
+ items,
+ is_add_on: isAddOn,
+ is_default: false,
+ version: 1,
+ group: "",
+ created_at: Date.now(),
+ };
};
export const constructProduct = ({
- id,
- items,
- type,
- interval,
- group,
- intervalCount,
- isAnnual = false,
- trial = false,
- excludeBase = false,
- isDefault = true,
- isAddOn = false,
- freeTrial,
- forcePaidDefault = false,
+ id,
+ items,
+ type,
+ interval,
+ group,
+ intervalCount,
+ isAnnual = false,
+ trial = false,
+ excludeBase = false,
+ isDefault = true,
+ isAddOn = false,
+ freeTrial,
+ forcePaidDefault = false,
}: {
- id?: string;
- items: ProductItem[];
- type: "free" | "pro" | "premium" | "growth" | "one_off";
- interval?: BillingInterval;
- group?: string;
- intervalCount?: number;
- isAnnual?: boolean;
- trial?: boolean;
- excludeBase?: boolean;
- isDefault?: boolean;
- isAddOn?: boolean;
- freeTrial?: CreateFreeTrial;
- forcePaidDefault?: boolean;
+ id?: string;
+ items: ProductItem[];
+ type: "free" | "pro" | "premium" | "growth" | "one_off";
+ interval?: BillingInterval;
+ group?: string;
+ intervalCount?: number;
+ isAnnual?: boolean;
+ trial?: boolean;
+ excludeBase?: boolean;
+ isDefault?: boolean;
+ isAddOn?: boolean;
+ freeTrial?: CreateFreeTrial;
+ forcePaidDefault?: boolean;
}) => {
- let price = 0;
- if (type == "pro") {
- price = 20;
- } else if (type == "premium") {
- price = 50;
- } else if (type == "growth") {
- price = 100;
- }
+ let price = 0;
+ if (type == "pro") {
+ price = 20;
+ } else if (type == "premium") {
+ price = 50;
+ } else if (type == "growth") {
+ price = 100;
+ }
- if (price && !excludeBase) {
- items.push(
- constructPriceItem({
- price: isAnnual ? price * 10 : price,
- interval: isAnnual
- ? BillingInterval.Year
- : interval
- ? interval
- : BillingInterval.Month,
- intervalCount: intervalCount || 1,
- })
- );
- }
+ if (price && !excludeBase) {
+ items.push(
+ constructPriceItem({
+ price: isAnnual ? price * 10 : price,
+ interval: isAnnual
+ ? BillingInterval.Year
+ : interval
+ ? interval
+ : BillingInterval.Month,
+ intervalCount: intervalCount || 1,
+ }),
+ );
+ }
- if (type == "one_off") {
- items.push(
- constructPriceItem({
- price: 10,
- interval: null,
- })
- );
- }
+ if (type == "one_off") {
+ items.push(
+ constructPriceItem({
+ price: 10,
+ interval: null,
+ }),
+ );
+ }
- let id_ =
- id ||
- (isAnnual ? `${type}-annual` : interval ? `${type}-${interval}` : type);
+ let id_ =
+ id ||
+ (isAnnual ? `${type}-annual` : interval ? `${type}-${interval}` : type);
- let product: ProductV2 = {
- id: id_,
- name: id
- ? keyToTitle(id)
- : isAnnual
- ? `${keyToTitle(type)} (Annual)`
- : interval
- ? `${keyToTitle(type)} (${interval})`
- : keyToTitle(type),
- items,
- is_add_on: isAddOn,
- is_default: (type == "free" && isDefault) || forcePaidDefault,
- version: 1,
- group: group || "",
- free_trial:
- freeTrial || trial
- ? (CreateFreeTrialSchema.parse({
- length: 7,
- duration: FreeTrialDuration.Day,
- unique_fingerprint: false,
- card_required: true,
- }) as any)
- : null,
- created_at: Date.now(),
- };
+ let product: ProductV2 = {
+ id: id_,
+ name: id
+ ? keyToTitle(id)
+ : isAnnual
+ ? `${keyToTitle(type)} (Annual)`
+ : interval
+ ? `${keyToTitle(type)} (${interval})`
+ : keyToTitle(type),
+ items,
+ is_add_on: isAddOn,
+ is_default: (type == "free" && isDefault) || forcePaidDefault,
+ version: 1,
+ group: group || "",
+ free_trial:
+ freeTrial || trial
+ ? (CreateFreeTrialSchema.parse({
+ length: 7,
+ duration: FreeTrialDuration.Day,
+ unique_fingerprint: false,
+ card_required: true,
+ }) as any)
+ : null,
+ created_at: Date.now(),
+ };
- return product;
+ return product;
};
export const constructCoupon = ({
- id,
- promoCode,
- discountType = RewardType.FixedDiscount,
- discountValue = 10,
+ id,
+ promoCode,
+ discountType = RewardType.FixedDiscount,
+ discountValue = 10,
}: {
- id: string;
- promoCode: string;
- discountType?: RewardType;
- discountValue?: number;
+ id: string;
+ promoCode: string;
+ discountType?: RewardType;
+ discountValue?: number;
}) => {
- const reward: CreateReward = {
- id,
- name: keyToTitle(id),
- promo_codes: [{ code: promoCode }],
- type: discountType,
- discount_config: {
- discount_value: discountValue,
- duration_type: CouponDurationType.Forever,
- duration_value: 1,
- should_rollover: true,
- apply_to_all: true,
- price_ids: [],
- },
- };
+ const reward: CreateReward = {
+ id,
+ name: keyToTitle(id),
+ promo_codes: [{ code: promoCode }],
+ type: discountType,
+ discount_config: {
+ discount_value: discountValue,
+ duration_type: CouponDurationType.Forever,
+ duration_value: 1,
+ should_rollover: true,
+ apply_to_all: true,
+ price_ids: [],
+ },
+ };
- return reward;
+ return reward;
};
diff --git a/server/src/utils/scriptUtils/genScriptUtils.ts b/server/src/utils/scriptUtils/genScriptUtils.ts
index cd758f4d6..5a3af1a27 100644
--- a/server/src/utils/scriptUtils/genScriptUtils.ts
+++ b/server/src/utils/scriptUtils/genScriptUtils.ts
@@ -2,27 +2,27 @@ import csv from "csv-parser";
import fs from "fs";
export const parseCsv = ({
- path,
- delimiter = ",",
+ path,
+ delimiter = ",",
}: {
- path: string;
- delimiter?: string;
+ path: string;
+ delimiter?: string;
}) => {
- return new Promise((resolve, reject) => {
- const stream = fs.createReadStream(path);
- const results: any[] = [];
- let 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;
+ return new Promise((resolve, reject) => {
+ const stream = fs.createReadStream(path);
+ const results: any[] = [];
+ let 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/getAllAutumnCustomers.ts b/server/src/utils/scriptUtils/getAll/getAllAutumnCustomers.ts
index d4267d018..b1ae00cec 100644
--- a/server/src/utils/scriptUtils/getAll/getAllAutumnCustomers.ts
+++ b/server/src/utils/scriptUtils/getAll/getAllAutumnCustomers.ts
@@ -1,43 +1,43 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- ACTIVE_STATUSES,
- RELEVANT_STATUSES,
+ ACTIVE_STATUSES,
+ RELEVANT_STATUSES,
} from "@/internal/customers/cusProducts/CusProductService.js";
import {
- AppEnv,
- CusProductStatus,
- Customer,
- customers,
- entities,
- Entity,
- FullCusProduct,
- FullCustomer,
+ AppEnv,
+ CusProductStatus,
+ Customer,
+ customers,
+ entities,
+ Entity,
+ FullCusProduct,
+ FullCustomer,
} from "@autumn/shared";
import { and, desc, eq, gt, lt, sql } from "drizzle-orm";
let cusProductsQuery = ({
- orgId,
- env,
- inStatuses = RELEVANT_STATUSES,
- lastProductId,
- pageSize = 250,
+ orgId,
+ env,
+ inStatuses = RELEVANT_STATUSES,
+ lastProductId,
+ pageSize = 250,
}: {
- orgId: string;
- env: AppEnv;
- inStatuses?: CusProductStatus[];
- lastProductId?: string;
- pageSize?: number;
+ orgId: string;
+ env: AppEnv;
+ inStatuses?: CusProductStatus[];
+ lastProductId?: string;
+ pageSize?: number;
}) => {
- const withStatusFilter = () => {
- return inStatuses
- ? sql`AND cp.status = ANY(ARRAY[${sql.join(
- inStatuses.map((status) => sql`${status}`),
- sql`, `
- )}])`
- : sql``;
- };
+ const withStatusFilter = () => {
+ return inStatuses
+ ? sql`AND cp.status = ANY(ARRAY[${sql.join(
+ inStatuses.map((status) => sql`${status}`),
+ sql`, `,
+ )}])`
+ : sql``;
+ };
- return sql`
+ return sql`
SELECT
cp.*,
row_to_json(prod) AS product,
@@ -97,136 +97,136 @@ let cusProductsQuery = ({
`;
};
export const getAllFullCusProducts = async ({
- db,
- orgId,
- env,
- inStatuses = RELEVANT_STATUSES,
+ db,
+ orgId,
+ env,
+ inStatuses = RELEVANT_STATUSES,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- inStatuses?: CusProductStatus[];
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ inStatuses?: CusProductStatus[];
}) => {
- let lastProductId = "";
- let allData: any[] = [];
- let pageSize = 500;
+ let lastProductId = "";
+ let allData: any[] = [];
+ let pageSize = 500;
- while (true) {
- const data = await db.execute(
- cusProductsQuery({
- orgId,
- env,
- inStatuses,
- lastProductId,
- pageSize,
- })
- );
+ while (true) {
+ const data = await db.execute(
+ cusProductsQuery({
+ orgId,
+ env,
+ inStatuses,
+ lastProductId,
+ pageSize,
+ }),
+ );
- if (data.length === 0) break;
+ if (data.length === 0) break;
- console.log(`Fetched ${data.length} customer products`);
- allData.push(...data);
- lastProductId = data[data.length - 1].id as string;
- }
+ console.log(`Fetched ${data.length} customer products`);
+ allData.push(...data);
+ lastProductId = data[data.length - 1].id as string;
+ }
- return allData as FullCusProduct[];
+ return allData as FullCusProduct[];
};
export const getAllCustomers = async ({
- db,
- orgId,
- env,
+ db,
+ orgId,
+ env,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
}) => {
- let lastCustomerId = "";
- let allData: any[] = [];
- let pageSize = 500;
+ let lastCustomerId = "";
+ let allData: any[] = [];
+ let pageSize = 500;
- while (true) {
- const data = await db.query.customers.findMany({
- where: and(
- eq(customers.org_id, orgId),
- eq(customers.env, env),
- lastCustomerId ? lt(customers.internal_id, lastCustomerId) : undefined
- ),
- orderBy: [desc(customers.internal_id)],
- limit: pageSize,
- });
+ while (true) {
+ const data = await db.query.customers.findMany({
+ where: and(
+ eq(customers.org_id, orgId),
+ eq(customers.env, env),
+ lastCustomerId ? lt(customers.internal_id, lastCustomerId) : undefined,
+ ),
+ orderBy: [desc(customers.internal_id)],
+ limit: pageSize,
+ });
- if (data.length === 0) break;
+ if (data.length === 0) break;
- console.log(`Fetched ${data.length} customers`);
- allData.push(...data);
- lastCustomerId = data[data.length - 1].internal_id as string;
- }
+ console.log(`Fetched ${data.length} customers`);
+ allData.push(...data);
+ lastCustomerId = data[data.length - 1].internal_id as string;
+ }
- return allData as Customer[];
+ return allData as Customer[];
};
export const getAllFullCustomers = async ({
- db,
- orgId,
- env,
+ db,
+ orgId,
+ env,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
}) => {
- let [customers, fullCusProducts] = await Promise.all([
- getAllCustomers({ db, orgId, env }),
- getAllFullCusProducts({ db, orgId, env }),
- ]);
+ let [customers, fullCusProducts] = await Promise.all([
+ getAllCustomers({ db, orgId, env }),
+ getAllFullCusProducts({ db, orgId, env }),
+ ]);
- let cusProdMap: Record = {};
- for (const cp of fullCusProducts) {
- let internalCusId = cp.internal_customer_id;
- if (!cusProdMap[internalCusId]) {
- cusProdMap[internalCusId] = [];
- }
- cusProdMap[internalCusId].push(cp);
- }
+ let cusProdMap: Record = {};
+ for (const cp of fullCusProducts) {
+ let internalCusId = cp.internal_customer_id;
+ if (!cusProdMap[internalCusId]) {
+ cusProdMap[internalCusId] = [];
+ }
+ cusProdMap[internalCusId].push(cp);
+ }
- return customers.map((customer) => {
- return {
- ...customer,
- customer_products: cusProdMap[customer.internal_id] || [],
- };
- }) as FullCustomer[];
+ return customers.map((customer) => {
+ return {
+ ...customer,
+ customer_products: cusProdMap[customer.internal_id] || [],
+ };
+ }) as FullCustomer[];
};
export const getAllEntities = async ({
- db,
- orgId,
- env,
+ db,
+ orgId,
+ env,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
}) => {
- let lastEntityId = "";
- let allData: any[] = [];
- let pageSize = 500;
+ let lastEntityId = "";
+ let allData: any[] = [];
+ let pageSize = 500;
- while (true) {
- const data = await db.query.entities.findMany({
- where: and(
- eq(entities.org_id, orgId),
- eq(entities.env, env),
- lastEntityId ? lt(entities.internal_id, lastEntityId) : undefined
- ),
- orderBy: [desc(entities.internal_id)],
- limit: pageSize,
- });
+ while (true) {
+ const data = await db.query.entities.findMany({
+ where: and(
+ eq(entities.org_id, orgId),
+ eq(entities.env, env),
+ lastEntityId ? lt(entities.internal_id, lastEntityId) : undefined,
+ ),
+ orderBy: [desc(entities.internal_id)],
+ limit: pageSize,
+ });
- if (data.length === 0) break;
+ if (data.length === 0) break;
- console.log(`Fetched ${data.length} entities`);
- allData.push(...data);
- lastEntityId = data[data.length - 1].internal_id as string;
- }
+ console.log(`Fetched ${data.length} entities`);
+ allData.push(...data);
+ lastEntityId = data[data.length - 1].internal_id as string;
+ }
- return allData as Entity[];
+ return allData as Entity[];
};
diff --git a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts b/server/src/utils/scriptUtils/getAll/getAllCusProds.ts
index 3b11d70d8..f4e6ce9c7 100644
--- a/server/src/utils/scriptUtils/getAll/getAllCusProds.ts
+++ b/server/src/utils/scriptUtils/getAll/getAllCusProds.ts
@@ -3,26 +3,24 @@ import { AppEnv, FullCusProduct } from "@autumn/shared";
import { sql } from "drizzle-orm";
let cusProductsQuery = ({
- lastProductId,
- internalProductId,
- pageSize = 250,
+ lastProductId,
+ internalProductId,
+ pageSize = 250,
}: {
- lastProductId?: string;
- internalProductId?: string;
- pageSize?: number;
+ 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``;
- // };
+ // const withStatusFilter = () => {
+ // return inStatuses
+ // ? sql`AND cp.status = ANY(ARRAY[${sql.join(
+ // inStatuses.map((status) => sql`${status}`),
+ // sql`, `,
+ // )}])`
+ // : sql``;
+ // };
-
-
- return sql`
+ return sql`
SELECT
cp.*,
row_to_json(prod) AS product,
@@ -82,31 +80,31 @@ let cusProductsQuery = ({
};
export const getAllFullCusProducts = async ({
- db,
- internalProductId,
+ db,
+ internalProductId,
}: {
- db: DrizzleCli;
- internalProductId: string;
+ db: DrizzleCli;
+ internalProductId: string;
}) => {
- let lastProductId = "";
- let allData: any[] = [];
- let pageSize = 500;
+ let lastProductId = "";
+ let allData: any[] = [];
+ let pageSize = 500;
- while (true) {
- const data = await db.execute(
- cusProductsQuery({
- lastProductId,
- pageSize,
- internalProductId,
- }),
- );
+ while (true) {
+ const data = await db.execute(
+ cusProductsQuery({
+ lastProductId,
+ pageSize,
+ internalProductId,
+ }),
+ );
- if (data.length === 0) break;
+ if (data.length === 0) break;
- console.log(`Fetched ${data.length} customer products`);
- allData.push(...data);
- lastProductId = data[data.length - 1].id as string;
- }
+ console.log(`Fetched ${data.length} customer products`);
+ allData.push(...data);
+ lastProductId = data[data.length - 1].id as string;
+ }
- return allData as FullCusProduct[];
-};
\ No newline at end of file
+ return allData as FullCusProduct[];
+};
diff --git a/server/src/utils/scriptUtils/getAll/getAllOrgs.ts b/server/src/utils/scriptUtils/getAll/getAllOrgs.ts
index 65535e6c3..9ba22a3b8 100644
--- a/server/src/utils/scriptUtils/getAll/getAllOrgs.ts
+++ b/server/src/utils/scriptUtils/getAll/getAllOrgs.ts
@@ -2,24 +2,24 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { organizations } from "@autumn/shared";
export const getAllOrgs = async (db: DrizzleCli) => {
- const orgs = [];
- let offset = 0;
- const limit = 200;
+ const orgs = [];
+ let offset = 0;
+ const limit = 200;
- while (true) {
- const batch = await db
- .select()
- .from(organizations)
- .limit(limit)
- .offset(offset);
- orgs.push(...batch);
+ while (true) {
+ const batch = await db
+ .select()
+ .from(organizations)
+ .limit(limit)
+ .offset(offset);
+ orgs.push(...batch);
- if (batch.length < limit) {
- break;
- }
+ if (batch.length < limit) {
+ break;
+ }
- offset += limit;
- console.log(`Fetched ${orgs.length} orgs`);
- }
- return orgs;
+ offset += limit;
+ console.log(`Fetched ${orgs.length} orgs`);
+ }
+ return orgs;
};
diff --git a/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts b/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts
index 6bc04e399..c25528061 100644
--- a/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts
+++ b/server/src/utils/scriptUtils/getAll/getAllStripeSubs.ts
@@ -2,94 +2,94 @@ import { timeout } from "@/utils/genUtils.js";
import { Stripe } from "stripe";
export const getAllStripeSubscriptions = async ({
- numPages,
- limit = 100,
- stripeCli,
- waitForSeconds,
+ numPages,
+ limit = 100,
+ stripeCli,
+ waitForSeconds,
}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
- waitForSeconds?: number;
+ numPages?: number;
+ limit?: number;
+ stripeCli: Stripe;
+ waitForSeconds?: number;
}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allSubscriptions: any[] = [];
+ 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"],
- });
+ let pageCount = 0;
+ while (hasMore) {
+ const response: any = await stripeCli.subscriptions.list({
+ limit,
+ starting_after: startingAfter || undefined,
+ expand: ["data.discounts.coupon"],
+ });
- allSubscriptions.push(...response.data);
+ allSubscriptions.push(...response.data);
- hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
+ hasMore = response.has_more;
+ startingAfter = response.data[response.data.length - 1].id;
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
+ pageCount++;
+ if (numPages && pageCount >= numPages) {
+ break;
+ }
- console.log("Fetched", allSubscriptions.length, "subscriptions");
- if (waitForSeconds) {
- await timeout(1000);
- }
- }
+ console.log("Fetched", allSubscriptions.length, "subscriptions");
+ if (waitForSeconds) {
+ await timeout(1000);
+ }
+ }
- return {
- subscriptions: allSubscriptions,
- total: allSubscriptions.length,
- };
+ return {
+ subscriptions: allSubscriptions,
+ total: allSubscriptions.length,
+ };
};
export const getAllStripeSchedules = async ({
- numPages,
- limit = 100,
- stripeCli,
- waitForSeconds,
+ numPages,
+ limit = 100,
+ stripeCli,
+ waitForSeconds,
}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
- waitForSeconds?: number;
+ numPages?: number;
+ limit?: number;
+ stripeCli: Stripe;
+ waitForSeconds?: number;
}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allSchedules: any[] = [];
+ 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"],
- });
+ 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;
- }
+ if (response.data.length === 0) {
+ break;
+ }
- allSchedules.push(...response.data);
+ allSchedules.push(...response.data);
- hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
+ hasMore = response.has_more;
+ startingAfter = response.data[response.data.length - 1].id;
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
+ pageCount++;
+ if (numPages && pageCount >= numPages) {
+ break;
+ }
- console.log("Fetched", allSchedules.length, "schedules");
- if (waitForSeconds) {
- await timeout(1000);
- }
- }
+ console.log("Fetched", allSchedules.length, "schedules");
+ if (waitForSeconds) {
+ await timeout(1000);
+ }
+ }
- return {
- schedules: allSchedules,
- total: allSchedules.length,
- };
+ return {
+ schedules: allSchedules,
+ total: allSchedules.length,
+ };
};
diff --git a/server/src/utils/scriptUtils/getAll/getAllUsers.ts b/server/src/utils/scriptUtils/getAll/getAllUsers.ts
index 52b02e8b5..101c895fc 100644
--- a/server/src/utils/scriptUtils/getAll/getAllUsers.ts
+++ b/server/src/utils/scriptUtils/getAll/getAllUsers.ts
@@ -4,25 +4,25 @@ import { user } from "@autumn/shared";
import { desc } from "drizzle-orm";
export const getAllUsers = async (db: DrizzleCli) => {
- const users = [];
- let offset = 0;
- const limit = 200;
+ 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);
+ 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;
- }
+ if (batch.length < limit) {
+ break;
+ }
- offset += limit;
- console.log(`Fetched ${users.length} users`);
- }
- return users;
+ offset += limit;
+ console.log(`Fetched ${users.length} users`);
+ }
+ return users;
};
diff --git a/server/src/utils/scriptUtils/initCustomer.ts b/server/src/utils/scriptUtils/initCustomer.ts
index 930ebf3ee..9c58a730b 100644
--- a/server/src/utils/scriptUtils/initCustomer.ts
+++ b/server/src/utils/scriptUtils/initCustomer.ts
@@ -1,8 +1,8 @@
import {
- type AppEnv,
- type Customer,
- type Organization,
- ProcessorType,
+ type AppEnv,
+ type Customer,
+ type Organization,
+ ProcessorType,
} from "@autumn/shared";
import type { Autumn } from "autumn-js";
import type Stripe from "stripe";
@@ -10,236 +10,236 @@ import type { DrizzleCli } from "@/db/initDrizzle.js";
import { CusService } from "@/internal/customers/CusService.js";
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
import {
- attachPmToCus,
- createStripeCustomer,
+ attachPmToCus,
+ createStripeCustomer,
} from "../../external/stripe/stripeCusUtils.js";
import { createStripeCli } from "../../external/stripe/utils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
export const createCusInStripe = async ({
- customer,
- org,
- env,
- db,
- testClockId,
+ customer,
+ org,
+ env,
+ db,
+ testClockId,
}: {
- customer: Customer;
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
- testClockId?: string;
+ customer: Customer;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
+ testClockId?: string;
}) => {
- const stripeCustomer = await createStripeCustomer({
- org,
- env,
- customer,
- testClockId,
- });
+ const stripeCustomer = await createStripeCustomer({
+ org,
+ env,
+ customer,
+ testClockId,
+ });
- await CusService.update({
- db,
- internalCusId: customer.internal_id,
- update: {
- processor: {
- type: ProcessorType.Stripe,
- id: stripeCustomer.id,
- },
- },
- });
+ await CusService.update({
+ db,
+ internalCusId: customer.internal_id,
+ update: {
+ processor: {
+ type: ProcessorType.Stripe,
+ id: stripeCustomer.id,
+ },
+ },
+ });
- customer.processor = {
- id: stripeCustomer.id,
- type: "stripe",
- };
+ customer.processor = {
+ id: stripeCustomer.id,
+ type: "stripe",
+ };
- return stripeCustomer;
+ return stripeCustomer;
};
export const initCustomer = async ({
- autumn,
- customerId,
- fingerprint,
- org,
- env,
- db,
- attachPm,
- withTestClock = true,
+ autumn,
+ customerId,
+ fingerprint,
+ org,
+ env,
+ db,
+ attachPm,
+ withTestClock = true,
}: {
- autumn: Autumn | AutumnInt;
- customerId: string;
- fingerprint?: string;
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
- attachPm?: "success" | "fail";
- withTestClock?: boolean;
+ autumn: Autumn | AutumnInt;
+ customerId: string;
+ fingerprint?: string;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
+ attachPm?: "success" | "fail";
+ withTestClock?: boolean;
}) => {
- const customerData = {
- id: customerId,
- name: customerId,
- email: `${customerId}@example.com`,
- fingerprint,
- };
+ const customerData = {
+ id: customerId,
+ name: customerId,
+ email: `${customerId}@example.com`,
+ fingerprint,
+ };
- const customer = await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env: env,
- });
+ const customer = await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env: env,
+ });
- if (customer) {
- await autumn.customers.delete(customerId);
- await deleteCusCache({
- db,
- customerId: customerId,
- org,
- env: env,
- });
- }
+ if (customer) {
+ await autumn.customers.delete(customerId);
+ await deleteCusCache({
+ db,
+ customerId: customerId,
+ org,
+ env: env,
+ });
+ }
- try {
- const res = await autumn.customers.create(customerData);
+ try {
+ const res = await autumn.customers.create(customerData);
- const customer = (await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env: env,
- })) as Customer;
+ const customer = (await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env: env,
+ })) as Customer;
- const stripeCli = createStripeCli({ org: org, env: env });
- let testClockId = "";
- if (withTestClock) {
- const testClock = await stripeCli.testHelpers.testClocks.create({
- frozen_time: Math.floor(Date.now() / 1000),
- });
- testClockId = testClock.id;
- }
+ const stripeCli = createStripeCli({ org: org, env: env });
+ let testClockId = "";
+ if (withTestClock) {
+ const testClock = await stripeCli.testHelpers.testClocks.create({
+ frozen_time: Math.floor(Date.now() / 1000),
+ });
+ testClockId = testClock.id;
+ }
- if (attachPm) {
- await attachPmToCus({
- customer,
- org: org,
- env: env,
- db: db,
- willFail: attachPm === "fail",
- testClockId: testClockId || undefined,
- });
- } else {
- await createCusInStripe({
- customer,
- org,
- env,
- db,
- testClockId: testClockId || undefined,
- });
- }
+ if (attachPm) {
+ await attachPmToCus({
+ customer,
+ org: org,
+ env: env,
+ db: db,
+ willFail: attachPm === "fail",
+ testClockId: testClockId || undefined,
+ });
+ } else {
+ await createCusInStripe({
+ customer,
+ org,
+ env,
+ db,
+ testClockId: testClockId || undefined,
+ });
+ }
- return {
- customer,
- testClockId: testClockId,
- };
- } catch (error) {
- console.log("Failed to create customer", error);
- throw error;
- }
+ return {
+ customer,
+ testClockId: testClockId,
+ };
+ } catch (error) {
+ console.log("Failed to create customer", error);
+ throw error;
+ }
};
export const attachPaymentMethod = async ({
- stripeCli,
- stripeCusId,
- type,
+ stripeCli,
+ stripeCusId,
+ type,
}: {
- stripeCli: Stripe;
- stripeCusId: string;
- type: "success" | "fail";
+ stripeCli: Stripe;
+ stripeCusId: string;
+ type: "success" | "fail";
}) => {
- try {
- const token = type === "fail" ? "tok_chargeCustomerFail" : "tok_visa";
- const pm = await stripeCli.paymentMethods.create({
- type: "card",
- card: {
- token,
- },
- });
+ try {
+ const token = type === "fail" ? "tok_chargeCustomerFail" : "tok_visa";
+ const pm = await stripeCli.paymentMethods.create({
+ type: "card",
+ card: {
+ token,
+ },
+ });
- await stripeCli.paymentMethods.attach(pm.id, {
- customer: stripeCusId,
- });
+ await stripeCli.paymentMethods.attach(pm.id, {
+ customer: stripeCusId,
+ });
- await stripeCli.customers.update(stripeCusId, {
- invoice_settings: {
- default_payment_method: pm.id,
- },
- });
- } catch (error) {
- console.log("failed to attach payment method", error);
- }
+ await stripeCli.customers.update(stripeCusId, {
+ invoice_settings: {
+ default_payment_method: pm.id,
+ },
+ });
+ } catch (error) {
+ console.log("failed to attach payment method", error);
+ }
};
// V2 initializes the customer in Stripe, then creates the customer in Autumn
export const initCustomerV2 = async ({
- autumn,
- customerId,
- org,
- env,
- db,
- attachPm,
- withTestClock = true,
+ autumn,
+ customerId,
+ org,
+ env,
+ db,
+ attachPm,
+ withTestClock = true,
}: {
- autumn: Autumn;
- customerId: string;
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
- attachPm?: "success" | "fail";
- withTestClock?: boolean;
+ autumn: Autumn;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
+ attachPm?: "success" | "fail";
+ withTestClock?: boolean;
}) => {
- const name = customerId;
- const email = `${customerId}@example.com`;
- const fingerprint_ = "";
- const stripeCli = createStripeCli({ org, env });
+ const name = customerId;
+ const email = `${customerId}@example.com`;
+ const fingerprint_ = "";
+ const stripeCli = createStripeCli({ org, env });
- let testClockId;
+ let testClockId;
- if (withTestClock) {
- const testClock = await stripeCli.testHelpers.testClocks.create({
- frozen_time: Math.floor(Date.now() / 1000),
- });
- testClockId = testClock.id;
- }
+ if (withTestClock) {
+ const testClock = await stripeCli.testHelpers.testClocks.create({
+ frozen_time: Math.floor(Date.now() / 1000),
+ });
+ testClockId = testClock.id;
+ }
- // 1. Create stripe customer
- const stripeCus = await stripeCli.customers.create({
- email,
- name,
- test_clock: testClockId,
- });
+ // 1. Create stripe customer
+ const stripeCus = await stripeCli.customers.create({
+ email,
+ name,
+ test_clock: testClockId,
+ });
- // 2. Create customer
- try {
- await autumn.customers.delete(customerId);
- } catch (error) {}
- await autumn.customers.create({
- id: customerId,
- name,
- email,
- fingerprint: fingerprint_,
- // @ts-expect-error
- stripe_id: stripeCus.id,
- });
+ // 2. Create customer
+ try {
+ await autumn.customers.delete(customerId);
+ } catch (error) {}
+ await autumn.customers.create({
+ id: customerId,
+ name,
+ email,
+ fingerprint: fingerprint_,
+ // @ts-expect-error
+ stripe_id: stripeCus.id,
+ });
- // 3. Attach payment method
- if (attachPm) {
- await attachPaymentMethod({
- stripeCli,
- stripeCusId: stripeCus.id,
- type: attachPm,
- });
- }
+ // 3. Attach payment method
+ if (attachPm) {
+ await attachPaymentMethod({
+ stripeCli,
+ stripeCusId: stripeCus.id,
+ type: attachPm,
+ });
+ }
- return {
- testClockId: testClockId || "",
- };
+ return {
+ testClockId: testClockId || "",
+ };
};
diff --git a/server/src/utils/scriptUtils/logUtils/logSubItems.ts b/server/src/utils/scriptUtils/logUtils/logSubItems.ts
index f6fe316f6..fa6636541 100644
--- a/server/src/utils/scriptUtils/logUtils/logSubItems.ts
+++ b/server/src/utils/scriptUtils/logUtils/logSubItems.ts
@@ -2,25 +2,25 @@ import { subItemToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
export const logSubItems = ({
- sub,
- subItems,
+ sub,
+ subItems,
}: {
- sub?: Stripe.Subscription;
- subItems?: Stripe.SubscriptionItem[];
+ sub?: Stripe.Subscription;
+ subItems?: Stripe.SubscriptionItem[];
}) => {
- let finalSubItems = subItems || sub!.items.data;
- for (const item of finalSubItems) {
- let isMetered = item.price.recurring?.usage_type === "metered";
- let isTiered = item.price.billing_scheme === "tiered";
+ let finalSubItems = subItems || sub!.items.data;
+ for (const item of finalSubItems) {
+ let isMetered = item.price.recurring?.usage_type === "metered";
+ let isTiered = item.price.billing_scheme === "tiered";
- if (isMetered) {
- console.log(`Usage price`);
- } else {
- let price = item.price.unit_amount! / 100;
- let subInterval = subItemToAutumnInterval(item);
- console.log(
- `${price} ${item.price.currency}${item.quantity !== 1 ? ` x ${item.quantity}` : ""} / ${subInterval?.intervalCount} ${subInterval?.interval}`
- );
- }
- }
+ if (isMetered) {
+ console.log(`Usage price`);
+ } else {
+ let price = item.price.unit_amount! / 100;
+ let subInterval = subItemToAutumnInterval(item);
+ console.log(
+ `${price} ${item.price.currency}${item.quantity !== 1 ? ` x ${item.quantity}` : ""} / ${subInterval?.intervalCount} ${subInterval?.interval}`,
+ );
+ }
+ }
};
diff --git a/server/src/utils/scriptUtils/resetOrgStripe.ts b/server/src/utils/scriptUtils/resetOrgStripe.ts
index 08c6c3a31..6b13789cc 100644
--- a/server/src/utils/scriptUtils/resetOrgStripe.ts
+++ b/server/src/utils/scriptUtils/resetOrgStripe.ts
@@ -1,25 +1,25 @@
import { Organization, AppEnv } from "@autumn/shared";
import {
- deactivateStripeMeters,
- deleteAllStripeProducts,
+ deactivateStripeMeters,
+ deleteAllStripeProducts,
} from "@/external/stripe/stripeProductUtils.js";
import { deleteAllStripeCustomers } from "@/external/stripe/stripeCusUtils.js";
export const resetOrgStripe = async ({ org }: { org: Organization }) => {
- const env = AppEnv.Sandbox;
+ const env = AppEnv.Sandbox;
- await deleteAllStripeCustomers({
- org,
- env,
- });
+ await deleteAllStripeCustomers({
+ org,
+ env,
+ });
- await deleteAllStripeProducts({
- org,
- env,
- });
+ await deleteAllStripeProducts({
+ org,
+ env,
+ });
- await deactivateStripeMeters({
- org,
- env,
- });
+ await deactivateStripeMeters({
+ org,
+ env,
+ });
};
diff --git a/server/src/utils/scriptUtils/scriptUtils.ts b/server/src/utils/scriptUtils/scriptUtils.ts
index 9679699bb..f46e59b21 100644
--- a/server/src/utils/scriptUtils/scriptUtils.ts
+++ b/server/src/utils/scriptUtils/scriptUtils.ts
@@ -14,188 +14,188 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { timeout } from "@/utils/genUtils.js";
export const getAllStripeCustomers = async ({
- numPages,
- limit = 100,
- stripeCli,
+ numPages,
+ limit = 100,
+ stripeCli,
}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
+ numPages?: number;
+ limit?: number;
+ stripeCli: Stripe;
}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allCustomers: any[] = [];
+ let hasMore = true;
+ let startingAfter: string | null = null;
+ const allCustomers: any[] = [];
- let pageCount = 0;
- while (hasMore) {
- const response: any = await stripeCli.customers.list({
- limit,
- starting_after: startingAfter || undefined,
- });
+ let pageCount = 0;
+ while (hasMore) {
+ const response: any = await stripeCli.customers.list({
+ limit,
+ starting_after: startingAfter || undefined,
+ });
- allCustomers.push(...response.data);
+ allCustomers.push(...response.data);
- hasMore = response.has_more;
+ hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
+ startingAfter = response.data[response.data.length - 1].id;
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
- }
+ pageCount++;
+ if (numPages && pageCount >= numPages) {
+ break;
+ }
+ }
- return {
- customers: allCustomers,
- total: allCustomers.length,
- };
+ return {
+ customers: allCustomers,
+ total: allCustomers.length,
+ };
};
export const getAllStripeSubscriptions = async ({
- numPages,
- limit = 100,
- stripeCli,
- waitForSeconds,
+ numPages,
+ limit = 100,
+ stripeCli,
+ waitForSeconds,
}: {
- numPages?: number;
- limit?: number;
- stripeCli: Stripe;
- waitForSeconds?: number;
+ numPages?: number;
+ limit?: number;
+ stripeCli: Stripe;
+ waitForSeconds?: number;
}) => {
- let hasMore = true;
- let startingAfter: string | null = null;
- const allSubscriptions: any[] = [];
+ 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"],
- });
+ let pageCount = 0;
+ while (hasMore) {
+ const response: any = await stripeCli.subscriptions.list({
+ limit,
+ starting_after: startingAfter || undefined,
+ expand: ["data.discounts"],
+ });
- allSubscriptions.push(...response.data);
+ allSubscriptions.push(...response.data);
- hasMore = response.has_more;
- startingAfter = response.data[response.data.length - 1].id;
+ hasMore = response.has_more;
+ startingAfter = response.data[response.data.length - 1].id;
- pageCount++;
- if (numPages && pageCount >= numPages) {
- break;
- }
+ pageCount++;
+ if (numPages && pageCount >= numPages) {
+ break;
+ }
- console.log("Fetched", allSubscriptions.length, "subscriptions");
- if (waitForSeconds) {
- await timeout(1000);
- }
- }
+ console.log("Fetched", allSubscriptions.length, "subscriptions");
+ if (waitForSeconds) {
+ await timeout(1000);
+ }
+ }
- return {
- subscriptions: allSubscriptions,
- total: allSubscriptions.length,
- };
+ return {
+ subscriptions: allSubscriptions,
+ total: allSubscriptions.length,
+ };
};
export const getCusSubsAndProducts = async (path: string) => {
- const customers = JSON.parse(
- fs.readFileSync(`${path}/customers.json`, "utf8")
- ) as Stripe.Customer[];
- const subs = JSON.parse(
- fs.readFileSync(`${path}/subscriptions.json`, "utf8")
- ) as Stripe.Subscription[];
- const products = JSON.parse(fs.readFileSync(`${path}/products.json`, "utf8"));
+ const customers = JSON.parse(
+ fs.readFileSync(`${path}/customers.json`, "utf8"),
+ ) as Stripe.Customer[];
+ const subs = JSON.parse(
+ fs.readFileSync(`${path}/subscriptions.json`, "utf8"),
+ ) as Stripe.Subscription[];
+ const products = JSON.parse(fs.readFileSync(`${path}/products.json`, "utf8"));
- return { customers, subs, products };
+ return { customers, subs, products };
};
export const saveCusSubsAndProducts = async ({
- stripeCli,
- path,
- orgId,
- env,
+ stripeCli,
+ path,
+ orgId,
+ env,
}: {
- stripeCli: Stripe;
- path: string;
- orgId: string;
- env: AppEnv;
+ stripeCli: Stripe;
+ path: string;
+ orgId: string;
+ env: AppEnv;
}) => {
- // Create directory if it doesn't exist
- if (!fs.existsSync(path)) {
- fs.mkdirSync(path, { recursive: true });
- }
+ // Create directory if it doesn't exist
+ if (!fs.existsSync(path)) {
+ fs.mkdirSync(path, { recursive: true });
+ }
- const { data: products } = await stripeCli.products.list({
- limit: 100,
- });
- fs.writeFileSync(`${path}/products.json`, JSON.stringify(products, null, 2));
+ const { data: products } = await stripeCli.products.list({
+ limit: 100,
+ });
+ fs.writeFileSync(`${path}/products.json`, JSON.stringify(products, null, 2));
- const { customers } = await getAllStripeCustomers({
- stripeCli,
- });
- fs.writeFileSync(
- `${path}/customers.json`,
- JSON.stringify(customers, null, 2)
- );
+ const { customers } = await getAllStripeCustomers({
+ stripeCli,
+ });
+ fs.writeFileSync(
+ `${path}/customers.json`,
+ JSON.stringify(customers, null, 2),
+ );
- const { subscriptions } = await getAllStripeSubscriptions({
- stripeCli,
- });
+ const { subscriptions } = await getAllStripeSubscriptions({
+ stripeCli,
+ });
- fs.writeFileSync(
- `${path}/subscriptions.json`,
- JSON.stringify(subscriptions, null, 2)
- );
+ fs.writeFileSync(
+ `${path}/subscriptions.json`,
+ JSON.stringify(subscriptions, null, 2),
+ );
};
export const initScript = async ({
- orgId,
- env,
+ orgId,
+ env,
}: {
- orgId: string;
- env: AppEnv;
+ orgId: string;
+ env: AppEnv;
}) => {
- const [org, autumnProducts, features] = await Promise.all([
- OrgService.get({ db, orgId }),
- ProductService.listFull({
- db,
- orgId,
- env,
- }),
- FeatureService.list({
- db,
- orgId,
- env,
- }),
- ]);
+ const [org, autumnProducts, features] = await Promise.all([
+ OrgService.get({ db, orgId }),
+ ProductService.listFull({
+ db,
+ orgId,
+ env,
+ }),
+ FeatureService.list({
+ db,
+ orgId,
+ env,
+ }),
+ ]);
- const stripeCli: Stripe = createStripeCli({ org, env });
+ const stripeCli: Stripe = createStripeCli({ org, env });
- const logger = createLogger();
+ const logger = createLogger();
- const req: ExtendedRequest = {
- orgId,
- env,
- org,
- db,
- features,
- logger,
- logtail: logger,
- } as ExtendedRequest;
+ const req: ExtendedRequest = {
+ orgId,
+ env,
+ org,
+ db,
+ features,
+ logger,
+ logtail: logger,
+ } as ExtendedRequest;
- return { stripeCli, autumnProducts, req };
+ return { stripeCli, autumnProducts, req };
};
export const getFirstOfNextMonthUnix = (hoursToSub?: number) => {
- let firstOfNextMonth = new UTCDate(new Date());
+ let firstOfNextMonth = new UTCDate(new Date());
- let nextMonth = firstOfNextMonth.getUTCMonth() + 1;
- firstOfNextMonth.setUTCDate(1);
- firstOfNextMonth.setUTCHours(12, 0, 0, 0);
- firstOfNextMonth.setUTCMonth(nextMonth);
+ let nextMonth = firstOfNextMonth.getUTCMonth() + 1;
+ firstOfNextMonth.setUTCDate(1);
+ firstOfNextMonth.setUTCHours(12, 0, 0, 0);
+ firstOfNextMonth.setUTCMonth(nextMonth);
- if (hoursToSub) {
- firstOfNextMonth = subHours(firstOfNextMonth, hoursToSub);
- }
+ if (hoursToSub) {
+ firstOfNextMonth = subHours(firstOfNextMonth, hoursToSub);
+ }
- return firstOfNextMonth.getTime();
+ return firstOfNextMonth.getTime();
};
diff --git a/server/src/utils/scriptUtils/testClockUtils.ts b/server/src/utils/scriptUtils/testClockUtils.ts
index a2a073489..ac7610520 100644
--- a/server/src/utils/scriptUtils/testClockUtils.ts
+++ b/server/src/utils/scriptUtils/testClockUtils.ts
@@ -1,111 +1,111 @@
import {
- addDays,
- addHours,
- addMinutes,
- addMonths,
- addWeeks,
- format,
+ addDays,
+ addHours,
+ addMinutes,
+ addMonths,
+ addWeeks,
+ format,
} from "date-fns";
import { Stripe } from "stripe";
import { timeout } from "../genUtils.js";
export const getStripeNow = async ({
- stripeCli,
- stripeCus,
- stripeSub,
- testClockId,
+ stripeCli,
+ stripeCus,
+ stripeSub,
+ testClockId,
}: {
- stripeCli: Stripe;
- stripeCus?: Stripe.Customer;
- stripeSub?: Stripe.Subscription;
- testClockId?: string;
+ stripeCli: Stripe;
+ stripeCus?: Stripe.Customer;
+ stripeSub?: Stripe.Subscription;
+ testClockId?: string;
}) => {
- if (testClockId) {
- try {
- let stripeClock =
- await stripeCli.testHelpers.testClocks.retrieve(testClockId);
- return stripeClock.frozen_time * 1000;
- } catch (error) {}
- }
+ if (testClockId) {
+ try {
+ let stripeClock =
+ await stripeCli.testHelpers.testClocks.retrieve(testClockId);
+ return stripeClock.frozen_time * 1000;
+ } catch (error) {}
+ }
- if (stripeSub && !stripeSub.livemode && stripeSub.test_clock) {
- try {
- const stripeClock = await stripeCli.testHelpers.testClocks.retrieve(
- stripeSub.test_clock as string,
- );
- return stripeClock.frozen_time * 1000;
- } catch (error) {}
- }
+ if (stripeSub && !stripeSub.livemode && stripeSub.test_clock) {
+ try {
+ const stripeClock = await stripeCli.testHelpers.testClocks.retrieve(
+ stripeSub.test_clock as string,
+ );
+ return stripeClock.frozen_time * 1000;
+ } catch (error) {}
+ }
- if (stripeCus && !stripeCus.livemode && stripeCus.test_clock) {
- try {
- const stripeClock = await stripeCli.testHelpers.testClocks.retrieve(
- stripeCus.test_clock as string,
- );
- return stripeClock.frozen_time * 1000;
- } catch (error) {}
- }
+ if (stripeCus && !stripeCus.livemode && stripeCus.test_clock) {
+ try {
+ const stripeClock = await stripeCli.testHelpers.testClocks.retrieve(
+ stripeCus.test_clock as string,
+ );
+ return stripeClock.frozen_time * 1000;
+ } catch (error) {}
+ }
- return Date.now();
+ return Date.now();
};
export const advanceTestClock = async ({
- stripeCli,
- testClockId,
- startingFrom,
- numberOfDays,
- numberOfWeeks,
- numberOfHours,
- numberOfMonths,
- advanceTo,
- waitForSeconds,
+ stripeCli,
+ testClockId,
+ startingFrom,
+ numberOfDays,
+ numberOfWeeks,
+ numberOfHours,
+ numberOfMonths,
+ advanceTo,
+ waitForSeconds,
}: {
- stripeCli: Stripe;
- testClockId: string;
- numberOfDays?: number;
- startingFrom?: Date;
- numberOfWeeks?: number;
- numberOfHours?: number;
- numberOfMonths?: number;
- advanceTo?: number;
- waitForSeconds?: number;
+ stripeCli: Stripe;
+ testClockId: string;
+ numberOfDays?: number;
+ startingFrom?: Date;
+ numberOfWeeks?: number;
+ numberOfHours?: number;
+ numberOfMonths?: number;
+ advanceTo?: number;
+ waitForSeconds?: number;
}) => {
- if (!startingFrom) {
- startingFrom = new Date();
- }
+ if (!startingFrom) {
+ startingFrom = new Date();
+ }
- if (numberOfDays) {
- advanceTo = addDays(startingFrom, numberOfDays).getTime();
- }
+ if (numberOfDays) {
+ advanceTo = addDays(startingFrom, numberOfDays).getTime();
+ }
- if (numberOfWeeks) {
- advanceTo = addWeeks(startingFrom, numberOfWeeks).getTime();
- }
+ if (numberOfWeeks) {
+ advanceTo = addWeeks(startingFrom, numberOfWeeks).getTime();
+ }
- if (numberOfHours) {
- advanceTo = addHours(startingFrom, numberOfHours).getTime();
- }
+ if (numberOfHours) {
+ advanceTo = addHours(startingFrom, numberOfHours).getTime();
+ }
- if (numberOfMonths) {
- advanceTo = addMonths(startingFrom, numberOfMonths).getTime();
- }
+ if (numberOfMonths) {
+ advanceTo = addMonths(startingFrom, numberOfMonths).getTime();
+ }
- if (!advanceTo) {
- advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
- }
+ if (!advanceTo) {
+ advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
+ }
- console.log(" - Advancing to: ", format(advanceTo, "yyyy MMM dd HH:mm:ss"));
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.floor(advanceTo / 1000),
- });
+ console.log(" - Advancing to: ", format(advanceTo, "yyyy MMM dd HH:mm:ss"));
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.floor(advanceTo / 1000),
+ });
- if (waitForSeconds) {
- await timeout(waitForSeconds * 1000);
- }
+ if (waitForSeconds) {
+ await timeout(waitForSeconds * 1000);
+ }
- return advanceTo;
+ return advanceTo;
- // await timeout(
- // waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING,
- // );
+ // await timeout(
+ // waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING,
+ // );
};
diff --git a/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts b/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts
index 7fed481d3..1f7220f8b 100644
--- a/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts
+++ b/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts
@@ -6,29 +6,29 @@ import { Organization } from "@autumn/shared";
import { AppEnv } from "autumn-js";
export const getCusSub = async ({
- db,
- org,
- customerId,
- productId,
+ db,
+ org,
+ customerId,
+ productId,
}: {
- db: DrizzleCli;
- org: Organization;
- customerId: string;
- productId: string;
+ db: DrizzleCli;
+ org: Organization;
+ customerId: string;
+ productId: string;
}) => {
- const env = AppEnv.Sandbox;
- const stripeCli = createStripeCli({ org, env });
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- env,
- orgId: org.id,
- });
+ const env = AppEnv.Sandbox;
+ const stripeCli = createStripeCli({ org, env });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ env,
+ orgId: org.id,
+ });
- let cusProduct = fullCus.customer_products.find(
- (cp) => cp.product.id == productId
- );
+ let cusProduct = fullCus.customer_products.find(
+ (cp) => cp.product.id == productId,
+ );
- const sub = await cusProductToSub({ cusProduct, stripeCli });
- return sub;
+ const sub = await cusProductToSub({ cusProduct, stripeCli });
+ return sub;
};
diff --git a/server/src/utils/timeUtils.ts b/server/src/utils/timeUtils.ts
index 858013804..30206add5 100644
--- a/server/src/utils/timeUtils.ts
+++ b/server/src/utils/timeUtils.ts
@@ -7,73 +7,73 @@ import { formatUnixToDate } from "./genUtils.js";
// Time conversion constants
export const TIME_MS = {
- SECOND: 1000,
- MINUTE: 1000 * 60,
- HOUR: 1000 * 60 * 60,
- DAY: 1000 * 60 * 60 * 24,
- WEEK: 1000 * 60 * 60 * 24 * 7,
+ SECOND: 1000,
+ MINUTE: 1000 * 60,
+ HOUR: 1000 * 60 * 60,
+ DAY: 1000 * 60 * 60 * 24,
+ WEEK: 1000 * 60 * 60 * 24 * 7,
} as const;
// Time conversion utility functions
export const toMilliseconds = {
- seconds: (n: number) => n * TIME_MS.SECOND,
- minutes: (n: number) => n * TIME_MS.MINUTE,
- hours: (n: number) => n * TIME_MS.HOUR,
- days: (n: number) => n * TIME_MS.DAY,
- weeks: (n: number) => n * TIME_MS.WEEK,
+ seconds: (n: number) => n * TIME_MS.SECOND,
+ minutes: (n: number) => n * TIME_MS.MINUTE,
+ hours: (n: number) => n * TIME_MS.HOUR,
+ days: (n: number) => n * TIME_MS.DAY,
+ weeks: (n: number) => n * TIME_MS.WEEK,
} as const;
// 1. Get next entitlement reset
export const getNextEntitlementReset = (
- prevReset: UTCDate | null,
- interval: EntInterval,
- intervalCount: number
+ prevReset: UTCDate | null,
+ interval: EntInterval,
+ intervalCount: number,
) => {
- if (!prevReset) {
- prevReset = new UTCDate();
- }
+ if (!prevReset) {
+ prevReset = new UTCDate();
+ }
- switch (interval) {
- case EntInterval.Minute:
- return add(prevReset, { minutes: intervalCount });
- case EntInterval.Hour:
- return add(prevReset, { hours: intervalCount });
- case EntInterval.Day:
- return add(prevReset, { days: intervalCount });
- case EntInterval.Week:
- return add(prevReset, { weeks: intervalCount });
- case EntInterval.Month:
- return add(prevReset, { months: intervalCount });
- case EntInterval.Quarter:
- return add(prevReset, { months: intervalCount * 3 });
- case EntInterval.SemiAnnual:
- return add(prevReset, { months: intervalCount * 6 });
- case EntInterval.Year:
- return add(prevReset, { years: intervalCount });
- default:
- throw new Error("Invalid duration");
- }
+ switch (interval) {
+ case EntInterval.Minute:
+ return add(prevReset, { minutes: intervalCount });
+ case EntInterval.Hour:
+ return add(prevReset, { hours: intervalCount });
+ case EntInterval.Day:
+ return add(prevReset, { days: intervalCount });
+ case EntInterval.Week:
+ return add(prevReset, { weeks: intervalCount });
+ case EntInterval.Month:
+ return add(prevReset, { months: intervalCount });
+ case EntInterval.Quarter:
+ return add(prevReset, { months: intervalCount * 3 });
+ case EntInterval.SemiAnnual:
+ return add(prevReset, { months: intervalCount * 6 });
+ case EntInterval.Year:
+ return add(prevReset, { years: intervalCount });
+ default:
+ throw new Error("Invalid duration");
+ }
};
export const getNextResetAt = ({
- curReset,
- interval,
- intervalCount = 1,
+ curReset,
+ interval,
+ intervalCount = 1,
}: {
- curReset: UTCDate | null;
- interval: EntInterval;
- intervalCount?: number;
+ curReset: UTCDate | null;
+ interval: EntInterval;
+ intervalCount?: number;
}) => {
- while (true) {
- const nextReset = getNextEntitlementReset(
- curReset,
- interval,
- intervalCount || 1
- );
+ while (true) {
+ const nextReset = getNextEntitlementReset(
+ curReset,
+ interval,
+ intervalCount || 1,
+ );
- if (nextReset.getTime() > Date.now()) {
- return nextReset.getTime();
- }
- curReset = nextReset;
- }
+ if (nextReset.getTime() > Date.now()) {
+ return nextReset.getTime();
+ }
+ curReset = nextReset;
+ }
};
diff --git a/server/src/utils/versionUtils.ts b/server/src/utils/versionUtils.ts
index 84c76f218..f15dce48d 100644
--- a/server/src/utils/versionUtils.ts
+++ b/server/src/utils/versionUtils.ts
@@ -1,40 +1,40 @@
import { Organization, APIVersion } from "@autumn/shared";
export const floatToVersion = (version: number) => {
- if (Object.values(APIVersion).includes(version)) {
- return version;
- }
+ if (Object.values(APIVersion).includes(version)) {
+ return version;
+ }
- return null;
+ return null;
};
export const getApiVersion = ({
- createdAt,
+ createdAt,
}: {
- createdAt: number;
+ createdAt: number;
}): APIVersion => {
- // v1.1 -- 17 April
+ // v1.1 -- 17 April
- let v1_2 = new Date("2025-05-05");
- let v1_1 = new Date("2025-04-17");
+ let v1_2 = new Date("2025-05-05");
+ let v1_1 = new Date("2025-04-17");
- if (createdAt >= v1_2.getTime()) {
- return APIVersion.v1_2;
- }
+ if (createdAt >= v1_2.getTime()) {
+ return APIVersion.v1_2;
+ }
- if (createdAt >= v1_1.getTime()) {
- return APIVersion.v1_1;
- }
+ if (createdAt >= v1_1.getTime()) {
+ return APIVersion.v1_1;
+ }
- return APIVersion.v1;
+ return APIVersion.v1;
};
export const orgToVersion = ({
- org,
- reqApiVersion,
+ org,
+ reqApiVersion,
}: {
- org: Organization;
- reqApiVersion?: number;
+ org: Organization;
+ reqApiVersion?: number;
}) => {
- return reqApiVersion || org.api_version || APIVersion.v1;
+ return reqApiVersion || org.api_version || APIVersion.v1;
};
diff --git a/server/src/websockets/WsManager.ts b/server/src/websockets/WsManager.ts
index c49109c76..d640e6b4f 100644
--- a/server/src/websockets/WsManager.ts
+++ b/server/src/websockets/WsManager.ts
@@ -1,18 +1,18 @@
export class WebsocketManager {
- public channels: Map>;
- public subscriptions: WeakMap>;
+ public channels: Map>;
+ public subscriptions: WeakMap>;
- constructor() {
- this.channels = new Map();
- this.subscriptions = new WeakMap();
- }
+ constructor() {
+ this.channels = new Map();
+ this.subscriptions = new WeakMap();
+ }
- // Initialize a client's subscription tracking
- initializeClient(ws: WebSocket) {
- // this.subscriptions.set(ws, new Set());
- // this.sendToClient(ws, {
- // type: "welcome",
- // channels: Array.from(this.channels.keys()),
- // });
- }
+ // Initialize a client's subscription tracking
+ initializeClient(ws: WebSocket) {
+ // this.subscriptions.set(ws, new Set());
+ // this.sendToClient(ws, {
+ // type: "welcome",
+ // channels: Array.from(this.channels.keys()),
+ // });
+ }
}
diff --git a/server/src/websockets/initWs.ts b/server/src/websockets/initWs.ts
index b0be1befa..b05f78715 100644
--- a/server/src/websockets/initWs.ts
+++ b/server/src/websockets/initWs.ts
@@ -3,125 +3,125 @@ import { AppEnv, ErrCode } from "@autumn/shared";
import { WebSocketServer, WebSocket } from "ws";
export enum SbChannelEvent {
- BalanceUpdated = "balance_updated",
+ BalanceUpdated = "balance_updated",
}
interface RouteInfo {
- pattern: RegExp;
- paramNames: string[];
- callback: (
- ws: WebSocket,
- req: http.IncomingMessage,
- params: Record,
- ) => Promise;
+ pattern: RegExp;
+ paramNames: string[];
+ callback: (
+ ws: WebSocket,
+ req: http.IncomingMessage,
+ params: Record,
+ ) => Promise;
}
const getPkey = async (req: any) => {
- const query = req.url.split("?")[1];
- const queryParams = new URLSearchParams(query);
- const pkey = req.headers["x-publishable-key"] || queryParams.get("pkey");
+ const query = req.url.split("?")[1];
+ const queryParams = new URLSearchParams(query);
+ const pkey = req.headers["x-publishable-key"] || queryParams.get("pkey");
- if (!pkey) {
- throw new Error("No publishable key found");
- }
+ if (!pkey) {
+ throw new Error("No publishable key found");
+ }
- if (typeof pkey !== "string") {
- throw new Error("Invalid publishable key");
- }
+ if (typeof pkey !== "string") {
+ throw new Error("Invalid publishable key");
+ }
- if (!pkey.startsWith("am_pk_test_") && !pkey.startsWith("am_pk_live_")) {
- throw new Error("Invalid publishable key");
- }
+ if (!pkey.startsWith("am_pk_test_") && !pkey.startsWith("am_pk_live_")) {
+ throw new Error("Invalid publishable key");
+ }
- const env = pkey.startsWith("am_pk_test_") ? AppEnv.Sandbox : AppEnv.Live;
+ const env = pkey.startsWith("am_pk_test_") ? AppEnv.Sandbox : AppEnv.Live;
- return {
- error: ErrCode.OrgNotFound,
- fallback: false,
- statusCode: 401,
- };
+ return {
+ error: ErrCode.OrgNotFound,
+ fallback: false,
+ statusCode: 401,
+ };
};
class WebSocketRouter {
- private wss: WebSocketServer;
- private routes: RouteInfo[] = [];
+ private wss: WebSocketServer;
+ private routes: RouteInfo[] = [];
- public on({
- route,
- callback,
- }: {
- route: string;
- callback: (
- ws: WebSocket,
- req: any,
- params: Record,
- ) => Promise;
- }) {
- const paramNames: string[] = [];
- const pattern = route.replace(/:([^/]+)/g, (_, paramName) => {
- paramNames.push(paramName);
- return "([^/]+)";
- });
- this.routes.push({
- pattern: new RegExp(`^${pattern}$`),
- paramNames,
- callback,
- });
- }
+ public on({
+ route,
+ callback,
+ }: {
+ route: string;
+ callback: (
+ ws: WebSocket,
+ req: any,
+ params: Record,
+ ) => Promise;
+ }) {
+ const paramNames: string[] = [];
+ const pattern = route.replace(/:([^/]+)/g, (_, paramName) => {
+ paramNames.push(paramName);
+ return "([^/]+)";
+ });
+ this.routes.push({
+ pattern: new RegExp(`^${pattern}$`),
+ paramNames,
+ callback,
+ });
+ }
- constructor(server: http.Server) {
- this.wss = new WebSocketServer({ server });
- this.wss.on("connection", (ws: WebSocket, req: http.IncomingMessage) =>
- this.handleConnection(ws as any, req as any),
- );
- }
+ constructor(server: http.Server) {
+ this.wss = new WebSocketServer({ server });
+ this.wss.on("connection", (ws: WebSocket, req: http.IncomingMessage) =>
+ this.handleConnection(ws as any, req as any),
+ );
+ }
- private async handleConnection(ws: WebSocket, req: any) {
- const path = req.url;
+ private async handleConnection(ws: WebSocket, req: any) {
+ const path = req.url;
- try {
- } catch (error) {
- console.log("Failed to get org from pkey");
- ws.close(1000, "Invalid publishable key");
- return;
- }
+ try {
+ } catch (error) {
+ console.log("Failed to get org from pkey");
+ ws.close(1000, "Invalid publishable key");
+ return;
+ }
- for (const route of this.routes) {
- const match = path.match(route.pattern);
- if (match) {
- // Extract params from match groups
- const params: Record = {};
- route.paramNames.forEach((name, index) => {
- params[name] = match[index + 1];
- });
- route.callback(ws, req, params);
- return;
- }
- }
+ for (const route of this.routes) {
+ const match = path.match(route.pattern);
+ if (match) {
+ // Extract params from match groups
+ const params: Record = {};
+ route.paramNames.forEach((name, index) => {
+ params[name] = match[index + 1];
+ });
+ route.callback(ws, req, params);
+ return;
+ }
+ }
- if (!path) {
- ws.close(1000, "No path found");
- return;
- }
+ if (!path) {
+ ws.close(1000, "No path found");
+ return;
+ }
- ws.close(1000, "Route not found");
- }
+ ws.close(1000, "Route not found");
+ }
}
export const initWs = (server: http.Server) => {
- const wsRouter = new WebSocketRouter(server);
+ const wsRouter = new WebSocketRouter(server);
- wsRouter.on({
- route: "/:customer_id/entitlements",
- callback: async (ws, req, params) => {
- console.log("entitlements", params);
- },
- });
+ wsRouter.on({
+ route: "/:customer_id/entitlements",
+ callback: async (ws, req, params) => {
+ console.log("entitlements", params);
+ },
+ });
- wsRouter.on({
- route: "/:customer_id/entitlements/:feature_id",
- callback: async (ws, req, params) => {
- console.log("entitlement", params);
- },
- });
+ wsRouter.on({
+ route: "/:customer_id/entitlements/:feature_id",
+ callback: async (ws, req, params) => {
+ console.log("entitlement", params);
+ },
+ });
};
diff --git a/server/src/workers.ts b/server/src/workers.ts
index 7079fd2da..6e8a46768 100644
--- a/server/src/workers.ts
+++ b/server/src/workers.ts
@@ -2,8 +2,8 @@ import { initWorkers } from "./queue/workersInit.js";
import { QueueManager } from "./queue/QueueManager.js";
const init = async () => {
- await QueueManager.getInstance(); // initialize the queue manager
- await initWorkers();
+ await QueueManager.getInstance(); // initialize the queue manager
+ await initWorkers();
};
init();
diff --git a/server/tests/00_setup.ts b/server/tests/00_setup.ts
index acebf86d9..b015071fc 100644
--- a/server/tests/00_setup.ts
+++ b/server/tests/00_setup.ts
@@ -4,15 +4,15 @@ dotenv.config();
import { AppEnv } from "@autumn/shared";
import { clearOrg, setupOrg } from "tests/utils/setup.js";
import {
- features,
- products,
- creditSystems,
- advanceProducts,
- attachProducts,
- rewards,
- oneTimeProducts,
- entityProducts,
- referralPrograms,
+ features,
+ products,
+ creditSystems,
+ advanceProducts,
+ attachProducts,
+ rewards,
+ oneTimeProducts,
+ entityProducts,
+ referralPrograms,
} from "./global.js";
import { initDrizzle } from "@/db/initDrizzle.js";
@@ -21,36 +21,36 @@ const DEFAULT_ENV = AppEnv.Sandbox;
import { Hyperbrowser } from "@hyperbrowser/sdk";
const hyperbrowser = new Hyperbrowser({
- apiKey: process.env.HYPERBROWSER_API_KEY,
+ apiKey: process.env.HYPERBROWSER_API_KEY,
});
describe("Initialize org for tests", () => {
- it("should initialize org", async function () {
- this.timeout(1000000000);
- this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
- this.env = DEFAULT_ENV;
- let { db, client } = initDrizzle();
+ it("should initialize org", async function () {
+ this.timeout(1000000000);
+ this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
+ this.env = DEFAULT_ENV;
+ let { db, client } = initDrizzle();
- this.db = db;
- this.client = client;
+ this.db = db;
+ this.client = client;
- await setupOrg({
- orgId: this.org.id,
- env: DEFAULT_ENV,
- features: { ...features, ...creditSystems } as any,
- products: {
- ...products,
- ...advanceProducts,
- ...attachProducts,
- ...oneTimeProducts,
- ...entityProducts,
- } as any,
- rewards: { ...rewards } as any,
- rewardTriggers: { ...referralPrograms } as any,
- });
+ await setupOrg({
+ orgId: this.org.id,
+ env: DEFAULT_ENV,
+ features: { ...features, ...creditSystems } as any,
+ products: {
+ ...products,
+ ...advanceProducts,
+ ...attachProducts,
+ ...oneTimeProducts,
+ ...entityProducts,
+ } as any,
+ rewards: { ...rewards } as any,
+ rewardTriggers: { ...referralPrograms } as any,
+ });
- console.log("--------------------------------");
- });
+ console.log("--------------------------------");
+ });
});
// after(async function () {
diff --git a/server/tests/advanced/advancedOthers/advancedOthers1.ts b/server/tests/advanced/advancedOthers/advancedOthers1.ts
index 48d42f563..52a567361 100644
--- a/server/tests/advanced/advancedOthers/advancedOthers1.ts
+++ b/server/tests/advanced/advancedOthers/advancedOthers1.ts
@@ -18,94 +18,94 @@ import { timeout } from "@/utils/genUtils.js";
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
describe(`${chalk.yellowBright("advancedOthers1: Testing convert collection method from send_invoice")}`, () => {
- let customerId = "advancedOthers1";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "advancedOthers1";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product and pay for it", async function () {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- invoice: true,
- enable_product_immediately: true,
- });
+ it("should attach pro product and pay for it", async function () {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ invoice: true,
+ enable_product_immediately: true,
+ });
- expect(res.invoice).to.exist;
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
+ expect(res.invoice).to.exist;
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
- const invoiceStripeId = res.invoice.stripe_id;
- const invoice = await stripeCli.invoices.finalizeInvoice(invoiceStripeId);
+ const invoiceStripeId = res.invoice.stripe_id;
+ const invoice = await stripeCli.invoices.finalizeInvoice(invoiceStripeId);
- await stripeCli.invoices.pay(invoiceStripeId);
- });
+ await stripeCli.invoices.pay(invoiceStripeId);
+ });
- it("should have collection method charge automatically", async function () {
- await timeout(5000);
+ it("should have collection method charge automatically", async function () {
+ await timeout(5000);
- const cusProduct = await getMainCusProduct({
- db,
- customerId,
- orgId: org.id,
- env,
- productGroup: pro.group,
- });
+ const cusProduct = await getMainCusProduct({
+ db,
+ customerId,
+ orgId: org.id,
+ env,
+ productGroup: pro.group,
+ });
- const sub = await cusProductToSub({
- cusProduct,
- stripeCli,
- });
+ const sub = await cusProductToSub({
+ cusProduct,
+ stripeCli,
+ });
- expect(sub?.collection_method).to.equal("charge_automatically");
- });
+ expect(sub?.collection_method).to.equal("charge_automatically");
+ });
});
diff --git a/server/tests/advanced/check/check1.ts b/server/tests/advanced/check/check1.ts
index 096edf7e2..18457c87a 100644
--- a/server/tests/advanced/check/check1.ts
+++ b/server/tests/advanced/check/check1.ts
@@ -15,120 +15,120 @@ import { timeout } from "@/utils/genUtils.js";
const creditCost = 0.2;
let freeProduct = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Action1 })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Action1 })],
+ type: "free",
+ isDefault: false,
});
const creditFeatureItem = constructFeatureItem({
- featureId: TestFeature.Credits,
+ featureId: TestFeature.Credits,
}) as LimitedItem;
let pro = constructProduct({
- id: "pro",
- items: [creditFeatureItem],
- type: "pro",
+ id: "pro",
+ items: [creditFeatureItem],
+ type: "pro",
});
const testCase = "check1";
describe(`${chalk.yellowBright("check1: Checking credit systems")}`, () => {
- const customerId = testCase;
- let testClockId: string;
- let customer: Customer;
- let stripeCli: Stripe;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ const customerId = testCase;
+ let testClockId: string;
+ let customer: Customer;
+ let stripeCli: Stripe;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [freeProduct, pro],
- prefix: testCase,
- });
- await createProducts({
- products: [freeProduct, pro],
- orgId: this.org.id,
- env: this.env,
- autumn: this.autumnJs,
- db: this.db,
- });
+ addPrefixToProducts({
+ products: [freeProduct, pro],
+ prefix: testCase,
+ });
+ await createProducts({
+ products: [freeProduct, pro],
+ orgId: this.org.id,
+ env: this.env,
+ autumn: this.autumnJs,
+ db: this.db,
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- it("should attach free product and check action1 allowed", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: freeProduct.id,
- });
+ it("should attach free product and check action1 allowed", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: freeProduct.id,
+ });
- let actionCheck = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Action1,
- });
+ let actionCheck = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Action1,
+ });
- let creditsCheck = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Credits,
- });
+ let creditsCheck = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Credits,
+ });
- expect(actionCheck.allowed).to.be.true;
- expect(creditsCheck.allowed).to.be.false;
- });
+ expect(actionCheck.allowed).to.be.true;
+ expect(creditsCheck.allowed).to.be.false;
+ });
- it("should attach pro product and check allowed", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should attach pro product and check allowed", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- let creditsCheck = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Credits,
- });
+ let creditsCheck = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Credits,
+ });
- let actionCheck = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Action1,
- });
+ let actionCheck = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Action1,
+ });
- expect(actionCheck.allowed).to.be.true;
- expect(creditsCheck.allowed).to.be.true;
- });
+ expect(actionCheck.allowed).to.be.true;
+ expect(creditsCheck.allowed).to.be.true;
+ });
- it("should use up credits and have correct check response", async function () {
- let usage = 50;
- let creditUsage = new Decimal(creditCost).mul(usage).toNumber();
+ it("should use up credits and have correct check response", async function () {
+ let usage = 50;
+ let creditUsage = new Decimal(creditCost).mul(usage).toNumber();
- let creditBalance = new Decimal(creditFeatureItem.included_usage)
- .sub(creditUsage)
- .toNumber();
+ let creditBalance = new Decimal(creditFeatureItem.included_usage)
+ .sub(creditUsage)
+ .toNumber();
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Action1,
- value: usage,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Action1,
+ value: usage,
+ });
- await timeout(3000);
+ await timeout(3000);
- let creditsCheck = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Credits,
- });
+ let creditsCheck = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Credits,
+ });
- expect(creditsCheck.balance).to.be.equal(creditBalance);
- });
+ expect(creditsCheck.balance).to.be.equal(creditBalance);
+ });
});
diff --git a/server/tests/advanced/coupons/coupon1.ts b/server/tests/advanced/coupons/coupon1.ts
index d44dbf480..7ab9b8d23 100644
--- a/server/tests/advanced/coupons/coupon1.ts
+++ b/server/tests/advanced/coupons/coupon1.ts
@@ -8,9 +8,9 @@ import { rewards } from "tests/global.js";
import { timeout } from "tests/utils/genUtils.js";
import {
- advanceTestClock,
- completeCheckoutForm,
- getDiscount,
+ advanceTestClock,
+ completeCheckoutForm,
+ getDiscount,
} from "tests/utils/stripeUtils.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { setupBefore } from "tests/before.js";
@@ -19,8 +19,8 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -32,200 +32,200 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "coupon1";
const pro = constructProduct({
- type: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const simulateOneCycle = async ({
- customerId,
- db,
- org,
- env,
- stripeCli,
- autumn,
- testClockId,
- couponAmount,
- curUnix,
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ autumn,
+ testClockId,
+ couponAmount,
+ curUnix,
}: {
- customerId: string;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- stripeCli: Stripe;
- autumn: AutumnInt;
- testClockId: string;
- couponAmount: number;
- curUnix: number;
+ customerId: string;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ stripeCli: Stripe;
+ autumn: AutumnInt;
+ testClockId: string;
+ couponAmount: number;
+ curUnix: number;
}) => {
- const usage = Math.random() * 100000 + 10000;
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: usage,
- });
+ const usage = Math.random() * 100000 + 10000;
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: usage,
+ });
- // Expected invoice total
- let expectedTotal = await getExpectedInvoiceTotal({
- usage: [{ featureId: TestFeature.Words, value: usage }],
- customerId,
- productId: pro.id,
- db,
- org,
- env,
- stripeCli,
- });
+ // Expected invoice total
+ let expectedTotal = await getExpectedInvoiceTotal({
+ usage: [{ featureId: TestFeature.Words, value: usage }],
+ customerId,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ stripeCli,
+ });
- couponAmount -= expectedTotal;
+ couponAmount -= expectedTotal;
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(curUnix, 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(curUnix, 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.invoices![0].total).to.equal(0);
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.invoices![0].total).to.equal(0);
- const cusDiscount = await getDiscount({
- stripeCli: stripeCli,
- stripeId: customer.stripe_id!,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli: stripeCli,
+ stripeId: customer.stripe_id!,
+ });
- expect(cusDiscount).to.exist;
+ expect(cusDiscount).to.exist;
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- rewards.rolloverAll.id
- );
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
+ rewards.rolloverAll.id,
+ );
- expect(cusDiscount.coupon?.amount_off).to.equal(
- Math.round(couponAmount * 100),
- `Expected stripe cus to have coupon amount ${couponAmount * 100}`
- );
+ expect(cusDiscount.coupon?.amount_off).to.equal(
+ Math.round(couponAmount * 100),
+ `Expected stripe cus to have coupon amount ${couponAmount * 100}`,
+ );
- return {
- couponAmount,
- curUnix,
- };
+ return {
+ couponAmount,
+ curUnix,
+ };
};
describe(
- chalk.yellow(
- `${testCase} - Testing invoice credits reward, apply to all product`
- ),
- () => {
- let customerId = "coupon1";
- let stripeCli: Stripe;
- let customer: Customer;
- let testClockId: string;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ chalk.yellow(
+ `${testCase} - Testing invoice credits reward, apply to all product`,
+ ),
+ () => {
+ let customerId = "coupon1";
+ let stripeCli: Stripe;
+ let customer: Customer;
+ let testClockId: string;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let couponAmount = rewards.rolloverAll.discount_config.discount_value;
- let curUnix = new Date().getTime();
+ let couponAmount = rewards.rolloverAll.discount_config.discount_value;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- customerId,
- org,
- env,
- db,
- autumn: this.autumnJs,
- });
+ const res = await initCustomer({
+ customerId,
+ org,
+ env,
+ db,
+ autumn: this.autumnJs,
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- products: [pro],
- orgId: org.id,
- env,
- db,
- autumn,
- });
+ await createProducts({
+ products: [pro],
+ orgId: org.id,
+ env,
+ db,
+ autumn,
+ });
- testClockId = res.testClockId;
- customer = res.customer;
- });
+ testClockId = res.testClockId;
+ customer = res.customer;
+ });
- // CYCLE 0
- it("should attach pro", async () => {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ // CYCLE 0
+ it("should attach pro", async () => {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- await completeCheckoutForm(
- res.checkout_url,
- undefined,
- rewards.rolloverAll.id
- );
+ await completeCheckoutForm(
+ res.checkout_url,
+ undefined,
+ rewards.rolloverAll.id,
+ );
- await timeout(10000);
+ await timeout(10000);
- couponAmount -= getBasePrice({ product: pro });
+ couponAmount -= getBasePrice({ product: pro });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({ customer, product: pro });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({ customer, product: pro });
- expect(customer.invoices![0].total).to.equal(0);
+ expect(customer.invoices![0].total).to.equal(0);
- const cusDiscount = await getDiscount({
- stripeCli,
- stripeId: customer.stripe_id!,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli,
+ stripeId: customer.stripe_id!,
+ });
- expect(cusDiscount).to.exist;
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- rewards.rolloverAll.id
- );
- expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
- });
+ expect(cusDiscount).to.exist;
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
+ rewards.rolloverAll.id,
+ );
+ expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
+ });
- it("should run one cycle and have correct invoice + coupon amount", async () => {
- const res = await simulateOneCycle({
- customerId,
- db,
- org,
- env,
- stripeCli,
- autumn,
- testClockId,
- couponAmount,
- curUnix: new Date().getTime(),
- });
+ it("should run one cycle and have correct invoice + coupon amount", async () => {
+ const res = await simulateOneCycle({
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ autumn,
+ testClockId,
+ couponAmount,
+ curUnix: new Date().getTime(),
+ });
- couponAmount = res.couponAmount;
- curUnix = res.curUnix;
- });
+ couponAmount = res.couponAmount;
+ curUnix = res.curUnix;
+ });
- // CYCLE 1
- it("should run another cycle and have correct invoice + coupon amount", async () => {
- const res = await simulateOneCycle({
- customerId,
- db,
- org,
- env,
- stripeCli,
- autumn,
- testClockId,
- couponAmount,
- curUnix,
- });
- });
- }
+ // CYCLE 1
+ it("should run another cycle and have correct invoice + coupon amount", async () => {
+ const res = await simulateOneCycle({
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ autumn,
+ testClockId,
+ couponAmount,
+ curUnix,
+ });
+ });
+ },
);
diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts
index 703f352bf..cfaa8db4e 100644
--- a/server/tests/advanced/coupons/coupon2.ts
+++ b/server/tests/advanced/coupons/coupon2.ts
@@ -1,7 +1,7 @@
import {
- advanceClockForInvoice,
- completeCheckoutForm,
- getDiscount,
+ advanceClockForInvoice,
+ completeCheckoutForm,
+ getDiscount,
} from "tests/utils/stripeUtils.js";
import chalk from "chalk";
@@ -9,12 +9,12 @@ import Stripe from "stripe";
import { expect } from "chai";
import {
- APIVersion,
- AppEnv,
- CouponDurationType,
- CreateReward,
- Organization,
- RewardType,
+ APIVersion,
+ AppEnv,
+ CouponDurationType,
+ CreateReward,
+ Organization,
+ RewardType,
} from "@autumn/shared";
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js";
@@ -28,8 +28,8 @@ import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
@@ -40,167 +40,167 @@ import { addHours, addMonths } from "date-fns";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const pro = constructProduct({
- type: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const testCase = "coupon2";
// Create reward input
const reward: CreateReward = {
- id: "usage",
- name: "usage",
- promo_codes: [{ code: "usage" }],
- type: RewardType.InvoiceCredits,
- discount_config: {
- discount_value: 10000,
- duration_type: CouponDurationType.Forever,
- duration_value: 1,
- should_rollover: true,
- apply_to_all: false,
- price_ids: [],
- },
+ id: "usage",
+ name: "usage",
+ promo_codes: [{ code: "usage" }],
+ type: RewardType.InvoiceCredits,
+ discount_config: {
+ discount_value: 10000,
+ duration_type: CouponDurationType.Forever,
+ duration_value: 1,
+ should_rollover: true,
+ apply_to_all: false,
+ price_ids: [],
+ },
};
describe(
- chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`),
- () => {
- let logger: any;
- let customerId = testCase;
- let stripeCli: Stripe;
- let testClockId: string;
+ chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`),
+ () => {
+ let logger: any;
+ let customerId = testCase;
+ let stripeCli: Stripe;
+ let testClockId: string;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let org: Organization;
- let env: AppEnv;
- let db: DrizzleCli;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let org: Organization;
+ let env: AppEnv;
+ let db: DrizzleCli;
- let couponAmount = reward.discount_config!.discount_value;
+ let couponAmount = reward.discount_config!.discount_value;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- org = this.org;
- env = this.env;
- db = this.db;
- stripeCli = this.stripeCli;
+ org = this.org;
+ env = this.env;
+ db = this.db;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ });
- testClockId = testClockId1;
+ testClockId = testClockId1;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- orgId: this.org.id,
- env: this.env,
- db: this.db,
- autumn,
- products: [pro],
- });
+ await createProducts({
+ orgId: this.org.id,
+ env: this.env,
+ db: this.db,
+ autumn,
+ products: [pro],
+ });
- await createReward({
- orgId: org.id,
- env,
- db,
- autumn,
- reward,
- productId: pro.id,
- onlyUsage: true,
- });
- });
+ await createReward({
+ orgId: org.id,
+ env,
+ db,
+ autumn,
+ reward,
+ productId: pro.id,
+ onlyUsage: true,
+ });
+ });
- // CYCLE 0
- it("should attach pro with promo code", async () => {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ // CYCLE 0
+ it("should attach pro with promo code", async () => {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- await completeCheckoutForm(res.checkout_url, undefined, reward.id);
+ await completeCheckoutForm(res.checkout_url, undefined, reward.id);
- await timeout(10000);
+ await timeout(10000);
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- it("should have fixed price invoice and correct remaining coupon amount", async () => {
- const customer = await autumn.customers.get(customerId);
- const fixedPrice = getBasePrice({ product: pro });
- expect(customer.invoices![0].total).to.equal(fixedPrice);
+ it("should have fixed price invoice and correct remaining coupon amount", async () => {
+ const customer = await autumn.customers.get(customerId);
+ const fixedPrice = getBasePrice({ product: pro });
+ expect(customer.invoices![0].total).to.equal(fixedPrice);
- const cusDiscount = await getDiscount({
- stripeCli,
- stripeId: customer.stripe_id!,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli,
+ stripeId: customer.stripe_id!,
+ });
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id);
- expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
- });
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id);
+ expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
+ });
- // CYCLE 1
- it("should track usage and have correct invoice amount", async () => {
- const usage = new Decimal(Math.random() * 1250120 + 10000)
- .toDecimalPlaces(2)
- .toNumber();
+ // CYCLE 1
+ it("should track usage and have correct invoice amount", async () => {
+ const usage = new Decimal(Math.random() * 1250120 + 10000)
+ .toDecimalPlaces(2)
+ .toNumber();
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: usage,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: usage,
+ });
- let usageTotal = await getExpectedInvoiceTotal({
- org,
- env,
- db,
- customerId,
- productId: pro.id,
- usage: [{ featureId: TestFeature.Words, value: usage }],
- stripeCli,
- onlyIncludeUsage: true,
- });
+ let usageTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ db,
+ customerId,
+ productId: pro.id,
+ usage: [{ featureId: TestFeature.Words, value: usage }],
+ stripeCli,
+ onlyIncludeUsage: true,
+ });
- let basePrice = getBasePrice({ product: pro });
+ let basePrice = getBasePrice({ product: pro });
- couponAmount = couponAmount - usageTotal;
+ couponAmount = couponAmount - usageTotal;
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice,
- ).getTime(),
- waitForSeconds: 20,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 20,
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.invoices![0].total).to.equal(basePrice);
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.invoices![0].total).to.equal(basePrice);
- const cusDiscount = await getDiscount({
- stripeCli,
- stripeId: customer.stripe_id!,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli,
+ stripeId: customer.stripe_id!,
+ });
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id);
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(reward.id);
- expect(cusDiscount.coupon?.amount_off).to.equal(
- Math.round(couponAmount * 100),
- );
- });
- },
+ expect(cusDiscount.coupon?.amount_off).to.equal(
+ Math.round(couponAmount * 100),
+ );
+ });
+ },
);
diff --git a/server/tests/advanced/coupons/coupon3.ts b/server/tests/advanced/coupons/coupon3.ts
index f28b689cf..7f09238b4 100644
--- a/server/tests/advanced/coupons/coupon3.ts
+++ b/server/tests/advanced/coupons/coupon3.ts
@@ -4,12 +4,12 @@ import Stripe from "stripe";
import { expect } from "chai";
import {
- APIVersion,
- AppEnv,
- CouponDurationType,
- CreateReward,
- Organization,
- RewardType,
+ APIVersion,
+ AppEnv,
+ CouponDurationType,
+ CreateReward,
+ Organization,
+ RewardType,
} from "@autumn/shared";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
@@ -17,163 +17,163 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
const pro = constructProduct({
- type: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const oneOff = constructProduct({
- type: "one_off",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 500,
- }),
- ],
+ type: "one_off",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 500,
+ }),
+ ],
});
// Create reward input
const rewardId = "attach_coupon";
const promoCode = "attach_coupon_code";
const reward: CreateReward = {
- id: rewardId,
- name: "attach_coupon",
- promo_codes: [{ code: promoCode }],
- type: RewardType.FixedDiscount,
- discount_config: {
- discount_value: 5,
- duration_type: CouponDurationType.OneOff,
- duration_value: 1,
- should_rollover: true,
- apply_to_all: true,
- price_ids: [],
- },
+ id: rewardId,
+ name: "attach_coupon",
+ promo_codes: [{ code: promoCode }],
+ type: RewardType.FixedDiscount,
+ discount_config: {
+ discount_value: 5,
+ duration_type: CouponDurationType.OneOff,
+ duration_value: 1,
+ should_rollover: true,
+ apply_to_all: true,
+ price_ids: [],
+ },
};
const testCase = "coupon3";
describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => {
- let customerId = testCase;
- let stripeCli: Stripe;
- let testClockId: string;
+ let customerId = testCase;
+ let stripeCli: Stripe;
+ let testClockId: string;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let org: Organization;
- let env: AppEnv;
- let db: DrizzleCli;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let org: Organization;
+ let env: AppEnv;
+ let db: DrizzleCli;
- let couponAmount = reward.discount_config!.discount_value;
+ let couponAmount = reward.discount_config!.discount_value;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- org = this.org;
- env = this.env;
- db = this.db;
- stripeCli = this.stripeCli;
+ org = this.org;
+ env = this.env;
+ db = this.db;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- testClockId = testClockId1;
+ testClockId = testClockId1;
- addPrefixToProducts({
- products: [pro, oneOff],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, oneOff],
+ prefix: testCase,
+ });
- await createProducts({
- orgId: this.org.id,
- env: this.env,
- db: this.db,
- autumn,
- products: [pro, oneOff],
- });
+ await createProducts({
+ orgId: this.org.id,
+ env: this.env,
+ db: this.db,
+ autumn,
+ products: [pro, oneOff],
+ });
- await createReward({
- orgId: org.id,
- env,
- db,
- autumn,
- reward,
- productId: pro.id,
- });
- });
+ await createReward({
+ orgId: org.id,
+ env,
+ db,
+ autumn,
+ reward,
+ productId: pro.id,
+ });
+ });
- // CYCLE 0
- it("should attach pro with reward ID", async () => {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- reward: rewardId,
- });
+ // CYCLE 0
+ it("should attach pro with reward ID", async () => {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ reward: rewardId,
+ });
- const customer = await autumn.customers.get(customerId);
- expectAttachCorrect({
- customer,
- product: pro,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectAttachCorrect({
+ customer,
+ product: pro,
+ });
- const invoice = customer.invoices![0];
- let basePrice = getBasePrice({ product: pro });
- expect(invoice.total).to.equal(basePrice - couponAmount);
- });
+ const invoice = customer.invoices![0];
+ let basePrice = getBasePrice({ product: pro });
+ expect(invoice.total).to.equal(basePrice - couponAmount);
+ });
- it("should attach one off with reward ID", async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: oneOff.id,
- reward: rewardId,
- });
+ it("should attach one off with reward ID", async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: oneOff.id,
+ reward: rewardId,
+ });
- const customer = await autumn.customers.get(customerId);
- expectAttachCorrect({
- customer,
- product: oneOff,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectAttachCorrect({
+ customer,
+ product: oneOff,
+ });
- const invoice = customer.invoices![0];
- let basePrice = getBasePrice({ product: oneOff });
- expect(invoice.total).to.equal(basePrice - couponAmount);
- expect(invoice.product_ids).to.include(oneOff.id);
- });
+ const invoice = customer.invoices![0];
+ let basePrice = getBasePrice({ product: oneOff });
+ expect(invoice.total).to.equal(basePrice - couponAmount);
+ expect(invoice.product_ids).to.include(oneOff.id);
+ });
- it("should attach one off with promo code", async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: oneOff.id,
- reward: promoCode,
- });
+ it("should attach one off with promo code", async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: oneOff.id,
+ reward: promoCode,
+ });
- const customer = await autumn.customers.get(customerId);
- expectAttachCorrect({
- customer,
- product: oneOff,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectAttachCorrect({
+ customer,
+ product: oneOff,
+ });
- expect(customer.invoices!.length).to.equal(3);
- let basePrice = getBasePrice({ product: oneOff });
- for (let i = 0; i < 2; i++) {
- let invoice = customer.invoices![i];
- expect(invoice.total).to.equal(basePrice - couponAmount);
- expect(invoice.product_ids).to.include(oneOff.id);
- }
- });
+ expect(customer.invoices!.length).to.equal(3);
+ let basePrice = getBasePrice({ product: oneOff });
+ for (let i = 0; i < 2; i++) {
+ let invoice = customer.invoices![i];
+ expect(invoice.total).to.equal(basePrice - couponAmount);
+ expect(invoice.product_ids).to.include(oneOff.id);
+ }
+ });
});
diff --git a/server/tests/advanced/customInterval/customInterval1.ts b/server/tests/advanced/customInterval/customInterval1.ts
index 86ef1adff..6759b1870 100644
--- a/server/tests/advanced/customInterval/customInterval1.ts
+++ b/server/tests/advanced/customInterval/customInterval1.ts
@@ -22,126 +22,126 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "customInterval1";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- intervalCount: 2,
- includedUsage: 500,
- }),
- ],
- intervalCount: 2,
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ intervalCount: 2,
+ includedUsage: 500,
+ }),
+ ],
+ intervalCount: 2,
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- intervalCount: 2,
- }),
- // constructArrearItem({ featureId: TestFeature.Words }),
- // constructArrearProratedItem({
- // featureId: TestFeature.Users,
- // pricePerUnit: 30,
- // }),
- ],
- intervalCount: 2,
- type: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ intervalCount: 2,
+ }),
+ // constructArrearItem({ featureId: TestFeature.Words }),
+ // constructArrearProratedItem({
+ // featureId: TestFeature.Users,
+ // pricePerUnit: 30,
+ // }),
+ ],
+ intervalCount: 2,
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interval count`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let usage = 100012;
- it("should upgrade to premium product and have correct invoice next cycle", async function () {
- const curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1).getTime(),
- waitForSeconds: 15,
- });
+ let usage = 100012;
+ it("should upgrade to premium product and have correct invoice next cycle", async function () {
+ const curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1).getTime(),
+ waitForSeconds: 15,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.invoices.length).to.equal(2);
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.invoices.length).to.equal(2);
- const nextUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(curUnix), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ const nextUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(curUnix), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const customer2 = await autumn.customers.get(customerId);
- const invoices = customer2.invoices;
- expect(invoices.length).to.equal(3);
- expect(invoices[0].product_ids).to.include(premium.id);
- expect(invoices[0].total).to.equal(getBasePrice({ product: premium }));
+ const customer2 = await autumn.customers.get(customerId);
+ const invoices = customer2.invoices;
+ expect(invoices.length).to.equal(3);
+ expect(invoices[0].product_ids).to.include(premium.id);
+ expect(invoices[0].total).to.equal(getBasePrice({ product: premium }));
- const wordsFeature = customer2.features[TestFeature.Words];
- // @ts-ignore
- expect(wordsFeature.interval_count).to.equal(2);
- });
+ const wordsFeature = customer2.features[TestFeature.Words];
+ // @ts-ignore
+ expect(wordsFeature.interval_count).to.equal(2);
+ });
});
diff --git a/server/tests/advanced/customInterval/customInterval2.ts b/server/tests/advanced/customInterval/customInterval2.ts
index c37cafdf3..dae9d1c8d 100644
--- a/server/tests/advanced/customInterval/customInterval2.ts
+++ b/server/tests/advanced/customInterval/customInterval2.ts
@@ -23,99 +23,99 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "customInterval2";
export let pro = constructRawProduct({
- id: "pro",
- items: [
- constructArrearItem({
- includedUsage: 0,
- featureId: TestFeature.Words,
- intervalCount: 2,
- }),
- ],
+ id: "pro",
+ items: [
+ constructArrearItem({
+ includedUsage: 0,
+ featureId: TestFeature.Words,
+ intervalCount: 2,
+ }),
+ ],
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let usage = 100012;
- it("should upgrade to premium product and have correct invoice next cycle", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: usage,
- });
+ let usage = 100012;
+ it("should upgrade to premium product and have correct invoice next cycle", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: usage,
+ });
- const curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 2),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ const curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 2),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const invoiceAmount = await getExpectedInvoiceTotal({
- customerId,
- productId: pro.id,
- usage: [{ featureId: TestFeature.Words, value: usage }],
- stripeCli,
- db,
- org,
- env,
- });
+ const invoiceAmount = await getExpectedInvoiceTotal({
+ customerId,
+ productId: pro.id,
+ usage: [{ featureId: TestFeature.Words, value: usage }],
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.invoices.length).to.equal(2);
- expect(invoiceAmount).to.equal(customer.invoices[0].total);
- });
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.invoices.length).to.equal(2);
+ expect(invoiceAmount).to.equal(customer.invoices[0].total);
+ });
});
diff --git a/server/tests/advanced/customInterval/customInterval3.ts b/server/tests/advanced/customInterval/customInterval3.ts
index 2bee550db..389014764 100644
--- a/server/tests/advanced/customInterval/customInterval3.ts
+++ b/server/tests/advanced/customInterval/customInterval3.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- LimitedItem,
- Organization,
- Product,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ LimitedItem,
+ Organization,
+ Product,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -15,14 +15,14 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructArrearItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
@@ -40,135 +40,135 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "customInterval3";
export let pro = constructProduct({
- type: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- intervalCount: 2,
- }),
- ],
- intervalCount: 2,
+ type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ intervalCount: 2,
+ }),
+ ],
+ intervalCount: 2,
});
const prepaidWordsItem = constructPrepaidItem({
- featureId: TestFeature.Words,
- price: 10,
- billingUnits: 1,
- includedUsage: 0,
- intervalCount: 2,
+ featureId: TestFeature.Words,
+ price: 10,
+ billingUnits: 1,
+ includedUsage: 0,
+ intervalCount: 2,
});
export const addOn = constructRawProduct({
- id: "addOn",
- items: [prepaidWordsItem],
- isAddOn: true,
+ id: "addOn",
+ items: [prepaidWordsItem],
+ isAddOn: true,
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on merged product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, addOn],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, addOn],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should upgrade to attached add on and have correct invoice next cycle", async function () {
- const curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 20).getTime(),
- waitForSeconds: 15,
- });
+ it("should upgrade to attached add on and have correct invoice next cycle", async function () {
+ const curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 20).getTime(),
+ waitForSeconds: 15,
+ });
- const wordBillingSets = 2;
- const wordsBillingUnits = prepaidWordsItem.billing_units! * wordBillingSets;
- await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- options: [
- {
- feature_id: TestFeature.Words,
- quantity: wordsBillingUnits,
- },
- ],
- });
+ const wordBillingSets = 2;
+ const wordsBillingUnits = prepaidWordsItem.billing_units! * wordBillingSets;
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ options: [
+ {
+ feature_id: TestFeature.Words,
+ quantity: wordsBillingUnits,
+ },
+ ],
+ });
- const customer = await autumn.customers.get(customerId);
- const proProduct = customer.products.find((p) => p.id === pro.id);
- const invoices = customer.invoices;
- expectProductAttached({
- customer,
- product: pro,
- });
+ const customer = await autumn.customers.get(customerId);
+ const proProduct = customer.products.find((p) => p.id === pro.id);
+ const invoices = customer.invoices;
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
- expectProductAttached({
- customer,
- product: addOn,
- });
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
- let expectedPrice = wordsBillingUnits * prepaidWordsItem.price!;
- const proratedPrice = calculateProrationAmount({
- amount: expectedPrice,
- periodStart: new Date().getTime(),
- periodEnd: addMonths(new Date(), 2).getTime(),
- now: curUnix!,
- });
+ let expectedPrice = wordsBillingUnits * prepaidWordsItem.price!;
+ const proratedPrice = calculateProrationAmount({
+ amount: expectedPrice,
+ periodStart: new Date().getTime(),
+ periodEnd: addMonths(new Date(), 2).getTime(),
+ now: curUnix!,
+ });
- expect(invoices[0].product_ids).to.include(addOn.id);
- expect(invoices[0].total).to.approximately(proratedPrice, 0.1);
+ expect(invoices[0].product_ids).to.include(addOn.id);
+ expect(invoices[0].total).to.approximately(proratedPrice, 0.1);
- const expectedAddonEnd = addMonths(new Date(), 2);
- const approximate = 1000 * 60 * 60 * 24; // +- 1 day
- const addOnProduct = customer.products.find((p) => p.id === addOn.id);
+ const expectedAddonEnd = addMonths(new Date(), 2);
+ const approximate = 1000 * 60 * 60 * 24; // +- 1 day
+ const addOnProduct = customer.products.find((p) => p.id === addOn.id);
- expect(addOnProduct?.current_period_end).to.be.approximately(
- expectedAddonEnd.getTime(),
- approximate
- );
- });
+ expect(addOnProduct?.current_period_end).to.be.approximately(
+ expectedAddonEnd.getTime(),
+ approximate,
+ );
+ });
});
diff --git a/server/tests/advanced/customInterval/customInterval4.ts b/server/tests/advanced/customInterval/customInterval4.ts
index d5dccdc5f..c428aba75 100644
--- a/server/tests/advanced/customInterval/customInterval4.ts
+++ b/server/tests/advanced/customInterval/customInterval4.ts
@@ -16,8 +16,8 @@ import { addPrefixToProducts } from "tests/attach/utils.js";
import { addMonths } from "date-fns";
import { expect } from "chai";
import {
- expectDowngradeCorrect,
- expectNextCycleCorrect,
+ expectDowngradeCorrect,
+ expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -25,128 +25,128 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "customInterval4";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 500,
- }),
- ],
- intervalCount: 2,
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 500,
+ }),
+ ],
+ intervalCount: 2,
+ type: "pro",
});
export let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 500,
- }),
- ],
- intervalCount: 2,
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 500,
+ }),
+ ],
+ intervalCount: 2,
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom intervals`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach premium product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach premium product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should have correct next cycle at on checkout", async function () {
- const checkout = await autumn.checkout({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should have correct next cycle at on checkout", async function () {
+ const checkout = await autumn.checkout({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- let expectedNextCycle = addMonths(new Date(), 2);
- expect(checkout.next_cycle?.starts_at).to.be.approximately(
- expectedNextCycle.getTime(),
- 1000 * 60 * 60 * 24
- );
+ let expectedNextCycle = addMonths(new Date(), 2);
+ expect(checkout.next_cycle?.starts_at).to.be.approximately(
+ expectedNextCycle.getTime(),
+ 1000 * 60 * 60 * 24,
+ );
- expect(checkout.total).to.equal(0);
- });
+ expect(checkout.total).to.equal(0);
+ });
- let preview: any;
- it("should downgrade to pro", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ let preview: any;
+ it("should downgrade to pro", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should have pro attached on next cycle", async function () {
- await expectNextCycleCorrect({
- preview: preview!,
- autumn,
- stripeCli,
- customerId,
- testClockId,
- product: pro,
- db,
- org,
- env,
- });
+ it("should have pro attached on next cycle", async function () {
+ await expectNextCycleCorrect({
+ preview: preview!,
+ autumn,
+ stripeCli,
+ customerId,
+ testClockId,
+ product: pro,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
- expect(invoices[0].total).to.equal(getBasePrice({ product: pro }));
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].total).to.equal(getBasePrice({ product: pro }));
+ });
});
diff --git a/server/tests/advanced/customInterval/customInterval5.ts b/server/tests/advanced/customInterval/customInterval5.ts
index 9d74a13d0..92e99b87f 100644
--- a/server/tests/advanced/customInterval/customInterval5.ts
+++ b/server/tests/advanced/customInterval/customInterval5.ts
@@ -21,144 +21,144 @@ const testCase = "customInterval5";
const includedUsage = 500;
const monthlyWords = constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage,
+ featureId: TestFeature.Words,
+ includedUsage,
});
const biMonthlyWords = constructFeatureItem({
- featureId: TestFeature.Words,
- intervalCount: 2,
- includedUsage,
+ featureId: TestFeature.Words,
+ intervalCount: 2,
+ includedUsage,
});
export let pro = constructProduct({
- items: [monthlyWords, biMonthlyWords],
- intervalCount: 2,
- type: "pro",
+ items: [monthlyWords, biMonthlyWords],
+ intervalCount: 2,
+ type: "pro",
});
const getBreakdown = ({
- customer,
- intervalCount,
+ customer,
+ intervalCount,
}: {
- customer: Customer;
- intervalCount: number;
+ customer: Customer;
+ intervalCount: number;
}) => {
- const wordsFeature = customer.features[TestFeature.Words];
- // @ts-ignore
- return wordsFeature.breakdown?.find(
- (b: any) => b.interval_count == intervalCount
- );
+ const wordsFeature = customer.features[TestFeature.Words];
+ // @ts-ignore
+ return wordsFeature.breakdown?.find(
+ (b: any) => b.interval_count == intervalCount,
+ );
};
describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- const wordsFeature = customer.features[TestFeature.Words];
- // @ts-ignore
- expect(wordsFeature.interval_count).to.equal(null);
- expect(wordsFeature.breakdown?.length).to.equal(2);
+ const customer = await autumn.customers.get(customerId);
+ const wordsFeature = customer.features[TestFeature.Words];
+ // @ts-ignore
+ expect(wordsFeature.interval_count).to.equal(null);
+ expect(wordsFeature.breakdown?.length).to.equal(2);
- expect(
- wordsFeature.breakdown?.some(
- (b: any) => b.interval_count == 1 && b.interval == "month"
- )
- ).to.equal(true);
- expect(
- wordsFeature.breakdown?.some(
- (b: any) => b.interval_count == 2 && b.interval == "month"
- )
- ).to.equal(true);
- });
+ expect(
+ wordsFeature.breakdown?.some(
+ (b: any) => b.interval_count == 1 && b.interval == "month",
+ ),
+ ).to.equal(true);
+ expect(
+ wordsFeature.breakdown?.some(
+ (b: any) => b.interval_count == 2 && b.interval == "month",
+ ),
+ ).to.equal(true);
+ });
- const trackVal = 300;
- it("should have correct breakdown after usage", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: trackVal,
- });
+ const trackVal = 300;
+ it("should have correct breakdown after usage", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: trackVal,
+ });
- await timeout(3000);
+ await timeout(3000);
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- // Should deduct
- const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 });
- const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 });
+ // Should deduct
+ const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 });
+ const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 });
- expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal);
- expect(biMonthlyBreakdown?.balance).to.equal(includedUsage);
+ expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal);
+ expect(biMonthlyBreakdown?.balance).to.equal(includedUsage);
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: trackVal,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: trackVal,
+ });
- await timeout(3000);
+ await timeout(3000);
- const customer2 = await autumn.customers.get(customerId);
- const monthlyBreakdown2 = getBreakdown({
- customer: customer2,
- intervalCount: 1,
- });
- const biMonthlyBreakdown2 = getBreakdown({
- customer: customer2,
- intervalCount: 2,
- });
+ const customer2 = await autumn.customers.get(customerId);
+ const monthlyBreakdown2 = getBreakdown({
+ customer: customer2,
+ intervalCount: 1,
+ });
+ const biMonthlyBreakdown2 = getBreakdown({
+ customer: customer2,
+ intervalCount: 2,
+ });
- expect(monthlyBreakdown2?.balance).to.equal(0);
- expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100);
- });
+ expect(monthlyBreakdown2?.balance).to.equal(0);
+ expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100);
+ });
});
diff --git a/server/tests/advanced/defaultTrial/defaultTrial1.test.ts b/server/tests/advanced/defaultTrial/defaultTrial1.test.ts
index 1dcff6d14..0c4aa7354 100644
--- a/server/tests/advanced/defaultTrial/defaultTrial1.test.ts
+++ b/server/tests/advanced/defaultTrial/defaultTrial1.test.ts
@@ -1,10 +1,10 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
// Manual customer creation - not using initCustomer to control test clock properly
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -13,9 +13,9 @@ import { setupBefore } from "tests/before.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import {
- defaultTrialFree,
- defaultTrialPro,
- setupDefaultTrialBefore,
+ defaultTrialFree,
+ defaultTrialPro,
+ setupDefaultTrialBefore,
} from "./defaultTrialBefore.test.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
@@ -31,60 +31,60 @@ import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "defaultTrial1";
describe(`${chalk.yellowBright(`advanced/${testCase}: ensure default trials are attached when creating a customer`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockID: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockID: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = Math.floor(new Date().getTime() / 1000);
+ let curUnix = Math.floor(new Date().getTime() / 1000);
- before(async function () {
- await setupBefore(this);
- await setupDefaultTrialBefore({});
- const { autumnJs } = this;
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ await setupDefaultTrialBefore({});
+ const { autumnJs } = this;
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- const res = await initCustomerV2({
- autumn: autumnJs,
- customerId: testCase,
- db,
- org,
- env,
- });
+ const res = await initCustomerV2({
+ autumn: autumnJs,
+ customerId: testCase,
+ db,
+ org,
+ env,
+ });
- testClockID = res.testClockId;
- });
+ testClockID = res.testClockId;
+ });
- it("should create a customer with the paid default trial", async function () {
- let customer = await autumn.customers.get(customerId);
+ it("should create a customer with the paid default trial", async function () {
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialPro,
- status: CusProductStatus.Trialing,
- });
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialPro,
+ status: CusProductStatus.Trialing,
+ });
+ });
- describe("ensure trials automatically cancel if no payment method is provided", () => {
- it("should expire after 7 days", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId: testClockID,
- numberOfDays: 8,
- waitForSeconds: 10,
- });
+ describe("ensure trials automatically cancel if no payment method is provided", () => {
+ it("should expire after 7 days", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId: testClockID,
+ numberOfDays: 8,
+ waitForSeconds: 10,
+ });
- let customer = await autumn.customers.get(customerId);
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialFree,
- status: CusProductStatus.Active,
- });
- });
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialFree,
+ status: CusProductStatus.Active,
+ });
+ });
+ });
});
diff --git a/server/tests/advanced/defaultTrial/defaultTrial2.test.ts b/server/tests/advanced/defaultTrial/defaultTrial2.test.ts
index 5d55bb7bf..bd3386ef8 100644
--- a/server/tests/advanced/defaultTrial/defaultTrial2.test.ts
+++ b/server/tests/advanced/defaultTrial/defaultTrial2.test.ts
@@ -1,10 +1,10 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
// Manual customer creation - not using initCustomer to control test clock properly
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -14,8 +14,8 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import {
- defaultTrialPro,
- setupDefaultTrialBefore,
+ defaultTrialPro,
+ setupDefaultTrialBefore,
} from "./defaultTrialBefore.test.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
import { addDays, addHours } from "date-fns";
@@ -28,61 +28,61 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "defaultTrial2";
describe(`${chalk.yellowBright(`advanced/${testCase}: ensure trial transitions into full product if payment method is valid`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockID: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockID: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = Math.floor(new Date().getTime() / 1000);
+ let curUnix = Math.floor(new Date().getTime() / 1000);
- before(async function () {
- await setupBefore(this);
- await setupDefaultTrialBefore({});
- const { autumnJs } = this;
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ await setupDefaultTrialBefore({});
+ const { autumnJs } = this;
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- const res = await initCustomerV2({
- autumn: autumnJs,
- customerId: testCase,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomerV2({
+ autumn: autumnJs,
+ customerId: testCase,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockID = res.testClockId;
- });
+ testClockID = res.testClockId;
+ });
- it("should create a customer with the paid default trial", async function () {
- let customer = await autumn.customers.get(customerId);
+ it("should create a customer with the paid default trial", async function () {
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialPro,
- });
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialPro,
+ });
+ });
- it("should be active after 7 days", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId: testClockID,
- advanceTo: addHours(
- addDays(new Date(), 7),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 10,
- });
+ it("should be active after 7 days", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId: testClockID,
+ advanceTo: addHours(
+ addDays(new Date(), 7),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 10,
+ });
- let customer = await autumn.customers.get(customerId);
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialPro,
- status: CusProductStatus.Active,
- });
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialPro,
+ status: CusProductStatus.Active,
+ });
+ });
});
diff --git a/server/tests/advanced/defaultTrial/defaultTrial3.test.ts b/server/tests/advanced/defaultTrial/defaultTrial3.test.ts
index 42b63f203..703472190 100644
--- a/server/tests/advanced/defaultTrial/defaultTrial3.test.ts
+++ b/server/tests/advanced/defaultTrial/defaultTrial3.test.ts
@@ -1,10 +1,10 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
// Manual customer creation - not using initCustomer to control test clock properly
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -14,9 +14,9 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import {
- defaultTrialFree,
- defaultTrialPro,
- setupDefaultTrialBefore,
+ defaultTrialFree,
+ defaultTrialPro,
+ setupDefaultTrialBefore,
} from "./defaultTrialBefore.test.js";
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
import { addDays, addHours } from "date-fns";
@@ -29,77 +29,77 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "defaultTrial3";
describe(`${chalk.yellowBright(`advanced/${testCase}: ensure trials cancel with bad payment method`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockID: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockID: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = Math.floor(new Date().getTime() / 1000);
+ let curUnix = Math.floor(new Date().getTime() / 1000);
- before(async function () {
- await setupBefore(this);
- await setupDefaultTrialBefore({});
- const { autumnJs } = this;
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ await setupDefaultTrialBefore({});
+ const { autumnJs } = this;
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- const res = await initCustomerV2({
- autumn: autumnJs,
- customerId: testCase,
- db,
- org,
- env,
- attachPm: "fail",
- });
+ const res = await initCustomerV2({
+ autumn: autumnJs,
+ customerId: testCase,
+ db,
+ org,
+ env,
+ attachPm: "fail",
+ });
- testClockID = res.testClockId;
- });
+ testClockID = res.testClockId;
+ });
- it("should create a customer with the paid default trial", async function () {
- let customer = await autumn.customers.get(customerId);
+ it("should create a customer with the paid default trial", async function () {
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialPro,
- });
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialPro,
+ });
+ });
- it("should cancel after 7 days", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId: testClockID,
- advanceTo: addHours(
- addDays(new Date(), 7),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ it("should cancel after 7 days", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId: testClockID,
+ advanceTo: addHours(
+ addDays(new Date(), 7),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- let customer = await autumn.customers.get(customerId);
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: defaultTrialPro,
- status: CusProductStatus.PastDue,
- });
+ expectProductAttached({
+ customer,
+ product: defaultTrialPro,
+ status: CusProductStatus.PastDue,
+ });
- // await advanceTestClock({
- // stripeCli,
- // testClockId: testClockID,
- // // should be massive so the stripe smart retry works in all settings
- // numberOfDays: 31,
- // waitForSeconds: 30,
- // });
+ // await advanceTestClock({
+ // stripeCli,
+ // testClockId: testClockID,
+ // // should be massive so the stripe smart retry works in all settings
+ // numberOfDays: 31,
+ // waitForSeconds: 30,
+ // });
- // customer = await autumn.customers.get(customerId);
+ // customer = await autumn.customers.get(customerId);
- // expectProductAttached({
- // customer,
- // product: defaultTrialFree,
- // status: CusProductStatus.Active,
- // });
- });
+ // expectProductAttached({
+ // customer,
+ // product: defaultTrialFree,
+ // status: CusProductStatus.Active,
+ // });
+ });
});
diff --git a/server/tests/advanced/defaultTrial/defaultTrialBefore.test.ts b/server/tests/advanced/defaultTrial/defaultTrialBefore.test.ts
index 30c428728..e9d01ee70 100644
--- a/server/tests/advanced/defaultTrial/defaultTrialBefore.test.ts
+++ b/server/tests/advanced/defaultTrial/defaultTrialBefore.test.ts
@@ -1,57 +1,57 @@
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import {
- APIVersion,
- FreeTrialDuration,
- ProductItemInterval,
+ APIVersion,
+ FreeTrialDuration,
+ ProductItemInterval,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
export let defaultTrialPro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- interval: ProductItemInterval.Month,
- }),
- ],
- isDefault: true,
- forcePaidDefault: true,
- id: "defaultTrial_pro",
- group: "defaultTrial",
- type: "pro",
- freeTrial: {
- length: 7,
- duration: FreeTrialDuration.Day,
- unique_fingerprint: false,
- card_required: false,
- },
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ interval: ProductItemInterval.Month,
+ }),
+ ],
+ isDefault: true,
+ forcePaidDefault: true,
+ id: "defaultTrial_pro",
+ group: "defaultTrial",
+ type: "pro",
+ freeTrial: {
+ length: 7,
+ duration: FreeTrialDuration.Day,
+ unique_fingerprint: false,
+ card_required: false,
+ },
});
export let defaultTrialFree = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 500,
- interval: ProductItemInterval.Month,
- }),
- ],
- id: "defaultTrial_free",
- group: "defaultTrial",
- type: "free",
- isDefault: true,
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 500,
+ interval: ProductItemInterval.Month,
+ }),
+ ],
+ id: "defaultTrial_free",
+ group: "defaultTrial",
+ type: "free",
+ isDefault: true,
});
export const setupDefaultTrialBefore = async ({}: {}) => {
- const autumn = new AutumnInt({ version: APIVersion.v1_2 });
- for (const product of [defaultTrialPro, defaultTrialFree]) {
- let res = await autumn.products.get(product.id);
+ const autumn = new AutumnInt({ version: APIVersion.v1_2 });
+ for (const product of [defaultTrialPro, defaultTrialFree]) {
+ let res = await autumn.products.get(product.id);
- if (res.code === "product_not_found") {
- try {
- await autumn.products.create(product);
- } catch (error) {}
- }
- }
+ if (res.code === "product_not_found") {
+ try {
+ await autumn.products.create(product);
+ } catch (error) {}
+ }
+ }
};
diff --git a/server/tests/advanced/multiFeature/multiFeature1.ts b/server/tests/advanced/multiFeature/multiFeature1.ts
index 27b0427b2..dcd7409d1 100644
--- a/server/tests/advanced/multiFeature/multiFeature1.ts
+++ b/server/tests/advanced/multiFeature/multiFeature1.ts
@@ -4,11 +4,11 @@ import { features } from "tests/global.js";
import { setupBefore } from "tests/before.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- ProductItemFeatureType,
- UsageModel,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ ProductItemFeatureType,
+ UsageModel,
} from "@autumn/shared";
import { createProducts } from "tests/utils/productUtils.js";
import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js";
@@ -21,229 +21,229 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly
let pro = {
- id: "multiFeature1Pro",
- name: "Multi Feature 1 Pro",
- items: {
- prepaid: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 50,
- price: 10,
- interval: BillingInterval.Month,
- usage_model: UsageModel.Prepaid,
- }),
- payPerUse: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 0,
- price: 0.5,
- interval: BillingInterval.Month,
- usage_model: UsageModel.PayPerUse,
- }),
- },
+ id: "multiFeature1Pro",
+ name: "Multi Feature 1 Pro",
+ items: {
+ prepaid: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 50,
+ price: 10,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.Prepaid,
+ }),
+ payPerUse: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 0,
+ price: 0.5,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.PayPerUse,
+ }),
+ },
};
let premium = {
- id: "multiFeature1Premium",
- name: "Multi Feature 1 Premium",
- items: {
- // Prepaid
- prepaid: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 100,
- price: 15,
- interval: BillingInterval.Month,
- usage_model: UsageModel.Prepaid,
- }),
+ id: "multiFeature1Premium",
+ name: "Multi Feature 1 Premium",
+ items: {
+ // Prepaid
+ prepaid: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 100,
+ price: 15,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.Prepaid,
+ }),
- // Pay per use
- payPerUse: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 0,
- price: 1,
- interval: BillingInterval.Month,
- usage_model: UsageModel.PayPerUse,
- }),
- },
+ // Pay per use
+ payPerUse: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 0,
+ price: 1,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.PayPerUse,
+ }),
+ },
};
export const getPrepaidAndUsageCusEnts = async ({
- customerId,
- db,
- orgId,
- env,
- featureId,
+ customerId,
+ db,
+ orgId,
+ env,
+ featureId,
}: {
- customerId: string;
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- featureId: string;
+ customerId: string;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ featureId: string;
}) => {
- let mainCusProduct = await getMainCusProduct({
- customerId,
- db,
- orgId,
- env,
- });
+ let mainCusProduct = await getMainCusProduct({
+ customerId,
+ db,
+ orgId,
+ env,
+ });
- let prepaidCusEnt = getPrepaidCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let prepaidCusEnt = getPrepaidCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- let usageCusEnt = getUsageCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let usageCusEnt = getUsageCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- return { prepaidCusEnt, usageCusEnt };
+ return { prepaidCusEnt, usageCusEnt };
};
const testCase = "multiFeature1";
describe(`${chalk.yellowBright(
- "multiFeature1: Testing prepaid + pay per use -> prepaid + pay per use",
+ "multiFeature1: Testing prepaid + pay per use -> prepaid + pay per use",
)}`, () => {
- let autumn: AutumnInt = new AutumnInt();
- let autumn2: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let autumn2: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let customerId = testCase;
- let prepaidQuantity = 10;
- let prepaidAllowance = pro.items.prepaid.included_usage + prepaidQuantity;
- let totalUsage = 0;
+ let prepaidQuantity = 10;
+ let prepaidAllowance = pro.items.prepaid.included_usage + prepaidQuantity;
+ let totalUsage = 0;
- let premiumPrepaidAllowance =
- premium.items.prepaid.included_usage + prepaidQuantity;
+ let premiumPrepaidAllowance =
+ premium.items.prepaid.included_usage + prepaidQuantity;
- let optionsList = [
- {
- feature_id: features.metered1.id,
- quantity: prepaidQuantity,
- },
- ];
+ let optionsList = [
+ {
+ feature_id: features.metered1.id,
+ quantity: prepaidQuantity,
+ },
+ ];
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ });
- autumn = this.autumn;
+ autumn = this.autumn;
- await createProducts({
- autumn,
- products: [pro, premium],
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
+ });
- it("should attach pro product to customer", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- options: optionsList,
- });
+ it("should attach pro product to customer", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ options: optionsList,
+ });
- let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(prepaidCusEnt?.balance).to.equal(
- prepaidQuantity + pro.items.prepaid.included_usage,
- );
+ expect(prepaidCusEnt?.balance).to.equal(
+ prepaidQuantity + pro.items.prepaid.included_usage,
+ );
- expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
- });
+ expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
+ });
- it("should use prepaid allowance first", async function () {
- let value = 60;
+ it("should use prepaid allowance first", async function () {
+ let value = 60;
- await autumn.track({
- customer_id: customerId,
- value,
- feature_id: features.metered1.id,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value,
+ feature_id: features.metered1.id,
+ });
- totalUsage += value;
+ totalUsage += value;
- await timeout(3000);
+ await timeout(3000);
- let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(prepaidCusEnt?.balance).to.equal(prepaidAllowance - value);
- expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
- });
+ expect(prepaidCusEnt?.balance).to.equal(prepaidAllowance - value);
+ expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
+ });
- it("should have correct usage / invoice after upgrade", async function () {
- let value = 60;
- await autumn.track({
- customer_id: customerId,
- value,
- feature_id: features.metered1.id,
- });
+ it("should have correct usage / invoice after upgrade", async function () {
+ let value = 60;
+ await autumn.track({
+ customer_id: customerId,
+ value,
+ feature_id: features.metered1.id,
+ });
- totalUsage += value;
+ totalUsage += value;
- await timeout(10000);
+ await timeout(10000);
- let { usageCusEnt } = await getPrepaidAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { usageCusEnt } = await getPrepaidAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- options: optionsList,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ options: optionsList,
+ });
- let { prepaidCusEnt, usageCusEnt: newUsageCusEnt } =
- await getPrepaidAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { prepaidCusEnt, usageCusEnt: newUsageCusEnt } =
+ await getPrepaidAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- // Check invoice too
- let { invoices } = await autumn2.customers.get(customerId);
+ // Check invoice too
+ let { invoices } = await autumn2.customers.get(customerId);
- let invoice1Amount =
- (premium.items.prepaid.price ?? 0) * prepaidQuantity -
- (pro.items.prepaid.price ?? 0) * prepaidQuantity;
+ let invoice1Amount =
+ (premium.items.prepaid.price ?? 0) * prepaidQuantity -
+ (pro.items.prepaid.price ?? 0) * prepaidQuantity;
- let invoice0Amount = value * (pro.items.payPerUse.price ?? 0);
+ let invoice0Amount = value * (pro.items.payPerUse.price ?? 0);
- let totalAmount = invoice1Amount + invoice0Amount;
+ let totalAmount = invoice1Amount + invoice0Amount;
- expect(invoices![0].total).to.equal(totalAmount);
+ expect(invoices![0].total).to.equal(totalAmount);
- let leftover = premiumPrepaidAllowance - totalUsage + value;
- expect(prepaidCusEnt?.balance).to.equal(Math.max(0, leftover));
- expect(newUsageCusEnt?.balance).to.equal(0);
- });
+ let leftover = premiumPrepaidAllowance - totalUsage + value;
+ expect(prepaidCusEnt?.balance).to.equal(Math.max(0, leftover));
+ expect(newUsageCusEnt?.balance).to.equal(0);
+ });
});
diff --git a/server/tests/advanced/multiFeature/multiFeature2.ts b/server/tests/advanced/multiFeature/multiFeature2.ts
index 52581d4c3..10ae4afb9 100644
--- a/server/tests/advanced/multiFeature/multiFeature2.ts
+++ b/server/tests/advanced/multiFeature/multiFeature2.ts
@@ -4,23 +4,23 @@ import { features } from "tests/global.js";
import { setupBefore } from "tests/before.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- EntInterval,
- ProductItemFeatureType,
- UsageModel,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ EntInterval,
+ ProductItemFeatureType,
+ UsageModel,
} from "@autumn/shared";
import { createProduct } from "tests/utils/productUtils.js";
import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js";
import {
- getLifetimeFreeCusEnt,
- getUsageCusEnt,
+ getLifetimeFreeCusEnt,
+ getUsageCusEnt,
} from "tests/utils/cusProductUtils/cusEntSearchUtils.js";
import {
- constructFeatureItem,
- constructFeaturePriceItem,
+ constructFeatureItem,
+ constructFeaturePriceItem,
} from "@/internal/products/product-items/productItemUtils.js";
import { timeout } from "@/utils/genUtils.js";
@@ -29,198 +29,198 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly
let pro = {
- id: "multiFeature2Pro",
- name: "Multi Feature 2 Pro",
- items: {
- lifetime: constructFeatureItem({
- feature_id: features.metered1.id,
- included_usage: 50,
- interval: EntInterval.Lifetime,
- }),
- payPerUse: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 0,
- price: 0.5,
- interval: BillingInterval.Month,
- usage_model: UsageModel.PayPerUse,
- }),
- },
+ id: "multiFeature2Pro",
+ name: "Multi Feature 2 Pro",
+ items: {
+ lifetime: constructFeatureItem({
+ feature_id: features.metered1.id,
+ included_usage: 50,
+ interval: EntInterval.Lifetime,
+ }),
+ payPerUse: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 0,
+ price: 0.5,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.PayPerUse,
+ }),
+ },
};
let premium = {
- id: "multiFeature2Premium",
- name: "Multi Feature 2 Premium",
- items: {
- // Pay per use
- payPerUse: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 0,
- price: 1,
- interval: BillingInterval.Month,
- usage_model: UsageModel.PayPerUse,
- }),
- },
+ id: "multiFeature2Premium",
+ name: "Multi Feature 2 Premium",
+ items: {
+ // Pay per use
+ payPerUse: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 0,
+ price: 1,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.PayPerUse,
+ }),
+ },
};
export const getLifetimeAndUsageCusEnts = async ({
- customerId,
- db,
- orgId,
- env,
- featureId,
+ customerId,
+ db,
+ orgId,
+ env,
+ featureId,
}: {
- customerId: string;
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- featureId: string;
+ customerId: string;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ featureId: string;
}) => {
- let mainCusProduct = await getMainCusProduct({
- customerId: customerId,
- db,
- orgId,
- env,
- });
+ let mainCusProduct = await getMainCusProduct({
+ customerId: customerId,
+ db,
+ orgId,
+ env,
+ });
- let lifetimeCusEnt = getLifetimeFreeCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let lifetimeCusEnt = getLifetimeFreeCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- let usageCusEnt = getUsageCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let usageCusEnt = getUsageCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- return { lifetimeCusEnt, usageCusEnt };
+ return { lifetimeCusEnt, usageCusEnt };
};
const testCase = "multiFeature2";
describe(`${chalk.yellowBright(
- "multiFeature2: Testing lifetime + pay per use -> pay per use"
+ "multiFeature2: Testing lifetime + pay per use -> pay per use",
)}`, () => {
- let autumn: AutumnInt = new AutumnInt();
- let autumn2: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let autumn2: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let customerId = testCase;
- let totalUsage = 0;
+ let totalUsage = 0;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ });
- autumn = this.autumn;
+ autumn = this.autumn;
- await createProduct({
- autumn,
- product: pro,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
+ await createProduct({
+ autumn,
+ product: pro,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
- await createProduct({
- autumn,
- product: premium,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
- });
+ await createProduct({
+ autumn,
+ product: premium,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
+ });
- it("should attach pro product to customer", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should attach pro product to customer", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt?.balance).to.equal(pro.items.lifetime.included_usage);
+ expect(lifetimeCusEnt?.balance).to.equal(pro.items.lifetime.included_usage);
- expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
- });
+ expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
+ });
- it("should use lifetime allowance first", async function () {
- let value = pro.items.lifetime.included_usage as number;
+ it("should use lifetime allowance first", async function () {
+ let value = pro.items.lifetime.included_usage as number;
- await autumn.events.send({
- customerId,
- value,
- featureId: features.metered1.id,
- });
+ await autumn.events.send({
+ customerId,
+ value,
+ featureId: features.metered1.id,
+ });
- totalUsage += value;
+ totalUsage += value;
- await timeout(3000);
+ await timeout(3000);
- let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt?.balance).to.equal(
- (pro.items.lifetime.included_usage as number) - value
- );
- expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
- });
+ expect(lifetimeCusEnt?.balance).to.equal(
+ (pro.items.lifetime.included_usage as number) - value,
+ );
+ expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
+ });
- it("should have correct usage after upgrade", async function () {
- let value = 20;
+ it("should have correct usage after upgrade", async function () {
+ let value = 20;
- await autumn.track({
- customer_id: customerId,
- value,
- feature_id: features.metered1.id,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value,
+ feature_id: features.metered1.id,
+ });
- await timeout(3000);
+ await timeout(3000);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- // return;
- let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } =
- await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ // return;
+ let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } =
+ await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt).to.not.exist;
- expect(newUsageCusEnt?.balance).to.equal(-50);
+ expect(lifetimeCusEnt).to.not.exist;
+ expect(newUsageCusEnt?.balance).to.equal(-50);
- // Check invoice too
- let res = await autumn2.customers.get(customerId);
- let invoices = res.invoices;
+ // Check invoice too
+ let res = await autumn2.customers.get(customerId);
+ let invoices = res.invoices;
- let invoice0Amount = value * (pro.items.payPerUse.price ?? 0);
- expect(invoices![0].total).to.equal(
- invoice0Amount,
- "Invoice 0 should be 0"
- );
- });
+ let invoice0Amount = value * (pro.items.payPerUse.price ?? 0);
+ expect(invoices![0].total).to.equal(
+ invoice0Amount,
+ "Invoice 0 should be 0",
+ );
+ });
});
diff --git a/server/tests/advanced/multiFeature/multiFeature3.ts b/server/tests/advanced/multiFeature/multiFeature3.ts
index 33a85718c..9a090ca1a 100644
--- a/server/tests/advanced/multiFeature/multiFeature3.ts
+++ b/server/tests/advanced/multiFeature/multiFeature3.ts
@@ -5,22 +5,22 @@ import { features } from "tests/global.js";
import { setupBefore } from "tests/before.js";
import {
- AppEnv,
- BillingInterval,
- EntInterval,
- ProductItemFeatureType,
- UsageModel,
+ AppEnv,
+ BillingInterval,
+ EntInterval,
+ ProductItemFeatureType,
+ UsageModel,
} from "@autumn/shared";
import { createProducts } from "tests/utils/productUtils.js";
import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js";
import {
- getLifetimeFreeCusEnt,
- getUsageCusEnt,
+ getLifetimeFreeCusEnt,
+ getUsageCusEnt,
} from "tests/utils/cusProductUtils/cusEntSearchUtils.js";
import {
- constructFeatureItem,
- constructFeaturePriceItem,
+ constructFeatureItem,
+ constructFeaturePriceItem,
} from "@/internal/products/product-items/productItemUtils.js";
import { timeout } from "@/utils/genUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
@@ -30,155 +30,155 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
// Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly
let pro = {
- id: "multiFeature3Pro",
- name: "Multi Feature 3 Pro",
- items: {
- lifetime: constructFeatureItem({
- feature_id: features.metered1.id,
- included_usage: 50,
- interval: EntInterval.Lifetime,
- }),
- payPerUse: constructFeaturePriceItem({
- feature_id: features.metered1.id,
- feature_type: ProductItemFeatureType.SingleUse,
- included_usage: 0,
- price: 0.5,
- interval: BillingInterval.Month,
- usage_model: UsageModel.PayPerUse,
- }),
- },
+ id: "multiFeature3Pro",
+ name: "Multi Feature 3 Pro",
+ items: {
+ lifetime: constructFeatureItem({
+ feature_id: features.metered1.id,
+ included_usage: 50,
+ interval: EntInterval.Lifetime,
+ }),
+ payPerUse: constructFeaturePriceItem({
+ feature_id: features.metered1.id,
+ feature_type: ProductItemFeatureType.SingleUse,
+ included_usage: 0,
+ price: 0.5,
+ interval: BillingInterval.Month,
+ usage_model: UsageModel.PayPerUse,
+ }),
+ },
};
export const getLifetimeAndUsageCusEnts = async ({
- customerId,
- db,
- orgId,
- env,
- featureId,
+ customerId,
+ db,
+ orgId,
+ env,
+ featureId,
}: {
- customerId: string;
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- featureId: string;
+ customerId: string;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ featureId: string;
}) => {
- let mainCusProduct = await getMainCusProduct({
- customerId,
- db,
- orgId,
- env,
- });
+ let mainCusProduct = await getMainCusProduct({
+ customerId,
+ db,
+ orgId,
+ env,
+ });
- let lifetimeCusEnt = getLifetimeFreeCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let lifetimeCusEnt = getLifetimeFreeCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- let usageCusEnt = getUsageCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let usageCusEnt = getUsageCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- return { lifetimeCusEnt, usageCusEnt };
+ return { lifetimeCusEnt, usageCusEnt };
};
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock",
+ "multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock",
)}`, () => {
- let autumn: AutumnInt = new AutumnInt();
- let customerId = "multiFeature3Customer";
+ let autumn: AutumnInt = new AutumnInt();
+ let customerId = "multiFeature3Customer";
- let totalUsage = 0;
+ let totalUsage = 0;
- let testClockId: string;
- before(async function () {
- await setupBefore(this);
+ let testClockId: string;
+ before(async function () {
+ await setupBefore(this);
- let { customer, testClockId: _testClockId } =
- await initCustomerWithTestClock({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
+ let { customer, testClockId: _testClockId } =
+ await initCustomerWithTestClock({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
- testClockId = _testClockId;
+ testClockId = _testClockId;
- autumn = this.autumn;
+ autumn = this.autumn;
- await createProducts({
- autumn,
- products: [pro],
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
+ });
- it("should attach pro product to customer", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should attach pro product to customer", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt?.balance).to.equal(pro.items.lifetime.included_usage);
+ expect(lifetimeCusEnt?.balance).to.equal(pro.items.lifetime.included_usage);
- expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
- });
+ expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage);
+ });
- let overageValue = 30;
- it("should use lifetime allowance + overage", async function () {
- let value = pro.items.lifetime.included_usage as number;
- value += overageValue;
+ let overageValue = 30;
+ it("should use lifetime allowance + overage", async function () {
+ let value = pro.items.lifetime.included_usage as number;
+ value += overageValue;
- await autumn.track({
- customer_id: customerId,
- value,
- feature_id: features.metered1.id,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value,
+ feature_id: features.metered1.id,
+ });
- totalUsage += value;
+ totalUsage += value;
- await timeout(3000);
+ await timeout(3000);
- let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt?.balance).to.equal(0);
- expect(usageCusEnt?.balance).to.equal(-overageValue);
- });
+ expect(lifetimeCusEnt?.balance).to.equal(0);
+ expect(usageCusEnt?.balance).to.equal(-overageValue);
+ });
- it("cycle 1:should have correct usage after first cycle", async function () {
- let advanceTo = addMonths(new Date(), 1).getTime();
- await advanceTestClock({
- stripeCli: this.stripeCli,
- testClockId,
- advanceTo,
- });
+ it("cycle 1:should have correct usage after first cycle", async function () {
+ let advanceTo = addMonths(new Date(), 1).getTime();
+ await advanceTestClock({
+ stripeCli: this.stripeCli,
+ testClockId,
+ advanceTo,
+ });
- let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
- customerId,
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- featureId: features.metered1.id,
- });
+ let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({
+ customerId,
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ featureId: features.metered1.id,
+ });
- expect(lifetimeCusEnt?.balance).to.equal(0);
- expect(usageCusEnt?.balance).to.equal(0);
- });
+ expect(lifetimeCusEnt?.balance).to.equal(0);
+ expect(usageCusEnt?.balance).to.equal(0);
+ });
});
diff --git a/server/tests/advanced/referrals/paid/referrals13.ts b/server/tests/advanced/referrals/paid/referrals13.ts
index 207447704..424ba4c8e 100644
--- a/server/tests/advanced/referrals/paid/referrals13.ts
+++ b/server/tests/advanced/referrals/paid/referrals13.ts
@@ -1,12 +1,12 @@
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- type AppEnv,
- CusExpand,
- CusProductStatus,
- ErrCode,
- type Organization,
- type ReferralCode,
- type RewardRedemption,
+ type AppEnv,
+ CusExpand,
+ CusProductStatus,
+ ErrCode,
+ type Organization,
+ type ReferralCode,
+ type RewardRedemption,
} from "@autumn/shared";
import { assert } from "chai";
import chalk from "chalk";
@@ -24,232 +24,232 @@ import { products, referralPrograms } from "../../../global.js";
export const group = "referrals13";
describe(`${chalk.yellowBright(
- "referrals13: Testing referrals - referrer on Pro, gets discount on next cycle - coupon-based"
+ "referrals13: Testing referrals - referrer on Pro, gets discount on next cycle - coupon-based",
)}`, () => {
- const mainCustomerId = "main-referral-13";
- const redeemer = "referral13-r1";
- const redeemerPM = "success";
- const autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- const testClockIds: string[] = [];
- let referralCode: ReferralCode;
+ const mainCustomerId = "main-referral-13";
+ const redeemer = "referral13-r1";
+ const redeemerPM = "success";
+ const autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ const testClockIds: string[] = [];
+ let referralCode: ReferralCode;
- let redemption: RewardRedemption;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let redemption: RewardRedemption;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- try {
- await Promise.all([
- autumn.customers.delete(mainCustomerId),
- autumn.customers.delete(redeemer),
- RewardRedemptionService._resetCustomerRedemptions({
- db,
- internalCustomerId: [mainCustomerId, redeemer],
- }),
- ]);
- } catch {}
+ try {
+ await Promise.all([
+ autumn.customers.delete(mainCustomerId),
+ autumn.customers.delete(redeemer),
+ RewardRedemptionService._resetCustomerRedemptions({
+ db,
+ internalCustomerId: [mainCustomerId, redeemer],
+ }),
+ ]);
+ } catch {}
- // Initialize main customer with Pro product already attached
- const res = await initCustomer({
- autumn: this.autumnJs,
- customerId: mainCustomerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ // Initialize main customer with Pro product already attached
+ const res = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: mainCustomerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockIds.push(res.testClockId);
+ testClockIds.push(res.testClockId);
- // Attach Pro product to main customer first
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: products.pro.id,
- });
+ // Attach Pro product to main customer first
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: products.pro.id,
+ });
- const redeemerRes = await initCustomer({
- autumn: this.autumnJs,
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: redeemerPM,
- withTestClock: true,
- });
+ const redeemerRes = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: redeemerPM,
+ withTestClock: true,
+ });
- testClockIds.push(redeemerRes.testClockId);
- });
+ testClockIds.push(redeemerRes.testClockId);
+ });
- it("should advance clock 10 days before redeeming", async () => {
- // Advance 10 days after Pro is attached
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 10,
- waitForSeconds: 10,
- stripeCli,
- })
- )
- );
- });
+ it("should advance clock 10 days before redeeming", async () => {
+ // Advance 10 days after Pro is attached
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 10,
+ waitForSeconds: 10,
+ stripeCli,
+ }),
+ ),
+ );
+ });
- it("should create code once", async () => {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.paidProductImmediateReferrer.id,
- });
+ it("should create code once", async () => {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.paidProductImmediateReferrer.id,
+ });
- assert.exists(referralCode.code);
- });
+ assert.exists(referralCode.code);
+ });
- it("should create redemption for redeemer and fail if redeemed again", async () => {
- redemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
+ it("should create redemption for redeemer and fail if redeemed again", async () => {
+ redemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
- // Try redeem for redeemer again
- try {
- await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
- assert.fail("Should not be able to redeem again");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
- }
- });
+ // Try redeem for redeemer again
+ try {
+ await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
+ assert.fail("Should not be able to redeem again");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
+ }
+ });
- it("should have referrer already on Pro, and redeemer gets free product", async () => {
- const redemptionResult = await autumn.redemptions.get(redemption.id);
- assert.equal(redemptionResult.redeemer_applied, true);
+ it("should have referrer already on Pro, and redeemer gets free product", async () => {
+ const redemptionResult = await autumn.redemptions.get(redemption.id);
+ assert.equal(redemptionResult.redeemer_applied, true);
- const mainProds = (await autumn.customers.get(mainCustomerId)).products;
- const redeemerProds = (await autumn.customers.get(redeemer)).products;
+ const mainProds = (await autumn.customers.get(mainCustomerId)).products;
+ const redeemerProds = (await autumn.customers.get(redeemer)).products;
- // Main customer (referrer) should have the pro product (already attached)
- assert.equal(mainProds.length, 1);
- assert.equal(mainProds[0].id, products.pro.id);
+ // Main customer (referrer) should have the pro product (already attached)
+ assert.equal(mainProds.length, 1);
+ assert.equal(mainProds[0].id, products.pro.id);
- // Redeemer should only have the free product (no pro product given in referrer-only program)
- assert.equal(redeemerProds.length, 1);
- assert.equal(redeemerProds[0].id, products.free.id);
+ // Redeemer should only have the free product (no pro product given in referrer-only program)
+ assert.equal(redeemerProds.length, 1);
+ assert.equal(redeemerProds[0].id, products.free.id);
- expectProductV1Attached({
- customer: await autumn.customers.get(mainCustomerId),
- product: products.pro,
- status: CusProductStatus.Active,
- });
+ expectProductV1Attached({
+ customer: await autumn.customers.get(mainCustomerId),
+ product: products.pro,
+ status: CusProductStatus.Active,
+ });
- // Verify redeemer only has free product
- expectProductV1Attached({
- customer: await autumn.customers.get(redeemer),
- product: products.free,
- status: CusProductStatus.Active,
- });
- });
+ // Verify redeemer only has free product
+ expectProductV1Attached({
+ customer: await autumn.customers.get(redeemer),
+ product: products.free,
+ status: CusProductStatus.Active,
+ });
+ });
- it("should advance test clock and verify referrer gets discount on next Pro cycle", async () => {
- // Advance 31 days from current time to trigger next billing cycle
- // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 31,
- waitForSeconds: 25,
- stripeCli,
- })
- )
- );
+ it("should advance test clock and verify referrer gets discount on next Pro cycle", async () => {
+ // Advance 31 days from current time to trigger next billing cycle
+ // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 31,
+ waitForSeconds: 25,
+ stripeCli,
+ }),
+ ),
+ );
- // Test that main customer's Pro invoice has discount applied
- const mainCustomerWithInvoices = await autumn.customers.get(
- mainCustomerId,
- {
- expand: [CusExpand.Invoices, CusExpand.Rewards],
- }
- );
+ // Test that main customer's Pro invoice has discount applied
+ const mainCustomerWithInvoices = await autumn.customers.get(
+ mainCustomerId,
+ {
+ expand: [CusExpand.Invoices, CusExpand.Rewards],
+ },
+ );
- const proInvoice = mainCustomerWithInvoices.invoices.find((x) =>
- x.product_ids.includes(products.pro.id)
- );
+ const proInvoice = mainCustomerWithInvoices.invoices.find((x) =>
+ x.product_ids.includes(products.pro.id),
+ );
- const expectedTotal = products.pro.prices[0].config.amount;
+ const expectedTotal = products.pro.prices[0].config.amount;
- const actualTotal = proInvoice?.total;
+ const actualTotal = proInvoice?.total;
- if (proInvoice) {
- // Should have a discount applied - invoice total should be less than full Pro price ($10)
- assert.isBelow(
- actualTotal!,
- expectedTotal, // $10 in cents
- "Pro invoice should have discount applied, making it less than full price"
- );
+ if (proInvoice) {
+ // Should have a discount applied - invoice total should be less than full Pro price ($10)
+ assert.isBelow(
+ actualTotal!,
+ expectedTotal, // $10 in cents
+ "Pro invoice should have discount applied, making it less than full price",
+ );
- // For referrer-only reward, the discount should make it significantly cheaper or free
- assert.isAtMost(
- actualTotal!,
- expectedTotal / 2, // $5 or less in cents - assuming at least 50% discount
- "Referrer should get substantial discount on Pro product"
- );
- }
+ // For referrer-only reward, the discount should make it significantly cheaper or free
+ assert.isAtMost(
+ actualTotal!,
+ expectedTotal / 2, // $5 or less in cents - assuming at least 50% discount
+ "Referrer should get substantial discount on Pro product",
+ );
+ }
- const dbCustomers = await Promise.all(
- [mainCustomerId, redeemer].map((x) =>
- CusService.getFull({
- db,
- idOrInternalId: x,
- orgId: org.id,
- env,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Expired,
- ],
- })
- )
- );
+ const dbCustomers = await Promise.all(
+ [mainCustomerId, redeemer].map((x) =>
+ CusService.getFull({
+ db,
+ idOrInternalId: x,
+ orgId: org.id,
+ env,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Expired,
+ ],
+ }),
+ ),
+ );
- const expectedProducts = [
- [
- // Main referrer - keeps Pro with discount applied
- { name: "Free", status: CusProductStatus.Expired },
- { name: "Pro", status: CusProductStatus.Active },
- ],
- [
- // Redeemer - only has free product (no reward in referrer-only program)
- { name: "Free", status: CusProductStatus.Active },
- ],
- ];
+ const expectedProducts = [
+ [
+ // Main referrer - keeps Pro with discount applied
+ { name: "Free", status: CusProductStatus.Expired },
+ { name: "Pro", status: CusProductStatus.Active },
+ ],
+ [
+ // Redeemer - only has free product (no reward in referrer-only program)
+ { name: "Free", status: CusProductStatus.Active },
+ ],
+ ];
- dbCustomers.forEach((customer, index) => {
- const expectedProductsForCustomer = expectedProducts[index];
- expectedProductsForCustomer.forEach((expectedProduct) => {
- const matchingProduct = customer.customer_products.find(
- (cp) =>
- cp.product.name === expectedProduct.name &&
- cp.status === expectedProduct.status
- );
- const unMatchedProduct = customer.customer_products.find(
- (cp) => cp.product.name === expectedProduct.name
- );
+ dbCustomers.forEach((customer, index) => {
+ const expectedProductsForCustomer = expectedProducts[index];
+ expectedProductsForCustomer.forEach((expectedProduct) => {
+ const matchingProduct = customer.customer_products.find(
+ (cp) =>
+ cp.product.name === expectedProduct.name &&
+ cp.status === expectedProduct.status,
+ );
+ const unMatchedProduct = customer.customer_products.find(
+ (cp) => cp.product.name === expectedProduct.name,
+ );
- assert.exists(
- matchingProduct,
- `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`
- );
- });
- });
- });
+ assert.exists(
+ matchingProduct,
+ `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`,
+ );
+ });
+ });
+ });
});
diff --git a/server/tests/advanced/referrals/paid/referrals14.ts b/server/tests/advanced/referrals/paid/referrals14.ts
index 12aa8246b..6b12ddf22 100644
--- a/server/tests/advanced/referrals/paid/referrals14.ts
+++ b/server/tests/advanced/referrals/paid/referrals14.ts
@@ -1,11 +1,11 @@
import {
- type AppEnv,
- CusExpand,
- CusProductStatus,
- ErrCode,
- type Organization,
- type ReferralCode,
- type RewardRedemption,
+ type AppEnv,
+ CusExpand,
+ CusProductStatus,
+ ErrCode,
+ type Organization,
+ type ReferralCode,
+ type RewardRedemption,
} from "@autumn/shared";
import { assert } from "chai";
import chalk from "chalk";
@@ -23,242 +23,242 @@ import { products, referralPrograms } from "../../../global.js";
export const group = "referrals14";
describe(`${chalk.yellowBright(
- "referrals14: Testing referrals - referrer on Premium (higher tier), gets pro_amount discount - coupon-based"
+ "referrals14: Testing referrals - referrer on Premium (higher tier), gets pro_amount discount - coupon-based",
)}`, () => {
- const mainCustomerId = "main-referral-14";
- const redeemer = "referral14-r1";
- const redeemerPM = "success";
- const autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- const testClockIds: string[] = [];
- let referralCode: ReferralCode;
+ const mainCustomerId = "main-referral-14";
+ const redeemer = "referral14-r1";
+ const redeemerPM = "success";
+ const autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ const testClockIds: string[] = [];
+ let referralCode: ReferralCode;
- let redemption: RewardRedemption;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let redemption: RewardRedemption;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- try {
- await Promise.all([
- autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
- autumn.customers.delete(redeemer, { deleteInStripe: true }),
- RewardRedemptionService._resetCustomerRedemptions({
- db,
- internalCustomerId: [mainCustomerId, redeemer],
- }),
- ]);
- } catch {}
+ try {
+ await Promise.all([
+ autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
+ autumn.customers.delete(redeemer, { deleteInStripe: true }),
+ RewardRedemptionService._resetCustomerRedemptions({
+ db,
+ internalCustomerId: [mainCustomerId, redeemer],
+ }),
+ ]);
+ } catch {}
- // Initialize main customer with Premium product already attached
- const res = await initCustomer({
- autumn: this.autumnJs,
- customerId: mainCustomerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ // Initialize main customer with Premium product already attached
+ const res = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: mainCustomerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockIds.push(res.testClockId);
+ testClockIds.push(res.testClockId);
- // Attach Premium product to main customer first (higher tier than Pro)
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: products.premium.id,
- });
+ // Attach Premium product to main customer first (higher tier than Pro)
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: products.premium.id,
+ });
- const redeemerRes = await initCustomer({
- autumn: this.autumnJs,
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: redeemerPM,
- withTestClock: true,
- });
+ const redeemerRes = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: redeemerPM,
+ withTestClock: true,
+ });
- testClockIds.push(redeemerRes.testClockId);
+ testClockIds.push(redeemerRes.testClockId);
- // Advance 10 days after Premium is attached, then redeem the code
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 10,
- waitForSeconds: 5,
- stripeCli,
- })
- )
- );
- });
+ // Advance 10 days after Premium is attached, then redeem the code
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 10,
+ waitForSeconds: 5,
+ stripeCli,
+ }),
+ ),
+ );
+ });
- it("should create code once", async () => {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.paidProductImmediateReferrer.id,
- });
+ it("should create code once", async () => {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.paidProductImmediateReferrer.id,
+ });
- assert.exists(referralCode.code);
+ assert.exists(referralCode.code);
- // Get referral code again
- const referralCode2 = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.paidProductImmediateReferrer.id,
- });
+ // Get referral code again
+ const referralCode2 = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.paidProductImmediateReferrer.id,
+ });
- assert.equal(referralCode2.code, referralCode.code);
- });
+ assert.equal(referralCode2.code, referralCode.code);
+ });
- it("should create redemption for redeemer and fail if redeemed again", async () => {
- redemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
+ it("should create redemption for redeemer and fail if redeemed again", async () => {
+ redemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
- // Try redeem for redeemer again
- try {
- await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
- assert.fail("Should not be able to redeem again");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
- }
- });
+ // Try redeem for redeemer again
+ try {
+ await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
+ assert.fail("Should not be able to redeem again");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
+ }
+ });
- it("should have referrer already on Premium, and redeemer gets free product", async () => {
- const redemptionResult = await autumn.redemptions.get(redemption.id);
- assert.equal(redemptionResult.redeemer_applied, true);
+ it("should have referrer already on Premium, and redeemer gets free product", async () => {
+ const redemptionResult = await autumn.redemptions.get(redemption.id);
+ assert.equal(redemptionResult.redeemer_applied, true);
- const mainCus = await autumn.customers.get(mainCustomerId);
- const redeemerCus = await autumn.customers.get(redeemer);
- const mainProds = mainCus.products;
- const redeemerProds = redeemerCus.products;
+ const mainCus = await autumn.customers.get(mainCustomerId);
+ const redeemerCus = await autumn.customers.get(redeemer);
+ const mainProds = mainCus.products;
+ const redeemerProds = redeemerCus.products;
- // Main customer (referrer) should have the premium product (already attached)
- assert.equal(mainProds.length, 1);
- assert.equal(mainProds[0].id, products.premium.id);
+ // Main customer (referrer) should have the premium product (already attached)
+ assert.equal(mainProds.length, 1);
+ assert.equal(mainProds[0].id, products.premium.id);
- // Redeemer should only have the free product (no pro product given in referrer-only program)
- assert.equal(redeemerProds.length, 1);
- assert.equal(redeemerProds[0].id, products.free.id);
+ // Redeemer should only have the free product (no pro product given in referrer-only program)
+ assert.equal(redeemerProds.length, 1);
+ assert.equal(redeemerProds[0].id, products.free.id);
- expectProductV1Attached({
- customer: mainCus,
- product: products.premium,
- status: CusProductStatus.Active,
- });
+ expectProductV1Attached({
+ customer: mainCus,
+ product: products.premium,
+ status: CusProductStatus.Active,
+ });
- // Verify redeemer only has free product
- expectProductV1Attached({
- customer: redeemerCus,
- product: products.free,
- status: CusProductStatus.Active,
- });
- });
+ // Verify redeemer only has free product
+ expectProductV1Attached({
+ customer: redeemerCus,
+ product: products.free,
+ status: CusProductStatus.Active,
+ });
+ });
- it("should advance test clock and verify referrer gets pro_amount discount on Premium cycle", async () => {
- // Advance 21 more days (total 31 days from start) to trigger next billing cycle
- // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 31,
- waitForSeconds: 25,
- stripeCli,
- })
- )
- );
+ it("should advance test clock and verify referrer gets pro_amount discount on Premium cycle", async () => {
+ // Advance 21 more days (total 31 days from start) to trigger next billing cycle
+ // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 31,
+ waitForSeconds: 25,
+ stripeCli,
+ }),
+ ),
+ );
- // Test that main customer's Premium invoice has pro_amount discount applied
- const mainCustomerWithInvoices = await autumn.customers.get(
- mainCustomerId,
- {
- expand: [CusExpand.Invoices],
- }
- );
+ // Test that main customer's Premium invoice has pro_amount discount applied
+ const mainCustomerWithInvoices = await autumn.customers.get(
+ mainCustomerId,
+ {
+ expand: [CusExpand.Invoices],
+ },
+ );
- const premiumInvoice = mainCustomerWithInvoices.invoices.find((x) =>
- x.product_ids.includes(products.premium.id)
- );
- if (premiumInvoice) {
- // Premium costs $50, Pro costs $10 - so referrer should get $10 discount on Premium
- // Expected: Premium ($50) - Pro amount ($10) = $40
- console.log(products.premium.prices);
- const premiumPrice = products.premium.prices[0].config.amount; // $50
- const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
- const expectedTotal = premiumPrice - proAmount; // $40
+ const premiumInvoice = mainCustomerWithInvoices.invoices.find((x) =>
+ x.product_ids.includes(products.premium.id),
+ );
+ if (premiumInvoice) {
+ // Premium costs $50, Pro costs $10 - so referrer should get $10 discount on Premium
+ // Expected: Premium ($50) - Pro amount ($10) = $40
+ console.log(products.premium.prices);
+ const premiumPrice = products.premium.prices[0].config.amount; // $50
+ const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
+ const expectedTotal = premiumPrice - proAmount; // $40
- // The invoice total should be exactly Premium price minus pro_amount
- assert.equal(
- premiumInvoice.total,
- expectedTotal,
- `Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${premiumInvoice.total}`
- );
+ // The invoice total should be exactly Premium price minus pro_amount
+ assert.equal(
+ premiumInvoice.total,
+ expectedTotal,
+ `Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${premiumInvoice.total}`,
+ );
- // Verify that the discount was applied (total is less than full Premium price)
- assert.isBelow(
- premiumInvoice.total,
- premiumPrice,
- "Referrer on Premium should get pro_amount discount, making it less than full Premium price"
- );
- }
+ // Verify that the discount was applied (total is less than full Premium price)
+ assert.isBelow(
+ premiumInvoice.total,
+ premiumPrice,
+ "Referrer on Premium should get pro_amount discount, making it less than full Premium price",
+ );
+ }
- const dbCustomers = await Promise.all(
- [mainCustomerId, redeemer].map((x) =>
- CusService.getFull({
- db,
- idOrInternalId: x,
- orgId: org.id,
- env,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Expired,
- ],
- })
- )
- );
+ const dbCustomers = await Promise.all(
+ [mainCustomerId, redeemer].map((x) =>
+ CusService.getFull({
+ db,
+ idOrInternalId: x,
+ orgId: org.id,
+ env,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Expired,
+ ],
+ }),
+ ),
+ );
- const expectedProducts = [
- [
- // Main referrer - keeps Premium with pro_amount discount applied
- { name: "Free", status: CusProductStatus.Expired },
- { name: "Premium", status: CusProductStatus.Active },
- ],
- [
- // Redeemer - only has free product (no reward in referrer-only program)
- { name: "Free", status: CusProductStatus.Active },
- ],
- ];
+ const expectedProducts = [
+ [
+ // Main referrer - keeps Premium with pro_amount discount applied
+ { name: "Free", status: CusProductStatus.Expired },
+ { name: "Premium", status: CusProductStatus.Active },
+ ],
+ [
+ // Redeemer - only has free product (no reward in referrer-only program)
+ { name: "Free", status: CusProductStatus.Active },
+ ],
+ ];
- dbCustomers.forEach((customer, index) => {
- const expectedProductsForCustomer = expectedProducts[index];
- expectedProductsForCustomer.forEach((expectedProduct) => {
- const matchingProduct = customer.customer_products.find(
- (cp) =>
- cp.product.name === expectedProduct.name &&
- cp.status === expectedProduct.status
- );
- const unMatchedProduct = customer.customer_products.find(
- (cp) => cp.product.name === expectedProduct.name
- );
+ dbCustomers.forEach((customer, index) => {
+ const expectedProductsForCustomer = expectedProducts[index];
+ expectedProductsForCustomer.forEach((expectedProduct) => {
+ const matchingProduct = customer.customer_products.find(
+ (cp) =>
+ cp.product.name === expectedProduct.name &&
+ cp.status === expectedProduct.status,
+ );
+ const unMatchedProduct = customer.customer_products.find(
+ (cp) => cp.product.name === expectedProduct.name,
+ );
- assert.exists(
- matchingProduct,
- `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`
- );
- });
- });
- });
+ assert.exists(
+ matchingProduct,
+ `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`,
+ );
+ });
+ });
+ });
});
diff --git a/server/tests/advanced/referrals/paid/referrals15.ts b/server/tests/advanced/referrals/paid/referrals15.ts
index 59b884853..1f8c15c82 100644
--- a/server/tests/advanced/referrals/paid/referrals15.ts
+++ b/server/tests/advanced/referrals/paid/referrals15.ts
@@ -1,11 +1,11 @@
import {
- type AppEnv,
- CusExpand,
- CusProductStatus,
- ErrCode,
- type Organization,
- type ReferralCode,
- type RewardRedemption,
+ type AppEnv,
+ CusExpand,
+ CusProductStatus,
+ ErrCode,
+ type Organization,
+ type ReferralCode,
+ type RewardRedemption,
} from "@autumn/shared";
import { assert } from "chai";
import chalk from "chalk";
@@ -23,274 +23,274 @@ import { products, referralPrograms } from "../../../global.js";
export const group = "referrals15";
describe(`${chalk.yellowBright(
- "referrals15: Testing referrals - referrer starts with no product, gets pro_amount discount - immediate, both - coupon-based"
+ "referrals15: Testing referrals - referrer starts with no product, gets pro_amount discount - immediate, both - coupon-based",
)}`, () => {
- const mainCustomerId = "main-referral-15";
- const redeemer = "referral15-r1";
- const redeemerPM = "success";
- const autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- const testClockIds: string[] = [];
- let referralCode: ReferralCode;
+ const mainCustomerId = "main-referral-15";
+ const redeemer = "referral15-r1";
+ const redeemerPM = "success";
+ const autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ const testClockIds: string[] = [];
+ let referralCode: ReferralCode;
- let redemption: RewardRedemption;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let redemption: RewardRedemption;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- try {
- await Promise.all([
- autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
- autumn.customers.delete(redeemer, { deleteInStripe: true }),
- RewardRedemptionService._resetCustomerRedemptions({
- db,
- internalCustomerId: [mainCustomerId, redeemer],
- }),
- ]);
- } catch {}
+ try {
+ await Promise.all([
+ autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
+ autumn.customers.delete(redeemer, { deleteInStripe: true }),
+ RewardRedemptionService._resetCustomerRedemptions({
+ db,
+ internalCustomerId: [mainCustomerId, redeemer],
+ }),
+ ]);
+ } catch {}
- // Initialize main customer with NO paid product (just free tier)
- const res = await initCustomer({
- autumn: this.autumnJs,
- customerId: mainCustomerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ // Initialize main customer with NO paid product (just free tier)
+ const res = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: mainCustomerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockIds.push(res.testClockId);
+ testClockIds.push(res.testClockId);
- const redeemerRes = await initCustomer({
- autumn: this.autumnJs,
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: redeemerPM,
- withTestClock: true,
- });
+ const redeemerRes = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: redeemerPM,
+ withTestClock: true,
+ });
- testClockIds.push(redeemerRes.testClockId);
- });
+ testClockIds.push(redeemerRes.testClockId);
+ });
- it("should advance clock 10 days before redeeming", async () => {
- // Advance 10 days after setup
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 10,
- waitForSeconds: 10,
- stripeCli,
- })
- )
- );
- });
+ it("should advance clock 10 days before redeeming", async () => {
+ // Advance 10 days after setup
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 10,
+ waitForSeconds: 10,
+ stripeCli,
+ }),
+ ),
+ );
+ });
- it("should create code once", async () => {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.paidProductImmediateAll.id,
- });
+ it("should create code once", async () => {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.paidProductImmediateAll.id,
+ });
- assert.exists(referralCode.code);
+ assert.exists(referralCode.code);
- // Get referral code again
- const referralCode2 = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.paidProductImmediateAll.id,
- });
+ // Get referral code again
+ const referralCode2 = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.paidProductImmediateAll.id,
+ });
- assert.equal(referralCode2.code, referralCode.code);
- });
+ assert.equal(referralCode2.code, referralCode.code);
+ });
- it("should create redemption for redeemer and fail if redeemed again", async () => {
- redemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
+ it("should create redemption for redeemer and fail if redeemed again", async () => {
+ redemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
- // Try redeem for redeemer again
- try {
- await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
- assert.fail("Should not be able to redeem again");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
- }
- });
+ // Try redeem for redeemer again
+ try {
+ await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
+ assert.fail("Should not be able to redeem again");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
+ }
+ });
- it("should have both referrer and redeemer get pro product", async () => {
- const redemptionResult = await autumn.redemptions.get(redemption.id);
- assert.equal(redemptionResult.redeemer_applied, true);
+ it("should have both referrer and redeemer get pro product", async () => {
+ const redemptionResult = await autumn.redemptions.get(redemption.id);
+ assert.equal(redemptionResult.redeemer_applied, true);
- const mainCus = await autumn.customers.get(mainCustomerId);
- const redeemerCus = await autumn.customers.get(redeemer);
- const mainProds = mainCus.products;
- const redeemerProds = redeemerCus.products;
+ const mainCus = await autumn.customers.get(mainCustomerId);
+ const redeemerCus = await autumn.customers.get(redeemer);
+ const mainProds = mainCus.products;
+ const redeemerProds = redeemerCus.products;
- // Main customer (referrer) should now have the pro product
- assert.equal(mainProds.length, 1);
- assert.equal(mainProds[0].id, products.pro.id);
+ // Main customer (referrer) should now have the pro product
+ assert.equal(mainProds.length, 1);
+ assert.equal(mainProds[0].id, products.pro.id);
- // Redeemer should also have the pro product (both get reward)
- assert.equal(redeemerProds.length, 1);
- assert.equal(redeemerProds[0].id, products.pro.id);
+ // Redeemer should also have the pro product (both get reward)
+ assert.equal(redeemerProds.length, 1);
+ assert.equal(redeemerProds[0].id, products.pro.id);
- expectProductV1Attached({
- customer: mainCus,
- product: products.pro,
- status: CusProductStatus.Active,
- });
+ expectProductV1Attached({
+ customer: mainCus,
+ product: products.pro,
+ status: CusProductStatus.Active,
+ });
- expectProductV1Attached({
- customer: redeemerCus,
- product: products.pro,
- status: CusProductStatus.Active,
- });
- });
+ expectProductV1Attached({
+ customer: redeemerCus,
+ product: products.pro,
+ status: CusProductStatus.Active,
+ });
+ });
- it("should advance test clock and verify both customers get pro_amount discount on Pro cycle", async () => {
- // Advance 31 days from current time to trigger next billing cycle
- // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
- await Promise.all(
- testClockIds.map((x) =>
- advanceTestClock({
- testClockId: x,
- numberOfDays: 31,
- waitForSeconds: 25,
- stripeCli,
- })
- )
- );
+ it("should advance test clock and verify both customers get pro_amount discount on Pro cycle", async () => {
+ // Advance 31 days from current time to trigger next billing cycle
+ // Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
+ await Promise.all(
+ testClockIds.map((x) =>
+ advanceTestClock({
+ testClockId: x,
+ numberOfDays: 31,
+ waitForSeconds: 25,
+ stripeCli,
+ }),
+ ),
+ );
- // Test that both customers' Pro invoices have pro_amount discount applied
- const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([
- autumn.customers.get(mainCustomerId, {
- expand: [CusExpand.Invoices],
- }),
- autumn.customers.get(redeemer, {
- expand: [CusExpand.Invoices],
- }),
- ]);
+ // Test that both customers' Pro invoices have pro_amount discount applied
+ const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([
+ autumn.customers.get(mainCustomerId, {
+ expand: [CusExpand.Invoices],
+ }),
+ autumn.customers.get(redeemer, {
+ expand: [CusExpand.Invoices],
+ }),
+ ]);
- // console.log(
- // "Main Customer Invoices:\n",
- // mainCustomerWithInvoices.invoices
- // .map(
- // (x) =>
- // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`,
- // )
- // .join("\n"),
- // );
+ // console.log(
+ // "Main Customer Invoices:\n",
+ // mainCustomerWithInvoices.invoices
+ // .map(
+ // (x) =>
+ // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`,
+ // )
+ // .join("\n"),
+ // );
- // console.log(
- // "Redeemer Invoices:\n",
- // redeemerWithInvoices.invoices
- // .map(
- // (x) =>
- // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`,
- // )
- // .join("\n"),
- // );
+ // console.log(
+ // "Redeemer Invoices:\n",
+ // redeemerWithInvoices.invoices
+ // .map(
+ // (x) =>
+ // `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`,
+ // )
+ // .join("\n"),
+ // );
- // Check main customer (referrer) invoice
- const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) =>
- x.product_ids.includes(products.pro.id)
- );
- if (mainProInvoice) {
- // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
- const proPrice = products.pro.prices[0].config.amount; // $10
- const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
- const expectedTotal = proPrice - proAmount; // $0
+ // Check main customer (referrer) invoice
+ const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) =>
+ x.product_ids.includes(products.pro.id),
+ );
+ if (mainProInvoice) {
+ // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
+ const proPrice = products.pro.prices[0].config.amount; // $10
+ const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
+ const expectedTotal = proPrice - proAmount; // $0
- // console.log("Main customer expected total:", expectedTotal);
- // console.log("Main customer Pro invoice total:", mainProInvoice.total);
+ // console.log("Main customer expected total:", expectedTotal);
+ // console.log("Main customer Pro invoice total:", mainProInvoice.total);
- assert.equal(
- mainProInvoice.total,
- expectedTotal,
- `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}`
- );
- }
+ assert.equal(
+ mainProInvoice.total,
+ expectedTotal,
+ `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}`,
+ );
+ }
- // Check redeemer invoice
- const redeemerProInvoice = redeemerWithInvoices.invoices.find((x) =>
- x.product_ids.includes(products.pro.id)
- );
- if (redeemerProInvoice) {
- // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
- const proPrice = products.pro.prices[0].config.amount; // $10
- const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
- const expectedTotal = proPrice - proAmount; // $0
+ // Check redeemer invoice
+ const redeemerProInvoice = redeemerWithInvoices.invoices.find((x) =>
+ x.product_ids.includes(products.pro.id),
+ );
+ if (redeemerProInvoice) {
+ // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
+ const proPrice = products.pro.prices[0].config.amount; // $10
+ const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
+ const expectedTotal = proPrice - proAmount; // $0
- // console.log("Redeemer expected total:", expectedTotal);
- // console.log("Redeemer Pro invoice total:", redeemerProInvoice.total);
+ // console.log("Redeemer expected total:", expectedTotal);
+ // console.log("Redeemer Pro invoice total:", redeemerProInvoice.total);
- assert.equal(
- redeemerProInvoice.total,
- expectedTotal,
- `Redeemer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${redeemerProInvoice.total}`
- );
- }
+ assert.equal(
+ redeemerProInvoice.total,
+ expectedTotal,
+ `Redeemer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${redeemerProInvoice.total}`,
+ );
+ }
- const dbCustomers = await Promise.all(
- [mainCustomerId, redeemer].map((x) =>
- CusService.getFull({
- db,
- idOrInternalId: x,
- orgId: org.id,
- env,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Expired,
- ],
- })
- )
- );
+ const dbCustomers = await Promise.all(
+ [mainCustomerId, redeemer].map((x) =>
+ CusService.getFull({
+ db,
+ idOrInternalId: x,
+ orgId: org.id,
+ env,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Expired,
+ ],
+ }),
+ ),
+ );
- const expectedProducts = [
- [
- // Main referrer - has Pro with pro_amount discount applied
- { name: "Free", status: CusProductStatus.Expired },
- { name: "Pro", status: CusProductStatus.Active },
- ],
- [
- // Redeemer - also has Pro with pro_amount discount applied
- { name: "Free", status: CusProductStatus.Expired },
- { name: "Pro", status: CusProductStatus.Active },
- ],
- ];
+ const expectedProducts = [
+ [
+ // Main referrer - has Pro with pro_amount discount applied
+ { name: "Free", status: CusProductStatus.Expired },
+ { name: "Pro", status: CusProductStatus.Active },
+ ],
+ [
+ // Redeemer - also has Pro with pro_amount discount applied
+ { name: "Free", status: CusProductStatus.Expired },
+ { name: "Pro", status: CusProductStatus.Active },
+ ],
+ ];
- dbCustomers.forEach((customer, index) => {
- const expectedProductsForCustomer = expectedProducts[index];
- expectedProductsForCustomer.forEach((expectedProduct) => {
- const matchingProduct = customer.customer_products.find(
- (cp) =>
- cp.product.name === expectedProduct.name &&
- cp.status === expectedProduct.status
- );
- const unMatchedProduct = customer.customer_products.find(
- (cp) => cp.product.name === expectedProduct.name
- );
+ dbCustomers.forEach((customer, index) => {
+ const expectedProductsForCustomer = expectedProducts[index];
+ expectedProductsForCustomer.forEach((expectedProduct) => {
+ const matchingProduct = customer.customer_products.find(
+ (cp) =>
+ cp.product.name === expectedProduct.name &&
+ cp.status === expectedProduct.status,
+ );
+ const unMatchedProduct = customer.customer_products.find(
+ (cp) => cp.product.name === expectedProduct.name,
+ );
- assert.exists(
- matchingProduct,
- `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`
- );
- });
- });
- });
+ assert.exists(
+ matchingProduct,
+ `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}. ${unMatchedProduct ? `However ${unMatchedProduct.product.name} with status ${unMatchedProduct.status} was found instead` : ""}`,
+ );
+ });
+ });
+ });
});
diff --git a/server/tests/advanced/referrals/referrals1.ts b/server/tests/advanced/referrals/referrals1.ts
index ca481b1b8..f3ab1f077 100644
--- a/server/tests/advanced/referrals/referrals1.ts
+++ b/server/tests/advanced/referrals/referrals1.ts
@@ -4,12 +4,12 @@ import chalk from "chalk";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { setupBefore } from "tests/before.js";
import {
- AppEnv,
- Customer,
- ErrCode,
- Organization,
- ReferralCode,
- RewardRedemption,
+ AppEnv,
+ Customer,
+ ErrCode,
+ Organization,
+ ReferralCode,
+ RewardRedemption,
} from "@autumn/shared";
import { timeout } from "tests/utils/genUtils.js";
@@ -26,244 +26,244 @@ import { createProducts } from "tests/utils/productUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "referrals1: Testing referrals (on checkout)"
+ "referrals1: Testing referrals (on checkout)",
)}`, () => {
- let mainCustomerId = "main-referral-1";
- let alternateCustomerId = "alternate-referral-1";
- let redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"];
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- let testClockId: string;
- let referralCode: ReferralCode;
+ let mainCustomerId = "main-referral-1";
+ let alternateCustomerId = "alternate-referral-1";
+ let redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"];
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let referralCode: ReferralCode;
- let redemptions: RewardRedemption[] = [];
- let mainCustomer: any;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let redemptions: RewardRedemption[] = [];
+ let mainCustomer: any;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- addPrefixToProducts({
- products: [pro],
- prefix: mainCustomerId,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: mainCustomerId,
+ });
- await createProducts({
- autumn: this.autumnJs,
- products: [pro],
- db,
- orgId: org.id,
- env,
- customerId: mainCustomerId,
- });
+ await createProducts({
+ autumn: this.autumnJs,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId: mainCustomerId,
+ });
- const res = await initCustomer({
- autumn: this.autumnJs,
- customerId: mainCustomerId,
- fingerprint: "main-referral-1",
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: this.autumnJs,
+ customerId: mainCustomerId,
+ fingerprint: "main-referral-1",
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- mainCustomer = res.customer;
- testClockId = res.testClockId;
+ mainCustomer = res.customer;
+ testClockId = res.testClockId;
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: pro.id,
- });
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: pro.id,
+ });
- let batchCreate = [];
- for (let redeemer of redeemers) {
- batchCreate.push(
- initCustomer({
- autumn: this.autumnJs,
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- })
- );
- }
+ let batchCreate = [];
+ for (let redeemer of redeemers) {
+ batchCreate.push(
+ initCustomer({
+ autumn: this.autumnJs,
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ }),
+ );
+ }
- batchCreate.push(
- initCustomer({
- autumn: this.autumnJs,
- customerId: alternateCustomerId,
- fingerprint: "main-referral-1",
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- })
- );
- await Promise.all(batchCreate);
- });
+ batchCreate.push(
+ initCustomer({
+ autumn: this.autumnJs,
+ customerId: alternateCustomerId,
+ fingerprint: "main-referral-1",
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ }),
+ );
+ await Promise.all(batchCreate);
+ });
- it("should create code once", async function () {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.onCheckout.id,
- });
+ it("should create code once", async function () {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.onCheckout.id,
+ });
- assert.exists(referralCode.code);
+ assert.exists(referralCode.code);
- // Get referral code again
- let referralCode2 = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.onCheckout.id,
- });
+ // Get referral code again
+ let referralCode2 = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.onCheckout.id,
+ });
- assert.equal(referralCode2.code, referralCode.code);
- });
+ assert.equal(referralCode2.code, referralCode.code);
+ });
- it("should fail if same customer tries to redeem code again", async function () {
- try {
- await autumn.referrals.redeem({
- customerId: mainCustomerId,
- code: referralCode.code,
- });
- assert.fail("Own customer should not be able to redeem code");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode);
- }
+ it("should fail if same customer tries to redeem code again", async function () {
+ try {
+ await autumn.referrals.redeem({
+ customerId: mainCustomerId,
+ code: referralCode.code,
+ });
+ assert.fail("Own customer should not be able to redeem code");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode);
+ }
- try {
- await autumn.referrals.redeem({
- customerId: alternateCustomerId,
- code: referralCode.code,
- });
- assert.fail(
- "Own customer (same fingerprint) should not be able to redeem code"
- );
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode);
- }
- });
+ try {
+ await autumn.referrals.redeem({
+ customerId: alternateCustomerId,
+ code: referralCode.code,
+ });
+ assert.fail(
+ "Own customer (same fingerprint) should not be able to redeem code",
+ );
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerCannotRedeemOwnCode);
+ }
+ });
- it("should create redemption for each redeemer and fail if redeemed again", async function () {
- for (let redeemer of redeemers) {
- let redemption: RewardRedemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
+ it("should create redemption for each redeemer and fail if redeemed again", async function () {
+ for (let redeemer of redeemers) {
+ let redemption: RewardRedemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
- redemptions.push(redemption);
- }
+ redemptions.push(redemption);
+ }
- // Try redeem for redeemer1 again
- try {
- let redemption1 = await autumn.referrals.redeem({
- customerId: redeemers[0],
- code: referralCode.code,
- });
- assert.fail("Should not be able to redeem again");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
- }
- });
+ // Try redeem for redeemer1 again
+ try {
+ let redemption1 = await autumn.referrals.redeem({
+ customerId: redeemers[0],
+ code: referralCode.code,
+ });
+ assert.fail("Should not be able to redeem again");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
+ }
+ });
- // return;
+ // return;
- it("should be triggered (and applied) when redeemers check out", async function () {
- for (let i = 0; i < redeemers.length; i++) {
- let redeemer = redeemers[i];
+ it("should be triggered (and applied) when redeemers check out", async function () {
+ for (let i = 0; i < redeemers.length; i++) {
+ let redeemer = redeemers[i];
- await autumn.attach({
- customer_id: redeemer,
- product_id: products.pro.id,
- });
+ await autumn.attach({
+ customer_id: redeemer,
+ product_id: products.pro.id,
+ });
- await timeout(3000);
+ await timeout(3000);
- // Get redemption object
- let redemption = await autumn.redemptions.get(redemptions[i].id);
+ // Get redemption object
+ let redemption = await autumn.redemptions.get(redemptions[i].id);
- // Check if redemption is triggered
- let count = i + 1;
+ // Check if redemption is triggered
+ let count = i + 1;
- if (count > referralPrograms.onCheckout.max_redemptions) {
- assert.equal(redemption.triggered, false);
- assert.equal(redemption.applied, false);
- } else {
- assert.equal(redemption.triggered, true);
- assert.equal(redemption.applied, i == 0);
- }
+ if (count > referralPrograms.onCheckout.max_redemptions) {
+ assert.equal(redemption.triggered, false);
+ assert.equal(redemption.applied, false);
+ } else {
+ assert.equal(redemption.triggered, true);
+ assert.equal(redemption.applied, i == 0);
+ }
- // Check stripe customer
- let stripeCus = (await stripeCli.customers.retrieve(
- mainCustomer.processor?.id
- )) as Stripe.Customer;
+ // Check stripe customer
+ let stripeCus = (await stripeCli.customers.retrieve(
+ mainCustomer.processor?.id,
+ )) as Stripe.Customer;
- assert.notEqual(stripeCus.discount, null);
- }
- });
+ assert.notEqual(stripeCus.discount, null);
+ }
+ });
- let curTime = new Date();
- it("customer should have discount for first purchase", async function () {
- curTime = addDays(addDays(curTime, 7), 4);
- await advanceTestClock({
- testClockId,
- advanceTo: curTime.getTime(),
- stripeCli,
- });
+ let curTime = new Date();
+ it("customer should have discount for first purchase", async function () {
+ curTime = addDays(addDays(curTime, 7), 4);
+ await advanceTestClock({
+ testClockId,
+ advanceTo: curTime.getTime(),
+ stripeCli,
+ });
- // 1. Get invoice
- let { invoices } = await autumn.customers.get(mainCustomerId);
- assert.equal(invoices.length, 2);
- assert.equal(invoices[0].total, 0);
- });
+ // 1. Get invoice
+ let { invoices } = await autumn.customers.get(mainCustomerId);
+ assert.equal(invoices.length, 2);
+ assert.equal(invoices[0].total, 0);
+ });
- // it("customer should have discount for second purchase", async function () {
- // // 2. Check that customer has another discount
- // let stripeCus = (await stripeCli.customers.retrieve(
- // mainCustomer.processor?.id,
- // )) as Stripe.Customer;
+ // it("customer should have discount for second purchase", async function () {
+ // // 2. Check that customer has another discount
+ // let stripeCus = (await stripeCli.customers.retrieve(
+ // mainCustomer.processor?.id,
+ // )) as Stripe.Customer;
- // assert.notEqual(stripeCus.discount, null);
+ // assert.notEqual(stripeCus.discount, null);
- // // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
- // curTime = addHours(addMonths(new Date(), 1), 2);
- // await advanceTestClock({
- // testClockId,
- // advanceTo: curTime.getTime(),
- // stripeCli,
- // });
+ // // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
+ // curTime = addHours(addMonths(new Date(), 1), 2);
+ // await advanceTestClock({
+ // testClockId,
+ // advanceTo: curTime.getTime(),
+ // stripeCli,
+ // });
- // // 3. Advance test clock to 1 month + 12 days from start (trigger new invoice)
- // curTime = addDays(curTime, 12);
- // await advanceTestClock({
- // testClockId,
- // advanceTo: curTime.getTime(),
- // stripeCli,
- // });
+ // // 3. Advance test clock to 1 month + 12 days from start (trigger new invoice)
+ // curTime = addDays(curTime, 12);
+ // await advanceTestClock({
+ // testClockId,
+ // advanceTo: curTime.getTime(),
+ // stripeCli,
+ // });
- // // // 3. Get invoice again
- // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
+ // // // 3. Get invoice again
+ // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
- // assert.equal(invoices2.length, 3);
- // assert.equal(invoices2[0].total, 0);
- // });
+ // assert.equal(invoices2.length, 3);
+ // assert.equal(invoices2[0].total, 0);
+ // });
});
// const { testClockId: testClockId1, customer } =
diff --git a/server/tests/advanced/referrals/referrals2.ts b/server/tests/advanced/referrals/referrals2.ts
index 605de7231..6fdba167d 100644
--- a/server/tests/advanced/referrals/referrals2.ts
+++ b/server/tests/advanced/referrals/referrals2.ts
@@ -4,10 +4,10 @@ import chalk from "chalk";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { setupBefore } from "tests/before.js";
import {
- Customer,
- ErrCode,
- ReferralCode,
- RewardRedemption,
+ Customer,
+ ErrCode,
+ ReferralCode,
+ RewardRedemption,
} from "@autumn/shared";
import { timeout } from "tests/utils/genUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
@@ -18,142 +18,142 @@ import { initCustomer } from "tests/utils/init.js";
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "referrals2: Testing referrals (immediate redemption)"
+ "referrals2: Testing referrals (immediate redemption)",
)}`, () => {
- let mainCustomerId = "main-referral-2";
- let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- let testClockId: string;
- let referralCode: ReferralCode;
+ let mainCustomerId = "main-referral-2";
+ let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let referralCode: ReferralCode;
- let redemptions: RewardRedemption[] = [];
- let mainCustomer: Customer;
+ let redemptions: RewardRedemption[] = [];
+ let mainCustomer: Customer;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1, customer } =
- await initCustomerWithTestClock({
- customerId: mainCustomerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- testClockId = testClockId1;
- mainCustomer = customer;
+ const { testClockId: testClockId1, customer } =
+ await initCustomerWithTestClock({
+ customerId: mainCustomerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ testClockId = testClockId1;
+ mainCustomer = customer;
- let batchCreate = [];
- for (let redeemer of redeemers) {
- batchCreate.push(
- initCustomer({
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- })
- );
- }
+ let batchCreate = [];
+ for (let redeemer of redeemers) {
+ batchCreate.push(
+ initCustomer({
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ }),
+ );
+ }
- await Promise.all(batchCreate);
- });
+ await Promise.all(batchCreate);
+ });
- it("should create code once", async function () {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.immediate.id,
- });
+ it("should create code once", async function () {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.immediate.id,
+ });
- assert.exists(referralCode.code);
- });
+ assert.exists(referralCode.code);
+ });
- it("should create redemption for each redeemer and fail if redeemed again", async function () {
- for (let i = 0; i < redeemers.length; i++) {
- let redeemer = redeemers[i];
- let count = i + 1;
- try {
- let redemption: RewardRedemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
- redemptions.push(redemption);
+ it("should create redemption for each redeemer and fail if redeemed again", async function () {
+ for (let i = 0; i < redeemers.length; i++) {
+ let redeemer = redeemers[i];
+ let count = i + 1;
+ try {
+ let redemption: RewardRedemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
+ redemptions.push(redemption);
- if (count > referralPrograms.immediate.max_redemptions) {
- assert.equal(redemption.triggered, false);
- assert.equal(redemption.applied, false);
- } else {
- assert.fail("Should not be able to redeem again");
- }
- } catch (error) {
- if (count > referralPrograms.immediate.max_redemptions) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.ReferralCodeMaxRedemptionsReached);
- }
- }
- }
+ if (count > referralPrograms.immediate.max_redemptions) {
+ assert.equal(redemption.triggered, false);
+ assert.equal(redemption.applied, false);
+ } else {
+ assert.fail("Should not be able to redeem again");
+ }
+ } catch (error) {
+ if (count > referralPrograms.immediate.max_redemptions) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.ReferralCodeMaxRedemptionsReached);
+ }
+ }
+ }
- // Check stripe customer
- let stripeCus = (await stripeCli.customers.retrieve(
- mainCustomer.processor?.id
- )) as Stripe.Customer;
+ // Check stripe customer
+ let stripeCus = (await stripeCli.customers.retrieve(
+ mainCustomer.processor?.id,
+ )) as Stripe.Customer;
- assert.notEqual(stripeCus.discount, null);
- });
+ assert.notEqual(stripeCus.discount, null);
+ });
- let curTime = new Date();
- it("customer should have discount for first purchase", async function () {
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: products.proWithTrial.id,
- });
+ let curTime = new Date();
+ it("customer should have discount for first purchase", async function () {
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: products.proWithTrial.id,
+ });
- await timeout(3000);
+ await timeout(3000);
- curTime = addDays(addDays(curTime, 7), 4);
- await advanceTestClock({
- testClockId,
- advanceTo: curTime.getTime(),
- stripeCli,
- waitForSeconds: 30,
- });
+ curTime = addDays(addDays(curTime, 7), 4);
+ await advanceTestClock({
+ testClockId,
+ advanceTo: curTime.getTime(),
+ stripeCli,
+ waitForSeconds: 30,
+ });
- // 1. Get invoice
- let { invoices } = await autumn.customers.get(mainCustomerId);
+ // 1. Get invoice
+ let { invoices } = await autumn.customers.get(mainCustomerId);
- assert.equal(invoices!.length, 2);
- assert.equal(invoices![0].total, 0);
- });
+ assert.equal(invoices!.length, 2);
+ assert.equal(invoices![0].total, 0);
+ });
- // it("customer should have discount for second purchase", async function () {
- // // 2. Check that customer has another discount
- // let stripeCus = (await stripeCli.customers.retrieve(
- // mainCustomer.processor?.id,
- // )) as Stripe.Customer;
+ // it("customer should have discount for second purchase", async function () {
+ // // 2. Check that customer has another discount
+ // let stripeCus = (await stripeCli.customers.retrieve(
+ // mainCustomer.processor?.id,
+ // )) as Stripe.Customer;
- // assert.notEqual(stripeCus.discount, null);
+ // assert.notEqual(stripeCus.discount, null);
- // // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
- // curTime = addHours(addMonths(new Date(), 1), 2);
- // await advanceTestClock({
- // testClockId,
- // advanceTo: curTime.getTime(),
- // stripeCli,
- // });
+ // // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
+ // curTime = addHours(addMonths(new Date(), 1), 2);
+ // await advanceTestClock({
+ // testClockId,
+ // advanceTo: curTime.getTime(),
+ // stripeCli,
+ // });
- // // 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
- // curTime = addDays(curTime, 8);
- // await advanceTestClock({
- // testClockId,
- // advanceTo: curTime.getTime(),
- // stripeCli,
- // });
+ // // 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
+ // curTime = addDays(curTime, 8);
+ // await advanceTestClock({
+ // testClockId,
+ // advanceTo: curTime.getTime(),
+ // stripeCli,
+ // });
- // // // 3. Get invoice again
- // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
+ // // // 3. Get invoice again
+ // let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
- // assert.equal(invoices2!.length, 3);
- // assert.equal(invoices2![0].total, 0);
- // });
+ // assert.equal(invoices2!.length, 3);
+ // assert.equal(invoices2![0].total, 0);
+ // });
});
diff --git a/server/tests/advanced/referrals/referrals3.ts b/server/tests/advanced/referrals/referrals3.ts
index 0e31ca541..d4b229704 100644
--- a/server/tests/advanced/referrals/referrals3.ts
+++ b/server/tests/advanced/referrals/referrals3.ts
@@ -4,10 +4,10 @@ import chalk from "chalk";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { setupBefore } from "tests/before.js";
import {
- Customer,
- ErrCode,
- ReferralCode,
- RewardRedemption,
+ Customer,
+ ErrCode,
+ ReferralCode,
+ RewardRedemption,
} from "@autumn/shared";
import { timeout } from "tests/utils/genUtils.js";
import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js";
@@ -17,125 +17,125 @@ import { compareProductEntitlements } from "tests/utils/compare.js";
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "referrals3: Testing free product referrals",
+ "referrals3: Testing free product referrals",
)}`, () => {
- let mainCustomerId = "main-referral-3";
- let redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"];
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- let testClockId: string;
- let referralCode: ReferralCode;
+ let mainCustomerId = "main-referral-3";
+ let redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"];
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let referralCode: ReferralCode;
- let redemptions: RewardRedemption[] = [];
- let mainCustomer: Customer;
+ let redemptions: RewardRedemption[] = [];
+ let mainCustomer: Customer;
- before(async function () {
- await setupBefore(this);
- autumn = this.autumn;
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ autumn = this.autumn;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1, customer } =
- await initCustomerWithTestClock({
- customerId: mainCustomerId,
- db: this.db,
- org: this.org,
- env: this.env,
- fingerprint: "main-referral-3",
- });
- testClockId = testClockId1;
- mainCustomer = customer;
+ const { testClockId: testClockId1, customer } =
+ await initCustomerWithTestClock({
+ customerId: mainCustomerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ fingerprint: "main-referral-3",
+ });
+ testClockId = testClockId1;
+ mainCustomer = customer;
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: products.proWithTrial.id,
- });
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: products.proWithTrial.id,
+ });
- let batchCreate = [];
- for (let redeemer of redeemers) {
- batchCreate.push(
- initCustomer({
- customerId: redeemer,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- }),
- );
- }
+ let batchCreate = [];
+ for (let redeemer of redeemers) {
+ batchCreate.push(
+ initCustomer({
+ customerId: redeemer,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ }),
+ );
+ }
- await Promise.all(batchCreate);
- });
+ await Promise.all(batchCreate);
+ });
- it("should create code once", async function () {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.freeProduct.id,
- });
+ it("should create code once", async function () {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.freeProduct.id,
+ });
- assert.exists(referralCode.code);
- });
+ assert.exists(referralCode.code);
+ });
- it("should create redemption for each redeemer and fail if redeemed again", async function () {
- for (let redeemer of redeemers) {
- let redemption: RewardRedemption = await autumn.referrals.redeem({
- customerId: redeemer,
- code: referralCode.code,
- });
+ it("should create redemption for each redeemer and fail if redeemed again", async function () {
+ for (let redeemer of redeemers) {
+ let redemption: RewardRedemption = await autumn.referrals.redeem({
+ customerId: redeemer,
+ code: referralCode.code,
+ });
- redemptions.push(redemption);
+ redemptions.push(redemption);
- // assert.equal(redemption.triggered, false);
- // assert.equal(redemption.applied, false);
- }
+ // assert.equal(redemption.triggered, false);
+ // assert.equal(redemption.applied, false);
+ }
- // Try redeem for redeemer1 again
- try {
- let redemption1 = await autumn.referrals.redeem({
- customerId: redeemers[0],
- code: referralCode.code,
- });
- assert.fail("Should not be able to redeem again");
- } catch (error) {
- assert.instanceOf(error, AutumnError);
- assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
- }
- });
+ // Try redeem for redeemer1 again
+ try {
+ let redemption1 = await autumn.referrals.redeem({
+ customerId: redeemers[0],
+ code: referralCode.code,
+ });
+ assert.fail("Should not be able to redeem again");
+ } catch (error) {
+ assert.instanceOf(error, AutumnError);
+ assert.equal(error.code, ErrCode.CustomerAlreadyRedeemedReferralCode);
+ }
+ });
- it("should be triggered (and applied) when redeemers check out", async function () {
- for (let i = 0; i < redeemers.length; i++) {
- let redeemer = redeemers[i];
+ it("should be triggered (and applied) when redeemers check out", async function () {
+ for (let i = 0; i < redeemers.length; i++) {
+ let redeemer = redeemers[i];
- await autumn.attach({
- customer_id: redeemer,
- product_id: products.pro.id,
- });
+ await autumn.attach({
+ customer_id: redeemer,
+ product_id: products.pro.id,
+ });
- await timeout(3000);
+ await timeout(3000);
- // Get redemption object
- let redemption = await autumn.redemptions.get(redemptions[i].id);
+ // Get redemption object
+ let redemption = await autumn.redemptions.get(redemptions[i].id);
- // Check if redemption is triggered
- let count = i + 1;
+ // Check if redemption is triggered
+ let count = i + 1;
- if (count > referralPrograms.freeProduct.max_redemptions) {
- assert.equal(redemption.triggered, false);
- assert.equal(redemption.applied, false);
- } else {
- // 1. Check that main customer has free add on
- compareProductEntitlements({
- customerId: mainCustomerId,
- product: products.freeAddOn,
- features,
- quantity: count,
- });
+ if (count > referralPrograms.freeProduct.max_redemptions) {
+ assert.equal(redemption.triggered, false);
+ assert.equal(redemption.applied, false);
+ } else {
+ // 1. Check that main customer has free add on
+ compareProductEntitlements({
+ customerId: mainCustomerId,
+ product: products.freeAddOn,
+ features,
+ quantity: count,
+ });
- compareProductEntitlements({
- customerId: redeemer,
- product: products.freeAddOn,
- features,
- });
- }
- }
- });
+ compareProductEntitlements({
+ customerId: redeemer,
+ product: products.freeAddOn,
+ features,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/advanced/referrals/referrals4.ts b/server/tests/advanced/referrals/referrals4.ts
index de28a7d13..741d63dcd 100644
--- a/server/tests/advanced/referrals/referrals4.ts
+++ b/server/tests/advanced/referrals/referrals4.ts
@@ -15,111 +15,111 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "referrals4: Testing free product referrals with trial"
+ "referrals4: Testing free product referrals with trial",
)}`, () => {
- let mainCustomerId = "main-referral-4";
- // let redeemers = ["referral4-r1", "referral4-r2"];
- let redeemerId = "referral4-r1";
+ let mainCustomerId = "main-referral-4";
+ // let redeemers = ["referral4-r1", "referral4-r2"];
+ let redeemerId = "referral4-r1";
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- let referralCode: ReferralCode;
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ let referralCode: ReferralCode;
- let redemptions: RewardRedemption[] = [];
- let mainCustomer: Customer;
- let redeemer: Customer;
+ let redemptions: RewardRedemption[] = [];
+ let mainCustomer: Customer;
+ let redeemer: Customer;
- let testClockId: string;
- before(async function () {
- await setupBefore(this);
- autumn = this.autumn;
- stripeCli = this.stripeCli;
+ let testClockId: string;
+ before(async function () {
+ await setupBefore(this);
+ autumn = this.autumn;
+ stripeCli = this.stripeCli;
- await initCustomer({
- customerId: mainCustomerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
+ await initCustomer({
+ customerId: mainCustomerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
- await autumn.attach({
- customer_id: mainCustomerId,
- product_id: products.proWithTrial.id,
- });
+ await autumn.attach({
+ customer_id: mainCustomerId,
+ product_id: products.proWithTrial.id,
+ });
- let { testClockId: testClockId1, customer } =
- await initCustomerWithTestClock({
- customerId: redeemerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
+ let { testClockId: testClockId1, customer } =
+ await initCustomerWithTestClock({
+ customerId: redeemerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
- testClockId = testClockId1;
- redeemer = customer;
- });
+ testClockId = testClockId1;
+ redeemer = customer;
+ });
- it("should create referral code", async function () {
- referralCode = await autumn.referrals.createCode({
- customerId: mainCustomerId,
- referralId: referralPrograms.freeProduct.id,
- });
+ it("should create referral code", async function () {
+ referralCode = await autumn.referrals.createCode({
+ customerId: mainCustomerId,
+ referralId: referralPrograms.freeProduct.id,
+ });
- assert.exists(referralCode.code);
- });
+ assert.exists(referralCode.code);
+ });
- it("should create redemption for each redeemer and fail if redeemed again", async function () {
- let redemption: RewardRedemption = await autumn.referrals.redeem({
- customerId: redeemerId,
- code: referralCode.code,
- });
+ it("should create redemption for each redeemer and fail if redeemed again", async function () {
+ let redemption: RewardRedemption = await autumn.referrals.redeem({
+ customerId: redeemerId,
+ code: referralCode.code,
+ });
- redemptions.push(redemption);
- });
+ redemptions.push(redemption);
+ });
- it("should not be triggered because of trial", async function () {
- await autumn.attach({
- customer_id: redeemerId,
- product_id: products.proWithTrial.id,
- });
+ it("should not be triggered because of trial", async function () {
+ await autumn.attach({
+ customer_id: redeemerId,
+ product_id: products.proWithTrial.id,
+ });
- await timeout(3000);
+ await timeout(3000);
- // Get redemption object
- let redemption = await autumn.redemptions.get(redemptions[0].id);
+ // Get redemption object
+ let redemption = await autumn.redemptions.get(redemptions[0].id);
- assert.equal(redemption.triggered, false);
- });
+ assert.equal(redemption.triggered, false);
+ });
- it("should be triggered after trial ends", async function () {
- let advanceTo = addHours(
- addDays(new Date(), 7),
- hoursToFinalizeInvoice
- ).getTime();
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo,
- waitForSeconds: 30,
- });
+ it("should be triggered after trial ends", async function () {
+ let advanceTo = addHours(
+ addDays(new Date(), 7),
+ hoursToFinalizeInvoice,
+ ).getTime();
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo,
+ waitForSeconds: 30,
+ });
- let redemption = await autumn.redemptions.get(redemptions[0].id);
+ let redemption = await autumn.redemptions.get(redemptions[0].id);
- assert.equal(redemption.triggered, true);
+ assert.equal(redemption.triggered, true);
- compareProductEntitlements({
- customerId: mainCustomerId,
- product: products.freeAddOn,
- features,
- quantity: 1,
- });
+ compareProductEntitlements({
+ customerId: mainCustomerId,
+ product: products.freeAddOn,
+ features,
+ quantity: 1,
+ });
- compareProductEntitlements({
- customerId: redeemerId,
- product: products.freeAddOn,
- features,
- quantity: 1,
- });
- });
+ compareProductEntitlements({
+ customerId: redeemerId,
+ product: products.freeAddOn,
+ features,
+ quantity: 1,
+ });
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover1.ts b/server/tests/advanced/rollovers/rollover1.ts
index 6f727a49f..9c7ed1e25 100644
--- a/server/tests/advanced/rollovers/rollover1.ts
+++ b/server/tests/advanced/rollovers/rollover1.ts
@@ -4,13 +4,13 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- ProductItemInterval,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ ProductItemInterval,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -27,171 +27,171 @@ import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
let rolloverConfig = { max: 500, length: 1, duration: RolloverDuration.Month };
const messagesItem = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 400,
- interval: ProductItemInterval.Month,
- rolloverConfig,
+ featureId: TestFeature.Messages,
+ includedUsage: 400,
+ interval: ProductItemInterval.Month,
+ rolloverConfig,
}) as LimitedItem;
export let free = constructProduct({
- items: [messagesItem],
- type: "free",
- isDefault: false,
+ items: [messagesItem],
+ type: "free",
+ isDefault: false,
});
const testCase = "rollover1";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- it("should attach free product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
- });
+ it("should attach free product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
+ });
- let messageUsage = 250;
- let curBalance = messagesItem.included_usage;
+ let messageUsage = 250;
+ let curBalance = messagesItem.included_usage;
- it("should create track messages, reset, and have correct rollover", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: messageUsage,
- });
+ it("should create track messages, reset, and have correct rollover", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: messageUsage,
+ });
- await timeout(3000);
+ await timeout(3000);
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Messages,
- });
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Messages,
+ });
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- let expectedRollover = Math.min(
- messagesItem.included_usage - messageUsage,
- rolloverConfig.max
- );
+ let expectedRollover = Math.min(
+ messagesItem.included_usage - messageUsage,
+ rolloverConfig.max,
+ );
- let expectedBalance = messagesItem.included_usage + expectedRollover;
+ let expectedBalance = messagesItem.included_usage + expectedRollover;
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(expectedBalance);
- // @ts-ignore
- expect(msgesFeature?.rollovers[0].balance).to.equal(expectedRollover);
- curBalance = expectedBalance;
- });
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(expectedBalance);
+ // @ts-ignore
+ expect(msgesFeature?.rollovers[0].balance).to.equal(expectedRollover);
+ curBalance = expectedBalance;
+ });
- // let usage2 = 50;
- it("should reset again and have correct rollover", async function () {
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Messages,
- });
+ // let usage2 = 50;
+ it("should reset again and have correct rollover", async function () {
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Messages,
+ });
- let expectedRollover = Math.min(curBalance, rolloverConfig.max);
- let expectedBalance = messagesItem.included_usage + expectedRollover;
+ let expectedRollover = Math.min(curBalance, rolloverConfig.max);
+ let expectedBalance = messagesItem.included_usage + expectedRollover;
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(expectedBalance);
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(expectedBalance);
- // @ts-ignore (oldest rollover should be 100 (150 - 50))
- expect(msgesFeature?.rollovers[0].balance).to.equal(100);
- // @ts-ignore (newest rollover should be 400 (msges.included_usage))
- expect(msgesFeature?.rollovers[1].balance).to.equal(400);
- });
+ // @ts-ignore (oldest rollover should be 100 (150 - 50))
+ expect(msgesFeature?.rollovers[0].balance).to.equal(100);
+ // @ts-ignore (newest rollover should be 400 (msges.included_usage))
+ expect(msgesFeature?.rollovers[1].balance).to.equal(400);
+ });
- it("should track messages and deduct from rollovers first", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: 150,
- });
+ it("should track messages and deduct from rollovers first", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: 150,
+ });
- await timeout(3000);
+ await timeout(3000);
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- // @ts-ignore
- let rollover1 = msgesFeature?.rollovers[0];
- // @ts-ignore
- let rollover2 = msgesFeature?.rollovers[1];
+ // @ts-ignore
+ let rollover1 = msgesFeature?.rollovers[0];
+ // @ts-ignore
+ let rollover2 = msgesFeature?.rollovers[1];
- expect(rollover1.balance).to.equal(0);
- expect(rollover2.balance).to.equal(350);
- });
+ expect(rollover1.balance).to.equal(0);
+ expect(rollover2.balance).to.equal(350);
+ });
- it("should track and deduct from rollover + original balance", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: 400,
- });
+ it("should track and deduct from rollover + original balance", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: 400,
+ });
- await timeout(3000);
+ await timeout(3000);
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- // @ts-ignore
- let rollovers = msgesFeature.rollovers;
- expect(rollovers![0].balance).to.equal(0);
- expect(rollovers![1].balance).to.equal(0);
- expect(msgesFeature.balance).to.equal(messagesItem.included_usage - 50);
- });
+ // @ts-ignore
+ let rollovers = msgesFeature.rollovers;
+ expect(rollovers![0].balance).to.equal(0);
+ expect(rollovers![1].balance).to.equal(0);
+ expect(msgesFeature.balance).to.equal(messagesItem.included_usage - 50);
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover2.ts b/server/tests/advanced/rollovers/rollover2.ts
index f02cd2100..940ec7b6e 100644
--- a/server/tests/advanced/rollovers/rollover2.ts
+++ b/server/tests/advanced/rollovers/rollover2.ts
@@ -4,13 +4,13 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- ProductItemInterval,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ ProductItemInterval,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -31,197 +31,197 @@ import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
let rolloverConfig = { max: 500, length: 1, duration: RolloverDuration.Month };
const msgesItem = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 400,
- interval: ProductItemInterval.Month,
- rolloverConfig,
- entityFeatureId: TestFeature.Users,
+ featureId: TestFeature.Messages,
+ includedUsage: 400,
+ interval: ProductItemInterval.Month,
+ rolloverConfig,
+ entityFeatureId: TestFeature.Users,
}) as LimitedItem;
export let free = constructProduct({
- items: [msgesItem],
- type: "free",
- isDefault: false,
+ items: [msgesItem],
+ type: "free",
+ isDefault: false,
});
const testCase = "rollover2";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item (per entity)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- const entities: any[] = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities: any[] = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ it("should attach pro product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- await autumn.entities.create(customerId, entities);
- });
+ await autumn.entities.create(customerId, entities);
+ });
- let entity1Id = entities[0].id;
- let entity2Id = entities[1].id;
- let newEntity1Balance = 300;
- let newEntity2Balance = 200;
- let includedUsage = msgesItem.included_usage;
- let usages = [
- {
- entityId: entity1Id,
- usage: includedUsage - newEntity1Balance,
- rollover: newEntity1Balance,
- },
- {
- entityId: entity2Id,
- usage: includedUsage - newEntity2Balance,
- rollover: newEntity2Balance,
- },
- ];
+ let entity1Id = entities[0].id;
+ let entity2Id = entities[1].id;
+ let newEntity1Balance = 300;
+ let newEntity2Balance = 200;
+ let includedUsage = msgesItem.included_usage;
+ let usages = [
+ {
+ entityId: entity1Id,
+ usage: includedUsage - newEntity1Balance,
+ rollover: newEntity1Balance,
+ },
+ {
+ entityId: entity2Id,
+ usage: includedUsage - newEntity2Balance,
+ rollover: newEntity2Balance,
+ },
+ ];
- it("should create track messages, reset, and have correct rollover", async function () {
- for (const usage of usages) {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: usage.usage,
- entity_id: usage.entityId,
- });
- }
+ it("should create track messages, reset, and have correct rollover", async function () {
+ for (const usage of usages) {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: usage.usage,
+ entity_id: usage.entityId,
+ });
+ }
- await timeout(3000);
+ await timeout(3000);
- // Run reset cusEnt on ...
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Messages,
- });
+ // Run reset cusEnt on ...
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Messages,
+ });
- for (const usage of usages) {
- let entity = await autumn.entities.get(customerId, usage.entityId);
- let msgesFeature = entity.features[TestFeature.Messages];
- let expectedRollover = Math.min(usage.rollover, rolloverConfig.max);
+ for (const usage of usages) {
+ let entity = await autumn.entities.get(customerId, usage.entityId);
+ let msgesFeature = entity.features[TestFeature.Messages];
+ let expectedRollover = Math.min(usage.rollover, rolloverConfig.max);
- expect(msgesFeature.rollovers.length).to.equal(1);
- expect(msgesFeature.balance).to.equal(includedUsage + expectedRollover);
- expect(msgesFeature.rollovers[0].balance).to.equal(expectedRollover);
- }
- });
+ expect(msgesFeature.rollovers.length).to.equal(1);
+ expect(msgesFeature.balance).to.equal(includedUsage + expectedRollover);
+ expect(msgesFeature.rollovers[0].balance).to.equal(expectedRollover);
+ }
+ });
- it("should reset again and have correct rollovers", async function () {
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Messages,
- });
+ it("should reset again and have correct rollovers", async function () {
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Messages,
+ });
- let entity1 = await autumn.entities.get(customerId, entity1Id);
- let entity1Msges = entity1.features[TestFeature.Messages];
- // 400, 300 -> 400, 100 (max is 500)
- let rollovers = entity1Msges.rollovers;
- expect(rollovers[0].balance).to.equal(100);
- expect(rollovers[1].balance).to.equal(400);
+ let entity1 = await autumn.entities.get(customerId, entity1Id);
+ let entity1Msges = entity1.features[TestFeature.Messages];
+ // 400, 300 -> 400, 100 (max is 500)
+ let rollovers = entity1Msges.rollovers;
+ expect(rollovers[0].balance).to.equal(100);
+ expect(rollovers[1].balance).to.equal(400);
- let entity2 = await autumn.entities.get(customerId, entity2Id);
- let entity2Msges = entity2.features[TestFeature.Messages];
- // 400, 200 -> 400, 0 (max is 500)
- let rollovers2 = entity2Msges.rollovers;
- expect(rollovers2[0].balance).to.equal(100);
- expect(rollovers2[1].balance).to.equal(400);
- });
+ let entity2 = await autumn.entities.get(customerId, entity2Id);
+ let entity2Msges = entity2.features[TestFeature.Messages];
+ // 400, 200 -> 400, 0 (max is 500)
+ let rollovers2 = entity2Msges.rollovers;
+ expect(rollovers2[0].balance).to.equal(100);
+ expect(rollovers2[1].balance).to.equal(400);
+ });
- it("should track and deduct from oldest rollovers first", async function () {
- for (const entity of entities) {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: 150,
- entity_id: entity.id,
- });
+ it("should track and deduct from oldest rollovers first", async function () {
+ for (const entity of entities) {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: 150,
+ entity_id: entity.id,
+ });
- await timeout(2000);
- let entRes = await autumn.entities.get(customerId, entity.id);
- let msgesFeature = entRes.features[TestFeature.Messages];
- let rollovers = msgesFeature.rollovers;
- expect(rollovers[0].balance).to.equal(0);
- expect(rollovers[1].balance).to.equal(350);
- expect(msgesFeature.balance).to.equal(includedUsage + 350);
- }
- });
+ await timeout(2000);
+ let entRes = await autumn.entities.get(customerId, entity.id);
+ let msgesFeature = entRes.features[TestFeature.Messages];
+ let rollovers = msgesFeature.rollovers;
+ expect(rollovers[0].balance).to.equal(0);
+ expect(rollovers[1].balance).to.equal(350);
+ expect(msgesFeature.balance).to.equal(includedUsage + 350);
+ }
+ });
- it("should track past rollovers and deduct from original balance", async function () {
- for (const entity of entities) {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: 400,
- entity_id: entity.id,
- });
- await timeout(2000);
+ it("should track past rollovers and deduct from original balance", async function () {
+ for (const entity of entities) {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: 400,
+ entity_id: entity.id,
+ });
+ await timeout(2000);
- let entRes = await autumn.entities.get(customerId, entity.id);
- let msgesFeature = entRes.features[TestFeature.Messages];
- let rollovers = msgesFeature.rollovers;
- expect(rollovers[0].balance).to.equal(0);
- expect(rollovers[1].balance).to.equal(0);
- expect(msgesFeature.balance).to.equal(includedUsage - 50);
- }
- });
+ let entRes = await autumn.entities.get(customerId, entity.id);
+ let msgesFeature = entRes.features[TestFeature.Messages];
+ let rollovers = msgesFeature.rollovers;
+ expect(rollovers[0].balance).to.equal(0);
+ expect(rollovers[1].balance).to.equal(0);
+ expect(msgesFeature.balance).to.equal(includedUsage - 50);
+ }
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover3.ts b/server/tests/advanced/rollovers/rollover3.ts
index b4a34c996..17351ea00 100644
--- a/server/tests/advanced/rollovers/rollover3.ts
+++ b/server/tests/advanced/rollovers/rollover3.ts
@@ -4,12 +4,12 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -27,100 +27,100 @@ import { addMonths } from "date-fns";
let rolloverConfig = { max: 500, length: 1, duration: RolloverDuration.Month };
const messagesItem = constructArrearProratedItem({
- featureId: TestFeature.Messages,
- includedUsage: 400,
- rolloverConfig,
+ featureId: TestFeature.Messages,
+ includedUsage: 400,
+ rolloverConfig,
}) as LimitedItem;
export let pro = constructProduct({
- items: [messagesItem],
- type: "pro",
- isDefault: false,
+ items: [messagesItem],
+ type: "pro",
+ isDefault: false,
});
const testCase = "rollover3";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- it("should attach pro product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
- });
+ it("should attach pro product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
+ });
- let rollover = 250;
- let curBalance = messagesItem.included_usage;
+ let rollover = 250;
+ let curBalance = messagesItem.included_usage;
- it("should create track messages, reset, and have correct rollover", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: messagesItem.included_usage - rollover,
- });
+ it("should create track messages, reset, and have correct rollover", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: messagesItem.included_usage - rollover,
+ });
- await timeout(3000);
+ await timeout(3000);
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1).getTime(),
- waitForSeconds: 20,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1).getTime(),
+ waitForSeconds: 20,
+ });
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- let expectedBalance = messagesItem.included_usage + rollover;
+ let expectedBalance = messagesItem.included_usage + rollover;
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(expectedBalance);
- // @ts-ignore
- expect(msgesFeature?.rollovers[0].balance).to.equal(rollover);
- curBalance = expectedBalance;
- });
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(expectedBalance);
+ // @ts-ignore
+ expect(msgesFeature?.rollovers[0].balance).to.equal(rollover);
+ curBalance = expectedBalance;
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover4.ts b/server/tests/advanced/rollovers/rollover4.ts
index 7e191a047..08e0717a3 100644
--- a/server/tests/advanced/rollovers/rollover4.ts
+++ b/server/tests/advanced/rollovers/rollover4.ts
@@ -4,12 +4,12 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -28,130 +28,130 @@ import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
let rolloverConfig = { max: 400, length: 1, duration: RolloverDuration.Month };
const messagesItem = constructPrepaidItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- billingUnits: 300,
- price: 10,
- rolloverConfig,
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ billingUnits: 300,
+ price: 10,
+ rolloverConfig,
}) as LimitedItem;
export let pro = constructProduct({
- items: [messagesItem],
- type: "pro",
- isDefault: false,
+ items: [messagesItem],
+ type: "pro",
+ isDefault: false,
});
const testCase = "rollover4";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- let paidQuantity = 300;
- let balance = paidQuantity + messagesItem.included_usage;
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: paidQuantity,
- },
- ];
+ let paidQuantity = 300;
+ let balance = paidQuantity + messagesItem.included_usage;
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: paidQuantity,
+ },
+ ];
- it("should attach pro product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- options,
- });
- });
+ it("should attach pro product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ options,
+ });
+ });
- let rollover = 50;
- it("should create track messages, reset, and have correct rollover", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: balance - rollover,
- });
+ let rollover = 50;
+ it("should create track messages, reset, and have correct rollover", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: balance - rollover,
+ });
- await timeout(3000);
+ await timeout(3000);
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1).getTime(),
- waitForSeconds: 20,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1).getTime(),
+ waitForSeconds: 20,
+ });
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
- // @ts-ignore
- let rollovers = msgesFeature?.rollovers;
+ // @ts-ignore
+ let rollovers = msgesFeature?.rollovers;
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(balance + rollover);
- expect(rollovers[0].balance).to.equal(rollover);
- });
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(balance + rollover);
+ expect(rollovers[0].balance).to.equal(rollover);
+ });
- // let usage2 = 50;
- it("should reset again and have correct rollover", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(curUnix, 1).getTime(),
- waitForSeconds: 20,
- });
+ // let usage2 = 50;
+ it("should reset again and have correct rollover", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(curUnix, 1).getTime(),
+ waitForSeconds: 20,
+ });
- let newRollover = Math.min(balance + rollover, rolloverConfig.max);
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
- // @ts-ignore
- let rollovers = msgesFeature?.rollovers;
+ let newRollover = Math.min(balance + rollover, rolloverConfig.max);
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
+ // @ts-ignore
+ let rollovers = msgesFeature?.rollovers;
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(balance + newRollover);
- expect(rollovers[0].balance).to.equal(0);
- expect(rollovers[1].balance).to.equal(400);
- });
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(balance + newRollover);
+ expect(rollovers[0].balance).to.equal(0);
+ expect(rollovers[1].balance).to.equal(400);
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover5.ts b/server/tests/advanced/rollovers/rollover5.ts
index 54162e863..1e5f88ad9 100644
--- a/server/tests/advanced/rollovers/rollover5.ts
+++ b/server/tests/advanced/rollovers/rollover5.ts
@@ -4,12 +4,12 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -27,114 +27,114 @@ let freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
let proRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
- rolloverConfig: freeRollover,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
+ rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
- rolloverConfig: proRollover,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
+ rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
- items: [freeMsges],
- type: "free",
- isDefault: false,
+ items: [freeMsges],
+ type: "free",
+ isDefault: false,
});
const pro = constructProduct({
- items: [proMsges],
- type: "pro",
+ items: [proMsges],
+ type: "pro",
});
const testCase = "rollover5";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free, pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free, pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- it("should attach free product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
- });
+ it("should attach free product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
+ });
- it("should create rollovers", async function () {
- await resetAndGetCusEnt({
- customer,
- db,
- productGroup: testCase,
- featureId: TestFeature.Messages,
- });
- await resetAndGetCusEnt({
- customer,
- db,
- productGroup: testCase,
- featureId: TestFeature.Messages,
- });
+ it("should create rollovers", async function () {
+ await resetAndGetCusEnt({
+ customer,
+ db,
+ productGroup: testCase,
+ featureId: TestFeature.Messages,
+ });
+ await resetAndGetCusEnt({
+ customer,
+ db,
+ productGroup: testCase,
+ featureId: TestFeature.Messages,
+ });
- // Attach pro
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ // Attach pro
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
- let freeRolloverBalance = freeMsges.included_usage * 2;
- let proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance);
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
+ let freeRolloverBalance = freeMsges.included_usage * 2;
+ let proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance);
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(
- proMsges.included_usage + proRolloverBalance
- );
- // @ts-ignore
- let rollovers = msgesFeature?.rollovers;
- expect(rollovers[0].balance).to.equal(100);
- expect(rollovers[1].balance).to.equal(500);
- });
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(
+ proMsges.included_usage + proRolloverBalance,
+ );
+ // @ts-ignore
+ let rollovers = msgesFeature?.rollovers;
+ expect(rollovers[0].balance).to.equal(100);
+ expect(rollovers[1].balance).to.equal(500);
+ });
});
diff --git a/server/tests/advanced/rollovers/rollover6.ts b/server/tests/advanced/rollovers/rollover6.ts
index c9dc16ffa..5452edfda 100644
--- a/server/tests/advanced/rollovers/rollover6.ts
+++ b/server/tests/advanced/rollovers/rollover6.ts
@@ -4,12 +4,12 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -30,125 +30,125 @@ let freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
let proRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
- rolloverConfig: freeRollover,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
+ rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
- rolloverConfig: proRollover,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
+ rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
- items: [freeMsges],
- type: "free",
- isDefault: false,
+ items: [freeMsges],
+ type: "free",
+ isDefault: false,
});
const pro = constructProduct({
- items: [proMsges],
- type: "pro",
+ items: [proMsges],
+ type: "pro",
});
const testCase = "rollover6";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free, pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free, pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- it("should attach free product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
- });
+ it("should attach free product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
+ });
- it("should create rollovers", async function () {
- await resetAndGetCusEnt({
- customer,
- db,
- productGroup: testCase,
- featureId: TestFeature.Messages,
- });
- await resetAndGetCusEnt({
- customer,
- db,
- productGroup: testCase,
- featureId: TestFeature.Messages,
- });
+ it("should create rollovers", async function () {
+ await resetAndGetCusEnt({
+ customer,
+ db,
+ productGroup: testCase,
+ featureId: TestFeature.Messages,
+ });
+ await resetAndGetCusEnt({
+ customer,
+ db,
+ productGroup: testCase,
+ featureId: TestFeature.Messages,
+ });
- // Attach pro
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ // Attach pro
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(curUnix, 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 20,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(curUnix, 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 20,
+ });
- let cus = await autumn.customers.get(customerId);
- let msgesFeature = cus.features[TestFeature.Messages];
- let proRolloverBalance = proMsges.included_usage * 2;
- let freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance);
+ let cus = await autumn.customers.get(customerId);
+ let msgesFeature = cus.features[TestFeature.Messages];
+ let proRolloverBalance = proMsges.included_usage * 2;
+ let freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance);
- expect(msgesFeature).to.exist;
- expect(msgesFeature?.balance).to.equal(
- freeMsges.included_usage + freeRolloverBalance
- );
+ expect(msgesFeature).to.exist;
+ expect(msgesFeature?.balance).to.equal(
+ freeMsges.included_usage + freeRolloverBalance,
+ );
- // @ts-ignore
- let rollovers = msgesFeature?.rollovers;
- expect(rollovers[0].balance).to.equal(100);
- expect(rollovers[1].balance).to.equal(500);
- });
+ // @ts-ignore
+ let rollovers = msgesFeature?.rollovers;
+ expect(rollovers[0].balance).to.equal(100);
+ expect(rollovers[1].balance).to.equal(500);
+ });
});
diff --git a/server/tests/advanced/rollovers/rolloverTestUtils.ts b/server/tests/advanced/rollovers/rolloverTestUtils.ts
index 2a9ce061e..d959a4ddc 100644
--- a/server/tests/advanced/rollovers/rolloverTestUtils.ts
+++ b/server/tests/advanced/rollovers/rolloverTestUtils.ts
@@ -7,54 +7,54 @@ import { Customer } from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
export const resetAndGetCusEnt = async ({
- db,
- customer,
- productGroup,
- featureId,
+ db,
+ customer,
+ productGroup,
+ featureId,
}: {
- db: DrizzleCli;
- customer: Customer;
- productGroup: string;
- featureId: string;
+ db: DrizzleCli;
+ customer: Customer;
+ productGroup: string;
+ featureId: string;
}) => {
- // Run reset cusEnt on ...
- let mainCusProduct = await getMainCusProduct({
- db,
- internalCustomerId: customer.internal_id,
- productGroup,
- });
+ // Run reset cusEnt on ...
+ let mainCusProduct = await getMainCusProduct({
+ db,
+ internalCustomerId: customer.internal_id,
+ productGroup,
+ });
- let cusEnt = cusProductToCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ let cusEnt = cusProductToCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- const updatedCusEnt = await resetCustomerEntitlement({
- db,
- cusEnt: {
- ...cusEnt!,
- customer,
- },
- cacheEnabledOrgs: [],
- });
+ const updatedCusEnt = await resetCustomerEntitlement({
+ db,
+ cusEnt: {
+ ...cusEnt!,
+ customer,
+ },
+ cacheEnabledOrgs: [],
+ });
- if (updatedCusEnt) {
- await CusEntService.upsert({
- db,
- data: [updatedCusEnt],
- });
- }
+ if (updatedCusEnt) {
+ await CusEntService.upsert({
+ db,
+ data: [updatedCusEnt],
+ });
+ }
- mainCusProduct = await getMainCusProduct({
- db,
- internalCustomerId: customer.internal_id,
- productGroup,
- });
+ mainCusProduct = await getMainCusProduct({
+ db,
+ internalCustomerId: customer.internal_id,
+ productGroup,
+ });
- cusEnt = cusProductToCusEnt({
- cusProduct: mainCusProduct!,
- featureId,
- });
+ cusEnt = cusProductToCusEnt({
+ cusProduct: mainCusProduct!,
+ featureId,
+ });
- return cusEnt;
+ return cusEnt;
};
diff --git a/server/tests/advanced/usage/usage1.ts b/server/tests/advanced/usage/usage1.ts
index 270ddb85f..648ea1ec1 100644
--- a/server/tests/advanced/usage/usage1.ts
+++ b/server/tests/advanced/usage/usage1.ts
@@ -15,111 +15,111 @@ import Stripe from "stripe";
const testCase = "usage1";
describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
- const NUM_EVENTS = 50;
- const customerId = testCase;
- let testClockId: string;
- let customer: Customer;
- let stripeCli: Stripe;
+ const NUM_EVENTS = 50;
+ const customerId = testCase;
+ let testClockId: string;
+ let customer: Customer;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- it("should attach usage based product", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithOverage.id,
- });
+ it("should attach usage based product", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithOverage.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
+ const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.proWithOverage,
- cusRes: res,
- });
- });
+ compareMainProduct({
+ sent: products.proWithOverage,
+ cusRes: res,
+ });
+ });
- it("usage1: should send metered1 events", async function () {
- const batchUpdates = [];
- for (let i = 0; i < NUM_EVENTS; i++) {
- batchUpdates.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- })
- );
- }
+ it("usage1: should send metered1 events", async function () {
+ const batchUpdates = [];
+ for (let i = 0; i < NUM_EVENTS; i++) {
+ batchUpdates.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
- await Promise.all(batchUpdates);
- await timeout(25000);
- });
+ await Promise.all(batchUpdates);
+ await timeout(25000);
+ });
- it("should have correct metered1 balance after sending events", async function () {
- const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
+ it("should have correct metered1 balance after sending events", async function () {
+ const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
- expect(res!.allowed).to.be.true;
+ expect(res!.allowed).to.be.true;
- const balance = res!.balances.find(
- (balance: any) => balance.feature_id === features.metered1.id
- );
+ const balance = res!.balances.find(
+ (balance: any) => balance.feature_id === features.metered1.id,
+ );
- const proOverageAmt =
- products.proWithOverage.entitlements.metered1.allowance;
+ const proOverageAmt =
+ products.proWithOverage.entitlements.metered1.allowance;
- expect(res!.allowed, "should be allowed").to.be.true;
+ expect(res!.allowed, "should be allowed").to.be.true;
- expect(balance?.balance, "should have correct metered1 balance").to.equal(
- proOverageAmt! - NUM_EVENTS
- );
+ expect(balance?.balance, "should have correct metered1 balance").to.equal(
+ proOverageAmt! - NUM_EVENTS,
+ );
- expect(balance?.usage_allowed, "should have usage_allowed").to.be.true;
- });
+ expect(balance?.usage_allowed, "should have usage_allowed").to.be.true;
+ });
- // Check invoice
- it("should advance stripe test clock and wait for event", async function () {
- await advanceClockForInvoice({
- stripeCli,
- testClockId,
- waitForMeterUpdate: true,
- });
- });
+ // Check invoice
+ it("should advance stripe test clock and wait for event", async function () {
+ await advanceClockForInvoice({
+ stripeCli,
+ testClockId,
+ waitForMeterUpdate: true,
+ });
+ });
- it("should have correct invoice amount", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
- const invoices = cusRes!.invoices;
+ it("should have correct invoice amount", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ const invoices = cusRes!.invoices;
- // calculate price
- const price = calculateMetered1Price({
- product: products.proWithOverage,
- numEvents: NUM_EVENTS,
- metered1Feature: features.metered1,
- });
+ // calculate price
+ const price = calculateMetered1Price({
+ product: products.proWithOverage,
+ numEvents: NUM_EVENTS,
+ metered1Feature: features.metered1,
+ });
- expect(invoices.length).to.equal(2);
+ expect(invoices.length).to.equal(2);
- const invoice = invoices[0];
+ const invoice = invoices[0];
- const basePrice = v1ProductToBasePrice({
- prices: products.proWithOverage.prices,
- });
+ const basePrice = v1ProductToBasePrice({
+ prices: products.proWithOverage.prices,
+ });
- expect(invoice.total).to.equal(
- price + basePrice,
- "invoice total should be usage price + base price"
- );
- });
+ expect(invoice.total).to.equal(
+ price + basePrice,
+ "invoice total should be usage price + base price",
+ );
+ });
});
diff --git a/server/tests/advanced/usage/usage2.ts b/server/tests/advanced/usage/usage2.ts
index 545878ac6..56a5e371e 100644
--- a/server/tests/advanced/usage/usage2.ts
+++ b/server/tests/advanced/usage/usage2.ts
@@ -8,8 +8,8 @@ import { createStripeCli } from "@/external/stripe/utils.js";
import { Decimal } from "decimal.js";
import {
- checkUsageInvoiceAmount,
- getCreditsUsed,
+ checkUsageInvoiceAmount,
+ getCreditsUsed,
} from "../../utils/advancedUsageUtils.js";
import chalk from "chalk";
@@ -21,126 +21,126 @@ import Stripe from "stripe";
const testCase = "usage2";
describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
- const customerId = testCase;
- const PRECISION = 10;
- const ASSERT_INVOICE_AMOUNT = true;
- const CREDIT_MULTIPLIER = 100000;
+ const customerId = testCase;
+ const PRECISION = 10;
+ const ASSERT_INVOICE_AMOUNT = true;
+ const CREDIT_MULTIPLIER = 100000;
- let testClockId = "";
- let totalCreditsUsed = 0;
+ let testClockId = "";
+ let totalCreditsUsed = 0;
- let stripeCli: Stripe;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { testClockId: createdTestClockId } = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ before(async function () {
+ await setupBefore(this);
+ const { testClockId: createdTestClockId } = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- testClockId = createdTestClockId;
+ testClockId = createdTestClockId;
- stripeCli = this.stripeCli;
- });
+ stripeCli = this.stripeCli;
+ });
- it("should attach gpu system starter", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuSystemStarter.id,
- });
+ it("should attach gpu system starter", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuSystemStarter.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: advanceProducts.gpuSystemStarter,
- cusRes: res,
- });
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: advanceProducts.gpuSystemStarter,
+ cusRes: res,
+ });
+ });
- // Use up events
- it("should send events and have correct balance (up to 10 DP)", async function () {
- let eventCount = 20;
+ // Use up events
+ it("should send events and have correct balance (up to 10 DP)", async function () {
+ let eventCount = 20;
- const batchEvents = [];
- for (let i = 0; i < eventCount; i++) {
- let randomVal = new Decimal(Math.random().toFixed(PRECISION))
- .mul(CREDIT_MULTIPLIER)
- .mul(Math.random() > 0.2 ? 1 : -1)
- .toNumber();
- let gpuId = i % 2 == 0 ? features.gpu1.id : features.gpu2.id;
+ const batchEvents = [];
+ for (let i = 0; i < eventCount; i++) {
+ let randomVal = new Decimal(Math.random().toFixed(PRECISION))
+ .mul(CREDIT_MULTIPLIER)
+ .mul(Math.random() > 0.2 ? 1 : -1)
+ .toNumber();
+ let gpuId = i % 2 == 0 ? features.gpu1.id : features.gpu2.id;
- let creditsUsed = getCreditsUsed(
- creditSystems.gpuCredits,
- gpuId,
- randomVal,
- );
+ let creditsUsed = getCreditsUsed(
+ creditSystems.gpuCredits,
+ gpuId,
+ randomVal,
+ );
- totalCreditsUsed = new Decimal(totalCreditsUsed)
- .plus(creditsUsed)
- .toNumber();
+ totalCreditsUsed = new Decimal(totalCreditsUsed)
+ .plus(creditsUsed)
+ .toNumber();
- batchEvents.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: gpuId,
- properties: { value: randomVal },
- }),
- );
- }
+ batchEvents.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: gpuId,
+ properties: { value: randomVal },
+ }),
+ );
+ }
- await Promise.all(batchEvents);
+ await Promise.all(batchEvents);
- await timeout(10000);
+ await timeout(10000);
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- creditSystems.gpuCredits.id,
- true,
- );
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ creditSystems.gpuCredits.id,
+ true,
+ );
- let creditAllowance =
- advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
+ let creditAllowance =
+ advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
- expect(allowed).to.be.true;
- expect(balanceObj!.balance).to.equal(
- new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(),
- );
- // console.log(" - Total credits used: ", totalCreditsUsed);
- // console.log(" - Balance: ", balanceObj!.balance);
- });
+ expect(allowed).to.be.true;
+ expect(balanceObj!.balance).to.equal(
+ new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(),
+ );
+ // console.log(" - Total credits used: ", totalCreditsUsed);
+ // console.log(" - Balance: ", balanceObj!.balance);
+ });
- // Check invoice.created event
- it("should have correct invoice amount / updated meter balance", async function () {
- await advanceClockForInvoice({
- stripeCli,
- testClockId,
- waitForMeterUpdate: ASSERT_INVOICE_AMOUNT,
- });
+ // Check invoice.created event
+ it("should have correct invoice amount / updated meter balance", async function () {
+ await advanceClockForInvoice({
+ stripeCli,
+ testClockId,
+ waitForMeterUpdate: ASSERT_INVOICE_AMOUNT,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- const invoices = res!.invoices;
+ const res = await AutumnCli.getCustomer(customerId);
+ const invoices = res!.invoices;
- if (ASSERT_INVOICE_AMOUNT) {
- await checkUsageInvoiceAmount({
- invoices,
- totalUsage: totalCreditsUsed,
- product: advanceProducts.gpuSystemStarter,
- featureId: creditSystems.gpuCredits.id,
- });
- } else {
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- creditSystems.gpuCredits.id,
- true,
- );
+ if (ASSERT_INVOICE_AMOUNT) {
+ await checkUsageInvoiceAmount({
+ invoices,
+ totalUsage: totalCreditsUsed,
+ product: advanceProducts.gpuSystemStarter,
+ featureId: creditSystems.gpuCredits.id,
+ });
+ } else {
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ creditSystems.gpuCredits.id,
+ true,
+ );
- let allowance =
- advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
+ let allowance =
+ advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
- assert.equal(balanceObj.balance, allowance);
- }
- });
+ assert.equal(balanceObj.balance, allowance);
+ }
+ });
});
diff --git a/server/tests/advanced/usage/usage3.ts b/server/tests/advanced/usage/usage3.ts
index 088b7272e..21d4c76c5 100644
--- a/server/tests/advanced/usage/usage3.ts
+++ b/server/tests/advanced/usage/usage3.ts
@@ -19,122 +19,122 @@ const testCase = "usage3";
const ASSERT_INVOICE_AMOUNT = true;
describe(`${chalk.yellowBright(
- "usage3: upgrade from GPU starter monthly to GPU pro monthly"
+ "usage3: upgrade from GPU starter monthly to GPU pro monthly",
)}`, () => {
- const customerId = "usage3";
- let testClockId = "";
- let totalCreditsUsed = 0;
- let stripeCli: Stripe;
- let curUnix = 0;
+ const customerId = "usage3";
+ let testClockId = "";
+ let totalCreditsUsed = 0;
+ let stripeCli: Stripe;
+ let curUnix = 0;
- before(async function () {
- await setupBefore(this);
- let { testClockId: insertedTestClockId } = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ before(async function () {
+ await setupBefore(this);
+ let { testClockId: insertedTestClockId } = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- testClockId = insertedTestClockId;
- stripeCli = this.stripeCli;
- });
+ testClockId = insertedTestClockId;
+ stripeCli = this.stripeCli;
+ });
- // 1. Attach GPU starter monthly
- it("usage3: should attach GPU starter monthly", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuSystemStarter.id,
- });
- });
+ // 1. Attach GPU starter monthly
+ it("usage3: should attach GPU starter monthly", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuSystemStarter.id,
+ });
+ });
- // 2. Send 20 events
- it("usage3: should send 20 events", async function () {
- let eventCount = 20;
- const { creditsUsed } = await sendGPUEvents({
- customerId,
- eventCount,
- });
+ // 2. Send 20 events
+ it("usage3: should send 20 events", async function () {
+ let eventCount = 20;
+ const { creditsUsed } = await sendGPUEvents({
+ customerId,
+ eventCount,
+ });
- totalCreditsUsed = creditsUsed;
- });
+ totalCreditsUsed = creditsUsed;
+ });
- // 3. Advance test clock by 15 days and upgrade
- it("should advance test clock by 15 days and upgrade to GPU pro monthly", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- numberOfDays: 15,
- });
+ // 3. Advance test clock by 15 days and upgrade
+ it("should advance test clock by 15 days and upgrade to GPU pro monthly", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ numberOfDays: 15,
+ });
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuSystemPro.id,
- });
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuSystemPro.id,
+ });
- // MAKE SURE STRIPE SUB ONLY HAS GPU PRO
+ // MAKE SURE STRIPE SUB ONLY HAS GPU PRO
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: advanceProducts.gpuSystemPro,
- cusRes: res,
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: advanceProducts.gpuSystemPro,
+ cusRes: res,
+ });
- let subscriptionId = res.products[0].subscription_ids![0]!;
- await checkSubscriptionContainsProducts({
- db: this.db,
- org: this.org,
- env: this.env,
- subscriptionId,
- productIds: [advanceProducts.gpuSystemPro.id],
- });
- });
+ let subscriptionId = res.products[0].subscription_ids![0]!;
+ await checkSubscriptionContainsProducts({
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ subscriptionId,
+ productIds: [advanceProducts.gpuSystemPro.id],
+ });
+ });
- // 4. Check invoice for 15 days of starter usage
- it("should have invoice for 15 days of starter usage", async function () {
- const res = await AutumnCli.getCustomer(customerId);
- const invoices = res!.invoices;
+ // 4. Check invoice for 15 days of starter usage
+ it("should have invoice for 15 days of starter usage", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
+ const invoices = res!.invoices;
- let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount;
- let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount;
+ let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount;
+ let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount;
- let { subs } = await getSubsFromCusId({
- db: this.db,
- org: this.org,
- env: this.env,
- customerId,
- stripeCli,
- productId: advanceProducts.gpuSystemPro.id,
- });
+ let { subs } = await getSubsFromCusId({
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ customerId,
+ stripeCli,
+ productId: advanceProducts.gpuSystemPro.id,
+ });
- let sub = subs[0];
+ let sub = subs[0];
- const { start, end } = subToPeriodStartEnd({ sub });
- let baseDiff = calculateProrationAmount({
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- amount: basePrice2 - basePrice1,
- allowNegative: true,
- });
+ const { start, end } = subToPeriodStartEnd({ sub });
+ let baseDiff = calculateProrationAmount({
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ amount: basePrice2 - basePrice1,
+ allowNegative: true,
+ });
- let usagePrice = advanceProducts.gpuSystemStarter.prices[1];
- let overage =
- totalCreditsUsed -
- advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
+ let usagePrice = advanceProducts.gpuSystemStarter.prices[1];
+ let overage =
+ totalCreditsUsed -
+ advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
- let overagePrice = priceToInvoiceAmount({
- price: usagePrice,
- overage,
- });
+ let overagePrice = priceToInvoiceAmount({
+ price: usagePrice,
+ overage,
+ });
- let calculatedTotal = new Decimal(baseDiff)
- .plus(overagePrice)
- .toDecimalPlaces(2)
- .toNumber();
+ let calculatedTotal = new Decimal(baseDiff)
+ .plus(overagePrice)
+ .toDecimalPlaces(2)
+ .toNumber();
- expect(invoices[0].total).to.equal(calculatedTotal);
- });
+ expect(invoices[0].total).to.equal(calculatedTotal);
+ });
});
diff --git a/server/tests/advanced/usage/usage4.ts b/server/tests/advanced/usage/usage4.ts
index 9d86810ec..ee5aa858f 100644
--- a/server/tests/advanced/usage/usage4.ts
+++ b/server/tests/advanced/usage/usage4.ts
@@ -8,9 +8,9 @@ import { expect } from "chai";
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
import {
- sendGPUEvents,
- checkCreditBalance,
- checkUsageInvoiceAmount,
+ sendGPUEvents,
+ checkCreditBalance,
+ checkUsageInvoiceAmount,
} from "../../utils/advancedUsageUtils.js";
import Stripe from "stripe";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
@@ -21,92 +21,92 @@ import { setupBefore } from "tests/before.js";
const testCase = "usage4";
describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
- const customerId = testCase;
- let totalCreditsUsed = 0;
+ const customerId = testCase;
+ let totalCreditsUsed = 0;
- let testClockId = "";
- let customer: Customer;
- let stripeCli: Stripe;
+ let testClockId = "";
+ let customer: Customer;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- let res = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ before(async function () {
+ await setupBefore(this);
+ let res = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- testClockId = res.testClockId;
- customer = res.customer;
- stripeCli = this.stripeCli;
- });
+ testClockId = res.testClockId;
+ customer = res.customer;
+ stripeCli = this.stripeCli;
+ });
- it("should attach GPU starter annual", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuStarterAnnual.id,
- });
+ it("should attach GPU starter annual", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuStarterAnnual.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: advanceProducts.gpuStarterAnnual,
- cusRes: res,
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: advanceProducts.gpuStarterAnnual,
+ cusRes: res,
+ });
- expect(res!.invoices.length).to.equal(1);
- });
+ expect(res!.invoices.length).to.equal(1);
+ });
- it("should send 20 events and have correct balance", async function () {
- let eventCount = 20;
- const { creditsUsed } = await sendGPUEvents({
- customerId,
- eventCount,
- });
+ it("should send 20 events and have correct balance", async function () {
+ let eventCount = 20;
+ const { creditsUsed } = await sendGPUEvents({
+ customerId,
+ eventCount,
+ });
- totalCreditsUsed = creditsUsed;
- await checkCreditBalance({
- customerId,
- featureId: creditSystems.gpuCredits.id,
- totalCreditsUsed,
- originalAllowance:
- advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
- });
- });
+ totalCreditsUsed = creditsUsed;
+ await checkCreditBalance({
+ customerId,
+ featureId: creditSystems.gpuCredits.id,
+ totalCreditsUsed,
+ originalAllowance:
+ advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
+ });
+ });
- it("should have invoice after a month and correct balance", async function () {
- await advanceClockForInvoice({
- stripeCli,
- testClockId,
- waitForMeterUpdate: true,
- });
+ it("should have invoice after a month and correct balance", async function () {
+ await advanceClockForInvoice({
+ stripeCli,
+ testClockId,
+ waitForMeterUpdate: true,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- const invoices = res!.invoices;
+ const res = await AutumnCli.getCustomer(customerId);
+ const invoices = res!.invoices;
- let invoiceIndex = invoices.findIndex((invoice: any) =>
- invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id)
- );
+ let invoiceIndex = invoices.findIndex((invoice: any) =>
+ invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id),
+ );
- await checkUsageInvoiceAmount({
- invoices,
- totalUsage: totalCreditsUsed,
- product: advanceProducts.gpuStarterAnnual,
- featureId: creditSystems.gpuCredits.id,
- invoiceIndex,
- includeBase: false,
- });
+ await checkUsageInvoiceAmount({
+ invoices,
+ totalUsage: totalCreditsUsed,
+ product: advanceProducts.gpuStarterAnnual,
+ featureId: creditSystems.gpuCredits.id,
+ invoiceIndex,
+ includeBase: false,
+ });
- await checkCreditBalance({
- customerId,
- featureId: creditSystems.gpuCredits.id,
- totalCreditsUsed: 0,
- originalAllowance:
- advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
- });
- });
+ await checkCreditBalance({
+ customerId,
+ featureId: creditSystems.gpuCredits.id,
+ totalCreditsUsed: 0,
+ originalAllowance:
+ advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
+ });
+ });
});
// // Advance by 1 year and check if latest invoice is correct
diff --git a/server/tests/advanced/usageLimit/usageLimit1.ts b/server/tests/advanced/usageLimit/usageLimit1.ts
index ccbfc2832..b1d25b8b6 100644
--- a/server/tests/advanced/usageLimit/usageLimit1.ts
+++ b/server/tests/advanced/usageLimit/usageLimit1.ts
@@ -16,132 +16,132 @@ import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 0,
- usageLimit: 2,
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 0,
+ usageLimit: 2,
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "usageLimit1";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "Entity 3",
- feature_id: TestFeature.Users,
- },
- {
- id: "4",
- name: "Entity 4",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "Entity 3",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "4",
+ name: "Entity 4",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
- it("should create more entities than the limit and hit error", async function () {
- await expectAutumnError({
- errCode: ErrCode.FeatureLimitReached,
- func: async () => {
- await autumn.entities.create(customerId, entities);
- },
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
+ it("should create more entities than the limit and hit error", async function () {
+ await expectAutumnError({
+ errCode: ErrCode.FeatureLimitReached,
+ func: async () => {
+ await autumn.entities.create(customerId, entities);
+ },
+ });
+ });
- it("should create entities one by one, then hit usage limit", async function () {
- await autumn.entities.create(customerId, entities[0]);
- await autumn.entities.create(customerId, entities[1]);
+ it("should create entities one by one, then hit usage limit", async function () {
+ await autumn.entities.create(customerId, entities[0]);
+ await autumn.entities.create(customerId, entities[1]);
- await expectAutumnError({
- errCode: ErrCode.FeatureLimitReached,
- func: async () => {
- await autumn.entities.create(customerId, entities[2]);
- },
- });
- });
+ await expectAutumnError({
+ errCode: ErrCode.FeatureLimitReached,
+ func: async () => {
+ await autumn.entities.create(customerId, entities[2]);
+ },
+ });
+ });
- it("should have correct check and get customer value", async function () {
- const check = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- });
- const customer = await autumn.customers.get(customerId);
+ it("should have correct check and get customer value", async function () {
+ const check = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ });
+ const customer = await autumn.customers.get(customerId);
- expect(check.balance).to.equal(-2);
- // @ts-ignore
- expect(check.usage_limit).to.equal(userItem.usage_limit);
+ expect(check.balance).to.equal(-2);
+ // @ts-ignore
+ expect(check.usage_limit).to.equal(userItem.usage_limit);
- // @ts-ignore
- expect(customer.features[TestFeature.Users].usage_limit).to.equal(
- userItem.usage_limit
- );
- });
+ // @ts-ignore
+ expect(customer.features[TestFeature.Users].usage_limit).to.equal(
+ userItem.usage_limit,
+ );
+ });
});
diff --git a/server/tests/advanced/usageLimit/usageLimit2.ts b/server/tests/advanced/usageLimit/usageLimit2.ts
index 94725971e..7289499c0 100644
--- a/server/tests/advanced/usageLimit/usageLimit2.ts
+++ b/server/tests/advanced/usageLimit/usageLimit2.ts
@@ -9,8 +9,8 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
@@ -19,171 +19,171 @@ import { timeout } from "@/utils/genUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructArrearItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- billingUnits: 1,
- price: 0.5,
- usageLimit: 500,
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ billingUnits: 1,
+ price: 0.5,
+ usageLimit: 500,
}) as LimitedItem;
export let pro = constructProduct({
- items: [messageItem],
- type: "pro",
+ items: [messageItem],
+ type: "pro",
});
const addOnMessages = constructFeatureItem({
- featureId: TestFeature.Messages,
- interval: null,
- includedUsage: 250,
+ featureId: TestFeature.Messages,
+ interval: null,
+ includedUsage: 250,
}) as LimitedItem;
const messageAddOn = constructProduct({
- type: "one_off",
- items: [addOnMessages],
+ type: "one_off",
+ items: [addOnMessages],
});
const testCase = "usageLimit2";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, messageAddOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, messageAddOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, messageAddOn],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, messageAddOn],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let initialUsage =
- messageItem.included_usage + messageItem.usage_limit! + 1000;
+ let initialUsage =
+ messageItem.included_usage + messageItem.usage_limit! + 1000;
- it("should track more messages than limit and not surpass", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: initialUsage,
- });
+ it("should track more messages than limit and not surpass", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: initialUsage,
+ });
- await timeout(2000);
+ await timeout(2000);
- let check = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
- let customer = await autumn.customers.get(customerId);
+ let check = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
+ let customer = await autumn.customers.get(customerId);
- let expectedBalance = messageItem.included_usage - messageItem.usage_limit!;
+ let expectedBalance = messageItem.included_usage - messageItem.usage_limit!;
- expect(check.balance).to.equal(expectedBalance);
- expect(check.allowed).to.equal(false);
- // @ts-ignore
- expect(check.usage_limit!).to.equal(messageItem.usage_limit!);
- // @ts-ignore
- expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
- messageItem.usage_limit!
- );
- });
+ expect(check.balance).to.equal(expectedBalance);
+ expect(check.allowed).to.equal(false);
+ // @ts-ignore
+ expect(check.usage_limit!).to.equal(messageItem.usage_limit!);
+ // @ts-ignore
+ expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
+ messageItem.usage_limit!,
+ );
+ });
- it("should purchase add ons and have correct check results", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: messageAddOn.id,
- });
+ it("should purchase add ons and have correct check results", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: messageAddOn.id,
+ });
- let check = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
- let customer = await autumn.customers.get(customerId);
- let expectedBalance =
- messageItem.included_usage -
- messageItem.usage_limit! +
- addOnMessages.included_usage;
+ let check = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
+ let customer = await autumn.customers.get(customerId);
+ let expectedBalance =
+ messageItem.included_usage -
+ messageItem.usage_limit! +
+ addOnMessages.included_usage;
- expect(check.balance).to.equal(expectedBalance);
- expect(check.allowed).to.equal(true);
+ expect(check.balance).to.equal(expectedBalance);
+ expect(check.allowed).to.equal(true);
- // @ts-ignore
- expect(check.usage_limit!).to.equal(
- messageItem.usage_limit! + addOnMessages.included_usage
- );
- // @ts-ignore
- expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
- messageItem.usage_limit! + addOnMessages.included_usage
- );
- });
+ // @ts-ignore
+ expect(check.usage_limit!).to.equal(
+ messageItem.usage_limit! + addOnMessages.included_usage,
+ );
+ // @ts-ignore
+ expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
+ messageItem.usage_limit! + addOnMessages.included_usage,
+ );
+ });
- it("should use up all add ons and have correct check results", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: addOnMessages.included_usage + 500,
- });
+ it("should use up all add ons and have correct check results", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: addOnMessages.included_usage + 500,
+ });
- await timeout(2000);
+ await timeout(2000);
- let check = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
- let customer = await autumn.customers.get(customerId);
+ let check = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
+ let customer = await autumn.customers.get(customerId);
- let expectedBalance = messageItem.included_usage - messageItem.usage_limit!;
- expect(check.balance).to.equal(expectedBalance);
- expect(check.allowed).to.equal(false);
- // @ts-ignore
- expect(check.usage_limit!).to.equal(
- messageItem.usage_limit! + addOnMessages.included_usage
- );
- // @ts-ignore
- expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
- messageItem.usage_limit! + addOnMessages.included_usage
- );
- });
+ let expectedBalance = messageItem.included_usage - messageItem.usage_limit!;
+ expect(check.balance).to.equal(expectedBalance);
+ expect(check.allowed).to.equal(false);
+ // @ts-ignore
+ expect(check.usage_limit!).to.equal(
+ messageItem.usage_limit! + addOnMessages.included_usage,
+ );
+ // @ts-ignore
+ expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
+ messageItem.usage_limit! + addOnMessages.included_usage,
+ );
+ });
});
diff --git a/server/tests/advanced/usageLimit/usageLimit3.ts b/server/tests/advanced/usageLimit/usageLimit3.ts
index 5dbea0f4f..d9d86d6a8 100644
--- a/server/tests/advanced/usageLimit/usageLimit3.ts
+++ b/server/tests/advanced/usageLimit/usageLimit3.ts
@@ -3,11 +3,11 @@ import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- ErrCode,
- LimitedItem,
- Organization,
+ APIVersion,
+ AppEnv,
+ ErrCode,
+ LimitedItem,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -15,8 +15,8 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
@@ -26,16 +26,16 @@ import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructPrepaidItem({
- featureId: TestFeature.Messages,
- includedUsage: 50,
- billingUnits: 100,
- price: 8,
- usageLimit: 500,
+ featureId: TestFeature.Messages,
+ includedUsage: 50,
+ billingUnits: 100,
+ price: 8,
+ usageLimit: 500,
}) as LimitedItem;
export let pro = constructProduct({
- items: [messageItem],
- type: "pro",
+ items: [messageItem],
+ type: "pro",
});
// const addOnMessages = constructFeatureItem({
@@ -52,102 +52,102 @@ export let pro = constructProduct({
const testCase = "usageLimit3";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product with quantity exceeding usage limit and get an error", async function () {
- expectAutumnError({
- errCode: ErrCode.InvalidOptions,
- func: async () => {
- return await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 600,
- },
- ],
- });
- },
- });
- });
- it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 100,
- },
- ],
- });
+ it("should attach pro product with quantity exceeding usage limit and get an error", async function () {
+ expectAutumnError({
+ errCode: ErrCode.InvalidOptions,
+ func: async () => {
+ return await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 600,
+ },
+ ],
+ });
+ },
+ });
+ });
+ it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 100,
+ },
+ ],
+ });
- expectAutumnError({
- errCode: ErrCode.InvalidOptions,
- func: async () => {
- return await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 600,
- },
- ],
- });
- },
- });
- });
+ expectAutumnError({
+ errCode: ErrCode.InvalidOptions,
+ func: async () => {
+ return await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 600,
+ },
+ ],
+ });
+ },
+ });
+ });
});
diff --git a/server/tests/advanced/usageLimit/usageLimit4.ts b/server/tests/advanced/usageLimit/usageLimit4.ts
index 99fb3c557..596058f69 100644
--- a/server/tests/advanced/usageLimit/usageLimit4.ts
+++ b/server/tests/advanced/usageLimit/usageLimit4.ts
@@ -3,11 +3,11 @@ import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- ErrCode,
- LimitedItem,
- Organization,
+ APIVersion,
+ AppEnv,
+ ErrCode,
+ LimitedItem,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -15,9 +15,9 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearProratedItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearProratedItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { addPrefixToProducts } from "tests/attach/utils.js";
@@ -27,92 +27,92 @@ import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const messageItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- includedUsage: 1,
- pricePerUnit: 10,
- usageLimit: 3,
+ featureId: TestFeature.Users,
+ includedUsage: 1,
+ pricePerUnit: 10,
+ usageLimit: 3,
}) as LimitedItem;
export let pro = constructProduct({
- items: [messageItem],
- type: "pro",
+ items: [messageItem],
+ type: "pro",
});
const testCase = "usageLimit4";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product with quantity exceeding usage limit and get an error", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
- it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async function () {
- await expectAutumnError({
- errCode: ErrCode.InvalidInputs,
- func: async () => {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: messageItem.usage_limit! + 1,
- });
- },
- });
+ it("should attach pro product with quantity exceeding usage limit and get an error", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
+ it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async function () {
+ await expectAutumnError({
+ errCode: ErrCode.InvalidInputs,
+ func: async () => {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: messageItem.usage_limit! + 1,
+ });
+ },
+ });
- const check = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- });
+ const check = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ });
- expect(check.balance).to.equal(0);
- expect(check.allowed).to.equal(true);
- });
+ expect(check.balance).to.equal(0);
+ expect(check.allowed).to.equal(true);
+ });
});
diff --git a/server/tests/alex/00_setup.ts b/server/tests/alex/00_setup.ts
index fb9839322..3acfa4492 100644
--- a/server/tests/alex/00_setup.ts
+++ b/server/tests/alex/00_setup.ts
@@ -7,18 +7,18 @@ const ORG_SLUG = process.env.TESTS_ORG!;
const DEFAULT_ENV = AppEnv.Sandbox;
describe("Initialize org for tests", () => {
- it("should initialize org", async function () {
- this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
- this.env = DEFAULT_ENV;
- await setupOrg({
- orgId: this.org.id,
- env: DEFAULT_ENV,
- features: { ...alexFeatures } as any,
- products: { ...alexProducts },
- rewards: {},
- rewardTriggers: {},
- });
+ it("should initialize org", async function () {
+ this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
+ this.env = DEFAULT_ENV;
+ await setupOrg({
+ orgId: this.org.id,
+ env: DEFAULT_ENV,
+ features: { ...alexFeatures } as any,
+ products: { ...alexProducts },
+ rewards: {},
+ rewardTriggers: {},
+ });
- console.log("--------------------------------");
- });
+ console.log("--------------------------------");
+ });
});
diff --git a/server/tests/alex/01_free.ts b/server/tests/alex/01_free.ts
index 0846bef63..60cfbdebd 100644
--- a/server/tests/alex/01_free.ts
+++ b/server/tests/alex/01_free.ts
@@ -6,36 +6,36 @@ import { runEventsAndCheckBalances } from "./utils.js";
import chalk from "chalk";
describe(chalk.yellowBright("Free customer"), () => {
- let customerId = "alex-free-customer";
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- // name: null,
- // email: null,
- },
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ let customerId = "alex-free-customer";
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ // name: null,
+ // email: null,
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- // 1. Check that customer has correct product & entitlements
- it("GET customer has correct product & entitlements", async function () {
- const customer = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.free,
- cusRes: customer,
- });
- });
+ // 1. Check that customer has correct product & entitlements
+ it("GET customer has correct product & entitlements", async function () {
+ const customer = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.free,
+ cusRes: customer,
+ });
+ });
- // 3. Run /events for each feature, and check that the balance is updated correctly
- it("should run /events for each feature and have correct balance afterwards", async function () {
- let entitlements = Object.values(alexProducts.free.entitlements);
+ // 3. Run /events for each feature, and check that the balance is updated correctly
+ it("should run /events for each feature and have correct balance afterwards", async function () {
+ let entitlements = Object.values(alexProducts.free.entitlements);
- await runEventsAndCheckBalances({
- customerId,
- entitlements,
- });
- });
+ await runEventsAndCheckBalances({
+ customerId,
+ entitlements,
+ });
+ });
});
diff --git a/server/tests/alex/02_pro.ts b/server/tests/alex/02_pro.ts
index 5287875fc..10d388ce6 100644
--- a/server/tests/alex/02_pro.ts
+++ b/server/tests/alex/02_pro.ts
@@ -10,49 +10,49 @@ import { runEventsAndCheckBalances } from "./utils.js";
import chalk from "chalk";
describe(chalk.yellowBright("Pro entitlements"), () => {
- let customerId = "alex-pro-customer";
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- // name: "Alex Pro Customer",
- email: "alex-pro-customer@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ let customerId = "alex-pro-customer";
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ // name: "Alex Pro Customer",
+ email: "alex-pro-customer@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should upgrade to Pro after calling /attach", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ it("should upgrade to Pro after calling /attach", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(20000);
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(20000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- expect(typeof cusRes.customer.name).to.equal("string");
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ expect(typeof cusRes.customer.name).to.equal("string");
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
- // Check invoice is correct
- const invoices = cusRes.invoices;
- expect(invoices.length).to.equal(1);
- expect(invoices[0].total).to.equal(0);
- expect(invoices[0].status).to.equal(InvoiceStatus.Paid);
- });
+ // Check invoice is correct
+ const invoices = cusRes.invoices;
+ expect(invoices.length).to.equal(1);
+ expect(invoices[0].total).to.equal(0);
+ expect(invoices[0].status).to.equal(InvoiceStatus.Paid);
+ });
- it("should run event sending and check balances for each entitlement", async function () {
- await runEventsAndCheckBalances({
- customerId,
- entitlements: Object.values(alexProducts.pro.entitlements),
- });
- });
+ it("should run event sending and check balances for each entitlement", async function () {
+ await runEventsAndCheckBalances({
+ customerId,
+ entitlements: Object.values(alexProducts.pro.entitlements),
+ });
+ });
});
diff --git a/server/tests/alex/03_premium.ts b/server/tests/alex/03_premium.ts
index 18c48f6a1..29aae7f54 100644
--- a/server/tests/alex/03_premium.ts
+++ b/server/tests/alex/03_premium.ts
@@ -9,43 +9,43 @@ import { runEventsAndCheckBalances } from "./utils.js";
import chalk from "chalk";
describe(chalk.yellowBright("Premium plan"), () => {
- let customerId = "alex-premium-customer";
+ let customerId = "alex-premium-customer";
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: "Alex Premium Customer",
- email: "alex-premium-customer@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- // attachPm: true,
- });
- });
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: "Alex Premium Customer",
+ email: "alex-premium-customer@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ // attachPm: true,
+ });
+ });
- it("should attach premium product", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productId: alexProducts.premium.id,
- });
+ it("should attach premium product", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.premium.id,
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(10000);
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(10000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.premium,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.premium,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should send events and check balances", async function () {
- await runEventsAndCheckBalances({
- customerId,
- entitlements: Object.values(alexProducts.premium.entitlements),
- });
- });
+ it("should send events and check balances", async function () {
+ await runEventsAndCheckBalances({
+ customerId,
+ entitlements: Object.values(alexProducts.premium.entitlements),
+ });
+ });
});
diff --git a/server/tests/alex/04_topups.ts b/server/tests/alex/04_topups.ts
index 47e0bd257..e0222413c 100644
--- a/server/tests/alex/04_topups.ts
+++ b/server/tests/alex/04_topups.ts
@@ -3,197 +3,197 @@ import { AutumnCli } from "tests/cli/AutumnCli.js";
import { initCustomer } from "tests/utils/init.js";
import { alexFeatures, alexProducts } from "./init.js";
import {
- AllowanceType,
- CusProductStatus,
- EntInterval,
- Entitlement,
+ AllowanceType,
+ CusProductStatus,
+ EntInterval,
+ Entitlement,
} from "@autumn/shared";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
import { expect } from "chai";
import {
- checkFeatureHasCorrectBalance,
- runEventsAndCheckBalances,
+ checkFeatureHasCorrectBalance,
+ runEventsAndCheckBalances,
} from "./utils.js";
import chalk from "chalk";
describe(chalk.yellowBright("Top ups"), () => {
- let customerId = "alex-top-up-customer";
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: "Alex Top Up Customer",
- email: "alex-top-up-customer@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ let customerId = "alex-top-up-customer";
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: "Alex Top Up Customer",
+ email: "alex-top-up-customer@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- it("should attach pro product", async function () {
- await timeout(5000);
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ it("should attach pro product", async function () {
+ await timeout(5000);
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- let overrideQuantity = 5;
- let billingUnits = alexProducts.topUpMessages.prices[0].config.billing_units;
- let prodEnt = alexProducts.topUpMessages.entitlements.topUpMessage;
- let leftoverBalance = 0;
- it("should attach top up messages through force checkout", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productId: alexProducts.topUpMessages.id,
- forceCheckout: true,
- });
+ let overrideQuantity = 5;
+ let billingUnits = alexProducts.topUpMessages.prices[0].config.billing_units;
+ let prodEnt = alexProducts.topUpMessages.entitlements.topUpMessage;
+ let leftoverBalance = 0;
+ it("should attach top up messages through force checkout", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.topUpMessages.id,
+ forceCheckout: true,
+ });
- await completeCheckoutForm(res.checkout_url, overrideQuantity);
- await timeout(10000);
+ await completeCheckoutForm(res.checkout_url, overrideQuantity);
+ await timeout(10000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- // Get product
- const product = cusRes.add_ons.find(
- (p: any) => p.id === alexProducts.topUpMessages.id
- );
- expect(product).to.exist;
- expect(product.status).to.equal(CusProductStatus.Active);
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ // Get product
+ const product = cusRes.add_ons.find(
+ (p: any) => p.id === alexProducts.topUpMessages.id,
+ );
+ expect(product).to.exist;
+ expect(product.status).to.equal(CusProductStatus.Active);
- // Check quantity is correct
- let cusEnt = cusRes.entitlements.find(
- (e: any) => e.feature_id === alexFeatures.topUpMessage.id
- );
+ // Check quantity is correct
+ let cusEnt = cusRes.entitlements.find(
+ (e: any) => e.feature_id === alexFeatures.topUpMessage.id,
+ );
- expect(cusEnt).to.exist;
- expect(cusEnt.balance).to.equal(overrideQuantity * billingUnits);
- expect(cusEnt.interval).to.equal(prodEnt.interval);
- });
+ expect(cusEnt).to.exist;
+ expect(cusEnt.balance).to.equal(overrideQuantity * billingUnits);
+ expect(cusEnt.interval).to.equal(prodEnt.interval);
+ });
- // Try buy again
- it("should buy top ups again and have correct balance", async function () {
- // 1. Update leftover balance
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- alexFeatures.topUpMessage.id,
- true
- );
- leftoverBalance = balanceObj.balance;
+ // Try buy again
+ it("should buy top ups again and have correct balance", async function () {
+ // 1. Update leftover balance
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ alexFeatures.topUpMessage.id,
+ true,
+ );
+ leftoverBalance = balanceObj.balance;
- const res = await AutumnCli.attach({
- customerId,
- productId: alexProducts.topUpMessages.id,
- forceCheckout: true,
- });
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.topUpMessages.id,
+ forceCheckout: true,
+ });
- await completeCheckoutForm(res.checkout_url, overrideQuantity);
- await timeout(10000);
+ await completeCheckoutForm(res.checkout_url, overrideQuantity);
+ await timeout(10000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- // Get product
- const product = cusRes.add_ons.find(
- (p: any) => p.id === alexProducts.topUpMessages.id
- );
- expect(product).to.exist;
- expect(product.status).to.equal(CusProductStatus.Active);
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ // Get product
+ const product = cusRes.add_ons.find(
+ (p: any) => p.id === alexProducts.topUpMessages.id,
+ );
+ expect(product).to.exist;
+ expect(product.status).to.equal(CusProductStatus.Active);
- // Check quantity is correct
- let cusEnt = cusRes.entitlements.find(
- (e: any) => e.feature_id === alexFeatures.topUpMessage.id
- );
- expect(cusEnt).to.exist;
- expect(cusEnt.balance).to.equal(
- leftoverBalance + overrideQuantity * billingUnits
- );
- expect(cusEnt.interval).to.equal(prodEnt.interval);
- });
+ // Check quantity is correct
+ let cusEnt = cusRes.entitlements.find(
+ (e: any) => e.feature_id === alexFeatures.topUpMessage.id,
+ );
+ expect(cusEnt).to.exist;
+ expect(cusEnt.balance).to.equal(
+ leftoverBalance + overrideQuantity * billingUnits,
+ );
+ expect(cusEnt.interval).to.equal(prodEnt.interval);
+ });
});
describe(chalk.yellowBright("Testing o1 message top up"), () => {
- let customerId = "alex-o1-top-up-customer";
- let o1TopUpQuantity = Math.floor(Math.random() * 15);
- let billingUnits = alexProducts.o1TopUps.prices[0].config.billing_units;
- let proAllowance = alexProducts.pro.entitlements.o1Message.allowance!;
+ let customerId = "alex-o1-top-up-customer";
+ let o1TopUpQuantity = Math.floor(Math.random() * 15);
+ let billingUnits = alexProducts.o1TopUps.prices[0].config.billing_units;
+ let proAllowance = alexProducts.pro.entitlements.o1Message.allowance!;
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: "Alex O1 Top Up Customer",
- email: "alex-o1-top-up-customer@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: "Alex O1 Top Up Customer",
+ email: "alex-o1-top-up-customer@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- it("should attach pro product", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ it("should attach pro product", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should buy o1 messages and have correct balance", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productId: alexProducts.o1TopUps.id,
- forceCheckout: true,
- });
+ it("should buy o1 messages and have correct balance", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.o1TopUps.id,
+ forceCheckout: true,
+ });
- await completeCheckoutForm(res.checkout_url, o1TopUpQuantity);
- await timeout(14000);
+ await completeCheckoutForm(res.checkout_url, o1TopUpQuantity);
+ await timeout(14000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- // Get product
- const product = cusRes.add_ons.find(
- (p: any) => p.id === alexProducts.o1TopUps.id
- );
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ // Get product
+ const product = cusRes.add_ons.find(
+ (p: any) => p.id === alexProducts.o1TopUps.id,
+ );
- expect(product).to.exist;
- expect(product.status).to.equal(CusProductStatus.Active);
+ expect(product).to.exist;
+ expect(product.status).to.equal(CusProductStatus.Active);
- // Check quantity is correct
- await checkFeatureHasCorrectBalance({
- customerId,
- feature: alexFeatures.o1Message,
- entitlement: alexProducts.o1TopUps.entitlements.o1Message,
- expectedBalance: o1TopUpQuantity * billingUnits + proAllowance,
- });
- });
+ // Check quantity is correct
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature: alexFeatures.o1Message,
+ entitlement: alexProducts.o1TopUps.entitlements.o1Message,
+ expectedBalance: o1TopUpQuantity * billingUnits + proAllowance,
+ });
+ });
- it("should send events and check balances", async function () {
- let allowance = o1TopUpQuantity * billingUnits + proAllowance;
+ it("should send events and check balances", async function () {
+ let allowance = o1TopUpQuantity * billingUnits + proAllowance;
- await runEventsAndCheckBalances({
- customerId,
- entitlements: [
- {
- interval: EntInterval.Lifetime,
- feature_id: alexFeatures.o1Message.id,
- allowance,
- allowance_type: AllowanceType.Fixed,
- } as Entitlement, // manual entitlement because in advance pricing
- ],
- });
- });
+ await runEventsAndCheckBalances({
+ customerId,
+ entitlements: [
+ {
+ interval: EntInterval.Lifetime,
+ feature_id: alexFeatures.o1Message.id,
+ allowance,
+ allowance_type: AllowanceType.Fixed,
+ } as Entitlement, // manual entitlement because in advance pricing
+ ],
+ });
+ });
});
diff --git a/server/tests/alex/05_cancel.ts b/server/tests/alex/05_cancel.ts
index 5d9ecef61..0e2ac2392 100644
--- a/server/tests/alex/05_cancel.ts
+++ b/server/tests/alex/05_cancel.ts
@@ -16,192 +16,192 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
// CANCEL AT
const getProductFromCusRes = ({
- cusRes,
- productId,
+ cusRes,
+ productId,
}: {
- cusRes: any;
- productId: string;
+ cusRes: any;
+ productId: string;
}) => {
- return cusRes.products.find((p: any) => p.id === productId);
+ return cusRes.products.find((p: any) => p.id === productId);
};
describe(chalk.yellowBright("05_cancel"), () => {
- describe("Testing cancel_at_period_end and cancel now", () => {
- let stripeCli: Stripe;
- let customerId = "alex-cancel-customer";
+ describe("Testing cancel_at_period_end and cancel now", () => {
+ let stripeCli: Stripe;
+ let customerId = "alex-cancel-customer";
- before("initializing customer", async function () {
- stripeCli = createStripeCli({
- org: this.org,
- env: this.env,
- });
- await initCustomer({
- customer_data: {
- id: customerId,
- name: "Alex Cancel Customer",
- email: "alex-cancel-customer@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ before("initializing customer", async function () {
+ stripeCli = createStripeCli({
+ org: this.org,
+ env: this.env,
+ });
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: "Alex Cancel Customer",
+ email: "alex-cancel-customer@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- it("should attach pro product ", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ it("should attach pro product ", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should cancel at period end", async function () {
- // 1. Get pro product
- const cusRes = await AutumnCli.getCustomer(customerId);
- const proProduct = getProductFromCusRes({
- cusRes,
- productId: alexProducts.pro.id,
- });
+ it("should cancel at period end", async function () {
+ // 1. Get pro product
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ const proProduct = getProductFromCusRes({
+ cusRes,
+ productId: alexProducts.pro.id,
+ });
- for (const subId of proProduct.subscription_ids) {
- await stripeCli.subscriptions.update(subId, {
- cancel_at_period_end: true,
- });
- }
+ for (const subId of proProduct.subscription_ids) {
+ await stripeCli.subscriptions.update(subId, {
+ cancel_at_period_end: true,
+ });
+ }
- await timeout(5000);
+ await timeout(5000);
- const newCusRes = await AutumnCli.getCustomer(customerId);
- const newProProduct = getProductFromCusRes({
- cusRes: newCusRes,
- productId: alexProducts.pro.id,
- });
+ const newCusRes = await AutumnCli.getCustomer(customerId);
+ const newProProduct = getProductFromCusRes({
+ cusRes: newCusRes,
+ productId: alexProducts.pro.id,
+ });
- expect(newProProduct).to.exist;
- expect(newProProduct.canceled_at).to.not.equal(null);
- expect(newProProduct.status).to.equal(CusProductStatus.Trialing);
- });
+ expect(newProProduct).to.exist;
+ expect(newProProduct.canceled_at).to.not.equal(null);
+ expect(newProProduct.status).to.equal(CusProductStatus.Trialing);
+ });
- it("should cancel now", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
- const proProduct = getProductFromCusRes({
- cusRes,
- productId: alexProducts.pro.id,
- });
+ it("should cancel now", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ const proProduct = getProductFromCusRes({
+ cusRes,
+ productId: alexProducts.pro.id,
+ });
- for (const subId of proProduct.subscription_ids) {
- await stripeCli.subscriptions.cancel(subId);
- }
+ for (const subId of proProduct.subscription_ids) {
+ await stripeCli.subscriptions.cancel(subId);
+ }
- await timeout(5000);
+ await timeout(5000);
- const newCusRes = await AutumnCli.getCustomer(customerId);
- const newProProduct = getProductFromCusRes({
- cusRes: newCusRes,
- productId: alexProducts.pro.id,
- });
+ const newCusRes = await AutumnCli.getCustomer(customerId);
+ const newProProduct = getProductFromCusRes({
+ cusRes: newCusRes,
+ productId: alexProducts.pro.id,
+ });
- expect(newProProduct).to.not.exist;
+ expect(newProProduct).to.not.exist;
- const freeProduct = getProductFromCusRes({
- cusRes: newCusRes,
- productId: alexProducts.free.id,
- });
+ const freeProduct = getProductFromCusRes({
+ cusRes: newCusRes,
+ productId: alexProducts.free.id,
+ });
- expect(freeProduct).to.exist;
- expect(freeProduct.status).to.equal(CusProductStatus.Active);
- });
- });
+ expect(freeProduct).to.exist;
+ expect(freeProduct.status).to.equal(CusProductStatus.Active);
+ });
+ });
- describe("Testing past due", () => {
- let customerId = "alex-past-due-customer";
- let customer: Customer;
- let testClockId: string;
- let stripeCli: Stripe;
- before("initializing customer", async function () {
- stripeCli = createStripeCli({
- org: this.org,
- env: this.env,
- });
- const { testClockId: testClockId_, customer: customer_ } =
- await initCustomerWithTestClock({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- });
- testClockId = testClockId_;
- customer = customer_;
- });
+ describe("Testing past due", () => {
+ let customerId = "alex-past-due-customer";
+ let customer: Customer;
+ let testClockId: string;
+ let stripeCli: Stripe;
+ before("initializing customer", async function () {
+ stripeCli = createStripeCli({
+ org: this.org,
+ env: this.env,
+ });
+ const { testClockId: testClockId_, customer: customer_ } =
+ await initCustomerWithTestClock({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ });
+ testClockId = testClockId_;
+ customer = customer_;
+ });
- it("should attach pro product ", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
- await timeout(5000);
+ it("should attach pro product ", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
+ await timeout(5000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should attach failed card", async function () {
- await attachFailedPaymentMethod({ stripeCli, customer });
- });
+ it("should attach failed card", async function () {
+ await attachFailedPaymentMethod({ stripeCli, customer });
+ });
- it("should advance clock to next cycle", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addDays(new Date(), 7),
- hoursToFinalizeInvoice,
- ).getTime(),
- });
+ it("should advance clock to next cycle", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addDays(new Date(), 7),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ });
- await timeout(5000);
- });
+ await timeout(5000);
+ });
- // // TODO: Edit so that it doesn't auto cancel for unit test org
- // it("should have expired / past_dued pro product", async function () {
- // const cusRes = await AutumnCli.getCustomer(customerId);
+ // // TODO: Edit so that it doesn't auto cancel for unit test org
+ // it("should have expired / past_dued pro product", async function () {
+ // const cusRes = await AutumnCli.getCustomer(customerId);
- // let org = this.org;
+ // let org = this.org;
- // console.log("Cancel on past due:", org.config.cancel_on_past_due);
- // if (org.config.cancel_on_past_due) {
- // const proProduct = getProductFromCusRes({
- // cusRes,
- // productId: alexProducts.pro.id,
- // });
+ // console.log("Cancel on past due:", org.config.cancel_on_past_due);
+ // if (org.config.cancel_on_past_due) {
+ // const proProduct = getProductFromCusRes({
+ // cusRes,
+ // productId: alexProducts.pro.id,
+ // });
- // expect(proProduct).to.not.exist;
+ // expect(proProduct).to.not.exist;
- // const freeProduct = getProductFromCusRes({
- // cusRes,
- // productId: alexProducts.free.id,
- // });
+ // const freeProduct = getProductFromCusRes({
+ // cusRes,
+ // productId: alexProducts.free.id,
+ // });
- // expect(freeProduct).to.exist;
- // expect(freeProduct.status).to.equal(CusProductStatus.Active);
- // } else {
- // compareMainProduct({
- // sent: alexProducts.pro,
- // cusRes,
- // status: CusProductStatus.PastDue,
- // });
- // }
- // });
- });
+ // expect(freeProduct).to.exist;
+ // expect(freeProduct.status).to.equal(CusProductStatus.Active);
+ // } else {
+ // compareMainProduct({
+ // sent: alexProducts.pro,
+ // cusRes,
+ // status: CusProductStatus.PastDue,
+ // });
+ // }
+ // });
+ });
});
diff --git a/server/tests/alex/06_switch.ts b/server/tests/alex/06_switch.ts
index b17d9f247..63b7610d8 100644
--- a/server/tests/alex/06_switch.ts
+++ b/server/tests/alex/06_switch.ts
@@ -13,198 +13,198 @@ import chalk from "chalk";
import { timeout } from "tests/utils/genUtils.js";
describe(
- chalk.yellowBright(
- "06_switch: Testing upgrades / downgrades from pro <-> premium",
- ),
- () => {
- let customerId = "alex-upgrade-downgrade-customer";
- let testClockId = "";
- let fingerprint = "fp1";
+ chalk.yellowBright(
+ "06_switch: Testing upgrades / downgrades from pro <-> premium",
+ ),
+ () => {
+ let customerId = "alex-upgrade-downgrade-customer";
+ let testClockId = "";
+ let fingerprint = "fp1";
- let stripeCli: Stripe;
- before("initializing customer", async function () {
- stripeCli = createStripeCli({
- org: this.org,
- env: this.env,
- });
- const { testClockId: newTestClockId } = await initCustomerWithTestClock({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- fingerprint,
- });
- testClockId = newTestClockId;
- });
+ let stripeCli: Stripe;
+ before("initializing customer", async function () {
+ stripeCli = createStripeCli({
+ org: this.org,
+ env: this.env,
+ });
+ const { testClockId: newTestClockId } = await initCustomerWithTestClock({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ fingerprint,
+ });
+ testClockId = newTestClockId;
+ });
- describe("First upgrade from pro to premium (trial to trial)", () => {
- it("should attach pro product", async function () {
- await timeout(10000);
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ describe("First upgrade from pro to premium (trial to trial)", () => {
+ it("should attach pro product", async function () {
+ await timeout(10000);
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Trialing,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should upgrade to premium", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.premium.id,
- });
+ it("should upgrade to premium", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.premium.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.premium,
- cusRes,
- status: CusProductStatus.Trialing,
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.premium,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
- // Should have 2 invoices
- expect(cusRes.invoices.length).to.equal(2);
- expect(cusRes.invoices[0].total).to.equal(0);
- });
- });
+ // Should have 2 invoices
+ expect(cusRes.invoices.length).to.equal(2);
+ expect(cusRes.invoices[0].total).to.equal(0);
+ });
+ });
- describe("Downgrade from premium to pro (trial to paid)", () => {
- it("should attach pro product (downgrade from premium trial)", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ describe("Downgrade from premium to pro (trial to paid)", () => {
+ it("should attach pro product (downgrade from premium trial)", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.premium,
- cusRes,
- status: CusProductStatus.Trialing,
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.premium,
+ cusRes,
+ status: CusProductStatus.Trialing,
+ });
- let proProduct = cusRes.products.find(
- (p: any) => p.id === alexProducts.pro.id,
- );
+ let proProduct = cusRes.products.find(
+ (p: any) => p.id === alexProducts.pro.id,
+ );
- expect(proProduct.status).to.equal(CusProductStatus.Scheduled);
- expect(proProduct.starts_at).to.exist;
- expect(proProduct.starts_at).to.be.greaterThan(Date.now());
+ expect(proProduct.status).to.equal(CusProductStatus.Scheduled);
+ expect(proProduct.starts_at).to.exist;
+ expect(proProduct.starts_at).to.be.greaterThan(Date.now());
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(new Date(proProduct.starts_at), 10).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(new Date(proProduct.starts_at), 10).getTime(),
+ });
+ });
- it("should have pro product and last invoice for $20", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Active,
- });
+ it("should have pro product and last invoice for $20", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Active,
+ });
- const lastInvoice = cusRes.invoices[0];
- expect(lastInvoice.total).to.equal(20);
- });
- });
+ const lastInvoice = cusRes.invoices[0];
+ expect(lastInvoice.total).to.equal(20);
+ });
+ });
- describe("Upgrade from pro to premium (paid to paid)", () => {
- // Now, advance 15 days and upgrade again, and check that the new invoice is for between 25 and 35 (because of the prorated amount)
- it("should advance clock by 15 days and attach premium", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 7 + 15).getTime(),
- });
+ describe("Upgrade from pro to premium (paid to paid)", () => {
+ // Now, advance 15 days and upgrade again, and check that the new invoice is for between 25 and 35 (because of the prorated amount)
+ it("should advance clock by 15 days and attach premium", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 7 + 15).getTime(),
+ });
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.premium.id,
- });
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.premium.id,
+ });
- await timeout(10000);
+ await timeout(10000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.premium,
- cusRes,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.premium,
+ cusRes,
+ });
+ });
- it("should have new invoice for roughly 15 days of premium (due to prorated)", async function () {
- let premiumPrice = alexProducts.premium.prices[0].config.amount;
- let proPrice = alexProducts.pro.prices[0].config.amount;
+ it("should have new invoice for roughly 15 days of premium (due to prorated)", async function () {
+ let premiumPrice = alexProducts.premium.prices[0].config.amount;
+ let proPrice = alexProducts.pro.prices[0].config.amount;
- let proratedAmount = ((premiumPrice - proPrice) * 15) / 30;
- const cusRes = await AutumnCli.getCustomer(customerId);
- const lastInvoice = cusRes.invoices[0];
+ let proratedAmount = ((premiumPrice - proPrice) * 15) / 30;
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ const lastInvoice = cusRes.invoices[0];
- // Expect invoice to be prorated amount +/- 10%
- expect(lastInvoice.product_ids[0]).to.equal(alexProducts.premium.id);
- expect(lastInvoice.total).to.be.greaterThan(proratedAmount * 0.9);
- expect(lastInvoice.total).to.be.lessThan(proratedAmount * 1.1);
- });
- });
- },
+ // Expect invoice to be prorated amount +/- 10%
+ expect(lastInvoice.product_ids[0]).to.equal(alexProducts.premium.id);
+ expect(lastInvoice.total).to.be.greaterThan(proratedAmount * 0.9);
+ expect(lastInvoice.total).to.be.lessThan(proratedAmount * 1.1);
+ });
+ });
+ },
);
// Also, downgrade and cancel pro
describe(chalk.yellowBright("06_switch: Testing fingerprint"), () => {
- let customerId = "alex-fingerprint-test";
- let fingerprint = "fp1";
+ let customerId = "alex-fingerprint-test";
+ let fingerprint = "fp1";
- before("initializing customer", async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: "Alex Fingerprint Test",
- email: "alex-fingerprint-test@test.com",
- fingerprint,
- },
- attachPm: true,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ before("initializing customer", async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: "Alex Fingerprint Test",
+ email: "alex-fingerprint-test@test.com",
+ fingerprint,
+ },
+ attachPm: true,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should attach pro product and have invoice for $20", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.pro.id,
- });
+ it("should attach pro product and have invoice for $20", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.pro.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.pro,
- cusRes,
- status: CusProductStatus.Active,
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.pro,
+ cusRes,
+ status: CusProductStatus.Active,
+ });
- const invoices = cusRes.invoices;
- expect(invoices[0].total).to.equal(20);
- });
+ const invoices = cusRes.invoices;
+ expect(invoices[0].total).to.equal(20);
+ });
- it("should attach premium product and have invoice for $30", async function () {
- await AutumnCli.attach({
- customerId,
- productId: alexProducts.premium.id,
- });
+ it("should attach premium product and have invoice for $30", async function () {
+ await AutumnCli.attach({
+ customerId,
+ productId: alexProducts.premium.id,
+ });
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: alexProducts.premium,
- cusRes,
- status: CusProductStatus.Active,
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: alexProducts.premium,
+ cusRes,
+ status: CusProductStatus.Active,
+ });
- const invoices = cusRes.invoices;
- expect(invoices[0].total).to.equal(30);
- });
+ const invoices = cusRes.invoices;
+ expect(invoices[0].total).to.equal(30);
+ });
});
diff --git a/server/tests/alex/init.ts b/server/tests/alex/init.ts
index 96fb70854..a0ee7b268 100644
--- a/server/tests/alex/init.ts
+++ b/server/tests/alex/init.ts
@@ -1,19 +1,19 @@
import dotenv from "dotenv";
dotenv.config();
import {
- AggregateType,
- AllowanceType,
- AppEnv,
- BillingInterval,
- EntInterval,
+ AggregateType,
+ AllowanceType,
+ AppEnv,
+ BillingInterval,
+ EntInterval,
} from "@autumn/shared";
import { FeatureType } from "@autumn/shared";
import {
- initEntitlement,
- initFeature,
- initFreeTrial,
- initPrice,
- initProduct,
+ initEntitlement,
+ initFeature,
+ initFreeTrial,
+ initPrice,
+ initProduct,
} from "../utils/init.js";
import { createSupabaseClient } from "@/external/supabaseUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
@@ -21,442 +21,442 @@ import { initDrizzle } from "@/db/initDrizzle.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
export const alexFeatures = {
- chatMessage: initFeature({
- id: "chatMessage",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "chat_message",
- }),
- deepseekMessage: initFeature({
- id: "deepseekMessage",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "deepseek_message",
- }),
- o1Message: initFeature({
- id: "o1Message",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "o1_message",
- }),
- applyCode: initFeature({
- id: "applyCode",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "apply",
- }),
- gitCommit: initFeature({
- id: "gitCommit",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "git_commit",
- }),
- tabToComplete: initFeature({
- id: "tabToComplete",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "suggestion",
- }),
- topUpMessage: initFeature({
- id: "topUpMessage",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "top_up_message",
- }),
- voiceInput: initFeature({
- id: "voiceInput",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "voice_input",
- }),
- figmaIntegration: initFeature({
- id: "figmaIntegration",
- type: FeatureType.Boolean,
- eventName: "figma_integration",
- }),
- githubIssuesIntegration: initFeature({
- id: "githubIssuesIntegration",
- type: FeatureType.Boolean,
- eventName: "github_issues_integration",
- }),
- linearIntegration: initFeature({
- id: "linearIntegration",
- type: FeatureType.Boolean,
- eventName: "linear_integration",
- }),
- canAddSeats: initFeature({
- id: "canAddSeats",
- type: FeatureType.Boolean,
- eventName: "can_add_seats",
- }),
- seats: initFeature({
- id: "seats",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "seats",
- }),
+ chatMessage: initFeature({
+ id: "chatMessage",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "chat_message",
+ }),
+ deepseekMessage: initFeature({
+ id: "deepseekMessage",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "deepseek_message",
+ }),
+ o1Message: initFeature({
+ id: "o1Message",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "o1_message",
+ }),
+ applyCode: initFeature({
+ id: "applyCode",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "apply",
+ }),
+ gitCommit: initFeature({
+ id: "gitCommit",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "git_commit",
+ }),
+ tabToComplete: initFeature({
+ id: "tabToComplete",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "suggestion",
+ }),
+ topUpMessage: initFeature({
+ id: "topUpMessage",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "top_up_message",
+ }),
+ voiceInput: initFeature({
+ id: "voiceInput",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "voice_input",
+ }),
+ figmaIntegration: initFeature({
+ id: "figmaIntegration",
+ type: FeatureType.Boolean,
+ eventName: "figma_integration",
+ }),
+ githubIssuesIntegration: initFeature({
+ id: "githubIssuesIntegration",
+ type: FeatureType.Boolean,
+ eventName: "github_issues_integration",
+ }),
+ linearIntegration: initFeature({
+ id: "linearIntegration",
+ type: FeatureType.Boolean,
+ eventName: "linear_integration",
+ }),
+ canAddSeats: initFeature({
+ id: "canAddSeats",
+ type: FeatureType.Boolean,
+ eventName: "can_add_seats",
+ }),
+ seats: initFeature({
+ id: "seats",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "seats",
+ }),
};
export const alexProducts = {
- free: initProduct({
- id: "free",
- isDefault: true,
- entitlements: {
- chatMessage: initEntitlement({
- feature: alexFeatures.chatMessage,
- allowance: 50,
- interval: EntInterval.Month,
- }),
- applyCode: initEntitlement({
- feature: alexFeatures.applyCode,
- allowance: 5,
- interval: EntInterval.Month,
- }),
- tabToComplete: initEntitlement({
- feature: alexFeatures.tabToComplete,
- allowance: 50,
- interval: EntInterval.Month,
- }),
- voiceInput: initEntitlement({
- feature: alexFeatures.voiceInput,
- allowance: 5,
- interval: EntInterval.Month,
- }),
- gitCommit: initEntitlement({
- feature: alexFeatures.gitCommit,
- allowance: 5,
- interval: EntInterval.Month,
- }),
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 5,
- interval: EntInterval.Lifetime,
- }),
- deepseekMessage: initEntitlement({
- feature: alexFeatures.deepseekMessage,
- allowance: 50,
- interval: EntInterval.Month,
- }),
- },
- prices: [],
- freeTrial: null,
- }),
+ free: initProduct({
+ id: "free",
+ isDefault: true,
+ entitlements: {
+ chatMessage: initEntitlement({
+ feature: alexFeatures.chatMessage,
+ allowance: 50,
+ interval: EntInterval.Month,
+ }),
+ applyCode: initEntitlement({
+ feature: alexFeatures.applyCode,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
+ tabToComplete: initEntitlement({
+ feature: alexFeatures.tabToComplete,
+ allowance: 50,
+ interval: EntInterval.Month,
+ }),
+ voiceInput: initEntitlement({
+ feature: alexFeatures.voiceInput,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
+ gitCommit: initEntitlement({
+ feature: alexFeatures.gitCommit,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 5,
+ interval: EntInterval.Lifetime,
+ }),
+ deepseekMessage: initEntitlement({
+ feature: alexFeatures.deepseekMessage,
+ allowance: 50,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ }),
- pro: initProduct({
- id: "pro",
- isDefault: false,
- entitlements: {
- chatMessage: initEntitlement({
- feature: alexFeatures.chatMessage,
- allowance: 500,
- interval: EntInterval.Month,
- }),
- deepseekMessage: initEntitlement({
- feature: alexFeatures.deepseekMessage,
- allowanceType: AllowanceType.Unlimited,
- }),
- applyCode: initEntitlement({
- feature: alexFeatures.applyCode,
- allowanceType: AllowanceType.Unlimited,
- }),
- gitCommit: initEntitlement({
- feature: alexFeatures.gitCommit,
- allowanceType: AllowanceType.Unlimited,
- }),
- voiceInput: initEntitlement({
- feature: alexFeatures.voiceInput,
- allowanceType: AllowanceType.Unlimited,
- }),
- tabToComplete: initEntitlement({
- feature: alexFeatures.tabToComplete,
- allowanceType: AllowanceType.Unlimited,
- }),
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 5,
- interval: EntInterval.Lifetime,
- }),
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- amount: 20.0, // $20.00
- billingInterval: BillingInterval.Month,
- type: "monthly",
- }),
- ],
- freeTrial: initFreeTrial({
- length: 7,
- uniqueFingerprint: true,
- }),
- }),
+ pro: initProduct({
+ id: "pro",
+ isDefault: false,
+ entitlements: {
+ chatMessage: initEntitlement({
+ feature: alexFeatures.chatMessage,
+ allowance: 500,
+ interval: EntInterval.Month,
+ }),
+ deepseekMessage: initEntitlement({
+ feature: alexFeatures.deepseekMessage,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ applyCode: initEntitlement({
+ feature: alexFeatures.applyCode,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ gitCommit: initEntitlement({
+ feature: alexFeatures.gitCommit,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ voiceInput: initEntitlement({
+ feature: alexFeatures.voiceInput,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ tabToComplete: initEntitlement({
+ feature: alexFeatures.tabToComplete,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 5,
+ interval: EntInterval.Lifetime,
+ }),
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ amount: 20.0, // $20.00
+ billingInterval: BillingInterval.Month,
+ type: "monthly",
+ }),
+ ],
+ freeTrial: initFreeTrial({
+ length: 7,
+ uniqueFingerprint: true,
+ }),
+ }),
- topUpMessages: initProduct({
- id: "topUpMessages",
- isDefault: false,
- isAddOn: true,
- entitlements: {
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- amount: 9,
- billingUnits: 250,
- billingInterval: BillingInterval.OneOff,
- feature: alexFeatures.topUpMessage,
- }),
- ],
- freeTrial: null,
- }),
+ topUpMessages: initProduct({
+ id: "topUpMessages",
+ isDefault: false,
+ isAddOn: true,
+ entitlements: {
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ amount: 9,
+ billingUnits: 250,
+ billingInterval: BillingInterval.OneOff,
+ feature: alexFeatures.topUpMessage,
+ }),
+ ],
+ freeTrial: null,
+ }),
- o1TopUps: initProduct({
- id: "o1TopUps",
- isDefault: false,
- isAddOn: true,
- entitlements: {
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- amount: 9,
- billingUnits: 25,
- billingInterval: BillingInterval.OneOff,
- feature: alexFeatures.o1Message,
- }),
- ],
- freeTrial: null,
- }),
+ o1TopUps: initProduct({
+ id: "o1TopUps",
+ isDefault: false,
+ isAddOn: true,
+ entitlements: {
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ amount: 9,
+ billingUnits: 25,
+ billingInterval: BillingInterval.OneOff,
+ feature: alexFeatures.o1Message,
+ }),
+ ],
+ freeTrial: null,
+ }),
- premium: initProduct({
- id: "premium",
- isDefault: false,
- entitlements: {
- chatMessage: initEntitlement({
- feature: alexFeatures.chatMessage,
- allowance: 1000,
- interval: EntInterval.Month,
- }),
- deepseekMessage: initEntitlement({
- feature: alexFeatures.deepseekMessage,
- allowanceType: AllowanceType.Unlimited,
- }),
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 5,
- interval: EntInterval.Month,
- }),
- applyCode: initEntitlement({
- feature: alexFeatures.applyCode,
- allowanceType: AllowanceType.Unlimited,
- }),
- gitCommit: initEntitlement({
- feature: alexFeatures.gitCommit,
- allowanceType: AllowanceType.Unlimited,
- }),
- voiceInput: initEntitlement({
- feature: alexFeatures.voiceInput,
- allowanceType: AllowanceType.Unlimited,
- }),
- tabToComplete: initEntitlement({
- feature: alexFeatures.tabToComplete,
- allowanceType: AllowanceType.Unlimited,
- }),
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- figmaIntegration: initEntitlement({
- feature: alexFeatures.figmaIntegration,
- allowanceType: AllowanceType.Unlimited,
- }),
- githubIssuesIntegration: initEntitlement({
- feature: alexFeatures.githubIssuesIntegration,
- allowanceType: AllowanceType.Unlimited,
- }),
- linearIntegration: initEntitlement({
- feature: alexFeatures.linearIntegration,
- allowanceType: AllowanceType.Unlimited,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 50,
- }),
- ],
- freeTrial: initFreeTrial({
- length: 7,
- uniqueFingerprint: true,
- }),
- }),
+ premium: initProduct({
+ id: "premium",
+ isDefault: false,
+ entitlements: {
+ chatMessage: initEntitlement({
+ feature: alexFeatures.chatMessage,
+ allowance: 1000,
+ interval: EntInterval.Month,
+ }),
+ deepseekMessage: initEntitlement({
+ feature: alexFeatures.deepseekMessage,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
+ applyCode: initEntitlement({
+ feature: alexFeatures.applyCode,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ gitCommit: initEntitlement({
+ feature: alexFeatures.gitCommit,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ voiceInput: initEntitlement({
+ feature: alexFeatures.voiceInput,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ tabToComplete: initEntitlement({
+ feature: alexFeatures.tabToComplete,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ figmaIntegration: initEntitlement({
+ feature: alexFeatures.figmaIntegration,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ githubIssuesIntegration: initEntitlement({
+ feature: alexFeatures.githubIssuesIntegration,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ linearIntegration: initEntitlement({
+ feature: alexFeatures.linearIntegration,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 50,
+ }),
+ ],
+ freeTrial: initFreeTrial({
+ length: 7,
+ uniqueFingerprint: true,
+ }),
+ }),
- proTeam: initProduct({
- id: "proTeam",
- isDefault: false,
- isAddOn: true,
- entitlements: {
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- tabToComplete: initEntitlement({
- feature: alexFeatures.tabToComplete,
- allowanceType: AllowanceType.Unlimited,
- }),
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 5,
- interval: EntInterval.Lifetime,
- }),
- chatMessage: initEntitlement({
- feature: alexFeatures.chatMessage,
- allowance: 500,
- interval: EntInterval.Month,
- }),
- deepseekMessage: initEntitlement({
- feature: alexFeatures.deepseekMessage,
- allowanceType: AllowanceType.Unlimited,
- }),
- applyCode: initEntitlement({
- feature: alexFeatures.applyCode,
- allowanceType: AllowanceType.Unlimited,
- }),
- gitCommit: initEntitlement({
- feature: alexFeatures.gitCommit,
- allowanceType: AllowanceType.Unlimited,
- }),
- voiceInput: initEntitlement({
- feature: alexFeatures.voiceInput,
- allowanceType: AllowanceType.Unlimited,
- }),
- },
- prices: [],
- freeTrial: null,
- }),
- teamManager: initProduct({
- id: "teamManager",
- isDefault: false,
- isAddOn: true,
- entitlements: {
- topUpMessage: initEntitlement({
- feature: alexFeatures.topUpMessage,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- chatMessage: initEntitlement({
- feature: alexFeatures.chatMessage,
- allowance: 1000,
- interval: EntInterval.Month,
- }),
- deepseekMessage: initEntitlement({
- feature: alexFeatures.deepseekMessage,
- allowanceType: AllowanceType.Unlimited,
- }),
- o1Message: initEntitlement({
- feature: alexFeatures.o1Message,
- allowance: 5,
- interval: EntInterval.Month,
- }),
+ proTeam: initProduct({
+ id: "proTeam",
+ isDefault: false,
+ isAddOn: true,
+ entitlements: {
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ tabToComplete: initEntitlement({
+ feature: alexFeatures.tabToComplete,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 5,
+ interval: EntInterval.Lifetime,
+ }),
+ chatMessage: initEntitlement({
+ feature: alexFeatures.chatMessage,
+ allowance: 500,
+ interval: EntInterval.Month,
+ }),
+ deepseekMessage: initEntitlement({
+ feature: alexFeatures.deepseekMessage,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ applyCode: initEntitlement({
+ feature: alexFeatures.applyCode,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ gitCommit: initEntitlement({
+ feature: alexFeatures.gitCommit,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ voiceInput: initEntitlement({
+ feature: alexFeatures.voiceInput,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ }),
+ teamManager: initProduct({
+ id: "teamManager",
+ isDefault: false,
+ isAddOn: true,
+ entitlements: {
+ topUpMessage: initEntitlement({
+ feature: alexFeatures.topUpMessage,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ chatMessage: initEntitlement({
+ feature: alexFeatures.chatMessage,
+ allowance: 1000,
+ interval: EntInterval.Month,
+ }),
+ deepseekMessage: initEntitlement({
+ feature: alexFeatures.deepseekMessage,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ o1Message: initEntitlement({
+ feature: alexFeatures.o1Message,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
- applyCode: initEntitlement({
- feature: alexFeatures.applyCode,
- allowanceType: AllowanceType.Unlimited,
- }),
- gitCommit: initEntitlement({
- feature: alexFeatures.gitCommit,
- allowanceType: AllowanceType.Unlimited,
- }),
- voiceInput: initEntitlement({
- feature: alexFeatures.voiceInput,
- allowanceType: AllowanceType.Unlimited,
- }),
+ applyCode: initEntitlement({
+ feature: alexFeatures.applyCode,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ gitCommit: initEntitlement({
+ feature: alexFeatures.gitCommit,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ voiceInput: initEntitlement({
+ feature: alexFeatures.voiceInput,
+ allowanceType: AllowanceType.Unlimited,
+ }),
- tabToComplete: initEntitlement({
- feature: alexFeatures.tabToComplete,
- allowanceType: AllowanceType.Unlimited,
- }),
- canAddSeats: initEntitlement({
- feature: alexFeatures.canAddSeats,
- }),
- seats: initEntitlement({
- feature: alexFeatures.seats,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- amount: 50,
- billingUnits: 1,
- billingInterval: BillingInterval.Month,
- feature: alexFeatures.seats,
- }),
- ],
- freeTrial: null,
- }),
+ tabToComplete: initEntitlement({
+ feature: alexFeatures.tabToComplete,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ canAddSeats: initEntitlement({
+ feature: alexFeatures.canAddSeats,
+ }),
+ seats: initEntitlement({
+ feature: alexFeatures.seats,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ amount: 50,
+ billingUnits: 1,
+ billingInterval: BillingInterval.Month,
+ feature: alexFeatures.seats,
+ }),
+ ],
+ freeTrial: null,
+ }),
};
let orgSlug = process.env.TESTS_ORG!;
before(async function () {
- try {
- this.env = AppEnv.Sandbox;
- const { db, client } = initDrizzle();
- this.db = db;
- this.client = client;
- this.org = await OrgService.getBySlug({
- db: this.db,
- slug: orgSlug,
- });
+ try {
+ this.env = AppEnv.Sandbox;
+ const { db, client } = initDrizzle();
+ this.db = db;
+ this.client = client;
+ this.org = await OrgService.getBySlug({
+ db: this.db,
+ slug: orgSlug,
+ });
- let dbFeatures = await FeatureService.list({
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
+ let dbFeatures = await FeatureService.list({
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
- for (const featureId in alexFeatures) {
- let feature = alexFeatures[featureId as keyof typeof alexFeatures];
- let dbFeature = dbFeatures.find((f: any) => f.id === feature.id);
- if (!dbFeature) {
- continue;
- throw new Error(`Feature ${feature.id} not found`);
- }
- alexFeatures[featureId as keyof typeof alexFeatures].internal_id =
- dbFeature.internal_id;
- if (feature.type === FeatureType.Metered) {
- // Ignore this for now
- // @ts-ignore eventName is manually set
- alexFeatures[featureId as keyof typeof alexFeatures].eventName =
- dbFeature.config?.filters[0].value[0];
- }
- }
- } catch (error) {
- console.error(error);
- }
+ for (const featureId in alexFeatures) {
+ let feature = alexFeatures[featureId as keyof typeof alexFeatures];
+ let dbFeature = dbFeatures.find((f: any) => f.id === feature.id);
+ if (!dbFeature) {
+ continue;
+ throw new Error(`Feature ${feature.id} not found`);
+ }
+ alexFeatures[featureId as keyof typeof alexFeatures].internal_id =
+ dbFeature.internal_id;
+ if (feature.type === FeatureType.Metered) {
+ // Ignore this for now
+ // @ts-ignore eventName is manually set
+ alexFeatures[featureId as keyof typeof alexFeatures].eventName =
+ dbFeature.config?.filters[0].value[0];
+ }
+ }
+ } catch (error) {
+ console.error(error);
+ }
});
after(async function () {
- await this.client.end();
+ await this.client.end();
});
diff --git a/server/tests/alex/utils.ts b/server/tests/alex/utils.ts
index 03de6e957..9d942d12a 100644
--- a/server/tests/alex/utils.ts
+++ b/server/tests/alex/utils.ts
@@ -1,8 +1,8 @@
import {
- Entitlement,
- FeatureType,
- AllowanceType,
- Feature,
+ Entitlement,
+ FeatureType,
+ AllowanceType,
+ Feature,
} from "@autumn/shared";
import { expect } from "chai";
import { timeout } from "tests/utils/genUtils.js";
@@ -10,232 +10,232 @@ import { AutumnCli } from "tests/cli/AutumnCli.js";
import { alexFeatures } from "./init.js";
const checkEntitledOnFeatures = async (customerId: string, product: any) => {
- const entitlements: Entitlement[] = Object.values(product.entitlements);
+ const entitlements: Entitlement[] = Object.values(product.entitlements);
- for (const entitlement of entitlements) {
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- entitlement.feature_id!,
- true
- );
+ for (const entitlement of entitlements) {
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ entitlement.feature_id!,
+ true,
+ );
- // Type assertion to tell TypeScript that feature_id is a key of alexFeatures
- let feature =
- alexFeatures[entitlement.feature_id! as keyof typeof alexFeatures];
+ // Type assertion to tell TypeScript that feature_id is a key of alexFeatures
+ let feature =
+ alexFeatures[entitlement.feature_id! as keyof typeof alexFeatures];
- try {
- if (feature.type === FeatureType.Boolean) {
- expect(allowed).to.equal(true);
- return;
- }
+ try {
+ if (feature.type === FeatureType.Boolean) {
+ expect(allowed).to.equal(true);
+ return;
+ }
- if (entitlement.allowance === 0) {
- expect(allowed).to.equal(false);
- expect(balanceObj?.balance).to.equal(0);
- return;
- } else if (entitlement.allowance_type === AllowanceType.Unlimited) {
- expect(allowed).to.equal(true);
- expect(balanceObj?.balance).to.equal(null);
- expect(balanceObj?.unlimited).to.equal(true);
- return;
- }
+ if (entitlement.allowance === 0) {
+ expect(allowed).to.equal(false);
+ expect(balanceObj?.balance).to.equal(0);
+ return;
+ } else if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ expect(allowed).to.equal(true);
+ expect(balanceObj?.balance).to.equal(null);
+ expect(balanceObj?.unlimited).to.equal(true);
+ return;
+ }
- expect(allowed).to.equal(true);
- expect(balanceObj?.balance).to.equal(entitlement.allowance);
- } catch (error) {
- console.log("Checking entitlement: ", entitlement);
- console.log("Expected balance: ", entitlement.allowance);
- console.log("Entitled response: ", { allowed, balanceObj });
- throw error;
- }
- }
+ expect(allowed).to.equal(true);
+ expect(balanceObj?.balance).to.equal(entitlement.allowance);
+ } catch (error) {
+ console.log("Checking entitlement: ", entitlement);
+ console.log("Expected balance: ", entitlement.allowance);
+ console.log("Entitled response: ", { allowed, balanceObj });
+ throw error;
+ }
+ }
};
const getFeatureFromEntitlement = (entitlement: Entitlement) => {
- return alexFeatures[
- entitlement.feature_id! as keyof typeof alexFeatures
- ] as Feature & { eventName: string };
+ return alexFeatures[
+ entitlement.feature_id! as keyof typeof alexFeatures
+ ] as Feature & { eventName: string };
};
export const checkFeatureHasCorrectBalance = async ({
- customerId,
- feature,
- entitlement,
- expectedBalance,
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance,
}: {
- customerId: string;
- feature: Feature;
- entitlement: Entitlement;
- expectedBalance: number;
+ customerId: string;
+ feature: Feature;
+ entitlement: Entitlement;
+ expectedBalance: number;
}) => {
- const [entitledRes, cusRes] = await Promise.all([
- AutumnCli.entitled(customerId, feature.id, true),
- AutumnCli.getCustomer(customerId),
- ]);
+ const [entitledRes, cusRes] = await Promise.all([
+ AutumnCli.entitled(customerId, feature.id, true),
+ AutumnCli.getCustomer(customerId),
+ ]);
- if (feature.type === FeatureType.Boolean) {
- console.log(" - Checking boolean feature: ", feature.id);
- const { allowed, balanceObj }: any = entitledRes;
- expect(allowed).to.equal(true);
- return;
- }
+ if (feature.type === FeatureType.Boolean) {
+ console.log(" - Checking boolean feature: ", feature.id);
+ const { allowed, balanceObj }: any = entitledRes;
+ expect(allowed).to.equal(true);
+ return;
+ }
- console.log(
- ` - Checking entitlement ${feature.id} has ${
- entitlement.allowance_type == AllowanceType.Unlimited
- ? "unlimited balance"
- : `balance of ${expectedBalance}`
- }`
- );
+ console.log(
+ ` - Checking entitlement ${feature.id} has ${
+ entitlement.allowance_type == AllowanceType.Unlimited
+ ? "unlimited balance"
+ : `balance of ${expectedBalance}`
+ }`,
+ );
- // Get ent from cusRes
- const { entitlements: cusEnts }: any = cusRes;
- const { allowed, balanceObj }: any = entitledRes;
+ // Get ent from cusRes
+ const { entitlements: cusEnts }: any = cusRes;
+ const { allowed, balanceObj }: any = entitledRes;
- const cusEnt = cusEnts.find(
- (e: any) =>
- e.feature_id === feature.id && e.interval == entitlement.interval
- );
+ const cusEnt = cusEnts.find(
+ (e: any) =>
+ e.feature_id === feature.id && e.interval == entitlement.interval,
+ );
- try {
- expect(cusEnt).to.exist;
- } catch (error) {
- console.log(
- `Expected cus ent ${feature.id}, interval ${entitlement.interval} to exist`
- );
- throw error;
- }
+ try {
+ expect(cusEnt).to.exist;
+ } catch (error) {
+ console.log(
+ `Expected cus ent ${feature.id}, interval ${entitlement.interval} to exist`,
+ );
+ throw error;
+ }
- if (entitlement.allowance_type === AllowanceType.Unlimited) {
- // Cus ent
- expect(cusEnt.balance).to.equal(null);
- expect(cusEnt.used).to.equal(null);
- expect(cusEnt.unlimited).to.equal(true);
+ if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ // Cus ent
+ expect(cusEnt.balance).to.equal(null);
+ expect(cusEnt.used).to.equal(null);
+ expect(cusEnt.unlimited).to.equal(true);
- // Entitled res
- expect(allowed).to.equal(true);
- expect(balanceObj?.balance).to.equal(null);
- expect(balanceObj?.unlimited).to.equal(true);
- return;
- }
+ // Entitled res
+ expect(allowed).to.equal(true);
+ expect(balanceObj?.balance).to.equal(null);
+ expect(balanceObj?.unlimited).to.equal(true);
+ return;
+ }
- if (expectedBalance === 0) {
- expect(allowed).to.equal(false);
- expect(balanceObj?.balance).to.equal(0);
- expect(cusEnt.balance).to.equal(0);
- return;
- }
+ if (expectedBalance === 0) {
+ expect(allowed).to.equal(false);
+ expect(balanceObj?.balance).to.equal(0);
+ expect(cusEnt.balance).to.equal(0);
+ return;
+ }
- expect(balanceObj?.balance).to.equal(expectedBalance);
- expect(cusEnt.balance).to.equal(expectedBalance);
+ expect(balanceObj?.balance).to.equal(expectedBalance);
+ expect(cusEnt.balance).to.equal(expectedBalance);
};
export const runEventsAndCheckBalances = async ({
- customerId,
- entitlements,
+ customerId,
+ entitlements,
}: {
- customerId: string;
- entitlements: Entitlement[];
+ customerId: string;
+ entitlements: Entitlement[];
}) => {
- for (const entitlement of entitlements) {
- if (
- entitlement.allowance === 0 &&
- entitlement.allowance_type !== AllowanceType.Unlimited
- ) {
- continue;
- }
+ for (const entitlement of entitlements) {
+ if (
+ entitlement.allowance === 0 &&
+ entitlement.allowance_type !== AllowanceType.Unlimited
+ ) {
+ continue;
+ }
- let feature = getFeatureFromEntitlement(entitlement);
+ let feature = getFeatureFromEntitlement(entitlement);
- if (
- entitlement.allowance_type === AllowanceType.Unlimited ||
- feature.type === FeatureType.Boolean
- ) {
- await checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: entitlement.allowance!,
- });
- continue;
- }
+ if (
+ entitlement.allowance_type === AllowanceType.Unlimited ||
+ feature.type === FeatureType.Boolean
+ ) {
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: entitlement.allowance!,
+ });
+ continue;
+ }
- // 1. Check that feature has full balance
- await checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: entitlement.allowance!,
- });
+ // 1. Check that feature has full balance
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: entitlement.allowance!,
+ });
- // console.log(" - Running events & entitled check for:", feature.id);
+ // console.log(" - Running events & entitled check for:", feature.id);
- let firstHalf = Math.min(Math.floor(entitlement.allowance! / 2), 50);
- let secondHalf = entitlement.allowance! - firstHalf;
+ let firstHalf = Math.min(Math.floor(entitlement.allowance! / 2), 50);
+ let secondHalf = entitlement.allowance! - firstHalf;
- // 1. Send first half
- let batchUpdate = [];
- for (let i = 0; i < firstHalf; i++) {
- batchUpdate.push(
- AutumnCli.sendEvent({
- customerId,
- eventName: feature.eventName!,
- })
- );
- }
+ // 1. Send first half
+ let batchUpdate = [];
+ for (let i = 0; i < firstHalf; i++) {
+ batchUpdate.push(
+ AutumnCli.sendEvent({
+ customerId,
+ eventName: feature.eventName!,
+ }),
+ );
+ }
- let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500;
- await timeout(timeoutMilli);
+ let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500;
+ await timeout(timeoutMilli);
- await Promise.all(batchUpdate);
+ await Promise.all(batchUpdate);
- await checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: entitlement.allowance! - firstHalf,
- });
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: entitlement.allowance! - firstHalf,
+ });
- if (secondHalf > 50) {
- continue;
- // TODO: Make balance 0 and check that it's blocked...
- await AutumnCli.sendEvent({
- customerId,
- eventName: feature.eventName!,
- properties: {
- value: secondHalf,
- },
- });
+ if (secondHalf > 50) {
+ continue;
+ // TODO: Make balance 0 and check that it's blocked...
+ await AutumnCli.sendEvent({
+ customerId,
+ eventName: feature.eventName!,
+ properties: {
+ value: secondHalf,
+ },
+ });
- await timeout(1000);
+ await timeout(1000);
- await checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: 0,
- });
- return;
- }
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: 0,
+ });
+ return;
+ }
- for (let i = 0; i < secondHalf; i++) {
- batchUpdate.push(
- AutumnCli.sendEvent({
- customerId,
- eventName: feature.eventName!,
- })
- );
- }
+ for (let i = 0; i < secondHalf; i++) {
+ batchUpdate.push(
+ AutumnCli.sendEvent({
+ customerId,
+ eventName: feature.eventName!,
+ }),
+ );
+ }
- await timeout(timeoutMilli);
+ await timeout(timeoutMilli);
- await Promise.all(batchUpdate);
+ await Promise.all(batchUpdate);
- await checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: 0,
- });
- }
+ await checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: 0,
+ });
+ }
};
diff --git a/server/tests/archives/01_product.ts b/server/tests/archives/01_product.ts
index 62718b17b..1a59a78e6 100644
--- a/server/tests/archives/01_product.ts
+++ b/server/tests/archives/01_product.ts
@@ -15,327 +15,327 @@ const monthlyQuantity = 2;
// UNCOMMENT FROM HERE
describe(`${chalk.yellowBright(
- "01_product: Testing attach -- free, pro & one-time / monthly add on",
+ "01_product: Testing attach -- free, pro & one-time / monthly add on",
)}`, () => {
- let customerId = "attach1";
- before(async function () {
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ let customerId = "attach1";
+ before(async function () {
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- describe("Create customer -- check free is active", () => {
- it("GET /customers/:id -- checking default product & entitlements", async function () {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.get(`/v1/customers/${customerId}`);
+ describe("Create customer -- check free is active", () => {
+ it("GET /customers/:id -- checking default product & entitlements", async function () {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.get(`/v1/customers/${customerId}`);
- compareMainProduct({
- sent: products.free,
- cusRes: data,
- });
- });
+ compareMainProduct({
+ sent: products.free,
+ cusRes: data,
+ });
+ });
- it("GET /entitled -- metered1", async function () {
- // Checking metered1 entitlement
- const expectedEntitlement = products.free.entitlements.metered1;
+ it("GET /entitled -- metered1", async function () {
+ // Checking metered1 entitlement
+ const expectedEntitlement = products.free.entitlements.metered1;
- const entitled: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- );
+ const entitled: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ );
- const metered1Balance = entitled!.balances.find(
- (balance: any) => balance.feature_id === features.metered1.id,
- );
+ const metered1Balance = entitled!.balances.find(
+ (balance: any) => balance.feature_id === features.metered1.id,
+ );
- try {
- expect(entitled!.allowed).to.be.true;
- expect(metered1Balance).to.exist;
- expect(metered1Balance!.balance).to.equal(
- expectedEntitlement.allowance,
- );
- expect(metered1Balance!.unlimited).to.not.exist;
- } catch (error) {
- console.group();
- console.group();
- console.log("Looking for: ", expectedEntitlement);
- console.log("Received (entitled res): ", entitled);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- });
+ try {
+ expect(entitled!.allowed).to.be.true;
+ expect(metered1Balance).to.exist;
+ expect(metered1Balance!.balance).to.equal(
+ expectedEntitlement.allowance,
+ );
+ expect(metered1Balance!.unlimited).to.not.exist;
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Looking for: ", expectedEntitlement);
+ console.log("Received (entitled res): ", entitled);
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ });
- it("GET /entitled -- boolean1", async function () {
- const entitled = await AutumnCli.entitled(
- customerId,
- features.boolean1.id,
- );
+ it("GET /entitled -- boolean1", async function () {
+ const entitled = await AutumnCli.entitled(
+ customerId,
+ features.boolean1.id,
+ );
- expect(entitled!.allowed).to.be.false;
- });
- });
+ expect(entitled!.allowed).to.be.false;
+ });
+ });
- describe("Attach pro -- check products & entitlements", () => {
- it("POST /attach -- attaching pro (force checkout)", async function () {
- const res = await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
+ describe("Attach pro -- check products & entitlements", () => {
+ it("POST /attach -- attaching pro (force checkout)", async function () {
+ const res = await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
- assert.exists(res.checkout_url);
+ assert.exists(res.checkout_url);
- await completeCheckoutForm(res.checkout_url);
- await timeout(10000); // for webhook to be processed
- console.log(` ${chalk.greenBright("Attached pro")}`);
- });
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(10000); // for webhook to be processed
+ console.log(` ${chalk.greenBright("Attached pro")}`);
+ });
- it("GET /customers/:id -- checking product & entitlements (pro)", async function () {
- const res = await AutumnCli.getCustomer(customerId);
- // console.log("Res: ", res);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- assert.isTrue(res.invoices.length > 0);
- });
+ it("GET /customers/:id -- checking product & entitlements (pro)", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
+ // console.log("Res: ", res);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ assert.isTrue(res.invoices.length > 0);
+ });
- // return;
+ // return;
- it("GET /entitled -- checking entitlements for metered1 && boolean1", async function () {
- const proEntitlements = products.pro.entitlements;
+ it("GET /entitled -- checking entitlements for metered1 && boolean1", async function () {
+ const proEntitlements = products.pro.entitlements;
- for (const entitlement of Object.values(proEntitlements)) {
- const allowance = entitlement.allowance;
+ for (const entitlement of Object.values(proEntitlements)) {
+ const allowance = entitlement.allowance;
- const res: any = await AutumnCli.entitled(
- customerId,
- entitlement.feature_id!,
- );
+ const res: any = await AutumnCli.entitled(
+ customerId,
+ entitlement.feature_id!,
+ );
- const entBalance = res!.balances.find(
- (b: any) => b.feature_id === entitlement.feature_id,
- );
+ const entBalance = res!.balances.find(
+ (b: any) => b.feature_id === entitlement.feature_id,
+ );
- try {
- expect(res!.allowed).to.be.true;
- expect(entBalance).to.exist;
- if (entitlement.allowance) {
- expect(entBalance!.balance).to.equal(allowance);
- }
- // console.log(` - ${entitlement.feature_id} -- Passed`);
- } catch (error) {
- console.group();
- console.group();
- console.log("Looking for: ", entitlement);
- console.log("Received: ", res);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- }
- });
- });
+ try {
+ expect(res!.allowed).to.be.true;
+ expect(entBalance).to.exist;
+ if (entitlement.allowance) {
+ expect(entBalance!.balance).to.equal(allowance);
+ }
+ // console.log(` - ${entitlement.feature_id} -- Passed`);
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Looking for: ", entitlement);
+ console.log("Received: ", res);
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ }
+ });
+ });
- const oneTimeBillingUnits =
- products.oneTimeAddOnMetered1.prices[0].config.billing_units;
- const monthlyBillingUnits =
- products.monthlyAddOnMetered1.prices[0].config.billing_units;
+ const oneTimeBillingUnits =
+ products.oneTimeAddOnMetered1.prices[0].config.billing_units;
+ const monthlyBillingUnits =
+ products.monthlyAddOnMetered1.prices[0].config.billing_units;
- describe("One time add on (force checkout)", () => {
- // PURCHASE ONE TIME ADD ON
+ describe("One time add on (force checkout)", () => {
+ // PURCHASE ONE TIME ADD ON
- it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () {
- try {
- for (let i = 0; i < oneTimePurchaseCount; i++) {
- const res = await AutumnCli.attach({
- customerId: customerId,
- productId: products.oneTimeAddOnMetered1.id,
- forceCheckout: true,
- });
+ it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () {
+ try {
+ for (let i = 0; i < oneTimePurchaseCount; i++) {
+ const res = await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.oneTimeAddOnMetered1.id,
+ forceCheckout: true,
+ });
- await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity);
- await timeout(10000); // for webhook to be processed
- console.log(` ${chalk.greenBright("Attached one time add on")}`);
- }
- } catch (error) {
- console.group();
- console.group();
- console.log("Failed to attach one time add on");
- console.log("Error data:", error);
- console.groupEnd();
- console.groupEnd();
- process.exit(1);
- }
- });
+ await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity);
+ await timeout(10000); // for webhook to be processed
+ console.log(` ${chalk.greenBright("Attached one time add on")}`);
+ }
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Failed to attach one time add on");
+ console.log("Error data:", error);
+ console.groupEnd();
+ console.groupEnd();
+ process.exit(1);
+ }
+ });
- // TODO: Attach one time add on again (with quantity?)
+ // TODO: Attach one time add on again (with quantity?)
- it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
+ it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
- // 1. Metered1 balance should be pro + one time add on
+ // 1. Metered1 balance should be pro + one time add on
- // Fetch balance
- const addOnBalance = cusRes.entitlements.find(
- (e: any) =>
- e.feature_id === features.metered1.id &&
- e.interval ==
- products.oneTimeAddOnMetered1.entitlements.metered1.interval,
- );
+ // Fetch balance
+ const addOnBalance = cusRes.entitlements.find(
+ (e: any) =>
+ e.feature_id === features.metered1.id &&
+ e.interval ==
+ products.oneTimeAddOnMetered1.entitlements.metered1.interval,
+ );
- const expectedAmt =
- (oneTimeOverrideQuantity || oneTimeQuantity) *
- oneTimeBillingUnits *
- oneTimePurchaseCount;
+ const expectedAmt =
+ (oneTimeOverrideQuantity || oneTimeQuantity) *
+ oneTimeBillingUnits *
+ oneTimePurchaseCount;
- try {
- assert.equal(addOnBalance!.balance, expectedAmt);
- assert.equal(cusRes.add_ons.length, 1);
- assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id);
- assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount);
- } catch (error) {
- console.group();
- console.group();
- console.log("GET customer, balances failed");
- console.log(
- "Add on entitlement:",
- products.oneTimeAddOnMetered1.entitlements.metered1,
- );
- console.log("Customer entitlements:", cusRes.entitlements);
+ try {
+ assert.equal(addOnBalance!.balance, expectedAmt);
+ assert.equal(cusRes.add_ons.length, 1);
+ assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id);
+ assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount);
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("GET customer, balances failed");
+ console.log(
+ "Add on entitlement:",
+ products.oneTimeAddOnMetered1.entitlements.metered1,
+ );
+ console.log("Customer entitlements:", cusRes.entitlements);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ });
- it("GET /entitled -- checking entitled for metered1", async function () {
- const res: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- );
+ it("GET /entitled -- checking entitled for metered1", async function () {
+ const res: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ );
- expect(res!.allowed).to.be.true;
+ expect(res!.allowed).to.be.true;
- // pro metered1
- const proMetered1Amt = products.pro.entitlements.metered1.allowance;
+ // pro metered1
+ const proMetered1Amt = products.pro.entitlements.metered1.allowance;
- const addOnBalance = res!.balances.find(
- (b: any) => b.feature_id === features.metered1.id,
- );
+ const addOnBalance = res!.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
- expect(res!.allowed).to.be.true;
- expect(addOnBalance!.balance).to.equal(
- proMetered1Amt! +
- (oneTimeOverrideQuantity || oneTimeQuantity) *
- oneTimeBillingUnits *
- oneTimePurchaseCount,
- );
- });
- });
+ expect(res!.allowed).to.be.true;
+ expect(addOnBalance!.balance).to.equal(
+ proMetered1Amt! +
+ (oneTimeOverrideQuantity || oneTimeQuantity) *
+ oneTimeBillingUnits *
+ oneTimePurchaseCount,
+ );
+ });
+ });
- // PURCHASE MONTHLY ADD ON
- describe("Monthly add on", () => {
- it("POST /attach -- attaching monthly add on", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.monthlyAddOnMetered1.id,
- forceCheckout: false,
- options: [
- {
- feature_id: features.metered1.id,
- quantity: monthlyQuantity * monthlyBillingUnits,
- },
- ],
- });
- await timeout(10000);
+ // PURCHASE MONTHLY ADD ON
+ describe("Monthly add on", () => {
+ it("POST /attach -- attaching monthly add on", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.monthlyAddOnMetered1.id,
+ forceCheckout: false,
+ options: [
+ {
+ feature_id: features.metered1.id,
+ quantity: monthlyQuantity * monthlyBillingUnits,
+ },
+ ],
+ });
+ await timeout(10000);
- console.log(` ${chalk.greenBright("Attached monthly top up")}`);
- });
+ console.log(` ${chalk.greenBright("Attached monthly top up")}`);
+ });
- it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
+ it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
- // 1. Metered1 balance should be pro + one time add on
- const proMetered1 = products.pro.entitlements.metered1.allowance;
+ // 1. Metered1 balance should be pro + one time add on
+ const proMetered1 = products.pro.entitlements.metered1.allowance;
- // Fetch balance
- const monthlyMetered1Balance = cusRes.entitlements.find(
- (e: any) =>
- e.feature_id === features.metered1.id &&
- e.interval ==
- products.monthlyAddOnMetered1.entitlements.metered1.interval,
- );
+ // Fetch balance
+ const monthlyMetered1Balance = cusRes.entitlements.find(
+ (e: any) =>
+ e.feature_id === features.metered1.id &&
+ e.interval ==
+ products.monthlyAddOnMetered1.entitlements.metered1.interval,
+ );
- try {
- assert.equal(
- monthlyMetered1Balance!.balance,
- proMetered1! + monthlyQuantity * monthlyBillingUnits,
- );
+ try {
+ assert.equal(
+ monthlyMetered1Balance!.balance,
+ proMetered1! + monthlyQuantity * monthlyBillingUnits,
+ );
- assert.equal(cusRes.add_ons.length, 2);
- const monthlyAddOnId = cusRes.add_ons.find(
- (a: any) => a.id === products.monthlyAddOnMetered1.id,
- );
+ assert.equal(cusRes.add_ons.length, 2);
+ const monthlyAddOnId = cusRes.add_ons.find(
+ (a: any) => a.id === products.monthlyAddOnMetered1.id,
+ );
- assert.exists(monthlyAddOnId);
- expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount);
- } catch (error) {
- console.group();
- console.group();
- console.log("GET customer, balances failed");
- console.log(
- "Add on entitlement:",
- products.monthlyAddOnMetered1.entitlements.metered1,
- );
- console.log("Customer entitlements:", cusRes.entitlements);
+ assert.exists(monthlyAddOnId);
+ expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount);
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("GET customer, balances failed");
+ console.log(
+ "Add on entitlement:",
+ products.monthlyAddOnMetered1.entitlements.metered1,
+ );
+ console.log("Customer entitlements:", cusRes.entitlements);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ });
- it("GET /entitled -- checking entitlements (monthly add on)", async function () {
- const res: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- );
+ it("GET /entitled -- checking entitlements (monthly add on)", async function () {
+ const res: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ );
- const metered1Balance = res!.balances.find(
- (b: any) => b.feature_id === features.metered1.id,
- );
+ const metered1Balance = res!.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
- const proMetered1Amt = products.pro.entitlements.metered1.allowance;
- const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits;
+ const proMetered1Amt = products.pro.entitlements.metered1.allowance;
+ const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits;
- const oneTimeAddOnMetered1Amt =
- (oneTimeOverrideQuantity || oneTimeQuantity) *
- oneTimeBillingUnits *
- oneTimePurchaseCount;
+ const oneTimeAddOnMetered1Amt =
+ (oneTimeOverrideQuantity || oneTimeQuantity) *
+ oneTimeBillingUnits *
+ oneTimePurchaseCount;
- try {
- expect(metered1Balance!.balance).to.equal(
- proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt,
- );
- } catch (error) {
- console.group();
- console.group();
- console.log("GET entitled, balances failed");
+ try {
+ expect(metered1Balance!.balance).to.equal(
+ proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt,
+ );
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("GET entitled, balances failed");
- console.log("/entitled response:", res);
- console.log("Pro metered1 amt:", proMetered1Amt);
- console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt);
- console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt);
+ console.log("/entitled response:", res);
+ console.log("Pro metered1 amt:", proMetered1Amt);
+ console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt);
+ console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- });
- });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ });
+ });
});
diff --git a/server/tests/archives/04_entitled.ts b/server/tests/archives/04_entitled.ts
index ce0096a8b..fbf948b03 100644
--- a/server/tests/archives/04_entitled.ts
+++ b/server/tests/archives/04_entitled.ts
@@ -6,240 +6,240 @@ import { timeout } from "../utils/genUtils.js";
import { expect } from "chai";
const checkEntitledOnProduct = async ({
- customerId,
- product,
- totalAllowance,
- finish = false,
- usageBased = false,
+ customerId,
+ product,
+ totalAllowance,
+ finish = false,
+ usageBased = false,
}: {
- customerId: string;
- product: any;
- totalAllowance?: number;
- finish?: boolean;
- usageBased?: boolean;
+ customerId: string;
+ product: any;
+ totalAllowance?: number;
+ finish?: boolean;
+ usageBased?: boolean;
}) => {
- // 1. Send events
- const allowance = totalAllowance || product.entitlements.metered1.allowance;
- // const randomNum = Math.floor(Math.random() * (allowance - 1));
- const randomNum = 3;
+ // 1. Send events
+ const allowance = totalAllowance || product.entitlements.metered1.allowance;
+ // const randomNum = Math.floor(Math.random() * (allowance - 1));
+ const randomNum = 3;
- const batchUpdates = [];
- for (let i = 0; i < randomNum; i++) {
- batchUpdates.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- }),
- );
- }
+ const batchUpdates = [];
+ for (let i = 0; i < randomNum; i++) {
+ batchUpdates.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
- await Promise.all(batchUpdates);
- await timeout(8000);
- let used = randomNum;
+ await Promise.all(batchUpdates);
+ await timeout(8000);
+ let used = randomNum;
- // 2. Check entitled
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- true,
- );
+ // 2. Check entitled
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ true,
+ );
- try {
- expect(allowed).to.be.true;
- expect(balanceObj!.balance).to.equal(allowance - randomNum);
+ try {
+ expect(allowed).to.be.true;
+ expect(balanceObj!.balance).to.equal(allowance - randomNum);
- if (!finish) {
- return used;
- }
- } catch (error) {
- console.group();
- console.group();
- console.log("Allowance: ", allowance, "Random num: ", randomNum);
- console.log("Expected balance to be: ", allowance - randomNum);
- console.log("Entitled res: ", { allowed, balanceObj });
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
+ if (!finish) {
+ return used;
+ }
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Allowance: ", allowance, "Random num: ", randomNum);
+ console.log("Expected balance to be: ", allowance - randomNum);
+ console.log("Entitled res: ", { allowed, balanceObj });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
- // Finish up
- const batchUpdates2 = [];
- for (let i = 0; i < allowance - randomNum; i++) {
- batchUpdates2.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- }),
- );
- }
- await Promise.all(batchUpdates2);
- await timeout(8000);
- used += allowance - randomNum;
+ // Finish up
+ const batchUpdates2 = [];
+ for (let i = 0; i < allowance - randomNum; i++) {
+ batchUpdates2.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
+ await Promise.all(batchUpdates2);
+ await timeout(8000);
+ used += allowance - randomNum;
- // 3. Check entitled again
- const { allowed: allowed2, balanceObj: balanceObj2 }: any =
- await AutumnCli.entitled(customerId, features.metered1.id, true);
- try {
- if (usageBased) {
- expect(allowed2).to.be.true;
- } else {
- expect(allowed2).to.be.false;
- }
- expect(balanceObj2!.balance).to.equal(0);
- return used;
- } catch (error) {
- console.group();
- console.group();
- console.log("Expected balance to be: ", 0);
- console.log("Entitled res: ", { allowed2, balanceObj2 });
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
+ // 3. Check entitled again
+ const { allowed: allowed2, balanceObj: balanceObj2 }: any =
+ await AutumnCli.entitled(customerId, features.metered1.id, true);
+ try {
+ if (usageBased) {
+ expect(allowed2).to.be.true;
+ } else {
+ expect(allowed2).to.be.false;
+ }
+ expect(balanceObj2!.balance).to.equal(0);
+ return used;
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Expected balance to be: ", 0);
+ console.log("Entitled res: ", { allowed2, balanceObj2 });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
};
// TODO: Add test case for unlimited feature
describe(`${chalk.yellowBright(
- "04_entitled: Testing /events and /entitled, for pro, one time top up",
+ "04_entitled: Testing /events and /entitled, for pro, one time top up",
)}`, () => {
- const customerId = "entitledCustomer";
+ const customerId = "entitledCustomer";
- let curAllowance = 0;
- const oneTimeBillingUnits =
- products.oneTimeAddOnMetered1.prices[0].config.billing_units!;
- let oneTimeQuantity = 2 * oneTimeBillingUnits;
+ let curAllowance = 0;
+ const oneTimeBillingUnits =
+ products.oneTimeAddOnMetered1.prices[0].config.billing_units!;
+ let oneTimeQuantity = 2 * oneTimeBillingUnits;
- before(async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: customerId,
- email: `test@test.com`,
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ before(async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: customerId,
+ email: `test@test.com`,
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- it("should have correct entitlements (free)", async function () {
- await checkEntitledOnProduct({
- customerId: customerId,
- product: products.free,
- finish: true,
- });
- });
+ it("should have correct entitlements (free)", async function () {
+ await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.free,
+ finish: true,
+ });
+ });
- it("should attach pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
- });
+ it("should attach pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
+ });
- it("should have correct entitlements (pro)", async function () {
- const used = await checkEntitledOnProduct({
- customerId: customerId,
- product: products.pro,
- finish: false,
- });
+ it("should have correct entitlements (pro)", async function () {
+ const used = await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.pro,
+ finish: false,
+ });
- curAllowance = products.pro.entitlements.metered1.allowance! - used;
- });
+ curAllowance = products.pro.entitlements.metered1.allowance! - used;
+ });
- it("should attach one time top up", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.oneTimeAddOnMetered1.id,
- options: [
- {
- feature_id: features.metered1.id,
- quantity: oneTimeQuantity,
- },
- ],
- });
- });
+ it("should attach one time top up", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.oneTimeAddOnMetered1.id,
+ options: [
+ {
+ feature_id: features.metered1.id,
+ quantity: oneTimeQuantity,
+ },
+ ],
+ });
+ });
- it("should have correct entitlements (one time top up)", async function () {
- // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity;
+ it("should have correct entitlements (one time top up)", async function () {
+ // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity;
- await checkEntitledOnProduct({
- customerId: customerId,
- product: products.oneTimeAddOnMetered1,
- finish: true,
- totalAllowance: curAllowance + oneTimeQuantity,
- });
- });
+ await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.oneTimeAddOnMetered1,
+ finish: true,
+ totalAllowance: curAllowance + oneTimeQuantity,
+ });
+ });
});
describe(`${chalk.yellowBright(
- "04_entitled: Testing /entitled & /events, for pro with overage",
+ "04_entitled: Testing /entitled & /events, for pro with overage",
)}`, () => {
- const customerId = "entitledCustomerUsageBased";
+ const customerId = "entitledCustomerUsageBased";
- before(async function () {
- await initCustomer({
- customer_data: {
- id: customerId,
- name: customerId,
- email: "test@test.com",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ before(async function () {
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: customerId,
+ email: "test@test.com",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- // PRO WITH OVERAGE
- it("should attach pro (with overage)", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithOverage.id,
- });
- });
+ // PRO WITH OVERAGE
+ it("should attach pro (with overage)", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithOverage.id,
+ });
+ });
- it("should have correct entitlements (pro with overage)", async function () {
- await checkEntitledOnProduct({
- customerId: customerId,
- product: products.proWithOverage,
- finish: true,
- totalAllowance: products.proWithOverage.entitlements.metered1.allowance!,
- usageBased: true,
- });
- });
+ it("should have correct entitlements (pro with overage)", async function () {
+ await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.proWithOverage,
+ finish: true,
+ totalAllowance: products.proWithOverage.entitlements.metered1.allowance!,
+ usageBased: true,
+ });
+ });
- it("should have correct usage-based balance (balance < 0)", async function () {
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- true,
- );
+ it("should have correct usage-based balance (balance < 0)", async function () {
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ true,
+ );
- expect(allowed).to.be.true;
- expect(balanceObj!.balance).to.equal(0);
+ expect(allowed).to.be.true;
+ expect(balanceObj!.balance).to.equal(0);
- // Sent 5 events
- const batchUpdates = [];
- for (let i = 0; i < 5; i++) {
- batchUpdates.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- }),
- );
- }
+ // Sent 5 events
+ const batchUpdates = [];
+ for (let i = 0; i < 5; i++) {
+ batchUpdates.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
- await Promise.all(batchUpdates);
- await timeout(5000);
+ await Promise.all(batchUpdates);
+ await timeout(5000);
- const { allowed: allowed2, balanceObj: balanceObj2 }: any =
- await AutumnCli.entitled(customerId, features.metered1.id, true);
+ const { allowed: allowed2, balanceObj: balanceObj2 }: any =
+ await AutumnCli.entitled(customerId, features.metered1.id, true);
- expect(allowed2).to.be.true;
- expect(balanceObj2!.balance).to.equal(-5);
- expect(balanceObj2!.usage_allowed).to.be.true;
- });
+ expect(allowed2).to.be.true;
+ expect(balanceObj2!.balance).to.equal(-5);
+ expect(balanceObj2!.usage_allowed).to.be.true;
+ });
});
diff --git a/server/tests/archives/08_pkey.ts b/server/tests/archives/08_pkey.ts
index 6274f65d2..d9426fd9b 100644
--- a/server/tests/archives/08_pkey.ts
+++ b/server/tests/archives/08_pkey.ts
@@ -10,155 +10,155 @@ import { AutumnCli } from "../cli/AutumnCli.js";
import chalk from "chalk";
describe(`${chalk.yellowBright("08_pkey: Testing publishable key")}`, () => {
- // 1. Initialize customer with card
- let customerId = "pkeyTestCustomer";
- const bearerPublicAxios = getPublicAxiosInstance({
- withBearer: true,
- });
+ // 1. Initialize customer with card
+ let customerId = "pkeyTestCustomer";
+ const bearerPublicAxios = getPublicAxiosInstance({
+ withBearer: true,
+ });
- before(async function () {
- this.timeout(30000);
- await initCustomer({
- customer_data: {
- id: customerId,
- name: customerId,
- email: "test@test.com",
- fingerprint: "fp1",
- },
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: true,
- });
- });
+ before(async function () {
+ this.timeout(30000);
+ await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: customerId,
+ email: "test@test.com",
+ fingerprint: "fp1",
+ },
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: true,
+ });
+ });
- it("should return a 401 if the pkey is invalid", async function () {
- this.timeout(30000);
- const axiosInstance = getPublicAxiosInstance({
- withBearer: true,
- pkey: "am_pk_test_invalid",
- });
+ it("should return a 401 if the pkey is invalid", async function () {
+ this.timeout(30000);
+ const axiosInstance = getPublicAxiosInstance({
+ withBearer: true,
+ pkey: "am_pk_test_invalid",
+ });
- try {
- const { data } = await axiosInstance.post("/v1/attach", {
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ try {
+ const { data } = await axiosInstance.post("/v1/attach", {
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- throw new Error("Should not be able to attach");
- } catch (error: any) {
- assert.equal(error.response.status, 401);
- }
- });
+ throw new Error("Should not be able to attach");
+ } catch (error: any) {
+ assert.equal(error.response.status, 401);
+ }
+ });
- it("should return checkout URL for both bearer key", async function () {
- this.timeout(30000);
- const axiosInstanceBearer = getPublicAxiosInstance({
- withBearer: true,
- });
+ it("should return checkout URL for both bearer key", async function () {
+ this.timeout(30000);
+ const axiosInstanceBearer = getPublicAxiosInstance({
+ withBearer: true,
+ });
- // 1. Should be able to upgrade to pro
- const { data } = await axiosInstanceBearer.post("/v1/attach", {
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ // 1. Should be able to upgrade to pro
+ const { data } = await axiosInstanceBearer.post("/v1/attach", {
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- assert.exists(data.checkout_url);
+ assert.exists(data.checkout_url);
- await completeCheckoutForm(data.checkout_url);
+ await completeCheckoutForm(data.checkout_url);
- await timeout(5000);
- });
+ await timeout(5000);
+ });
- it("should have customer with product", async function () {
- this.timeout(30000);
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- });
+ it("should have customer with product", async function () {
+ this.timeout(30000);
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ });
- it("should return error if try to upgrade or downgrade without pkey", async function () {
- this.timeout(30000);
- const axiosInstance = getPublicAxiosInstance({
- withBearer: true,
- });
+ it("should return error if try to upgrade or downgrade without pkey", async function () {
+ this.timeout(30000);
+ const axiosInstance = getPublicAxiosInstance({
+ withBearer: true,
+ });
- try {
- await axiosInstance.post("/v1/attach", {
- customer_id: customerId,
- product_id: products.premium.id,
- });
+ try {
+ await axiosInstance.post("/v1/attach", {
+ customer_id: customerId,
+ product_id: products.premium.id,
+ });
- throw new Error("Should not be able to attach");
- } catch (error: any) {
- assert.equal(error.response.status, 400);
- assert.equal(error.response.data.code, ErrCode.InvalidRequest);
- }
- });
+ throw new Error("Should not be able to attach");
+ } catch (error: any) {
+ assert.equal(error.response.status, 400);
+ assert.equal(error.response.data.code, ErrCode.InvalidRequest);
+ }
+ });
- it("should return error if try to downgrade to free", async function () {
- try {
- await bearerPublicAxios.post("/v1/attach", {
- customer_id: customerId,
- product_id: products.free.id,
- });
+ it("should return error if try to downgrade to free", async function () {
+ try {
+ await bearerPublicAxios.post("/v1/attach", {
+ customer_id: customerId,
+ product_id: products.free.id,
+ });
- throw new Error("Should not be able to attach");
- } catch (error: any) {
- assert.equal(error.response.status, 400);
- assert.equal(error.response.data.code, ErrCode.InvalidRequest);
- }
- });
+ throw new Error("Should not be able to attach");
+ } catch (error: any) {
+ assert.equal(error.response.status, 400);
+ assert.equal(error.response.data.code, ErrCode.InvalidRequest);
+ }
+ });
- // Next, check entitled for pro
- it("should return correct metered1 amount for pro", async function () {
- const { data } = await bearerPublicAxios.post("/v1/entitled", {
- customer_id: customerId,
- feature_id: features.metered1.id,
- });
+ // Next, check entitled for pro
+ it("should return correct metered1 amount for pro", async function () {
+ const { data } = await bearerPublicAxios.post("/v1/entitled", {
+ customer_id: customerId,
+ feature_id: features.metered1.id,
+ });
- assert.equal(data.allowed, true);
- const metered1Balance = data.balances.find(
- (b: any) => b.feature_id === features.metered1.id,
- );
- assert.equal(
- metered1Balance.balance,
- products.pro.entitlements.metered1.allowance,
- );
- });
+ assert.equal(data.allowed, true);
+ const metered1Balance = data.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
+ assert.equal(
+ metered1Balance.balance,
+ products.pro.entitlements.metered1.allowance,
+ );
+ });
- it("should return same balance for entitled with bearer and x-publishable-key", async function () {
- const { data } = await bearerPublicAxios.post("/v1/entitled", {
- customer_id: customerId,
- feature_id: features.metered1.id,
- });
+ it("should return same balance for entitled with bearer and x-publishable-key", async function () {
+ const { data } = await bearerPublicAxios.post("/v1/entitled", {
+ customer_id: customerId,
+ feature_id: features.metered1.id,
+ });
- assert.equal(data.allowed, true);
- const metered1Balance = data.balances.find(
- (b: any) => b.feature_id === features.metered1.id,
- );
- assert.equal(
- metered1Balance.balance,
- products.pro.entitlements.metered1.allowance,
- );
- });
+ assert.equal(data.allowed, true);
+ const metered1Balance = data.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
+ assert.equal(
+ metered1Balance.balance,
+ products.pro.entitlements.metered1.allowance,
+ );
+ });
- it("should return error when try to send event", async function () {
- try {
- await bearerPublicAxios.post("/v1/events", {
- customer_id: customerId,
- event_name: features.metered1.id,
- properties: {
- value: 10,
- },
- });
+ it("should return error when try to send event", async function () {
+ try {
+ await bearerPublicAxios.post("/v1/events", {
+ customer_id: customerId,
+ event_name: features.metered1.id,
+ properties: {
+ value: 10,
+ },
+ });
- throw new Error("Should not be able to send event");
- } catch (error: any) {
- assert.equal(error.response.status, 401);
- assert.equal(error.response.data.code, ErrCode.EndpointNotPublic);
- }
- });
+ throw new Error("Should not be able to send event");
+ } catch (error: any) {
+ assert.equal(error.response.status, 401);
+ assert.equal(error.response.data.code, ErrCode.EndpointNotPublic);
+ }
+ });
});
diff --git a/server/tests/archives/arrear_prorated/arrear_prorated3.ts b/server/tests/archives/arrear_prorated/arrear_prorated3.ts
index 02bf78167..ca65afdc9 100644
--- a/server/tests/archives/arrear_prorated/arrear_prorated3.ts
+++ b/server/tests/archives/arrear_prorated/arrear_prorated3.ts
@@ -15,264 +15,264 @@ import { Decimal } from "decimal.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const advanceAPThroughBalances = async ({
- stripeSub,
- stripeCli,
- testClockId,
- customerId,
- billingUnits,
- startingFrom,
- startingBalance,
+ stripeSub,
+ stripeCli,
+ testClockId,
+ customerId,
+ billingUnits,
+ startingFrom,
+ startingBalance,
}: {
- stripeSub: Stripe.Subscription;
- stripeCli: Stripe;
- testClockId: string;
- customerId: string;
- billingUnits: number;
- startingFrom?: number;
- startingBalance?: number;
+ stripeSub: Stripe.Subscription;
+ stripeCli: Stripe;
+ testClockId: string;
+ customerId: string;
+ billingUnits: number;
+ startingFrom?: number;
+ startingBalance?: number;
}) => {
- // 1. Get total period
- let totalPeriod =
- (stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
+ // 1. Get total period
+ let totalPeriod =
+ (stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
- // 2. Get allowance
- let allowance =
- advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
+ // 2. Get allowance
+ let allowance =
+ advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
- // 3. Get starting balance
- let balance = startingBalance || allowance;
+ // 3. Get starting balance
+ let balance = startingBalance || allowance;
- // 4. Get price per seat
- let pricePerSeat =
- advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
+ // 4. Get price per seat
+ let pricePerSeat =
+ advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
- let skipDays = 2;
- // 5. Get accrued price
- let accruedPrice = 0;
- if (startingBalance && startingBalance < 0) {
- let proratedPrice =
- (-startingBalance *
- pricePerSeat *
- (startingFrom! - stripeSub.current_period_start * 1000)) /
- totalPeriod;
+ let skipDays = 2;
+ // 5. Get accrued price
+ let accruedPrice = 0;
+ if (startingBalance && startingBalance < 0) {
+ let proratedPrice =
+ (-startingBalance *
+ pricePerSeat *
+ (startingFrom! - stripeSub.current_period_start * 1000)) /
+ totalPeriod;
- let previouslyPaid = pricePerSeat * -startingBalance;
- let priceToPay = proratedPrice - previouslyPaid;
+ let previouslyPaid = pricePerSeat * -startingBalance;
+ let priceToPay = proratedPrice - previouslyPaid;
- accruedPrice = priceToPay;
- // accruedPrice = Math.max(accruedPrice, 0);
- console.log(" 🔍 Starting balance: ", startingBalance);
- console.log(" 🔍 Starting price: ", accruedPrice);
- }
+ accruedPrice = priceToPay;
+ // accruedPrice = Math.max(accruedPrice, 0);
+ console.log(" 🔍 Starting balance: ", startingBalance);
+ console.log(" 🔍 Starting price: ", accruedPrice);
+ }
- let curTime = startingFrom || stripeSub.current_period_start * 1000;
- let numberOfEvents = 2;
+ let curTime = startingFrom || stripeSub.current_period_start * 1000;
+ let numberOfEvents = 2;
- console.group();
- console.group();
- for (let i = 0; i < numberOfEvents; i++) {
- let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
+ console.group();
+ console.group();
+ for (let i = 0; i < numberOfEvents; i++) {
+ let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
- let currentUsage = allowance - balance;
+ let currentUsage = allowance - balance;
- let nextBoundary =
- Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
+ let nextBoundary =
+ Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
- let prevBoundary = nextBoundary - billingUnits;
+ let prevBoundary = nextBoundary - billingUnits;
- let valueNeeded = 0;
- if (sign > 0) {
- // Add random amount to push above next boundary
- const valueToGetToNegative = balance + 1;
- valueNeeded =
- Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
+ let valueNeeded = 0;
+ if (sign > 0) {
+ // Add random amount to push above next boundary
+ const valueToGetToNegative = balance + 1;
+ valueNeeded =
+ Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
- valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
- } else {
- valueNeeded = -(
- Math.floor(Math.random() * 10) +
- (currentUsage - prevBoundary + 1)
- );
- }
+ valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
+ } else {
+ valueNeeded = -(
+ Math.floor(Math.random() * 10) +
+ (currentUsage - prevBoundary + 1)
+ );
+ }
- let newBalance = balance - valueNeeded;
- let totalUsage = allowance - newBalance;
+ let newBalance = balance - valueNeeded;
+ let totalUsage = allowance - newBalance;
- await AutumnCli.usage({
- customerId,
- featureId: features.seats.id,
- value: totalUsage,
- });
+ await AutumnCli.usage({
+ customerId,
+ featureId: features.seats.id,
+ value: totalUsage,
+ });
- await timeout(2000);
+ await timeout(2000);
- let prevBalance = balance;
- balance = newBalance;
+ let prevBalance = balance;
+ balance = newBalance;
- // Calculate prorated price only when crossing boundary
- let newPrice = Math.max(0, -balance * pricePerSeat);
- let prevCurTime = curTime;
- curTime = addDays(curTime, 2).getTime();
+ // Calculate prorated price only when crossing boundary
+ let newPrice = Math.max(0, -balance * pricePerSeat);
+ let prevCurTime = curTime;
+ curTime = addDays(curTime, 2).getTime();
- if (i === numberOfEvents - 1) {
- curTime = stripeSub.current_period_end * 1000;
- }
+ if (i === numberOfEvents - 1) {
+ curTime = stripeSub.current_period_end * 1000;
+ }
- let proratedPrice = new Decimal(newPrice)
- .mul(curTime - prevCurTime)
- .div(totalPeriod);
- accruedPrice = new Decimal(accruedPrice)
- .plus(proratedPrice)
- .toDecimalPlaces(2)
- .toNumber();
+ let proratedPrice = new Decimal(newPrice)
+ .mul(curTime - prevCurTime)
+ .div(totalPeriod);
+ accruedPrice = new Decimal(accruedPrice)
+ .plus(proratedPrice)
+ .toDecimalPlaces(2)
+ .toNumber();
- console.log(`Event ${i + 1}:`);
- console.log(` - Value added: ${valueNeeded}`);
- console.log(` - Balance: ${prevBalance} -> ${balance}`);
- console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
- console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
+ console.log(`Event ${i + 1}:`);
+ console.log(` - Value added: ${valueNeeded}`);
+ console.log(` - Balance: ${prevBalance} -> ${balance}`);
+ console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
+ console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
- await advanceTestClock({
- stripeCli,
- testClockId,
- numberOfDays: 2,
- startingFrom: new Date(prevCurTime),
- });
- }
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ numberOfDays: 2,
+ startingFrom: new Date(prevCurTime),
+ });
+ }
- console.groupEnd();
- console.groupEnd();
+ console.groupEnd();
+ console.groupEnd();
- // Advance test clock to end of period
+ // Advance test clock to end of period
- let advanceTo = addDays(addMonths(new Date(), 1), 2);
- let advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
+ let advanceTo = addDays(addMonths(new Date(), 1), 2);
+ let advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
- await advanceTestClock({
- stripeCli,
- testClockId,
- // numberOfHours: hoursToFinalizeInvoice,
- numberOfDays: 2,
- startingFrom: addMonths(advanceToStart, 1),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ // numberOfHours: hoursToFinalizeInvoice,
+ numberOfDays: 2,
+ startingFrom: addMonths(advanceToStart, 1),
+ });
- // Check invoice amount
- const res = await AutumnCli.getCustomer(customerId);
- let invoice = res.invoices[0];
+ // Check invoice amount
+ const res = await AutumnCli.getCustomer(customerId);
+ let invoice = res.invoices[0];
- let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
- let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
+ let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
+ let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
- let expectedInvoiceTotal = Number(
- (accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2),
- );
- console.log(
- `Invoice total = ${accruedPrice} (Accrued) + ${nextMonthUsagePrice} (Next month usage) + ${basePrice} (Base) = ${expectedInvoiceTotal}`,
- );
+ let expectedInvoiceTotal = Number(
+ (accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2),
+ );
+ console.log(
+ `Invoice total = ${accruedPrice} (Accrued) + ${nextMonthUsagePrice} (Next month usage) + ${basePrice} (Base) = ${expectedInvoiceTotal}`,
+ );
- expect(expectedInvoiceTotal).to.lte(
- new Decimal(invoice.total).plus(0.01).toNumber(),
- );
- expect(expectedInvoiceTotal).to.gte(
- new Decimal(invoice.total).minus(0.01).toNumber(),
- );
+ expect(expectedInvoiceTotal).to.lte(
+ new Decimal(invoice.total).plus(0.01).toNumber(),
+ );
+ expect(expectedInvoiceTotal).to.gte(
+ new Decimal(invoice.total).minus(0.01).toNumber(),
+ );
- return {
- balance,
- advancedTo: advanceTo.getTime(),
- };
+ return {
+ balance,
+ advancedTo: advanceTo.getTime(),
+ };
};
describe(`${chalk.yellowBright(
- "arrear_prorated3: Testing through /usage",
+ "arrear_prorated3: Testing through /usage",
)}`, () => {
- const customerId = "arrear_prorated3";
+ const customerId = "arrear_prorated3";
- let testClockId = "";
- let stripeCli: Stripe;
- let subId = "";
- let stripeSub: Stripe.Subscription;
- let billingUnits =
- advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
+ let testClockId = "";
+ let stripeCli: Stripe;
+ let subId = "";
+ let stripeSub: Stripe.Subscription;
+ let billingUnits =
+ advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
- before(async function () {
- const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
- {
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- },
- );
+ before(async function () {
+ const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
+ {
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ },
+ );
- stripeCli = createStripeCli({
- org: this.org,
- env: this.env,
- });
+ stripeCli = createStripeCli({
+ org: this.org,
+ env: this.env,
+ });
- testClockId = createdTestClockId;
- });
+ testClockId = createdTestClockId;
+ });
- it("arrear_prorated3: should attach in arrear prorated seats", async () => {
- await AutumnCli.attach({
- customerId,
- productId: advanceProducts.proratedArrearSeats.id,
- });
- });
+ it("arrear_prorated3: should attach in arrear prorated seats", async () => {
+ await AutumnCli.attach({
+ customerId,
+ productId: advanceProducts.proratedArrearSeats.id,
+ });
+ });
- it("arrear_prorated3: should have correct product", async function () {
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: advanceProducts.proratedArrearSeats,
- cusRes: res,
- });
+ it("arrear_prorated3: should have correct product", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: advanceProducts.proratedArrearSeats,
+ cusRes: res,
+ });
- // 2. Get subscription period start and period end
- subId = res.products[0].subscription_ids[0];
- stripeSub = await stripeCli.subscriptions.retrieve(subId);
+ // 2. Get subscription period start and period end
+ subId = res.products[0].subscription_ids[0];
+ stripeSub = await stripeCli.subscriptions.retrieve(subId);
- await checkSubscriptionContainsProducts({
- db: this.db,
- org: this.org,
- env: this.env,
- subscriptionId: subId,
- productIds: [advanceProducts.proratedArrearSeats.id],
- });
- });
+ await checkSubscriptionContainsProducts({
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ subscriptionId: subId,
+ productIds: [advanceProducts.proratedArrearSeats.id],
+ });
+ });
- let advancedTo: number;
- let balance: number;
- it("arrear_prorated3: should run first cycles and have correct invoice / balance", async () => {
- // Do it again
- let { advancedTo: advancedTo1, balance: balance1 } =
- await advanceAPThroughBalances({
- stripeSub,
- stripeCli,
- testClockId,
- customerId,
- billingUnits,
- });
+ let advancedTo: number;
+ let balance: number;
+ it("arrear_prorated3: should run first cycles and have correct invoice / balance", async () => {
+ // Do it again
+ let { advancedTo: advancedTo1, balance: balance1 } =
+ await advanceAPThroughBalances({
+ stripeSub,
+ stripeCli,
+ testClockId,
+ customerId,
+ billingUnits,
+ });
- advancedTo = advancedTo1;
- balance = balance1;
- });
+ advancedTo = advancedTo1;
+ balance = balance1;
+ });
- it("arrear_prorated3: should run second cycle and have correct invoice / balance", async () => {
- if (advancedTo) {
- console.log(
- ` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`,
- );
- }
+ it("arrear_prorated3: should run second cycle and have correct invoice / balance", async () => {
+ if (advancedTo) {
+ console.log(
+ ` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`,
+ );
+ }
- let newStripeSub = await stripeCli.subscriptions.retrieve(subId);
- await advanceAPThroughBalances({
- stripeSub: newStripeSub,
- stripeCli,
- testClockId,
- customerId,
- billingUnits,
- startingFrom: advancedTo,
- startingBalance: balance,
- });
- });
+ let newStripeSub = await stripeCli.subscriptions.retrieve(subId);
+ await advanceAPThroughBalances({
+ stripeSub: newStripeSub,
+ stripeCli,
+ testClockId,
+ customerId,
+ billingUnits,
+ startingFrom: advancedTo,
+ startingBalance: balance,
+ });
+ });
});
diff --git a/server/tests/archives/coupon1 copy.ts b/server/tests/archives/coupon1 copy.ts
index 49ca13452..f3918c904 100644
--- a/server/tests/archives/coupon1 copy.ts
+++ b/server/tests/archives/coupon1 copy.ts
@@ -12,10 +12,10 @@ import { AutumnCli } from "tests/cli/AutumnCli.js";
import { compareMainProduct } from "tests/utils/compare.js";
import {
- advanceClockForInvoice,
- advanceTestClock,
- completeCheckoutForm,
- getDiscount,
+ advanceClockForInvoice,
+ advanceTestClock,
+ completeCheckoutForm,
+ getDiscount,
} from "tests/utils/stripeUtils.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { setupBefore } from "tests/before.js";
@@ -24,165 +24,165 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
const testCase = "coupon1";
describe(
- chalk.yellow(`${testCase} -- Testing one-off rollover, apply to all`),
- () => {
- let customerId = "coupon1";
- let stripeCli: Stripe;
- let customer: Customer;
- let testClockId: string;
- let db, org, env;
+ chalk.yellow(`${testCase} -- Testing one-off rollover, apply to all`),
+ () => {
+ let customerId = "coupon1";
+ let stripeCli: Stripe;
+ let customer: Customer;
+ let testClockId: string;
+ let db, org, env;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let couponAmount = rewards.rolloverAll.discount_config.discount_value;
+ let couponAmount = rewards.rolloverAll.discount_config.discount_value;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
- autumn = this.autumnJs;
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
+ autumn = this.autumnJs;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- customerId,
- org: this.org,
- env: this.env,
- db: this.db,
- autumn: this.autumnJs,
- });
+ const res = await initCustomer({
+ customerId,
+ org: this.org,
+ env: this.env,
+ db: this.db,
+ autumn: this.autumnJs,
+ });
- testClockId = res.testClockId;
- customer = res.customer;
- });
+ testClockId = res.testClockId;
+ customer = res.customer;
+ });
- // CYCLE 0
- it("CYCLE 0: should attach pro with overage (through checkout)", async () => {
- couponAmount -= getFixedPriceAmount(products.proWithOverage);
+ // CYCLE 0
+ it("CYCLE 0: should attach pro with overage (through checkout)", async () => {
+ couponAmount -= getFixedPriceAmount(products.proWithOverage);
- const res = await AutumnCli.attach({
- customerId,
- productId: products.proWithOverage.id,
- forceCheckout: true,
- });
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: products.proWithOverage.id,
+ forceCheckout: true,
+ });
- await completeCheckoutForm(
- res.checkout_url,
- undefined,
- rewards.rolloverAll.id,
- );
+ await completeCheckoutForm(
+ res.checkout_url,
+ undefined,
+ rewards.rolloverAll.id,
+ );
- await timeout(20000);
+ await timeout(20000);
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.proWithOverage,
- cusRes,
- });
- });
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.proWithOverage,
+ cusRes,
+ });
+ });
- it("CYCLE 0: should have $0 invoice and correct remaining coupon amount", async () => {
- const cusRes = await AutumnCli.getCustomer(customerId);
- expect(cusRes.invoices[0].total).to.equal(0);
+ it("CYCLE 0: should have $0 invoice and correct remaining coupon amount", async () => {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ expect(cusRes.invoices[0].total).to.equal(0);
- const cusDiscount = await getDiscount({
- stripeCli: stripeCli,
- customer: cusRes.customer,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli: stripeCli,
+ customer: cusRes.customer,
+ });
- try {
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- rewards.rolloverAll.id,
- );
+ try {
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
+ rewards.rolloverAll.id,
+ );
- // Expect amount to be original amount - pro price
- expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
- } catch (error) {
- console.error("--------------------------------");
- console.error(
- "Expected stripe cus to have coupon",
- rewards.rolloverAll,
- );
- console.error("Actual stripe cus discount", cusDiscount);
- throw error;
- }
- });
+ // Expect amount to be original amount - pro price
+ expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
+ } catch (error) {
+ console.error("--------------------------------");
+ console.error(
+ "Expected stripe cus to have coupon",
+ rewards.rolloverAll,
+ );
+ console.error("Actual stripe cus discount", cusDiscount);
+ throw error;
+ }
+ });
- // CYCLE 1
- it("CYCLE 1: should set usage to -100 and advance clock by 1 month", async () => {
- const usage = 100;
- const res = await AutumnCli.usage({
- customerId,
- featureId: features.metered1.id,
- value: usage,
- });
+ // CYCLE 1
+ it("CYCLE 1: should set usage to -100 and advance clock by 1 month", async () => {
+ const usage = 100;
+ const res = await AutumnCli.usage({
+ customerId,
+ featureId: features.metered1.id,
+ value: usage,
+ });
- // Price
- const price = getPriceForOverage(
- products.proWithOverage.prices[1],
- -(products.proWithOverage.entitlements.metered1.allowance! - usage),
- );
+ // Price
+ const price = getPriceForOverage(
+ products.proWithOverage.prices[1],
+ -(products.proWithOverage.entitlements.metered1.allowance! - usage),
+ );
- couponAmount =
- couponAmount - (price + getFixedPriceAmount(products.proWithOverage));
+ couponAmount =
+ couponAmount - (price + getFixedPriceAmount(products.proWithOverage));
- await advanceClockForInvoice({
- stripeCli,
- testClockId,
- waitForMeterUpdate: true,
- });
- });
+ await advanceClockForInvoice({
+ stripeCli,
+ testClockId,
+ waitForMeterUpdate: true,
+ });
+ });
- it("CYCLE 1: should have $0 invoice and correct new coupon amount", async () => {
- const cusRes = await AutumnCli.getCustomer(customerId);
- expect(cusRes.invoices[0].total).to.equal(0);
+ it("CYCLE 1: should have $0 invoice and correct new coupon amount", async () => {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ expect(cusRes.invoices[0].total).to.equal(0);
- const cusDiscount = await getDiscount({
- stripeCli: stripeCli,
- customer,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli: stripeCli,
+ customer,
+ });
- try {
- expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
- rewards.rolloverAll.id,
- );
- expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
- } catch (error) {
- console.log("--------------------------------");
- console.log("coupon1, cycle 1 failed");
- console.log("Expected stripe cus to have coupon", rewards.rolloverAll);
- console.log("Actual stripe cus discount", cusDiscount);
- throw error;
- }
- });
+ try {
+ expect(getOriginalCouponId(cusDiscount.coupon?.id)).to.equal(
+ rewards.rolloverAll.id,
+ );
+ expect(cusDiscount.coupon?.amount_off).to.equal(couponAmount * 100);
+ } catch (error) {
+ console.log("--------------------------------");
+ console.log("coupon1, cycle 1 failed");
+ console.log("Expected stripe cus to have coupon", rewards.rolloverAll);
+ console.log("Actual stripe cus discount", cusDiscount);
+ throw error;
+ }
+ });
- // CYCLE 2
- it("CYCLE 2: should have $0 invoice and correct new coupon amount after 2nd cycle", async () => {
- await timeout(20000);
- let advanceTo = addHours(addMonths(new Date(), 2), 2);
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: advanceTo.getTime(),
- });
+ // CYCLE 2
+ it("CYCLE 2: should have $0 invoice and correct new coupon amount after 2nd cycle", async () => {
+ await timeout(20000);
+ let advanceTo = addHours(addMonths(new Date(), 2), 2);
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: advanceTo.getTime(),
+ });
- const cusDiscount = await getDiscount({
- stripeCli: stripeCli,
- customer,
- });
+ const cusDiscount = await getDiscount({
+ stripeCli: stripeCli,
+ customer,
+ });
- const newCouponAmount =
- couponAmount - getFixedPriceAmount(products.proWithOverage);
+ const newCouponAmount =
+ couponAmount - getFixedPriceAmount(products.proWithOverage);
- try {
- expect(cusDiscount.coupon?.amount_off).to.equal(newCouponAmount * 100);
- } catch (error) {
- console.log("--------------------------------");
- console.log("coupon1, cycle 2 failed");
- console.log("Expected coupon amount", newCouponAmount * 100);
- console.log("Stripe cus discount", cusDiscount);
- throw error;
- }
- });
- },
+ try {
+ expect(cusDiscount.coupon?.amount_off).to.equal(newCouponAmount * 100);
+ } catch (error) {
+ console.log("--------------------------------");
+ console.log("coupon1, cycle 2 failed");
+ console.log("Expected coupon amount", newCouponAmount * 100);
+ console.log("Stripe cus discount", cusDiscount);
+ throw error;
+ }
+ });
+ },
);
diff --git a/server/tests/attach/addOn/addOn1.ts b/server/tests/attach/addOn/addOn1.ts
index 426c84b4f..ab8250ae8 100644
--- a/server/tests/attach/addOn/addOn1.ts
+++ b/server/tests/attach/addOn/addOn1.ts
@@ -8,12 +8,12 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, replaceItems } from "../utils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearProratedItem,
- constructFeatureItem,
+ constructArrearProratedItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -21,138 +21,138 @@ import { expect } from "chai";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export let addOn = constructRawProduct({
- id: "add_on_1",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 200,
- }),
- ],
- isAddOn: true,
+ id: "add_on_1",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 200,
+ }),
+ ],
+ isAddOn: true,
});
const testCase = "addOn1";
describe(`${chalk.yellowBright(`${testCase}: Testing free add on, and updating free add on`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, addOn],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, addOn],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should should attach pro product, then add on product", async function () {
- await attachAndExpectCorrect({
- autumn,
- db,
- org,
- env,
- stripeCli,
- customerId,
- product: pro,
- });
- });
+ it("should should attach pro product, then add on product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ db,
+ org,
+ env,
+ stripeCli,
+ customerId,
+ product: pro,
+ });
+ });
- it("should should attach add on product", async function () {
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: addOn.id,
- });
+ it("should should attach add on product", async function () {
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: addOn.id,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expect(customer.products.length).to.equal(3);
- expectProductAttached({
- customer,
- product: addOn,
- });
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ expect(customer.products.length).to.equal(3);
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- const customItems = replaceItems({
- items: addOn.items,
- featureId: TestFeature.Messages,
- newItem: constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 400,
- }),
- });
+ const customItems = replaceItems({
+ items: addOn.items,
+ featureId: TestFeature.Messages,
+ newItem: constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 400,
+ }),
+ });
- it("should update add on product", async function () {
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: addOn.id,
- is_custom: true,
- items: customItems,
- });
+ it("should update add on product", async function () {
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: addOn.id,
+ is_custom: true,
+ items: customItems,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- is_custom: true,
- items: customItems,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ is_custom: true,
+ items: customItems,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expect(customer.products.length).to.equal(3);
- expectProductAttached({
- customer,
- product: addOn,
- });
- });
+ expect(customer.products.length).to.equal(3);
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
+ });
});
diff --git a/server/tests/attach/addOn/addOn2.test.ts b/server/tests/attach/addOn/addOn2.test.ts
index 0b7d00314..3ac20052a 100644
--- a/server/tests/attach/addOn/addOn2.test.ts
+++ b/server/tests/attach/addOn/addOn2.test.ts
@@ -7,8 +7,8 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -19,86 +19,86 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
export let pro = constructProduct({
- type: "pro",
- items: [],
+ type: "pro",
+ items: [],
});
export const addOn = constructRawProduct({
- id: "addOn",
- isAddOn: true,
- items: [
- constructFeatureItem({
- featureId: TestFeature.Credits,
- }),
- ],
+ id: "addOn",
+ isAddOn: true,
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Credits,
+ }),
+ ],
});
const testCase = "addOn2";
describe(`${chalk.yellowBright(`${testCase}: Testing attach free add on twice (should be treated as one off?)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await initCustomer({
- db,
- org,
- env,
- autumn: this.autumnJs,
- customerId,
- fingerprint: "test",
- attachPm: "success",
- });
+ await initCustomer({
+ db,
+ org,
+ env,
+ autumn: this.autumnJs,
+ customerId,
+ fingerprint: "test",
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro, addOn],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro, addOn],
+ });
+ });
- it("should attach pro product and free add on", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: true,
- });
+ it("should attach pro product and free add on", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: true,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: addOn,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
- expectFeaturesCorrect({
- customer,
- product: addOn,
- });
- });
+ expectFeaturesCorrect({
+ customer,
+ product: addOn,
+ });
+ });
});
diff --git a/server/tests/attach/basic/basic1.ts b/server/tests/attach/basic/basic1.ts
index 1ab930cf2..556a4b8f9 100644
--- a/server/tests/attach/basic/basic1.ts
+++ b/server/tests/attach/basic/basic1.ts
@@ -17,105 +17,105 @@ import { APIVersion } from "@autumn/shared";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
const freeProd = constructProduct({
- type: "free",
- isDefault: false,
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 1000,
- }),
- // constructFixedPrice({
- // price: 0,
- // }),
- ],
+ type: "free",
+ isDefault: false,
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 1000,
+ }),
+ // constructFixedPrice({
+ // price: 0,
+ // }),
+ ],
});
// UNCOMMENT FROM HERE
const testCase = "basic1";
describe(`${chalk.yellowBright("basic1: Testing attach free product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let db, org, env;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let db, org, env;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db,
- org,
- env,
- fingerprint: "test",
- withTestClock: false,
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ fingerprint: "test",
+ withTestClock: false,
+ });
- addPrefixToProducts({
- products: [freeProd],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [freeProd],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [freeProd],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [freeProd],
+ });
+ });
- it("should create customer and have default free active", async function () {
- const data = await AutumnCli.getCustomer(customerId);
+ it("should create customer and have default free active", async function () {
+ const data = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.free,
- cusRes: data,
- });
- });
+ compareMainProduct({
+ sent: products.free,
+ cusRes: data,
+ });
+ });
- it("should have correct entitlements", async function () {
- const expectedEntitlement = products.free.entitlements.metered1;
+ it("should have correct entitlements", async function () {
+ const expectedEntitlement = products.free.entitlements.metered1;
- const entitled = (await AutumnCli.entitled(
- customerId,
- features.metered1.id
- )) as any;
+ const entitled = (await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ )) as any;
- const metered1Balance = entitled.balances.find(
- (balance: any) => balance.feature_id === features.metered1.id
- );
+ const metered1Balance = entitled.balances.find(
+ (balance: any) => balance.feature_id === features.metered1.id,
+ );
- expect(entitled.allowed).to.be.true;
- expect(metered1Balance).to.exist;
- expect(metered1Balance.balance).to.equal(expectedEntitlement.allowance);
- expect(metered1Balance.unlimited).to.not.exist;
- });
+ expect(entitled.allowed).to.be.true;
+ expect(metered1Balance).to.exist;
+ expect(metered1Balance.balance).to.equal(expectedEntitlement.allowance);
+ expect(metered1Balance.unlimited).to.not.exist;
+ });
- it("should have correct boolean1 entitlement", async function () {
- const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
- expect(entitled!.allowed).to.be.false;
- });
+ it("should have correct boolean1 entitlement", async function () {
+ const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
+ expect(entitled!.allowed).to.be.false;
+ });
- it("should attach free (with $0 price) and force checkout and succeed", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: freeProd.id,
- force_checkout: true,
- });
+ it("should attach free (with $0 price) and force checkout and succeed", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: freeProd.id,
+ force_checkout: true,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: freeProd,
- });
+ expectProductAttached({
+ customer,
+ product: freeProd,
+ });
- expectFeaturesCorrect({
- customer,
- product: freeProd,
- });
- });
+ expectFeaturesCorrect({
+ customer,
+ product: freeProd,
+ });
+ });
});
diff --git a/server/tests/attach/basic/basic10.ts b/server/tests/attach/basic/basic10.ts
index 1aa47defb..4e6249a47 100644
--- a/server/tests/attach/basic/basic10.ts
+++ b/server/tests/attach/basic/basic10.ts
@@ -5,9 +5,9 @@ import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, oneTimeProducts } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import {
- getFixedPriceAmount,
- getUsagePriceTiers,
- timeout,
+ getFixedPriceAmount,
+ getUsagePriceTiers,
+ timeout,
} from "tests/utils/genUtils.js";
import { initCustomer } from "tests/utils/init.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
@@ -17,69 +17,69 @@ import { expect } from "chai";
const testCase = "basic10";
describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
- let customerId = testCase;
- let quantity = 1000;
- let options = [
- {
- feature_id: features.metered2.id,
- quantity,
- },
- ];
- before(async function () {
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ let customerId = testCase;
+ let quantity = 1000;
+ let options = [
+ {
+ feature_id: features.metered2.id,
+ quantity,
+ },
+ ];
+ before(async function () {
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should attach monthly with one time", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productIds: [
- oneTimeProducts.oneTimeMetered1.id,
- oneTimeProducts.oneTimeMetered2.id,
- ],
- options,
- });
+ it("should attach monthly with one time", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productIds: [
+ oneTimeProducts.oneTimeMetered1.id,
+ oneTimeProducts.oneTimeMetered2.id,
+ ],
+ options,
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(20000);
- });
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(20000);
+ });
- it("should have correct main product and entitlements", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
+ it("should have correct main product and entitlements", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: oneTimeProducts.oneTimeMetered1,
- cusRes,
- });
+ compareMainProduct({
+ sent: oneTimeProducts.oneTimeMetered1,
+ cusRes,
+ });
- compareMainProduct({
- sent: oneTimeProducts.oneTimeMetered2,
- cusRes,
- optionsList: options,
- });
+ compareMainProduct({
+ sent: oneTimeProducts.oneTimeMetered2,
+ cusRes,
+ optionsList: options,
+ });
- const invoices = cusRes.invoices;
- const metered1Amount = getFixedPriceAmount(oneTimeProducts.oneTimeMetered1);
- const metered2Tiers = getUsagePriceTiers({
- product: oneTimeProducts.oneTimeMetered2,
- featureId: features.metered2.id,
- });
+ const invoices = cusRes.invoices;
+ const metered1Amount = getFixedPriceAmount(oneTimeProducts.oneTimeMetered1);
+ const metered2Tiers = getUsagePriceTiers({
+ product: oneTimeProducts.oneTimeMetered2,
+ featureId: features.metered2.id,
+ });
- const metered2Amount = metered2Tiers[0].amount;
+ const metered2Amount = metered2Tiers[0].amount;
- let numBillingUnits = new Decimal(options[0].quantity).div(
- oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units
- );
+ let numBillingUnits = new Decimal(options[0].quantity).div(
+ oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units,
+ );
- const expectedTotal = new Decimal(metered2Amount)
- .mul(numBillingUnits)
- .add(metered1Amount)
- .toNumber();
+ const expectedTotal = new Decimal(metered2Amount)
+ .mul(numBillingUnits)
+ .add(metered1Amount)
+ .toNumber();
- expect(invoices[0].total).to.equal(expectedTotal);
- });
+ expect(invoices[0].total).to.equal(expectedTotal);
+ });
});
diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts
index f24c32206..caffd492c 100644
--- a/server/tests/attach/basic/basic2.ts
+++ b/server/tests/attach/basic/basic2.ts
@@ -12,77 +12,77 @@ import { timeout } from "tests/utils/genUtils.js";
// UNCOMMENT FROM HERE
const testCase = "basic2";
describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt();
- let db, org, env;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let db, org, env;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db,
- org,
- env,
- fingerprint: "test",
- });
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ fingerprint: "test",
+ });
+ });
- it("should attach pro through checkout", async function () {
- const { checkout_url } = await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ it("should attach pro through checkout", async function () {
+ const { checkout_url } = await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- await completeCheckoutForm(checkout_url);
- await timeout(12000);
- });
+ await completeCheckoutForm(checkout_url);
+ await timeout(12000);
+ });
- it("should have correct product & entitlements", async function () {
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- expect(res.invoices.length).to.be.greaterThan(0);
- });
+ it("should have correct product & entitlements", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ expect(res.invoices.length).to.be.greaterThan(0);
+ });
- it("should have correct result when calling /check", async function () {
- const proEntitlements = products.pro.entitlements;
+ it("should have correct result when calling /check", async function () {
+ const proEntitlements = products.pro.entitlements;
- for (const entitlement of Object.values(proEntitlements)) {
- const allowance = entitlement.allowance;
+ for (const entitlement of Object.values(proEntitlements)) {
+ const allowance = entitlement.allowance;
- const res: any = await AutumnCli.entitled(
- customerId,
- entitlement.feature_id!
- );
+ const res: any = await AutumnCli.entitled(
+ customerId,
+ entitlement.feature_id!,
+ );
- const entBalance = res!.balances.find(
- (b: any) => b.feature_id === entitlement.feature_id
- );
+ const entBalance = res!.balances.find(
+ (b: any) => b.feature_id === entitlement.feature_id,
+ );
- try {
- expect(res!.allowed).to.be.true;
- expect(entBalance).to.exist;
- if (entitlement.allowance) {
- expect(entBalance!.balance).to.equal(allowance);
- }
- // console.log(` - ${entitlement.feature_id} -- Passed`);
- } catch (error) {
- console.group();
- console.group();
- console.log("Looking for: ", entitlement);
- console.log("Received: ", res);
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
- }
- });
+ try {
+ expect(res!.allowed).to.be.true;
+ expect(entBalance).to.exist;
+ if (entitlement.allowance) {
+ expect(entBalance!.balance).to.equal(allowance);
+ }
+ // console.log(` - ${entitlement.feature_id} -- Passed`);
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Looking for: ", entitlement);
+ console.log("Received: ", res);
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
+ }
+ });
});
diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts
index eba70ead9..831bd9c85 100644
--- a/server/tests/attach/basic/basic3.ts
+++ b/server/tests/attach/basic/basic3.ts
@@ -17,143 +17,143 @@ import { createProducts } from "tests/utils/productUtils.js";
// const oneTimeOverrideQuantity = 4;
// const monthlyQuantity = 2;
let oneTimeItem = constructPrepaidItem({
- featureId: features.metered1.id,
- price: 9,
- billingUnits: 250,
- isOneOff: true,
+ featureId: features.metered1.id,
+ price: 9,
+ billingUnits: 250,
+ isOneOff: true,
});
let oneTime = constructRawProduct({
- id: "basic3_one_off",
- items: [oneTimeItem],
- isAddOn: true,
+ id: "basic3_one_off",
+ items: [oneTimeItem],
+ isAddOn: true,
});
let monthlyItem = constructPrepaidItem({
- featureId: features.metered1.id,
- price: 9,
- billingUnits: 250,
+ featureId: features.metered1.id,
+ price: 9,
+ billingUnits: 250,
});
let monthly = constructRawProduct({
- id: "basic3_monthly",
- items: [
- constructPrepaidItem({
- featureId: features.metered1.id,
- price: 9,
- billingUnits: 250,
- }),
- ],
+ id: "basic3_monthly",
+ items: [
+ constructPrepaidItem({
+ featureId: features.metered1.id,
+ price: 9,
+ billingUnits: 250,
+ }),
+ ],
});
// UNCOMMENT FROM HERE
const testCase = "basic3";
describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt();
- let db, org, env;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let db, org, env;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- await createProducts({
- autumn: this.autumnJs,
- db,
- orgId: org.id,
- env,
- products: [oneTime, monthly],
- });
- });
+ await createProducts({
+ autumn: this.autumnJs,
+ db,
+ orgId: org.id,
+ env,
+ products: [oneTime, monthly],
+ });
+ });
- it("should attach pro", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ it("should attach pro", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ });
- const oneTimeQuantity = 500;
- const oneTimeBillingUnits = oneTimeItem.billing_units;
- const oneTimePurchaseCount = 2;
+ const oneTimeQuantity = 500;
+ const oneTimeBillingUnits = oneTimeItem.billing_units;
+ const oneTimePurchaseCount = 2;
- it("should attach one time add on twice, force checkout", async function () {
- for (let i = 0; i < 2; i++) {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: oneTime.id,
- force_checkout: true,
- });
+ it("should attach one time add on twice, force checkout", async function () {
+ for (let i = 0; i < 2; i++) {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: oneTime.id,
+ force_checkout: true,
+ });
- await completeCheckoutForm(
- res.checkout_url,
- oneTimeQuantity / oneTimeBillingUnits!
- );
- await timeout(15000);
- }
- });
+ await completeCheckoutForm(
+ res.checkout_url,
+ oneTimeQuantity / oneTimeBillingUnits!,
+ );
+ await timeout(15000);
+ }
+ });
- it("should have correct product & entitlements", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
+ it("should have correct product & entitlements", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
- const addOnBalance = cusRes.entitlements.find(
- (e: any) =>
- e.feature_id === features.metered1.id &&
- e.interval ==
- products.oneTimeAddOnMetered1.entitlements.metered1.interval
- );
+ const addOnBalance = cusRes.entitlements.find(
+ (e: any) =>
+ e.feature_id === features.metered1.id &&
+ e.interval ==
+ products.oneTimeAddOnMetered1.entitlements.metered1.interval,
+ );
- const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
+ const expectedAmt = oneTimeQuantity * oneTimePurchaseCount;
- expect(addOnBalance!.balance).to.equal(
- expectedAmt,
- "add on balance should be correct"
- );
+ expect(addOnBalance!.balance).to.equal(
+ expectedAmt,
+ "add on balance should be correct",
+ );
- expect(cusRes.add_ons).to.have.lengthOf(
- 1,
- "should only have one add on product after two purchases (since they combine)"
- );
- expect(cusRes.add_ons[0].id).to.equal(
- oneTime.id,
- "add on product should exist"
- );
- expect(cusRes.invoices.length).to.equal(
- 1 + oneTimePurchaseCount,
- "invoices should be correct"
- );
- });
+ expect(cusRes.add_ons).to.have.lengthOf(
+ 1,
+ "should only have one add on product after two purchases (since they combine)",
+ );
+ expect(cusRes.add_ons[0].id).to.equal(
+ oneTime.id,
+ "add on product should exist",
+ );
+ expect(cusRes.invoices.length).to.equal(
+ 1 + oneTimePurchaseCount,
+ "invoices should be correct",
+ );
+ });
- it("should have correct /check result for metered1", async function () {
- const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
+ it("should have correct /check result for metered1", async function () {
+ const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
- expect(res!.allowed).to.be.true;
+ expect(res!.allowed).to.be.true;
- const proMetered1Amt = products.pro.entitlements.metered1.allowance;
- const addOnBalance = res!.balances.find(
- (b: any) => b.feature_id === features.metered1.id
- );
+ const proMetered1Amt = products.pro.entitlements.metered1.allowance;
+ const addOnBalance = res!.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
- expect(res!.allowed).to.be.true;
- expect(addOnBalance!.balance).to.equal(
- proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount
- );
- });
+ expect(res!.allowed).to.be.true;
+ expect(addOnBalance!.balance).to.equal(
+ proMetered1Amt! + oneTimeQuantity * oneTimePurchaseCount,
+ );
+ });
});
diff --git a/server/tests/attach/basic/basic4.ts b/server/tests/attach/basic/basic4.ts
index 508892cf3..8ecc7ce16 100644
--- a/server/tests/attach/basic/basic4.ts
+++ b/server/tests/attach/basic/basic4.ts
@@ -11,111 +11,111 @@ import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { createProducts } from "tests/utils/productUtils.js";
let monthlyItem = constructPrepaidItem({
- featureId: features.metered1.id,
- price: 9,
- billingUnits: 250,
+ featureId: features.metered1.id,
+ price: 9,
+ billingUnits: 250,
});
let monthly = constructRawProduct({
- id: "basic4_monthly",
- items: [monthlyItem],
+ id: "basic4_monthly",
+ items: [monthlyItem],
});
const testCase = "basic4";
describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt();
- let db, org, env;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let db, org, env;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- await createProducts({
- autumn: this.autumnJs,
- db,
- orgId: org.id,
- env,
- products: [monthly],
- });
- });
+ await createProducts({
+ autumn: this.autumnJs,
+ db,
+ orgId: org.id,
+ env,
+ products: [monthly],
+ });
+ });
- it("should attach pro", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ it("should attach pro", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ });
- const monthlyQuantity = 500;
+ const monthlyQuantity = 500;
- it("should attach monthly add on", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.monthlyAddOnMetered1.id,
- forceCheckout: false,
- options: [
- {
- feature_id: features.metered1.id,
- quantity: monthlyQuantity,
- },
- ],
- });
- });
+ it("should attach monthly add on", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.monthlyAddOnMetered1.id,
+ forceCheckout: false,
+ options: [
+ {
+ feature_id: features.metered1.id,
+ quantity: monthlyQuantity,
+ },
+ ],
+ });
+ });
- it("should have correct product & entitlements", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
- const proMetered1 = products.pro.entitlements.metered1.allowance;
+ it("should have correct product & entitlements", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ const proMetered1 = products.pro.entitlements.metered1.allowance;
- const monthlyMetered1Balance = cusRes.entitlements.find(
- (e: any) =>
- e.feature_id === features.metered1.id &&
- e.interval ==
- products.monthlyAddOnMetered1.entitlements.metered1.interval
- );
+ const monthlyMetered1Balance = cusRes.entitlements.find(
+ (e: any) =>
+ e.feature_id === features.metered1.id &&
+ e.interval ==
+ products.monthlyAddOnMetered1.entitlements.metered1.interval,
+ );
- expect(monthlyMetered1Balance!.balance).to.equal(
- proMetered1! + monthlyQuantity
- );
+ expect(monthlyMetered1Balance!.balance).to.equal(
+ proMetered1! + monthlyQuantity,
+ );
- expect(cusRes.add_ons).to.have.lengthOf(1);
- const monthlyAddOnId = cusRes.add_ons.find(
- (a: any) => a.id === products.monthlyAddOnMetered1.id
- );
+ expect(cusRes.add_ons).to.have.lengthOf(1);
+ const monthlyAddOnId = cusRes.add_ons.find(
+ (a: any) => a.id === products.monthlyAddOnMetered1.id,
+ );
- expect(monthlyAddOnId).to.exist;
- expect(cusRes.invoices.length).to.equal(2);
- });
+ expect(monthlyAddOnId).to.exist;
+ expect(cusRes.invoices.length).to.equal(2);
+ });
- it("should have correct /check result for metered1", async function () {
- const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
+ it("should have correct /check result for metered1", async function () {
+ const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
- const metered1Balance = res!.balances.find(
- (b: any) => b.feature_id === features.metered1.id
- );
+ const metered1Balance = res!.balances.find(
+ (b: any) => b.feature_id === features.metered1.id,
+ );
- const proMetered1Amt = products.pro.entitlements.metered1.allowance;
- const monthlyAddOnMetered1Amt = monthlyQuantity;
+ const proMetered1Amt = products.pro.entitlements.metered1.allowance;
+ const monthlyAddOnMetered1Amt = monthlyQuantity;
- expect(metered1Balance!.balance).to.equal(
- proMetered1Amt! + monthlyAddOnMetered1Amt
- );
- });
+ expect(metered1Balance!.balance).to.equal(
+ proMetered1Amt! + monthlyAddOnMetered1Amt,
+ );
+ });
});
diff --git a/server/tests/attach/basic/basic5.ts b/server/tests/attach/basic/basic5.ts
index 2b62cf030..53e776a6c 100644
--- a/server/tests/attach/basic/basic5.ts
+++ b/server/tests/attach/basic/basic5.ts
@@ -12,86 +12,86 @@ import { timeout } from "@/utils/genUtils.js";
const testCase = "basic5";
describe(`${chalk.yellowBright(
- "basic5: Testing cancel through Stripe at period end and now"
+ "basic5: Testing cancel through Stripe at period end and now",
)}`, () => {
- const customerId = testCase;
- let stripeCli: Stripe;
+ const customerId = testCase;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- });
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ });
- it("should attach pro product", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
- });
+ it("should attach pro product", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
+ });
- it("should cancel pro product (at period end)", async function () {
- const stripeCli = createStripeCli({ org: this.org, env: this.env });
- const cusRes: any = await AutumnCli.getCustomer(customerId);
+ it("should cancel pro product (at period end)", async function () {
+ const stripeCli = createStripeCli({ org: this.org, env: this.env });
+ const cusRes: any = await AutumnCli.getCustomer(customerId);
- const proProduct = cusRes.products.find(
- (p: any) => p.id === products.pro.id
- );
+ const proProduct = cusRes.products.find(
+ (p: any) => p.id === products.pro.id,
+ );
- for (const subId of proProduct.subscription_ids) {
- await stripeCli.subscriptions.update(subId, {
- cancel_at_period_end: true,
- });
- }
- await timeout(5000);
- });
+ for (const subId of proProduct.subscription_ids) {
+ await stripeCli.subscriptions.update(subId, {
+ cancel_at_period_end: true,
+ });
+ }
+ await timeout(5000);
+ });
- return;
+ return;
- it("should have pro product active, and canceled_at != null, and free scheduled", async function () {
- const cusRes: any = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: cusRes,
- });
+ it("should have pro product active, and canceled_at != null, and free scheduled", async function () {
+ const cusRes: any = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: cusRes,
+ });
- const proProduct = cusRes.products.find(
- (p: any) => p.id === products.pro.id
- );
- expect(proProduct.canceled_at).to.not.equal(null);
- expect(proProduct.status).to.equal(CusProductStatus.Active);
+ const proProduct = cusRes.products.find(
+ (p: any) => p.id === products.pro.id,
+ );
+ expect(proProduct.canceled_at).to.not.equal(null);
+ expect(proProduct.status).to.equal(CusProductStatus.Active);
- const freeProduct = cusRes.products.find(
- (p: any) => p.id === products.free.id
- );
- expect(freeProduct).to.exist;
- expect(freeProduct.status).to.equal(CusProductStatus.Scheduled);
- });
+ const freeProduct = cusRes.products.find(
+ (p: any) => p.id === products.free.id,
+ );
+ expect(freeProduct).to.exist;
+ expect(freeProduct.status).to.equal(CusProductStatus.Scheduled);
+ });
- it("should cancel pro product (now)", async function () {
- const cusRes: any = await AutumnCli.getCustomer(customerId);
- const proProduct = cusRes.products.find(
- (p: any) => p.id === products.pro.id
- );
+ it("should cancel pro product (now)", async function () {
+ const cusRes: any = await AutumnCli.getCustomer(customerId);
+ const proProduct = cusRes.products.find(
+ (p: any) => p.id === products.pro.id,
+ );
- for (const subId of proProduct.subscription_ids) {
- await stripeCli.subscriptions.cancel(subId);
- }
- await timeout(5000);
- });
+ for (const subId of proProduct.subscription_ids) {
+ await stripeCli.subscriptions.cancel(subId);
+ }
+ await timeout(5000);
+ });
- it("should have free product active, and no pro product", async function () {
- const cusRes: any = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.free,
- cusRes: cusRes,
- });
- });
+ it("should have free product active, and no pro product", async function () {
+ const cusRes: any = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.free,
+ cusRes: cusRes,
+ });
+ });
});
diff --git a/server/tests/attach/basic/basic6.ts b/server/tests/attach/basic/basic6.ts
index 863a25f2d..4365d1635 100644
--- a/server/tests/attach/basic/basic6.ts
+++ b/server/tests/attach/basic/basic6.ts
@@ -14,60 +14,60 @@ import { expect } from "chai";
const testCase = "basic6";
describe(`${chalk.yellowBright(
- "basic6: Testing subscription past_due"
+ "basic6: Testing subscription past_due",
)}`, () => {
- const customerId = testCase;
- let stripeCli: Stripe;
- let testClockId: string;
- let customer: Customer;
+ const customerId = testCase;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let customer: Customer;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId_, customer: customer_ } =
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- testClockId = testClockId_;
- customer = customer_;
- });
+ const { testClockId: testClockId_, customer: customer_ } =
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ testClockId = testClockId_;
+ customer = customer_;
+ });
- it("should attach pro product and switch to failed payment method", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
+ it("should attach pro product and switch to failed payment method", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
- await attachFailedPaymentMethod({
- stripeCli,
- customer,
- });
- });
+ await attachFailedPaymentMethod({
+ stripeCli,
+ customer,
+ });
+ });
- it("should advance to next cycle", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
- });
+ it("should advance to next cycle", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
+ });
- it("should have pro product in past due status", async function () {
- const cusRes: any = await AutumnCli.getCustomer(customerId);
- const proProduct = cusRes.products.find(
- (p: any) => p.id === products.pro.id
- );
- expect(proProduct).to.exist;
- expect(proProduct.status).to.equal(CusProductStatus.PastDue);
- });
+ it("should have pro product in past due status", async function () {
+ const cusRes: any = await AutumnCli.getCustomer(customerId);
+ const proProduct = cusRes.products.find(
+ (p: any) => p.id === products.pro.id,
+ );
+ expect(proProduct).to.exist;
+ expect(proProduct.status).to.equal(CusProductStatus.PastDue);
+ });
});
diff --git a/server/tests/attach/basic/basic7.ts b/server/tests/attach/basic/basic7.ts
index 98d40cef0..11d68246a 100644
--- a/server/tests/attach/basic/basic7.ts
+++ b/server/tests/attach/basic/basic7.ts
@@ -12,68 +12,68 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
const testCase = "basic7";
describe(`${chalk.yellowBright("basic7: Testing trial duplicates (same customer)")}`, () => {
- const customerId = testCase;
- let customerId2 = testCase + "2";
- const autumn = new AutumnInt();
+ const customerId = testCase;
+ let customerId2 = testCase + "2";
+ const autumn = new AutumnInt();
- before(async function () {
- await setupBefore(this);
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- });
+ before(async function () {
+ await setupBefore(this);
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ });
- it("should attach pro with trial and have correct product & invoice", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithTrial.id,
- });
+ it("should attach pro with trial and have correct product & invoice", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithTrial.id,
+ });
- const customer = await AutumnCli.getCustomer(customerId);
+ const customer = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.proWithTrial,
- cusRes: customer,
- status: CusProductStatus.Trialing,
- });
+ compareMainProduct({
+ sent: products.proWithTrial,
+ cusRes: customer,
+ status: CusProductStatus.Trialing,
+ });
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(1, "Invoice length should be 1");
- expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
- });
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(1, "Invoice length should be 1");
+ expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
+ });
- it("should cancel pro with trial", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: products.proWithTrial.id,
- cancel_immediately: true,
- });
- await timeout(5000); // for webhook to be processed
- });
+ it("should cancel pro with trial", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: products.proWithTrial.id,
+ cancel_immediately: true,
+ });
+ await timeout(5000); // for webhook to be processed
+ });
- it("should be able to attach pro with trial again (renewal flow)", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithTrial.id,
- });
+ it("should be able to attach pro with trial again (renewal flow)", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithTrial.id,
+ });
- const customer = await AutumnCli.getCustomer(customerId);
+ const customer = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.proWithTrial,
- cusRes: customer,
- });
+ compareMainProduct({
+ sent: products.proWithTrial,
+ cusRes: customer,
+ });
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(2, "Invoice length should be 1");
- expect(invoices[0].amount).to.equal(
- products.proWithTrial.prices[0].amount,
- "should have paid full amount (trial already used once)",
- );
- });
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(2, "Invoice length should be 1");
+ expect(invoices[0].amount).to.equal(
+ products.proWithTrial.prices[0].amount,
+ "should have paid full amount (trial already used once)",
+ );
+ });
});
diff --git a/server/tests/attach/basic/basic8.ts b/server/tests/attach/basic/basic8.ts
index 2cde7074a..7891a7fab 100644
--- a/server/tests/attach/basic/basic8.ts
+++ b/server/tests/attach/basic/basic8.ts
@@ -12,74 +12,74 @@ import { timeout } from "@/utils/genUtils.js";
const testCase = "basic8";
describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => {
- const customerId = testCase;
- let customerId2 = testCase + "2";
- const autumn = new AutumnInt();
+ const customerId = testCase;
+ let customerId2 = testCase + "2";
+ const autumn = new AutumnInt();
- before(async function () {
- const randFingerprint = Math.random().toString(36).substring(2, 15);
- await setupBefore(this);
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- fingerprint: randFingerprint,
- attachPm: "success",
- });
+ before(async function () {
+ const randFingerprint = Math.random().toString(36).substring(2, 15);
+ await setupBefore(this);
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ fingerprint: randFingerprint,
+ attachPm: "success",
+ });
- await initCustomer({
- customerId: customerId2,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- fingerprint: randFingerprint,
- attachPm: "success",
- });
- });
+ await initCustomer({
+ customerId: customerId2,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ fingerprint: randFingerprint,
+ attachPm: "success",
+ });
+ });
- it("should attach pro with trial and have correct product & invoice", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithTrial.id,
- });
+ it("should attach pro with trial and have correct product & invoice", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithTrial.id,
+ });
- const customer = await AutumnCli.getCustomer(customerId);
+ const customer = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.proWithTrial,
- cusRes: customer,
- status: CusProductStatus.Trialing,
- });
+ compareMainProduct({
+ sent: products.proWithTrial,
+ cusRes: customer,
+ status: CusProductStatus.Trialing,
+ });
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(1, "Invoice length should be 1");
- expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
- });
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(1, "Invoice length should be 1");
+ expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
+ });
- it("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async function () {
- await autumn.attach({
- customer_id: customerId2,
- product_id: products.proWithTrial.id,
- });
+ it("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async function () {
+ await autumn.attach({
+ customer_id: customerId2,
+ product_id: products.proWithTrial.id,
+ });
- // await timeout(5000); // for webhook to be processed
- const customer = await AutumnCli.getCustomer(customerId2);
+ // await timeout(5000); // for webhook to be processed
+ const customer = await AutumnCli.getCustomer(customerId2);
- compareMainProduct({
- sent: products.proWithTrial,
- cusRes: customer,
- status: CusProductStatus.Active,
- });
+ compareMainProduct({
+ sent: products.proWithTrial,
+ cusRes: customer,
+ status: CusProductStatus.Active,
+ });
- // Check invoice is equal monthly price
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(1, "Invoice length should be 1");
- expect(invoices[0].total).to.equal(
- 10,
- "Invoice total should be full price",
- );
- });
+ // Check invoice is equal monthly price
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(1, "Invoice length should be 1");
+ expect(invoices[0].total).to.equal(
+ 10,
+ "Invoice total should be full price",
+ );
+ });
});
diff --git a/server/tests/attach/basic/basic9.ts b/server/tests/attach/basic/basic9.ts
index d9e24ea1a..fbbf83939 100644
--- a/server/tests/attach/basic/basic9.ts
+++ b/server/tests/attach/basic/basic9.ts
@@ -9,47 +9,47 @@ import { compareMainProduct } from "tests/utils/compare.js";
const testCase = "basic9";
describe(`${chalk.yellowBright(
- "basic9: attach monthly with one time prepaid, and quantity = 0"
+ "basic9: attach monthly with one time prepaid, and quantity = 0",
)}`, () => {
- let customerId = testCase;
+ let customerId = testCase;
- let options = [
- {
- feature_id: features.metered1.id,
- quantity: 0,
- },
- {
- feature_id: features.metered2.id,
- quantity: 4,
- },
- ];
- before(async function () {
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- });
+ let options = [
+ {
+ feature_id: features.metered1.id,
+ quantity: 0,
+ },
+ {
+ feature_id: features.metered2.id,
+ quantity: 4,
+ },
+ ];
+ before(async function () {
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should attach monthly with one time", async function () {
- const res = await AutumnCli.attach({
- customerId,
- productId: products.monthlyWithOneTime.id,
- options,
- });
+ it("should attach monthly with one time", async function () {
+ const res = await AutumnCli.attach({
+ customerId,
+ productId: products.monthlyWithOneTime.id,
+ options,
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(12000);
- });
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(12000);
+ });
- it("should have correct main product and entitlements", async function () {
- const cusRes = await AutumnCli.getCustomer(customerId);
+ it("should have correct main product and entitlements", async function () {
+ const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.monthlyWithOneTime,
- cusRes,
- optionsList: options,
- });
- });
+ compareMainProduct({
+ sent: products.monthlyWithOneTime,
+ cusRes,
+ optionsList: options,
+ });
+ });
});
diff --git a/server/tests/attach/checkout/checkout3.ts b/server/tests/attach/checkout/checkout3.ts
index bd341473d..efe3a21ad 100644
--- a/server/tests/attach/checkout/checkout3.ts
+++ b/server/tests/attach/checkout/checkout3.ts
@@ -9,8 +9,8 @@ import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearProratedItem,
- constructFeatureItem,
+ constructArrearProratedItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
@@ -19,98 +19,98 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export const oneOff = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Users,
- includedUsage: 5,
- }),
- ],
- type: "one_off",
- isAddOn: true,
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Users,
+ includedUsage: 5,
+ }),
+ ],
+ type: "one_off",
+ isAddOn: true,
});
const testCase = "checkout3";
describe(`${chalk.yellowBright(`${testCase}: Testing multi attach checkout, pro + one off`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, oneOff],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, oneOff],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, oneOff],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, oneOff],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro and one off product", async function () {
- const res = await autumn.attach({
- customer_id: customerId,
- product_ids: [pro.id, oneOff.id],
- });
+ it("should attach pro and one off product", async function () {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_ids: [pro.id, oneOff.id],
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(10000);
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(10000);
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- expectProductAttached({
- customer,
- product: oneOff,
- });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ expectProductAttached({
+ customer,
+ product: oneOff,
+ });
- expectFeaturesCorrect({
- customer,
- product: pro,
- });
+ expectFeaturesCorrect({
+ customer,
+ product: pro,
+ });
- expectFeaturesCorrect({
- customer,
- product: oneOff,
- });
- });
+ expectFeaturesCorrect({
+ customer,
+ product: oneOff,
+ });
+ });
});
diff --git a/server/tests/attach/checkout/checkout4.ts b/server/tests/attach/checkout/checkout4.ts
index c4a43c373..0b8e69d5f 100644
--- a/server/tests/attach/checkout/checkout4.ts
+++ b/server/tests/attach/checkout/checkout4.ts
@@ -8,8 +8,8 @@ import { setupBefore } from "tests/before.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import {
- constructCoupon,
- constructProduct,
+ constructCoupon,
+ constructProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -20,94 +20,94 @@ import { expect } from "chai";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
const reward = constructCoupon({
- id: "checkout4",
- promoCode: "checkout4_code",
- discountType: RewardType.PercentageDiscount,
- discountValue: 50,
+ id: "checkout4",
+ promoCode: "checkout4_code",
+ discountType: RewardType.PercentageDiscount,
+ discountValue: 50,
});
const testCase = "checkout4";
describe(`${chalk.yellowBright(`${testCase}: Testing attach coupon`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt();
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- await createReward({
- orgId: org.id,
- env,
- db,
- autumn,
- reward,
- productId: pro.id,
- });
+ await createReward({
+ orgId: org.id,
+ env,
+ db,
+ autumn,
+ reward,
+ productId: pro.id,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro and one off product", async function () {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- reward: reward.id,
- });
+ it("should attach pro and one off product", async function () {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ reward: reward.id,
+ });
- await completeCheckoutForm(res.checkout_url);
- await timeout(10000);
+ await completeCheckoutForm(res.checkout_url);
+ await timeout(10000);
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
- expect(customer.invoices.length).to.equal(1);
- let totalPrice = getBasePrice({ product: pro });
- expect(customer.invoices[0].total).to.equal(totalPrice * 0.5);
- });
+ expect(customer.invoices.length).to.equal(1);
+ let totalPrice = getBasePrice({ product: pro });
+ expect(customer.invoices[0].total).to.equal(totalPrice * 0.5);
+ });
});
diff --git a/server/tests/attach/checkout/checkout5.ts b/server/tests/attach/checkout/checkout5.ts
index 0a4d51abc..2ffd5c22e 100644
--- a/server/tests/attach/checkout/checkout5.ts
+++ b/server/tests/attach/checkout/checkout5.ts
@@ -19,97 +19,97 @@ import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoice
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
const testCase = "checkout5";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout, no product till paid`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- invoice: true,
- });
+ it("should attach pro product", async function () {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ invoice: true,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices?.[0];
+ const invoice = customer.invoices?.[0];
- expect(invoice).to.exist;
- expect(invoice.total).to.equal(getBasePrice({ product: pro }));
- expect(invoice.status).to.equal("open");
+ expect(invoice).to.exist;
+ expect(invoice.total).to.equal(getBasePrice({ product: pro }));
+ expect(invoice.status).to.equal("open");
- const product = customer.products.find((p) => p.id === pro.id);
- expect(product).to.not.exist;
+ const product = customer.products.find((p) => p.id === pro.id);
+ expect(product).to.not.exist;
- await completeInvoiceCheckout({
- url: res.checkout_url,
- });
+ await completeInvoiceCheckout({
+ url: res.checkout_url,
+ });
- const customer2 = await autumn.customers.get(customerId);
+ const customer2 = await autumn.customers.get(customerId);
- const invoice2 = customer2.invoices?.[0];
+ const invoice2 = customer2.invoices?.[0];
- expect(customer2.invoices.length).to.equal(1);
- expect(invoice2).to.exist;
- expect(invoice2.status).to.equal("paid");
+ expect(customer2.invoices.length).to.equal(1);
+ expect(invoice2).to.exist;
+ expect(invoice2.status).to.equal("paid");
- expectProductAttached({
- customer: customer2,
- product: pro,
- });
+ expectProductAttached({
+ customer: customer2,
+ product: pro,
+ });
- expectFeaturesCorrect({
- customer: customer2,
- product: pro,
- });
- });
+ expectFeaturesCorrect({
+ customer: customer2,
+ product: pro,
+ });
+ });
});
diff --git a/server/tests/attach/checkout/checkout6.ts b/server/tests/attach/checkout/checkout6.ts
index 44c7f12f5..b484e260f 100644
--- a/server/tests/attach/checkout/checkout6.ts
+++ b/server/tests/attach/checkout/checkout6.ts
@@ -19,140 +19,140 @@ import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 250,
- }),
- ],
- type: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 250,
+ }),
+ ],
+ type: "premium",
});
const testCase = "checkout6";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout via checkout endpoint`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product via invoice checkout", async function () {
- const res = await autumn.checkout({
- customer_id: customerId,
- product_id: pro.id,
- invoice: true,
- });
+ it("should attach pro product via invoice checkout", async function () {
+ const res = await autumn.checkout({
+ customer_id: customerId,
+ product_id: pro.id,
+ invoice: true,
+ });
- expect(res.url).to.exist;
+ expect(res.url).to.exist;
- await completeInvoiceCheckout({
- url: res.url!,
- });
+ await completeInvoiceCheckout({
+ url: res.url!,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
- expectFeaturesCorrect({
- customer,
- product: pro,
- });
- });
+ expectFeaturesCorrect({
+ customer,
+ product: pro,
+ });
+ });
- it("should have no URL returned if try to attach premium (with invoice true)", async function () {
- await expectAutumnError({
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- invoice: true,
- });
- },
- });
+ it("should have no URL returned if try to attach premium (with invoice true)", async function () {
+ await expectAutumnError({
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ invoice: true,
+ });
+ },
+ });
- const res = await autumn.checkout({
- customer_id: customerId,
- product_id: premium.id,
- invoice: true,
- });
+ const res = await autumn.checkout({
+ customer_id: customerId,
+ product_id: premium.id,
+ invoice: true,
+ });
- expect(res.url).to.not.exist;
- });
+ expect(res.url).to.not.exist;
+ });
- it("should attach premium product via invoice enable immediately", async function () {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- invoice: true,
- enable_product_immediately: true,
- });
+ it("should attach premium product via invoice enable immediately", async function () {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ invoice: true,
+ enable_product_immediately: true,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: premium,
- });
+ expectProductAttached({
+ customer,
+ product: premium,
+ });
- expectFeaturesCorrect({
- customer,
- product: premium,
- });
+ expectFeaturesCorrect({
+ customer,
+ product: premium,
+ });
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
- expect(invoices[0].status).to.equal("draft");
- expect(invoices[0].total).to.equal(
- getBasePrice({ product: premium }) - getBasePrice({ product: pro })
- ); // proration...
- });
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].status).to.equal("draft");
+ expect(invoices[0].total).to.equal(
+ getBasePrice({ product: premium }) - getBasePrice({ product: pro }),
+ ); // proration...
+ });
});
diff --git a/server/tests/attach/checkout/checkout7.ts b/server/tests/attach/checkout/checkout7.ts
index f4950a57c..e91038933 100644
--- a/server/tests/attach/checkout/checkout7.ts
+++ b/server/tests/attach/checkout/checkout7.ts
@@ -9,12 +9,12 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js";
@@ -23,119 +23,119 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export let addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 10,
- isOneOff: true,
- }),
- ],
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 10,
+ isOneOff: true,
+ }),
+ ],
});
const testCase = "checkout7";
describe(`${chalk.yellowBright(`${testCase}: Testing invoice checkout with one off product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, addOn],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, addOn],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product, then add on product via invoice checkout", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should attach pro product, then add on product via invoice checkout", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- const options = [
- {
- quantity: 200,
- feature_id: TestFeature.Messages,
- },
- ];
+ const options = [
+ {
+ quantity: 200,
+ feature_id: TestFeature.Messages,
+ },
+ ];
- const res2 = await autumn.checkout({
- customer_id: customerId,
- product_id: addOn.id,
- invoice: true,
- options,
- });
+ const res2 = await autumn.checkout({
+ customer_id: customerId,
+ product_id: addOn.id,
+ invoice: true,
+ options,
+ });
- expect(res2.url).to.exist;
+ expect(res2.url).to.exist;
- await completeInvoiceCheckout({
- url: res2.url!,
- });
+ await completeInvoiceCheckout({
+ url: res2.url!,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: addOn,
- });
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
- expectFeaturesCorrect({
- customer,
- product: addOn,
- otherProducts: [pro],
- options,
- });
- });
+ expectFeaturesCorrect({
+ customer,
+ product: addOn,
+ otherProducts: [pro],
+ options,
+ });
+ });
- // it("should have no URL returned if try to attach add on (with invoice true)", async function () {
- // const res = await autumn.checkout({
- // customer_id: customerId,
- // product_id: addOn.id,
- // invoice: true,
- // });
+ // it("should have no URL returned if try to attach add on (with invoice true)", async function () {
+ // const res = await autumn.checkout({
+ // customer_id: customerId,
+ // product_id: addOn.id,
+ // invoice: true,
+ // });
- // expect(res.url).to.not.exist;
- // });
+ // expect(res.url).to.not.exist;
+ // });
});
diff --git a/server/tests/attach/downgrade/downgrade1.ts b/server/tests/attach/downgrade/downgrade1.ts
index 2d0490436..ce0f6f43e 100644
--- a/server/tests/attach/downgrade/downgrade1.ts
+++ b/server/tests/attach/downgrade/downgrade1.ts
@@ -19,100 +19,100 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "downgrade1";
let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let premium = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> pro`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach premium product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach premium product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- // let nextCycle = Date.now();
- let preview = null;
+ // let nextCycle = Date.now();
+ let preview = null;
- it("should downgrade to pro", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should downgrade to pro", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should have pro attached on next cycle", async function () {
- await expectNextCycleCorrect({
- preview: preview!,
- autumn,
- stripeCli,
- customerId,
- testClockId,
- product: pro,
- db,
- org,
- env,
- });
- });
+ it("should have pro attached on next cycle", async function () {
+ await expectNextCycleCorrect({
+ preview: preview!,
+ autumn,
+ stripeCli,
+ customerId,
+ testClockId,
+ product: pro,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade2.ts b/server/tests/attach/downgrade/downgrade2.ts
index 7cfb87f5b..94c608e4e 100644
--- a/server/tests/attach/downgrade/downgrade2.ts
+++ b/server/tests/attach/downgrade/downgrade2.ts
@@ -14,113 +14,113 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js";
import {
- expectDowngradeCorrect,
- expectNextCycleCorrect,
+ expectDowngradeCorrect,
+ expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "downgrade2";
let free = constructProduct({
- items: [
- constructFeatureItem({
- feature_id: TestFeature.Words,
- included_usage: 100,
- }),
- ],
- type: "free",
- isDefault: false,
+ items: [
+ constructFeatureItem({
+ feature_id: TestFeature.Words,
+ included_usage: 100,
+ }),
+ ],
+ type: "free",
+ isDefault: false,
});
let premium = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> free`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach premium product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach premium product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- // let nextCycle = Date.now();
- let preview = null;
- it("should downgrade to free", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: free,
- stripeCli,
- db,
- org,
- env,
- });
+ // let nextCycle = Date.now();
+ let preview = null;
+ it("should downgrade to free", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: free,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should have pro attached on next cycle", async function () {
- await expectNextCycleCorrect({
- preview: preview!,
- autumn,
- stripeCli,
- customerId,
- testClockId,
- product: free,
- db,
- org,
- env,
- });
- });
+ it("should have pro attached on next cycle", async function () {
+ await expectNextCycleCorrect({
+ preview: preview!,
+ autumn,
+ stripeCli,
+ customerId,
+ testClockId,
+ product: free,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade3.ts b/server/tests/attach/downgrade/downgrade3.ts
index d217fe218..228b1b86d 100644
--- a/server/tests/attach/downgrade/downgrade3.ts
+++ b/server/tests/attach/downgrade/downgrade3.ts
@@ -19,140 +19,140 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "downgrade3";
let free = constructProduct({
- items: [
- constructFeatureItem({
- feature_id: TestFeature.Words,
- included_usage: 100,
- }),
- ],
- type: "free",
- isDefault: false,
+ items: [
+ constructFeatureItem({
+ feature_id: TestFeature.Words,
+ included_usage: 100,
+ }),
+ ],
+ type: "free",
+ isDefault: false,
});
let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let premium = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -> free -> pro -> premium`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let customer: Customer;
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customer: Customer;
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free, pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, pro, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free, pro, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1, customer: customer_ } =
- await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1, customer: customer_ } =
+ await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- customer = customer_!;
- });
+ testClockId = testClockId1!;
+ customer = customer_!;
+ });
- it("should attach premium product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach premium product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- // let nextCycle = Date.now();
- let preview = null;
- it("should downgrade to pro", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ // let nextCycle = Date.now();
+ let preview = null;
+ it("should downgrade to pro", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should downgrade to free", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: free,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should downgrade to free", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: free,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should change downgrade to pro", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: premium,
- newProduct: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should change downgrade to pro", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: premium,
+ newProduct: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should renew premium", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- });
+ it("should renew premium", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: premium,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: premium,
+ });
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade4.ts b/server/tests/attach/downgrade/downgrade4.ts
index 5008a75d1..48b64f815 100644
--- a/server/tests/attach/downgrade/downgrade4.ts
+++ b/server/tests/attach/downgrade/downgrade4.ts
@@ -4,11 +4,11 @@ import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- Customer,
- Organization,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ Customer,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
@@ -18,8 +18,8 @@ import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- expectDowngradeCorrect,
- expectNextCycleCorrect,
+ expectDowngradeCorrect,
+ expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceMonths } from "tests/utils/stripeUtils.js";
@@ -28,127 +28,127 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "downgrade4";
let proQuarter = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- interval: BillingInterval.Quarter,
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ interval: BillingInterval.Quarter,
});
let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let premium = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: pro-quarter -> premium -> pro`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let customer: Customer;
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customer: Customer;
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [proQuarter, pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [proQuarter, pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [proQuarter, pro, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [proQuarter, pro, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1, customer: customer_ } =
- await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1, customer: customer_ } =
+ await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- customer = customer_!;
- });
+ testClockId = testClockId1!;
+ customer = customer_!;
+ });
- it("should attach pro quarterly product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proQuarter,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro quarterly product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proQuarter,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should downgrade to premium", async function () {
- await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: proQuarter,
- newProduct: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should downgrade to premium", async function () {
+ await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: proQuarter,
+ newProduct: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let preview = null;
+ let preview = null;
- it("should downgrade to pro", async function () {
- const { preview: preview_ } = await expectDowngradeCorrect({
- autumn,
- customerId,
- curProduct: proQuarter,
- newProduct: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should downgrade to pro", async function () {
+ const { preview: preview_ } = await expectDowngradeCorrect({
+ autumn,
+ customerId,
+ curProduct: proQuarter,
+ newProduct: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- preview = preview_;
- });
+ preview = preview_;
+ });
- it("should have correct invoice after cycle", async function () {
- await advanceMonths({ stripeCli, testClockId, numberOfMonths: 3 });
+ it("should have correct invoice after cycle", async function () {
+ await advanceMonths({ stripeCli, testClockId, numberOfMonths: 3 });
- await timeout(10000);
+ await timeout(10000);
- await expectNextCycleCorrect({
- preview: preview!,
- autumn,
- stripeCli,
- customerId,
- testClockId,
- product: pro,
- db,
- org,
- env,
- });
- });
+ await expectNextCycleCorrect({
+ preview: preview!,
+ autumn,
+ stripeCli,
+ customerId,
+ testClockId,
+ product: pro,
+ db,
+ org,
+ env,
+ });
+ });
- return;
+ return;
});
diff --git a/server/tests/attach/downgrade/downgrade5.ts b/server/tests/attach/downgrade/downgrade5.ts
index 5d6477634..15e9b8b24 100644
--- a/server/tests/attach/downgrade/downgrade5.ts
+++ b/server/tests/attach/downgrade/downgrade5.ts
@@ -17,102 +17,102 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "downgrade5";
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt();
- let testClockId: string;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt();
+ let testClockId: string;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- autumn: autumnJs,
- });
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ autumn: autumnJs,
+ });
- testClockId = testClockId_;
- });
+ testClockId = testClockId_;
+ });
- it("should attach premium", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.premium.id,
- });
- });
+ it("should attach premium", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.premium.id,
+ });
+ });
- it("should attach pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
- });
+ it("should attach pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
+ });
- it("should have correct product and entitlements for scheduled pro", async function () {
- const res = await AutumnCli.getCustomer(customerId);
+ it("should have correct product and entitlements for scheduled pro", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.premium,
- cusRes: res,
- });
+ compareMainProduct({
+ sent: products.premium,
+ cusRes: res,
+ });
- const { products: resProducts } = res;
+ const { products: resProducts } = res;
- const resPro = resProducts.find(
- (p: any) =>
- p.id === products.pro.id && p.status === CusProductStatus.Scheduled,
- );
+ const resPro = resProducts.find(
+ (p: any) =>
+ p.id === products.pro.id && p.status === CusProductStatus.Scheduled,
+ );
- expect(resPro).to.exist;
- });
+ expect(resPro).to.exist;
+ });
- it("should attach premium and remove scheduled pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.premium.id,
- });
+ it("should attach premium and remove scheduled pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.premium.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- const resPro = res.products.find(
- (p: any) =>
- p.id === products.pro.id && p.status === CusProductStatus.Scheduled,
- );
+ const res = await AutumnCli.getCustomer(customerId);
+ const resPro = res.products.find(
+ (p: any) =>
+ p.id === products.pro.id && p.status === CusProductStatus.Scheduled,
+ );
- expect(resPro).to.not.exist;
+ expect(resPro).to.not.exist;
- compareMainProduct({
- sent: products.premium,
- cusRes: res,
- });
- });
+ compareMainProduct({
+ sent: products.premium,
+ cusRes: res,
+ });
+ });
- // Advance time 1 month
- it("should attach pro, advance stripe clock and have pro is attached", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
+ // Advance time 1 month
+ it("should attach pro, advance stripe clock and have pro is attached", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice,
- ).getTime(),
- waitForSeconds: 15,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 15,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade6.ts b/server/tests/attach/downgrade/downgrade6.ts
index 9cb3e0790..54d82e3f2 100644
--- a/server/tests/attach/downgrade/downgrade6.ts
+++ b/server/tests/attach/downgrade/downgrade6.ts
@@ -9,55 +9,55 @@ import { setupBefore } from "tests/before.js";
const testCase = "downgrade6";
describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
- let customerId = testCase;
- let testClockId: string;
- let autumn: AutumnInt = new AutumnInt();
- let customer: Customer;
+ let customerId = testCase;
+ let testClockId: string;
+ let autumn: AutumnInt = new AutumnInt();
+ let customer: Customer;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- const { testClockId: testClockId_, customer: customer_ } =
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ const { testClockId: testClockId_, customer: customer_ } =
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- it("should attach premium", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premium.id,
- });
- });
+ it("should attach premium", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premium.id,
+ });
+ });
- it("should expire premium", async function () {
- // const cusProduct = await getMainCusProduct({
- // db: this.db,
- // internalCustomerId: customer.internal_id,
- // });
+ it("should expire premium", async function () {
+ // const cusProduct = await getMainCusProduct({
+ // db: this.db,
+ // internalCustomerId: customer.internal_id,
+ // });
- // await AutumnCli.expire(cusProduct!.id);
- await autumn.cancel({
- customer_id: customerId,
- product_id: products.premium.id,
- cancel_immediately: true,
- });
- });
+ // await AutumnCli.expire(cusProduct!.id);
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: products.premium.id,
+ cancel_immediately: true,
+ });
+ });
- it("should have correct product and entitlements after expiration", async function () {
- const res = await AutumnCli.getCustomer(customerId);
+ it("should have correct product and entitlements after expiration", async function () {
+ const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.free,
- cusRes: res,
- });
- });
+ compareMainProduct({
+ sent: products.free,
+ cusRes: res,
+ });
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade7.ts b/server/tests/attach/downgrade/downgrade7.ts
index a64565819..736baede1 100644
--- a/server/tests/attach/downgrade/downgrade7.ts
+++ b/server/tests/attach/downgrade/downgrade7.ts
@@ -12,75 +12,75 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
const testCase = "downgrade7";
describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}`, () => {
- let customerId = testCase;
- let testClockId: string;
- let customer: Customer;
- let autumn: AutumnInt = new AutumnInt();
+ let customerId = testCase;
+ let testClockId: string;
+ let customer: Customer;
+ let autumn: AutumnInt = new AutumnInt();
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- const { testClockId: testClockId_, customer: customer_ } =
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ const { testClockId: testClockId_, customer: customer_ } =
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- // 2. Get premium
- it("should attach premium, then attach pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.premium.id,
- });
+ // 2. Get premium
+ it("should attach premium, then attach pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.premium.id,
+ });
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
- });
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
+ });
- it("should expire scheduled product (pro)", async function () {
- // const cusProduct = await findCusProductById({
- // db: this.db,
- // internalCustomerId: customer.internal_id,
- // productId: products.pro.id,
- // });
+ it("should expire scheduled product (pro)", async function () {
+ // const cusProduct = await findCusProductById({
+ // db: this.db,
+ // internalCustomerId: customer.internal_id,
+ // productId: products.pro.id,
+ // });
- // expect(cusProduct).to.exist;
- await autumn.cancel({
- customer_id: customerId,
- product_id: products.pro.id,
- cancel_immediately: true,
- });
- // await AutumnCli.expire(cusProduct!.id);
- });
+ // expect(cusProduct).to.exist;
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ cancel_immediately: true,
+ });
+ // await AutumnCli.expire(cusProduct!.id);
+ });
- it("should have correct product and entitlements (premium)", async function () {
- this.timeout(30000);
- // Check that free is attached
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.premium,
- cusRes: res,
- });
+ it("should have correct product and entitlements (premium)", async function () {
+ this.timeout(30000);
+ // Check that free is attached
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.premium,
+ cusRes: res,
+ });
- const { subs } = await getSubsFromCusId({
- stripeCli: this.stripeCli,
- customerId: customerId,
- productId: products.premium.id,
- db: this.db,
- org: this.org,
- env: this.env,
- });
- expect(subs).to.have.lengthOf(1);
- expect(subs[0].canceled_at).to.be.null;
- });
+ const { subs } = await getSubsFromCusId({
+ stripeCli: this.stripeCli,
+ customerId: customerId,
+ productId: products.premium.id,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ });
+ expect(subs).to.have.lengthOf(1);
+ expect(subs[0].canceled_at).to.be.null;
+ });
});
diff --git a/server/tests/attach/downgrade/downgrade9.ts b/server/tests/attach/downgrade/downgrade9.ts
index 67944835c..42e42c3f4 100644
--- a/server/tests/attach/downgrade/downgrade9.ts
+++ b/server/tests/attach/downgrade/downgrade9.ts
@@ -2,8 +2,8 @@ import chalk from "chalk";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { advanceProducts } from "tests/global.js";
import {
- checkProductIsScheduled,
- compareMainProduct,
+ checkProductIsScheduled,
+ compareMainProduct,
} from "tests/utils/compare.js";
import { setupBefore } from "tests/before.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
@@ -26,46 +26,46 @@ CASE 1: Annual pro -> Annual starter
const testCase = "downgrade9";
describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Annual pro -> Annual starter")}`, () => {
- let customerId = testCase;
+ let customerId = testCase;
- before(async function () {
- await setupBefore(this);
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- withTestClock: false,
- });
- });
+ before(async function () {
+ await setupBefore(this);
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ withTestClock: false,
+ });
+ });
- it("should attach annual pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuProAnnual.id,
- });
+ it("should attach annual pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuProAnnual.id,
+ });
- let cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: advanceProducts.gpuProAnnual,
- cusRes,
- });
- });
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: advanceProducts.gpuProAnnual,
+ cusRes,
+ });
+ });
- it("should attach downgrade to annual starter", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: advanceProducts.gpuStarterAnnual.id,
- });
+ it("should attach downgrade to annual starter", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: advanceProducts.gpuStarterAnnual.id,
+ });
- let cusRes = await AutumnCli.getCustomer(customerId);
- checkProductIsScheduled({
- cusRes,
- product: advanceProducts.gpuStarterAnnual,
- });
- });
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ checkProductIsScheduled({
+ cusRes,
+ product: advanceProducts.gpuStarterAnnual,
+ });
+ });
});
// describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Quarterly pro -> Monthly pro")}`, () => {
diff --git a/server/tests/attach/entities/entity1.ts b/server/tests/attach/entities/entity1.ts
index a68519390..bff0cfc11 100644
--- a/server/tests/attach/entities/entity1.ts
+++ b/server/tests/attach/entities/entity1.ts
@@ -16,86 +16,86 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
const testCase = "aentity1";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach to entity via checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- // testClockId = testClockId1!;
- });
+ // testClockId = testClockId1!;
+ });
- const newEntities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product to entity 1", async function () {
- await autumn.entities.create(customerId, newEntities);
- let entityId = newEntities[0].id;
+ it("should attach pro product to entity 1", async function () {
+ await autumn.entities.create(customerId, newEntities);
+ let entityId = newEntities[0].id;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- entityId,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- entityId,
- });
- });
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ entityId,
+ });
+ });
});
diff --git a/server/tests/attach/entities/entity2.ts b/server/tests/attach/entities/entity2.ts
index aaafba646..865a7d415 100644
--- a/server/tests/attach/entities/entity2.ts
+++ b/server/tests/attach/entities/entity2.ts
@@ -22,131 +22,131 @@ import { expectInvoiceAfterUsage } from "tests/utils/expectUtils/expectSingleUse
const testCase = "aentity2";
export let proAnnual = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to entity via checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- addPrefixToProducts({
- products: [proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [proAnnual],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1;
- });
+ testClockId = testClockId1;
+ });
- const newEntities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let entityId = newEntities[0].id;
- it("should attach pro annual product to entity 2", async function () {
- await autumn.entities.create(customerId, newEntities);
- entityId = newEntities[0].id;
+ let entityId = newEntities[0].id;
+ it("should attach pro annual product to entity 2", async function () {
+ await autumn.entities.create(customerId, newEntities);
+ entityId = newEntities[0].id;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- entityId,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId,
+ });
+ });
- let usage = 1250130;
- it("should track usage", async function () {
- await autumn.track({
- customer_id: customerId,
- entity_id: entityId,
- feature_id: TestFeature.Words,
- value: usage,
- });
- await timeout(5000);
+ let usage = 1250130;
+ it("should track usage", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ entity_id: entityId,
+ feature_id: TestFeature.Words,
+ value: usage,
+ });
+ await timeout(5000);
- let entity = await autumn.entities.get(customerId, entityId);
+ let entity = await autumn.entities.get(customerId, entityId);
- expectFeaturesCorrect({
- customer: entity,
- product: proAnnual,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ expectFeaturesCorrect({
+ customer: entity,
+ product: proAnnual,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should have correct invoice after cycle", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(curUnix, 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ it("should have correct invoice after cycle", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(curUnix, 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- await expectInvoiceAfterUsage({
- autumn,
- customerId,
- entityId,
- featureId: TestFeature.Words,
- product: proAnnual,
- usage,
- stripeCli,
- db,
- org,
- env,
- numInvoices: 2,
- });
- });
+ await expectInvoiceAfterUsage({
+ autumn,
+ customerId,
+ entityId,
+ featureId: TestFeature.Words,
+ product: proAnnual,
+ usage,
+ stripeCli,
+ db,
+ org,
+ env,
+ numInvoices: 2,
+ });
+ });
});
diff --git a/server/tests/attach/entities/entity3.ts b/server/tests/attach/entities/entity3.ts
index d7a47be2a..c1211d093 100644
--- a/server/tests/attach/entities/entity3.ts
+++ b/server/tests/attach/entities/entity3.ts
@@ -21,122 +21,122 @@ import { expectInvoiceAfterUsage } from "tests/utils/expectUtils/expectSingleUse
const testCase = "aentity3";
export let proAnnual = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro annual to entity via checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [proAnnual],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1;
- });
+ testClockId = testClockId1;
+ });
- const newEntities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let entityId = newEntities[0].id;
- it("should attach pro product to entity 2", async function () {
- await autumn.entities.create(customerId, newEntities);
- entityId = newEntities[0].id;
+ let entityId = newEntities[0].id;
+ it("should attach pro product to entity 2", async function () {
+ await autumn.entities.create(customerId, newEntities);
+ entityId = newEntities[0].id;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- entityId,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId,
+ });
+ });
- let nextUsage = 1032100;
- it("should cancel and have correct final invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- entity_id: entityId,
- feature_id: TestFeature.Words,
- value: nextUsage,
- });
+ let nextUsage = 1032100;
+ it("should cancel and have correct final invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ entity_id: entityId,
+ feature_id: TestFeature.Words,
+ value: nextUsage,
+ });
- await autumn.cancel({
- customer_id: customerId,
- product_id: proAnnual.id,
- entity_id: entityId,
- });
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ entity_id: entityId,
+ });
- await timeout(5000);
+ await timeout(5000);
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(curUnix, 1),
- hoursToFinalizeInvoice
- ).getTime(),
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(curUnix, 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ });
- await expectInvoiceAfterUsage({
- autumn,
- customerId,
- entityId,
- featureId: TestFeature.Words,
- product: proAnnual,
- usage: nextUsage,
- stripeCli,
- db,
- org,
- env,
- numInvoices: 2,
- expectExpired: true,
- });
- });
+ await expectInvoiceAfterUsage({
+ autumn,
+ customerId,
+ entityId,
+ featureId: TestFeature.Words,
+ product: proAnnual,
+ usage: nextUsage,
+ stripeCli,
+ db,
+ org,
+ env,
+ numInvoices: 2,
+ expectExpired: true,
+ });
+ });
});
diff --git a/server/tests/attach/entities/entity4.ts b/server/tests/attach/entities/entity4.ts
index df63f4675..d692b7c20 100644
--- a/server/tests/attach/entities/entity4.ts
+++ b/server/tests/attach/entities/entity4.ts
@@ -18,168 +18,168 @@ import { timeout } from "@/utils/genUtils.js";
const testCase = "aentity4";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing attach pro diff entities and testing track / check`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1;
- });
+ testClockId = testClockId1;
+ });
- const newEntities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let entity1 = newEntities[0];
- let entity2 = newEntities[1];
+ let entity1 = newEntities[0];
+ let entity2 = newEntities[1];
- it("should attach pro product to entity 1", async function () {
- await autumn.entities.create(customerId, newEntities);
+ it("should attach pro product to entity 1", async function () {
+ await autumn.entities.create(customerId, newEntities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- entityId: entity1.id,
- numSubs: 1,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: entity1.id,
+ numSubs: 1,
+ });
+ });
- it("should attach pro product to entity 2", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- entityId: entity2.id,
- numSubs: 2,
- });
- });
+ it("should attach pro product to entity 2", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: entity2.id,
+ numSubs: 2,
+ });
+ });
- let entity1Usage = Math.random() * 1000000;
- it("should track usage on entity 1", async function () {
- await autumn.track({
- customer_id: customerId,
- entity_id: entity1.id,
- feature_id: TestFeature.Words,
- value: entity1Usage,
- });
- await timeout(3000);
+ let entity1Usage = Math.random() * 1000000;
+ it("should track usage on entity 1", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ entity_id: entity1.id,
+ feature_id: TestFeature.Words,
+ value: entity1Usage,
+ });
+ await timeout(3000);
- let entity1Res = await autumn.entities.get(customerId, entity1.id);
- let entity2Res = await autumn.entities.get(customerId, entity2.id);
+ let entity1Res = await autumn.entities.get(customerId, entity1.id);
+ let entity2Res = await autumn.entities.get(customerId, entity2.id);
- expectFeaturesCorrect({
- customer: entity1Res,
- product: pro,
- usage: [
- {
- featureId: TestFeature.Words,
- value: entity1Usage,
- },
- ],
- });
+ expectFeaturesCorrect({
+ customer: entity1Res,
+ product: pro,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: entity1Usage,
+ },
+ ],
+ });
- expectFeaturesCorrect({
- customer: entity2Res,
- product: pro,
- });
- });
+ expectFeaturesCorrect({
+ customer: entity2Res,
+ product: pro,
+ });
+ });
- let entity2Usage = Math.random() * 1000000;
- it("should track usage on entity 2", async function () {
- await autumn.track({
- customer_id: customerId,
- entity_id: entity2.id,
- feature_id: TestFeature.Words,
- value: entity2Usage,
- });
+ let entity2Usage = Math.random() * 1000000;
+ it("should track usage on entity 2", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ entity_id: entity2.id,
+ feature_id: TestFeature.Words,
+ value: entity2Usage,
+ });
- await timeout(3000);
+ await timeout(3000);
- let entity1Res = await autumn.entities.get(customerId, entity1.id);
- let entity2Res = await autumn.entities.get(customerId, entity2.id);
+ let entity1Res = await autumn.entities.get(customerId, entity1.id);
+ let entity2Res = await autumn.entities.get(customerId, entity2.id);
- expectFeaturesCorrect({
- customer: entity1Res,
- product: pro,
- usage: [
- {
- featureId: TestFeature.Words,
- value: entity1Usage,
- },
- ],
- });
+ expectFeaturesCorrect({
+ customer: entity1Res,
+ product: pro,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: entity1Usage,
+ },
+ ],
+ });
- expectFeaturesCorrect({
- customer: entity2Res,
- product: pro,
- usage: [
- {
- featureId: TestFeature.Words,
- value: entity2Usage,
- },
- ],
- });
- });
+ expectFeaturesCorrect({
+ customer: entity2Res,
+ product: pro,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: entity2Usage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/entities/entity5.ts b/server/tests/attach/entities/entity5.ts
index bdaefe613..12cf425c2 100644
--- a/server/tests/attach/entities/entity5.ts
+++ b/server/tests/attach/entities/entity5.ts
@@ -20,149 +20,149 @@ import { addHours, addMonths } from "date-fns";
const testCase = "aentity5";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 1500,
- }),
- ],
- type: "premium",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 1500,
+ }),
+ ],
+ type: "premium",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing downgrade entity product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1;
- });
+ testClockId = testClockId1;
+ });
- const newEntities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let entity1 = newEntities[0];
- let entity2 = newEntities[1];
+ let entity1 = newEntities[0];
+ let entity2 = newEntities[1];
- it("should attach premium product to entity 1", async function () {
- await autumn.entities.create(customerId, newEntities);
+ it("should attach premium product to entity 1", async function () {
+ await autumn.entities.create(customerId, newEntities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: entity1.id,
- numSubs: 1,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: entity1.id,
+ numSubs: 1,
+ });
+ });
- it("should attach premium product to entity 2", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: entity2.id,
- numSubs: 2,
- });
- });
+ it("should attach premium product to entity 2", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: entity2.id,
+ numSubs: 2,
+ });
+ });
- it("should attach pro product to entity 1", async function () {
- await autumn.attach({
- customer_id: customerId,
- entity_id: entity1.id,
- product_id: pro.id,
- });
+ it("should attach pro product to entity 1", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ entity_id: entity1.id,
+ product_id: pro.id,
+ });
- const entity = await autumn.entities.get(customerId, entity1.id);
- const proProd = entity.products.find((p: any) => p.id == pro.id);
- expect(proProd).to.exist;
- expect(proProd.status).to.equal(CusProductStatus.Scheduled);
- });
+ const entity = await autumn.entities.get(customerId, entity1.id);
+ const proProd = entity.products.find((p: any) => p.id == pro.id);
+ expect(proProd).to.exist;
+ expect(proProd.status).to.equal(CusProductStatus.Scheduled);
+ });
- it("should advance test clock and have pro attached to entity 1", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ it("should advance test clock and have pro attached to entity 1", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const entity = await autumn.entities.get(customerId, entity1.id);
- const proProd = entity.products.find((p: any) => p.id == pro.id);
- expect(proProd).to.exist;
- expect(proProd.status).to.equal(CusProductStatus.Active);
- expect(entity.products.length).to.equal(2);
+ const entity = await autumn.entities.get(customerId, entity1.id);
+ const proProd = entity.products.find((p: any) => p.id == pro.id);
+ expect(proProd).to.exist;
+ expect(proProd.status).to.equal(CusProductStatus.Active);
+ expect(entity.products.length).to.equal(2);
- const entity2Res = await autumn.entities.get(customerId, entity2.id);
- const premiumProd = entity2Res.products.find(
- (p: any) => p.id == premium.id
- );
- expect(premiumProd).to.exist;
- expect(premiumProd.status).to.equal(CusProductStatus.Active);
- expect(entity2Res.products.length).to.equal(2);
- });
+ const entity2Res = await autumn.entities.get(customerId, entity2.id);
+ const premiumProd = entity2Res.products.find(
+ (p: any) => p.id == premium.id,
+ );
+ expect(premiumProd).to.exist;
+ expect(premiumProd.status).to.equal(CusProductStatus.Active);
+ expect(entity2Res.products.length).to.equal(2);
+ });
});
diff --git a/server/tests/attach/free/free1.ts b/server/tests/attach/free/free1.ts
index 3c6539f9c..e171b1d69 100644
--- a/server/tests/attach/free/free1.ts
+++ b/server/tests/attach/free/free1.ts
@@ -1,13 +1,13 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CreateFreeTrialSchema,
- CusProductStatus,
- FreeTrialDuration,
- Organization,
- organizations,
+ APIVersion,
+ AppEnv,
+ CreateFreeTrialSchema,
+ CusProductStatus,
+ FreeTrialDuration,
+ Organization,
+ organizations,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -27,185 +27,185 @@ import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
const testCase = "free1";
let trial1 = CreateFreeTrialSchema.parse({
- length: 7,
- duration: FreeTrialDuration.Day,
+ length: 7,
+ duration: FreeTrialDuration.Day,
});
export let free = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- isDefault: false,
- freeTrial: trial1,
- type: "free",
- id: "enterprise_trial",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ isDefault: false,
+ freeTrial: trial1,
+ type: "free",
+ id: "enterprise_trial",
});
export let addOn = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Credits,
- includedUsage: 1000,
- }),
- ],
- isDefault: false,
- type: "free",
- isAddOn: true,
- id: "add_on",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Credits,
+ includedUsage: 1000,
+ }),
+ ],
+ isDefault: false,
+ type: "free",
+ isAddOn: true,
+ id: "add_on",
});
describe(`${chalk.yellowBright(`${testCase}: Testing free product with trial and attaching add on`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await db
- .update(organizations)
- .set({
- config: {
- ...this.org.config,
- multiple_trials: true,
- },
- })
- .where(eq(organizations.id, org.id));
+ await db
+ .update(organizations)
+ .set({
+ config: {
+ ...this.org.config,
+ multiple_trials: true,
+ },
+ })
+ .where(eq(organizations.id, org.id));
- await clearOrgCache({
- db,
- orgId: org.id,
- env,
- });
+ await clearOrgCache({
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [free, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, addOn],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free, addOn],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const approximateDiff = 1000 * 60 * 30; // 30 minutes
- it("should attach free product with trial", async function () {
- let attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: free.id,
- });
+ const approximateDiff = 1000 * 60 * 30; // 30 minutes
+ it("should attach free product with trial", async function () {
+ let attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- let attach = await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ let attach = await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- let customer = await autumn.customers.get(customerId);
- let freeProduct = customer.products.find((p) => p.id === free.id);
+ let customer = await autumn.customers.get(customerId);
+ let freeProduct = customer.products.find((p) => p.id === free.id);
- expect(freeProduct).to.exist;
- expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
- expect(freeProduct?.current_period_end).to.approximately(
- addDays(Date.now(), trial1.length).getTime(),
- approximateDiff
- );
- });
+ expect(freeProduct).to.exist;
+ expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
+ expect(freeProduct?.current_period_end).to.approximately(
+ addDays(Date.now(), trial1.length).getTime(),
+ approximateDiff,
+ );
+ });
- const trial2 = CreateFreeTrialSchema.parse({
- length: 14,
- duration: FreeTrialDuration.Day,
- });
+ const trial2 = CreateFreeTrialSchema.parse({
+ length: 14,
+ duration: FreeTrialDuration.Day,
+ });
- it("should update free product's trial end date", async function () {
- let attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: free.id,
- free_trial: trial2,
- is_custom: true,
- });
+ it("should update free product's trial end date", async function () {
+ let attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: free.id,
+ free_trial: trial2,
+ is_custom: true,
+ });
- let attach = await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- free_trial: trial2,
- is_custom: true,
- });
+ let attach = await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ free_trial: trial2,
+ is_custom: true,
+ });
- let customer = await autumn.customers.get(customerId);
- let freeProduct = customer.products.find((p) => p.id === free.id);
+ let customer = await autumn.customers.get(customerId);
+ let freeProduct = customer.products.find((p) => p.id === free.id);
- expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
- expect(freeProduct?.current_period_end).to.approximately(
- addDays(Date.now(), trial2.length).getTime(),
- approximateDiff
- );
- });
+ expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
+ expect(freeProduct?.current_period_end).to.approximately(
+ addDays(Date.now(), trial2.length).getTime(),
+ approximateDiff,
+ );
+ });
- it("should attach add on product", async function () {
- let attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: addOn.id,
- });
+ it("should attach add on product", async function () {
+ let attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: addOn.id,
+ });
- let attach = await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- });
+ let attach = await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ });
- let customer = await autumn.customers.get(customerId);
- let addOnProduct = customer.products.find((p) => p.id === addOn.id);
- let freeProduct = customer.products.find((p) => p.id === free.id);
+ let customer = await autumn.customers.get(customerId);
+ let addOnProduct = customer.products.find((p) => p.id === addOn.id);
+ let freeProduct = customer.products.find((p) => p.id === free.id);
- expect(addOnProduct).to.exist;
- expect(addOnProduct?.status).to.equal(CusProductStatus.Active);
- expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
- });
+ expect(addOnProduct).to.exist;
+ expect(addOnProduct?.status).to.equal(CusProductStatus.Active);
+ expect(freeProduct?.status).to.equal(CusProductStatus.Trialing);
+ });
- after(async function () {
- await db
- .update(organizations)
- .set({
- config: {
- ...this.org.config,
- multiple_trials: false,
- },
- })
- .where(eq(organizations.id, org.id));
+ after(async function () {
+ await db
+ .update(organizations)
+ .set({
+ config: {
+ ...this.org.config,
+ multiple_trials: false,
+ },
+ })
+ .where(eq(organizations.id, org.id));
- await clearOrgCache({
- db,
- orgId: org.id,
- env,
- });
+ await clearOrgCache({
+ db,
+ orgId: org.id,
+ env,
+ });
- await CacheManager.disconnect();
- });
+ await CacheManager.disconnect();
+ });
});
diff --git a/server/tests/attach/free/free2.ts b/server/tests/attach/free/free2.ts
index 9409e4f77..d0e87fdc8 100644
--- a/server/tests/attach/free/free2.ts
+++ b/server/tests/attach/free/free2.ts
@@ -1,10 +1,10 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- Organization,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -21,15 +21,15 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
export const free = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- isDefault: false,
- type: "free",
- id: "free",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ isDefault: false,
+ type: "free",
+ id: "free",
});
// export let addOn = constructProduct({
// items: [
@@ -46,83 +46,83 @@ export const free = constructProduct({
const testCase = "free2";
describe(`${chalk.yellowBright(`${testCase}: Testing free product with trial and attaching add on`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const approximateDiff = 1000 * 60 * 30; // 30 minutes
- it("should attach free product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ const approximateDiff = 1000 * 60 * 30; // 30 minutes
+ it("should attach free product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: free,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: free,
+ });
+ });
- const customItems = [
- ...free.items,
- constructPriceItem({
- price: 100,
- interval: BillingInterval.Month,
- }),
- ];
- it("should update free product with price", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: free,
- db,
- org,
- env,
- attachParams: {
- // @ts-ignore
- is_custom: true,
- items: customItems,
- },
- });
- });
+ const customItems = [
+ ...free.items,
+ constructPriceItem({
+ price: 100,
+ interval: BillingInterval.Month,
+ }),
+ ];
+ it("should update free product with price", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: free,
+ db,
+ org,
+ env,
+ attachParams: {
+ // @ts-ignore
+ is_custom: true,
+ items: customItems,
+ },
+ });
+ });
});
diff --git a/server/tests/attach/migrations/migration1.ts b/server/tests/attach/migrations/migration1.ts
index e902f1d0e..111b1417f 100644
--- a/server/tests/attach/migrations/migration1.ts
+++ b/server/tests/attach/migrations/migration1.ts
@@ -20,167 +20,167 @@ import { runMigrationTest } from "./runMigrationTest.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let messagesItem = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
}) as LimitedItem;
let wordsItem = constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 100,
+ featureId: TestFeature.Words,
+ includedUsage: 100,
}) as LimitedItem;
export let free = constructProduct({
- items: [messagesItem, wordsItem],
- type: "free",
- isDefault: false,
+ items: [messagesItem, wordsItem],
+ type: "free",
+ isDefault: false,
});
const testCase = "migrations1";
describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [free],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [free],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach free product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: free,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: true,
- });
- });
+ it("should attach free product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: free,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: true,
+ });
+ });
- let newFree: ProductV2;
- let increaseMessagesBy = 100;
- let reduceWordsBy = 50;
- it("should update product to new version", async function () {
- newFree = structuredClone(free);
+ let newFree: ProductV2;
+ let increaseMessagesBy = 100;
+ let reduceWordsBy = 50;
+ it("should update product to new version", async function () {
+ newFree = structuredClone(free);
- let newItems = replaceItems({
- items: free.items,
- featureId: TestFeature.Messages,
- newItem: constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage:
- (messagesItem.included_usage as number) + increaseMessagesBy,
- }),
- });
+ let newItems = replaceItems({
+ items: free.items,
+ featureId: TestFeature.Messages,
+ newItem: constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage:
+ (messagesItem.included_usage as number) + increaseMessagesBy,
+ }),
+ });
- newItems = replaceItems({
- items: newItems,
- featureId: TestFeature.Words,
- newItem: constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: (wordsItem.included_usage as number) - reduceWordsBy,
- }),
- });
+ newItems = replaceItems({
+ items: newItems,
+ featureId: TestFeature.Words,
+ newItem: constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: (wordsItem.included_usage as number) - reduceWordsBy,
+ }),
+ });
- newFree.items = newItems;
+ newFree.items = newItems;
- await autumn.products.update(free.id, {
- items: newItems,
- });
- });
+ await autumn.products.update(free.id, {
+ items: newItems,
+ });
+ });
- it("should attach track usage and get correct balance", async function () {
- let wordsUsage = 25;
- let messagesUsage = 20;
- await autumn.track({
- customer_id: customerId,
- value: wordsUsage,
- feature_id: TestFeature.Words,
- });
+ it("should attach track usage and get correct balance", async function () {
+ let wordsUsage = 25;
+ let messagesUsage = 20;
+ await autumn.track({
+ customer_id: customerId,
+ value: wordsUsage,
+ feature_id: TestFeature.Words,
+ });
- await autumn.track({
- customer_id: customerId,
- value: messagesUsage,
- feature_id: TestFeature.Messages,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value: messagesUsage,
+ feature_id: TestFeature.Messages,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(Date.now(), 1).getTime(),
- waitForSeconds: 30,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(Date.now(), 1).getTime(),
+ waitForSeconds: 30,
+ });
- let customer = await autumn.customers.get(customerId);
+ let customer = await autumn.customers.get(customerId);
- await autumn.migrate({
- from_product_id: free.id,
- to_product_id: newFree.id,
- from_version: 1,
- to_version: 2,
- });
+ await autumn.migrate({
+ from_product_id: free.id,
+ to_product_id: newFree.id,
+ from_version: 1,
+ to_version: 2,
+ });
- await timeout(4000);
+ await timeout(4000);
- // 1. Get features
- customer = await autumn.customers.get(customerId);
+ // 1. Get features
+ customer = await autumn.customers.get(customerId);
- await runMigrationTest({
- autumn,
- stripeCli,
- customerId,
- fromProduct: free,
- toProduct: newFree,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Words,
- value: wordsUsage,
- },
- {
- featureId: TestFeature.Messages,
- value: messagesUsage,
- },
- ],
- });
- });
+ await runMigrationTest({
+ autumn,
+ stripeCli,
+ customerId,
+ fromProduct: free,
+ toProduct: newFree,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: wordsUsage,
+ },
+ {
+ featureId: TestFeature.Messages,
+ value: messagesUsage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/migrations/migration2.ts b/server/tests/attach/migrations/migration2.ts
index 42fea7f29..88e40bc17 100644
--- a/server/tests/attach/migrations/migration2.ts
+++ b/server/tests/attach/migrations/migration2.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- AppEnv,
- BillingInterval,
- Organization,
- ProductItemInterval,
- ProductV2,
+ AppEnv,
+ BillingInterval,
+ Organization,
+ ProductItemInterval,
+ ProductV2,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,140 +25,140 @@ import { runMigrationTest } from "./runMigrationTest.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let wordsItem = constructArrearItem({
- featureId: TestFeature.Words,
+ featureId: TestFeature.Words,
});
export let pro = constructProduct({
- items: [wordsItem],
- type: "pro",
- isDefault: false,
+ items: [wordsItem],
+ type: "pro",
+ isDefault: false,
});
const testCase = "migrations2";
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach free product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach free product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let newPro: ProductV2;
- let increaseWordsBy = 1500;
- it("should update product to new version", async function () {
- newPro = structuredClone(pro);
+ let newPro: ProductV2;
+ let increaseWordsBy = 1500;
+ it("should update product to new version", async function () {
+ newPro = structuredClone(pro);
- let newItems = replaceItems({
- items: pro.items,
- featureId: TestFeature.Words,
- newItem: constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
- }),
- });
+ let newItems = replaceItems({
+ items: pro.items,
+ featureId: TestFeature.Words,
+ newItem: constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
+ }),
+ });
- newItems = replaceItems({
- items: newItems,
- interval: BillingInterval.Month,
- newItem: {
- price: 50,
- interval: ProductItemInterval.Month,
- },
- });
+ newItems = replaceItems({
+ items: newItems,
+ interval: BillingInterval.Month,
+ newItem: {
+ price: 50,
+ interval: ProductItemInterval.Month,
+ },
+ });
- newPro.items = newItems;
- await autumn.products.update(pro.id, {
- items: newItems,
- });
- });
+ newPro.items = newItems;
+ await autumn.products.update(pro.id, {
+ items: newItems,
+ });
+ });
- it("should attach track usage and get correct balance", async function () {
- let wordsUsage = 120000;
- await autumn.track({
- customer_id: customerId,
- value: wordsUsage,
- feature_id: TestFeature.Words,
- });
+ it("should attach track usage and get correct balance", async function () {
+ let wordsUsage = 120000;
+ await autumn.track({
+ customer_id: customerId,
+ value: wordsUsage,
+ feature_id: TestFeature.Words,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(Date.now(), 1).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(Date.now(), 1).getTime(),
+ });
- await autumn.migrate({
- from_product_id: pro.id,
- to_product_id: newPro.id,
- from_version: 1,
- to_version: 2,
- });
+ await autumn.migrate({
+ from_product_id: pro.id,
+ to_product_id: newPro.id,
+ from_version: 1,
+ to_version: 2,
+ });
- await timeout(4000);
+ await timeout(4000);
- await runMigrationTest({
- autumn,
- stripeCli,
- customerId,
- fromProduct: pro,
- toProduct: newPro,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Words,
- value: wordsUsage,
- },
- ],
- });
- });
+ await runMigrationTest({
+ autumn,
+ stripeCli,
+ customerId,
+ fromProduct: pro,
+ toProduct: newPro,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: wordsUsage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/migrations/migration3.ts b/server/tests/attach/migrations/migration3.ts
index 3f0dcde1a..529926a77 100644
--- a/server/tests/attach/migrations/migration3.ts
+++ b/server/tests/attach/migrations/migration3.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- AppEnv,
- BillingInterval,
- Organization,
- ProductItemInterval,
- ProductV2,
+ AppEnv,
+ BillingInterval,
+ Organization,
+ ProductItemInterval,
+ ProductV2,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -26,135 +26,135 @@ import { addDays } from "date-fns";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let wordsItem = constructArrearItem({
- featureId: TestFeature.Words,
+ featureId: TestFeature.Words,
});
export let pro = constructProduct({
- items: [wordsItem],
- type: "pro",
- isDefault: false,
- trial: true,
+ items: [wordsItem],
+ type: "pro",
+ isDefault: false,
+ trial: true,
});
const testCase = "migrations3";
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with trial`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach free product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach free product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let newPro: ProductV2;
- let increaseWordsBy = 1500;
- it("should update product to new version", async function () {
- newPro = structuredClone(pro);
+ let newPro: ProductV2;
+ let increaseWordsBy = 1500;
+ it("should update product to new version", async function () {
+ newPro = structuredClone(pro);
- let newItems = replaceItems({
- items: pro.items,
- featureId: TestFeature.Words,
- newItem: constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
- }),
- });
+ let newItems = replaceItems({
+ items: pro.items,
+ featureId: TestFeature.Words,
+ newItem: constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
+ }),
+ });
- newItems = replaceItems({
- items: newItems,
- interval: BillingInterval.Month,
- newItem: {
- price: 50,
- interval: ProductItemInterval.Month,
- },
- });
+ newItems = replaceItems({
+ items: newItems,
+ interval: BillingInterval.Month,
+ newItem: {
+ price: 50,
+ interval: ProductItemInterval.Month,
+ },
+ });
- newPro.items = newItems;
- newPro.version = 2;
- await autumn.products.update(pro.id, {
- items: newItems,
- });
- });
+ newPro.items = newItems;
+ newPro.version = 2;
+ await autumn.products.update(pro.id, {
+ items: newItems,
+ });
+ });
- it("should attach track usage and get correct balance", async function () {
- let wordsUsage = 120000;
- await autumn.track({
- customer_id: customerId,
- value: wordsUsage,
- feature_id: TestFeature.Words,
- });
+ it("should attach track usage and get correct balance", async function () {
+ let wordsUsage = 120000;
+ await autumn.track({
+ customer_id: customerId,
+ value: wordsUsage,
+ feature_id: TestFeature.Words,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(Date.now(), 4).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(Date.now(), 4).getTime(),
+ });
- // await timeout(5000);
+ // await timeout(5000);
- await runMigrationTest({
- autumn,
- stripeCli,
- customerId,
- fromProduct: pro,
- toProduct: newPro,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Words,
- value: wordsUsage,
- },
- ],
- });
- });
+ await runMigrationTest({
+ autumn,
+ stripeCli,
+ customerId,
+ fromProduct: pro,
+ toProduct: newPro,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: wordsUsage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/migrations/migration4.ts b/server/tests/attach/migrations/migration4.ts
index fd9413066..f30fdcf3f 100644
--- a/server/tests/attach/migrations/migration4.ts
+++ b/server/tests/attach/migrations/migration4.ts
@@ -16,121 +16,121 @@ import { timeout } from "@/utils/genUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let wordsItem = constructArrearItem({
- featureId: TestFeature.Words,
+ featureId: TestFeature.Words,
});
export let pro = constructProduct({
- items: [wordsItem],
- type: "pro",
- isDefault: false,
+ items: [wordsItem],
+ type: "pro",
+ isDefault: false,
});
let newWordsItem = constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 120100,
+ featureId: TestFeature.Words,
+ includedUsage: 120100,
});
let proWithTrial = constructProduct({
- items: [newWordsItem],
- type: "pro",
- isDefault: false,
- trial: true,
+ items: [newWordsItem],
+ type: "pro",
+ isDefault: false,
+ trial: true,
});
const testCase = "migrations4";
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro -> pro with trial (should not start trial)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proWithTrial],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proWithTrial],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should update product to new version", async function () {
- proWithTrial.version = 2;
- await autumn.products.update(pro.id, {
- items: proWithTrial.items,
- free_trial: proWithTrial.free_trial,
- });
- });
+ it("should update product to new version", async function () {
+ proWithTrial.version = 2;
+ await autumn.products.update(pro.id, {
+ items: proWithTrial.items,
+ free_trial: proWithTrial.free_trial,
+ });
+ });
- it("should attach track usage and get correct balance", async function () {
- let wordsUsage = 120000;
- await autumn.track({
- customer_id: customerId,
- value: wordsUsage,
- feature_id: TestFeature.Words,
- });
+ it("should attach track usage and get correct balance", async function () {
+ let wordsUsage = 120000;
+ await autumn.track({
+ customer_id: customerId,
+ value: wordsUsage,
+ feature_id: TestFeature.Words,
+ });
- await timeout(4000);
+ await timeout(4000);
- const { stripeSubs, cusProduct } = await runMigrationTest({
- autumn,
- stripeCli,
- customerId,
- fromProduct: pro,
- toProduct: proWithTrial,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Words,
- value: wordsUsage,
- },
- ],
- });
+ const { stripeSubs, cusProduct } = await runMigrationTest({
+ autumn,
+ stripeCli,
+ customerId,
+ fromProduct: pro,
+ toProduct: proWithTrial,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: wordsUsage,
+ },
+ ],
+ });
- expect(stripeSubs[0].trial_end).to.equal(null);
- expect(cusProduct?.free_trial).to.equal(null);
- });
+ expect(stripeSubs[0].trial_end).to.equal(null);
+ expect(cusProduct?.free_trial).to.equal(null);
+ });
});
diff --git a/server/tests/attach/migrations/runMigrationTest.ts b/server/tests/attach/migrations/runMigrationTest.ts
index 9d53e0e4b..e8873c301 100644
--- a/server/tests/attach/migrations/runMigrationTest.ts
+++ b/server/tests/attach/migrations/runMigrationTest.ts
@@ -3,8 +3,8 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { ProductV2, Organization } from "@autumn/shared";
import { AppEnv } from "autumn-js";
import {
- expectSubItemsCorrect,
- getSubsFromCusId,
+ expectSubItemsCorrect,
+ getSubsFromCusId,
} from "tests/utils/expectUtils/expectSubUtils.js";
import Stripe from "stripe";
import { expect } from "chai";
@@ -17,116 +17,116 @@ import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSub
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
export const expectSubsSame = ({
- subsBefore,
- subsAfter,
+ subsBefore,
+ subsAfter,
}: {
- subsBefore: Stripe.Subscription[];
- subsAfter: Stripe.Subscription[];
+ subsBefore: Stripe.Subscription[];
+ subsAfter: Stripe.Subscription[];
}) => {
- // let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
- // let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
- let subIdsBefore = subsBefore.map((sub) => sub.id);
- let subIdsAfter = subsAfter.map((sub) => sub.id);
- const periodsBefore = subsBefore.map((sub) => subToPeriodStartEnd({ sub }));
- const periodsAfter = subsAfter.map((sub) => subToPeriodStartEnd({ sub }));
+ // let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
+ // let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
+ let subIdsBefore = subsBefore.map((sub) => sub.id);
+ let subIdsAfter = subsAfter.map((sub) => sub.id);
+ const periodsBefore = subsBefore.map((sub) => subToPeriodStartEnd({ sub }));
+ const periodsAfter = subsAfter.map((sub) => subToPeriodStartEnd({ sub }));
- // expect(invoicesAfter).to.deep.equal(invoicesBefore);
- expect(subIdsAfter).to.deep.equal(subIdsBefore);
- expect(periodsBefore).to.deep.equal(periodsAfter);
+ // expect(invoicesAfter).to.deep.equal(invoicesBefore);
+ expect(subIdsAfter).to.deep.equal(subIdsBefore);
+ expect(periodsBefore).to.deep.equal(periodsAfter);
};
export const runMigrationTest = async ({
- autumn,
- stripeCli,
- customerId,
- fromProduct,
- toProduct,
- db,
- org,
- env,
- usage,
- numInvoices = 1,
+ autumn,
+ stripeCli,
+ customerId,
+ fromProduct,
+ toProduct,
+ db,
+ org,
+ env,
+ usage,
+ numInvoices = 1,
}: {
- autumn: AutumnInt;
- stripeCli: Stripe;
- customerId: string;
- fromProduct: ProductV2;
- toProduct: ProductV2;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- usage?: {
- featureId: string;
- value: number;
- }[];
- numInvoices?: number;
+ autumn: AutumnInt;
+ stripeCli: Stripe;
+ customerId: string;
+ fromProduct: ProductV2;
+ toProduct: ProductV2;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ usage?: {
+ featureId: string;
+ value: number;
+ }[];
+ numInvoices?: number;
}) => {
- const { subs: subsBefore } = await getSubsFromCusId({
- stripeCli,
- customerId,
- productId: fromProduct.id,
- db,
- org,
- env,
- });
+ const { subs: subsBefore } = await getSubsFromCusId({
+ stripeCli,
+ customerId,
+ productId: fromProduct.id,
+ db,
+ org,
+ env,
+ });
- const cusBefore = await autumn.customers.get(customerId);
+ const cusBefore = await autumn.customers.get(customerId);
- await autumn.migrate({
- from_product_id: fromProduct.id,
- to_product_id: toProduct.id,
- from_version: fromProduct.version,
- to_version: toProduct.version,
- });
+ await autumn.migrate({
+ from_product_id: fromProduct.id,
+ to_product_id: toProduct.id,
+ from_version: fromProduct.version,
+ to_version: toProduct.version,
+ });
- await timeout(10000);
+ await timeout(10000);
- const { subs: subsAfter } = await getSubsFromCusId({
- stripeCli,
- customerId,
- productId: toProduct.id,
- db,
- org,
- env,
- });
+ const { subs: subsAfter } = await getSubsFromCusId({
+ stripeCli,
+ customerId,
+ productId: toProduct.id,
+ db,
+ org,
+ env,
+ });
- expectSubsSame({ subsBefore, subsAfter });
+ expectSubsSame({ subsBefore, subsAfter });
- const cusAfter = await autumn.customers.get(customerId);
+ const cusAfter = await autumn.customers.get(customerId);
- expectFeaturesCorrect({
- customer: cusAfter,
- product: toProduct,
- usage,
- });
+ expectFeaturesCorrect({
+ customer: cusAfter,
+ product: toProduct,
+ usage,
+ });
- expectResetAtCorrect({ cusBefore, cusAfter });
- expectTrialEndsAtCorrect({ cusBefore, cusAfter });
+ expectResetAtCorrect({ cusBefore, cusAfter });
+ expectTrialEndsAtCorrect({ cusBefore, cusAfter });
- const { cusProduct } = await expectSubItemsCorrect({
- stripeCli,
- customerId,
- product: toProduct,
- db,
- org,
- env,
- });
+ const { cusProduct } = await expectSubItemsCorrect({
+ stripeCli,
+ customerId,
+ product: toProduct,
+ db,
+ org,
+ env,
+ });
- if (!isFreeProductV2({ product: toProduct })) {
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- }
+ if (!isFreeProductV2({ product: toProduct })) {
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ }
- // if (!isFreeProductV2({ product: toProduct })) {
- // expect(cusAfter.invoices.length).to.equal(numInvoices);
- // }
+ // if (!isFreeProductV2({ product: toProduct })) {
+ // expect(cusAfter.invoices.length).to.equal(numInvoices);
+ // }
- return {
- stripeSubs: subsAfter,
- cusProduct,
- };
+ return {
+ stripeSubs: subsAfter,
+ cusProduct,
+ };
};
diff --git a/server/tests/attach/multiProduct/multiProduct1.ts b/server/tests/attach/multiProduct/multiProduct1.ts
index e84332eb7..71fcf4e64 100644
--- a/server/tests/attach/multiProduct/multiProduct1.ts
+++ b/server/tests/attach/multiProduct/multiProduct1.ts
@@ -15,54 +15,54 @@ FLOW:
const testCase = "multiProduct1";
describe(
- chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`),
- () => {
- let customerId = testCase;
- let customer: Customer;
- before(async function () {
- await setupBefore(this);
- const res = await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- customer = res.customer;
- });
+ chalk.yellowBright(`${testCase}: Testing multi product attach, and upgrade`),
+ () => {
+ let customerId = testCase;
+ let customer: Customer;
+ before(async function () {
+ await setupBefore(this);
+ const res = await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ customer = res.customer;
+ });
- it("should attach pro group 1 and pro group 2", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productIds: [attachProducts.proGroup1.id, attachProducts.proGroup2.id],
- });
+ it("should attach pro group 1 and pro group 2", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productIds: [attachProducts.proGroup1.id, attachProducts.proGroup2.id],
+ });
- let cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({ sent: attachProducts.proGroup1, cusRes });
- compareMainProduct({ sent: attachProducts.proGroup2, cusRes });
- });
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({ sent: attachProducts.proGroup1, cusRes });
+ compareMainProduct({ sent: attachProducts.proGroup2, cusRes });
+ });
- it("should upgrade to premium group 1", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: attachProducts.premiumGroup1.id,
- });
+ it("should upgrade to premium group 1", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: attachProducts.premiumGroup1.id,
+ });
- // 1. Compare main product
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes });
- });
+ // 1. Compare main product
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes });
+ });
- it("should upgrade to premium group 2", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: attachProducts.premiumGroup2.id,
- });
+ it("should upgrade to premium group 2", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: attachProducts.premiumGroup2.id,
+ });
- // 1. Compare main product
- const cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes });
- });
- },
+ // 1. Compare main product
+ const cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes });
+ });
+ },
);
diff --git a/server/tests/attach/multiProduct/multiProduct2.ts b/server/tests/attach/multiProduct/multiProduct2.ts
index 0d123d487..c30013b4a 100644
--- a/server/tests/attach/multiProduct/multiProduct2.ts
+++ b/server/tests/attach/multiProduct/multiProduct2.ts
@@ -7,8 +7,8 @@ import { expect } from "chai";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { attachProducts } from "tests/global.js";
import {
- checkProductIsScheduled,
- compareMainProduct,
+ checkProductIsScheduled,
+ compareMainProduct,
} from "tests/utils/compare.js";
import { searchCusProducts } from "tests/utils/genUtils.js";
@@ -26,133 +26,133 @@ FLOW:
const testCase = "multiProduct2";
describe(`${chalk.yellowBright(
- "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free",
+ "multiProduct2: premium1->starter1, premium2->starter2, then premium2->pro2, then premium2->free",
)}`, () => {
- let customerId = testCase;
- let customer: Customer;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let customer: Customer;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- const res = await initCustomer({
- db: this.db,
- org: this.org,
- customerId,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- customer = res.customer;
- });
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ const res = await initCustomer({
+ db: this.db,
+ org: this.org,
+ customerId,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ customer = res.customer;
+ });
- it("should attach premium group 1 and premium group 2", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productIds: [
- attachProducts.premiumGroup1.id,
- attachProducts.premiumGroup2.id,
- ],
- });
+ it("should attach premium group 1 and premium group 2", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productIds: [
+ attachProducts.premiumGroup1.id,
+ attachProducts.premiumGroup2.id,
+ ],
+ });
- let cusRes = await AutumnCli.getCustomer(customerId);
- compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes });
- compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes });
- });
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes });
+ compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes });
+ });
- it("should downgrade to starter group 1 and starter group 2", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: attachProducts.starterGroup1.id,
- });
+ it("should downgrade to starter group 1 and starter group 2", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: attachProducts.starterGroup1.id,
+ });
- await AutumnCli.attach({
- customerId: customerId,
- productId: attachProducts.starterGroup2.id,
- });
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: attachProducts.starterGroup2.id,
+ });
- // Check starter group 1 scheduled and starter group 2 scheduled
- let cusRes = await AutumnCli.getCustomer(customerId);
- checkProductIsScheduled({
- product: attachProducts.starterGroup1,
- cusRes,
- });
- checkProductIsScheduled({
- product: attachProducts.starterGroup2,
- cusRes,
- });
+ // Check starter group 1 scheduled and starter group 2 scheduled
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ checkProductIsScheduled({
+ product: attachProducts.starterGroup1,
+ cusRes,
+ });
+ checkProductIsScheduled({
+ product: attachProducts.starterGroup2,
+ cusRes,
+ });
- // Check if scheduled id is the same
- const cusProducts = await CusProductService.list({
- db: this.db,
- internalCustomerId: customer.internal_id,
- inStatuses: [
- CusProductStatus.Active,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ],
- });
+ // Check if scheduled id is the same
+ const cusProducts = await CusProductService.list({
+ db: this.db,
+ internalCustomerId: customer.internal_id,
+ inStatuses: [
+ CusProductStatus.Active,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ],
+ });
- // 1. Pro group 1:
- const starter1 = searchCusProducts({
- cusProducts,
- productId: attachProducts.starterGroup1.id,
- });
+ // 1. Pro group 1:
+ const starter1 = searchCusProducts({
+ cusProducts,
+ productId: attachProducts.starterGroup1.id,
+ });
- const starter2 = searchCusProducts({
- cusProducts,
- productId: attachProducts.starterGroup2.id,
- });
+ const starter2 = searchCusProducts({
+ cusProducts,
+ productId: attachProducts.starterGroup2.id,
+ });
- expect(starter1).to.exist;
- expect(starter2).to.exist;
- expect(starter1?.scheduled_ids![0]).to.equal(starter2?.scheduled_ids![0]);
+ expect(starter1).to.exist;
+ expect(starter2).to.exist;
+ expect(starter1?.scheduled_ids![0]).to.equal(starter2?.scheduled_ids![0]);
- const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve(
- starter1?.scheduled_ids![0]!,
- );
+ const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve(
+ starter1?.scheduled_ids![0]!,
+ );
- // console.log(stripeSchedule);
- checkScheduleContainsProducts({
- db: this.db,
- schedule: stripeSchedule,
- productIds: [
- attachProducts.starterGroup1.id,
- attachProducts.starterGroup2.id,
- ],
- org: this.org,
- env: this.env,
- });
- });
+ // console.log(stripeSchedule);
+ checkScheduleContainsProducts({
+ db: this.db,
+ schedule: stripeSchedule,
+ productIds: [
+ attachProducts.starterGroup1.id,
+ attachProducts.starterGroup2.id,
+ ],
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should downgrade to free", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: attachProducts.freeGroup2.id,
- });
+ it("should downgrade to free", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: attachProducts.freeGroup2.id,
+ });
- let cusRes = await AutumnCli.getCustomer(customerId);
- checkProductIsScheduled({
- product: attachProducts.freeGroup2,
- cusRes,
- });
+ let cusRes = await AutumnCli.getCustomer(customerId);
+ checkProductIsScheduled({
+ product: attachProducts.freeGroup2,
+ cusRes,
+ });
- const cusProducts = await CusProductService.list({
- db: this.db,
- internalCustomerId: customer.internal_id,
- });
+ const cusProducts = await CusProductService.list({
+ db: this.db,
+ internalCustomerId: customer.internal_id,
+ });
- const starterGroup2 = searchCusProducts({
- cusProducts,
- productId: attachProducts.starterGroup2.id,
- });
+ const starterGroup2 = searchCusProducts({
+ cusProducts,
+ productId: attachProducts.starterGroup2.id,
+ });
- checkScheduleContainsProducts({
- db: this.db,
- scheduleId: starterGroup2?.scheduled_ids![0],
- productIds: [attachProducts.starterGroup2.id],
- org: this.org,
- env: this.env,
- });
- });
+ checkScheduleContainsProducts({
+ db: this.db,
+ scheduleId: starterGroup2?.scheduled_ids![0],
+ productIds: [attachProducts.starterGroup2.id],
+ org: this.org,
+ env: this.env,
+ });
+ });
});
diff --git a/server/tests/attach/newVersion/newVersion1.ts b/server/tests/attach/newVersion/newVersion1.ts
index cdaee2631..6dd4de492 100644
--- a/server/tests/attach/newVersion/newVersion1.ts
+++ b/server/tests/attach/newVersion/newVersion1.ts
@@ -2,11 +2,11 @@ import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- Organization,
- ProductV2,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ Organization,
+ ProductV2,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -27,168 +27,168 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
export let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const testCase = "newVersion1";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with new version`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let usage = 50000;
- let newPro: ProductV2;
- it("should update product to new version", async function () {
- newPro = structuredClone(pro);
- let newItems = replaceItems({
- items: pro.items,
- interval: BillingInterval.Month,
- newItem: constructPriceItem({
- price: 100,
- interval: BillingInterval.Month,
- }),
- });
+ let usage = 50000;
+ let newPro: ProductV2;
+ it("should update product to new version", async function () {
+ newPro = structuredClone(pro);
+ let newItems = replaceItems({
+ items: pro.items,
+ interval: BillingInterval.Month,
+ newItem: constructPriceItem({
+ price: 100,
+ interval: BillingInterval.Month,
+ }),
+ });
- newItems = replaceItems({
- items: newItems,
- featureId: TestFeature.Words,
- newItem: constructArrearItem({
- featureId: TestFeature.Words,
- price: 0.5,
- }),
- });
+ newItems = replaceItems({
+ items: newItems,
+ featureId: TestFeature.Words,
+ newItem: constructArrearItem({
+ featureId: TestFeature.Words,
+ price: 0.5,
+ }),
+ });
- newPro.version = 2;
- newPro.items = newItems;
+ newPro.version = 2;
+ newPro.items = newItems;
- await autumn.products.update(pro.id, {
- items: newItems,
- });
- });
+ await autumn.products.update(pro.id, {
+ items: newItems,
+ });
+ });
- it("should attach pro v2", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(Date.now(), 1).getTime(),
- });
+ it("should attach pro v2", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(Date.now(), 1).getTime(),
+ });
- await autumn.track({
- customer_id: customerId,
- value: usage,
- feature_id: TestFeature.Words,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value: usage,
+ feature_id: TestFeature.Words,
+ });
- await timeout(2000);
+ await timeout(2000);
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct: newPro,
- newVersion: 2,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct: newPro,
+ newVersion: 2,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should have correct invoice total on next cycle", async function () {
- const invoiceTotal = await getExpectedInvoiceTotal({
- org,
- env,
- customerId,
- productId: pro.id,
- stripeCli,
- db,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- onlyIncludeMonthly: true,
- });
+ it("should have correct invoice total on next cycle", async function () {
+ const invoiceTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ customerId,
+ productId: pro.id,
+ stripeCli,
+ db,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ onlyIncludeMonthly: true,
+ });
- let curUnix = Date.now();
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(curUnix, 1).getTime(),
- waitForSeconds: 30,
- });
+ let curUnix = Date.now();
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(curUnix, 1).getTime(),
+ waitForSeconds: 30,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
+ waitForSeconds: 10,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices[0];
- expect(invoice.total).to.equal(
- invoiceTotal,
- "invoice total after 1 cycle should be correct"
- );
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices[0];
+ expect(invoice.total).to.equal(
+ invoiceTotal,
+ "invoice total after 1 cycle should be correct",
+ );
+ });
});
diff --git a/server/tests/attach/newVersion/newVersion2.ts b/server/tests/attach/newVersion/newVersion2.ts
index 0ef09fbc8..3f7592e1e 100644
--- a/server/tests/attach/newVersion/newVersion2.ts
+++ b/server/tests/attach/newVersion/newVersion2.ts
@@ -2,11 +2,11 @@ import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- BillingInterval,
- Organization,
- ProductV2,
+ APIVersion,
+ AppEnv,
+ BillingInterval,
+ Organization,
+ ProductV2,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -23,142 +23,142 @@ import runUpdateEntsTest from "../updateEnts/expectUpdateEnts.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
export let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
const testCase = "newVersion2";
describe(`${chalk.yellowBright(`${testCase}: Testing attach new version for trial product`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let usage = 50000;
- let newPro: ProductV2;
- it("should update product to new version", async function () {
- newPro = structuredClone(pro);
- let newItems = replaceItems({
- items: pro.items,
- interval: BillingInterval.Month,
- newItem: constructPriceItem({
- price: 100,
- interval: BillingInterval.Month,
- }),
- });
+ let usage = 50000;
+ let newPro: ProductV2;
+ it("should update product to new version", async function () {
+ newPro = structuredClone(pro);
+ let newItems = replaceItems({
+ items: pro.items,
+ interval: BillingInterval.Month,
+ newItem: constructPriceItem({
+ price: 100,
+ interval: BillingInterval.Month,
+ }),
+ });
- newPro.version = 2;
- newPro.items = newItems;
+ newPro.version = 2;
+ newPro.items = newItems;
- await autumn.products.update(pro.id, {
- items: newItems,
- });
- });
+ await autumn.products.update(pro.id, {
+ items: newItems,
+ });
+ });
- return;
+ return;
- it("should attach pro v2", async function () {
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct: newPro,
- newVersion: 2,
- db,
- org,
- env,
- });
- });
+ it("should attach pro v2", async function () {
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct: newPro,
+ newVersion: 2,
+ db,
+ org,
+ env,
+ });
+ });
- // it("should have correct invoice total on next cycle", async function () {
- // const invoiceTotal = await getExpectedInvoiceTotal({
- // org,
- // env,
- // customerId,
- // productId: pro.id,
- // stripeCli,
- // db,
- // usage: [
- // {
- // featureId: TestFeature.Words,
- // value: usage,
- // },
- // ],
- // onlyIncludeMonthly: true,
- // });
+ // it("should have correct invoice total on next cycle", async function () {
+ // const invoiceTotal = await getExpectedInvoiceTotal({
+ // org,
+ // env,
+ // customerId,
+ // productId: pro.id,
+ // stripeCli,
+ // db,
+ // usage: [
+ // {
+ // featureId: TestFeature.Words,
+ // value: usage,
+ // },
+ // ],
+ // onlyIncludeMonthly: true,
+ // });
- // let curUnix = Date.now();
- // curUnix = await advanceTestClock({
- // stripeCli,
- // testClockId,
- // advanceTo: addMonths(curUnix, 1).getTime(),
- // waitForSeconds: 30,
- // });
+ // let curUnix = Date.now();
+ // curUnix = await advanceTestClock({
+ // stripeCli,
+ // testClockId,
+ // advanceTo: addMonths(curUnix, 1).getTime(),
+ // waitForSeconds: 30,
+ // });
- // await advanceTestClock({
- // stripeCli,
- // testClockId,
- // advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
- // waitForSeconds: 10,
- // });
+ // await advanceTestClock({
+ // stripeCli,
+ // testClockId,
+ // advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
+ // waitForSeconds: 10,
+ // });
- // const customer = await autumn.customers.get(customerId);
- // const invoice = customer.invoices[0];
- // expect(invoice.total).to.equal(
- // invoiceTotal,
- // "invoice total after 1 cycle should be correct"
- // );
- // });
+ // const customer = await autumn.customers.get(customerId);
+ // const invoice = customer.invoices[0];
+ // expect(invoice.total).to.equal(
+ // invoiceTotal,
+ // "invoice total after 1 cycle should be correct"
+ // );
+ // });
});
diff --git a/server/tests/attach/others/others1.ts b/server/tests/attach/others/others1.ts
index 6a1f463d6..67eaf5388 100644
--- a/server/tests/attach/others/others1.ts
+++ b/server/tests/attach/others/others1.ts
@@ -9,120 +9,120 @@ import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- expectDowngradeCorrect,
- expectNextCycleCorrect,
+ expectDowngradeCorrect,
+ expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
const testCase = "others1";
export let free = constructProduct({
- items: [],
- type: "free",
- isDefault: false,
+ items: [],
+ type: "free",
+ isDefault: false,
});
export let pro = constructProduct({
- items: [],
- type: "pro",
- trial: true,
+ items: [],
+ type: "pro",
+ trial: true,
});
export let premium = constructProduct({
- items: [],
- type: "premium",
- trial: true,
+ items: [],
+ type: "premium",
+ trial: true,
});
describe(`${chalk.yellowBright(`${testCase}: Testing trials: pro with trial -> premium with trial -> free`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free, pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn,
+ products: [free, pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product (with trial)", async function () {
- await attachAndExpectCorrect({
- autumn,
- stripeCli,
- customerId,
- product: pro,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product (with trial)", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ stripeCli,
+ customerId,
+ product: pro,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach premium product (with trial)", async function () {
- await attachAndExpectCorrect({
- autumn,
- stripeCli,
- customerId,
- product: premium,
- db,
- org,
- env,
- });
- });
+ it("should attach premium product (with trial)", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ stripeCli,
+ customerId,
+ product: premium,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach free product at the end of the trial", async function () {
- const { preview } = await expectDowngradeCorrect({
- autumn,
- stripeCli,
- customerId,
- curProduct: premium,
- newProduct: free,
- db,
- org,
- env,
- });
- expectNextCycleCorrect({
- autumn,
- preview,
- stripeCli,
- customerId,
- testClockId,
- product: free,
- db,
- org,
- env,
- });
- });
+ it("should attach free product at the end of the trial", async function () {
+ const { preview } = await expectDowngradeCorrect({
+ autumn,
+ stripeCli,
+ customerId,
+ curProduct: premium,
+ newProduct: free,
+ db,
+ org,
+ env,
+ });
+ expectNextCycleCorrect({
+ autumn,
+ preview,
+ stripeCli,
+ customerId,
+ testClockId,
+ product: free,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/others/others2.ts b/server/tests/attach/others/others2.ts
index 6dc49ab00..4ad607286 100644
--- a/server/tests/attach/others/others2.ts
+++ b/server/tests/attach/others/others2.ts
@@ -18,127 +18,127 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "others2";
export let oneOff = constructProduct({
- type: "one_off",
- items: [
- constructPrepaidItem({
- isOneOff: true,
- featureId: TestFeature.Messages,
- price: 8,
- billingUnits: 250,
- }),
- ],
+ type: "one_off",
+ items: [
+ constructPrepaidItem({
+ isOneOff: true,
+ featureId: TestFeature.Messages,
+ price: 8,
+ billingUnits: 250,
+ }),
+ ],
});
describe(`${chalk.yellowBright(`${testCase}: Testing one-off`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [oneOff],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [oneOff],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [oneOff],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn,
+ products: [oneOff],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: 500,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 500,
+ },
+ ];
- it("should attach one-off product", async function () {
- await attachAndExpectCorrect({
- autumn,
- stripeCli,
- customerId,
- product: oneOff,
- db,
- org,
- env,
- options,
- });
- });
+ it("should attach one-off product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ stripeCli,
+ customerId,
+ product: oneOff,
+ db,
+ org,
+ env,
+ options,
+ });
+ });
- const options2 = [
- {
- feature_id: TestFeature.Messages,
- quantity: 750,
- },
- ];
- it("should be able to attach again", async function () {
- await attachAndExpectCorrect({
- autumn,
- stripeCli,
- customerId,
- product: oneOff,
- db,
- org,
- env,
- options: options2,
- skipFeatureCheck: true,
- });
+ const options2 = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 750,
+ },
+ ];
+ it("should be able to attach again", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ stripeCli,
+ customerId,
+ product: oneOff,
+ db,
+ org,
+ env,
+ options: options2,
+ skipFeatureCheck: true,
+ });
- const totalBalance = options[0].quantity + options2[0].quantity;
- const customer = await autumn.customers.get(customerId);
+ const totalBalance = options[0].quantity + options2[0].quantity;
+ const customer = await autumn.customers.get(customerId);
- const balance = customer.features[TestFeature.Messages].balance;
- expect(balance).to.equal(totalBalance, "balance should be correct");
- });
+ const balance = customer.features[TestFeature.Messages].balance;
+ expect(balance).to.equal(totalBalance, "balance should be correct");
+ });
- // Payment failure
- it("should handle payment failure", async function () {
- let customer = await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ // Payment failure
+ it("should handle payment failure", async function () {
+ let customer = await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- await attachFailedPaymentMethod({
- stripeCli,
- customer: customer!,
- });
+ await attachFailedPaymentMethod({
+ stripeCli,
+ customer: customer!,
+ });
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: oneOff.id,
- options,
- });
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: oneOff.id,
+ options,
+ });
- expect(res.checkout_url).to.exist;
- });
+ expect(res.checkout_url).to.exist;
+ });
});
diff --git a/server/tests/attach/others/others3.ts b/server/tests/attach/others/others3.ts
index 0d1bd376f..cd67e0ef1 100644
--- a/server/tests/attach/others/others3.ts
+++ b/server/tests/attach/others/others3.ts
@@ -15,74 +15,74 @@ import { CusService } from "@/internal/customers/CusService.js";
const testCase = "others3";
export let pro = constructProduct({
- type: "pro",
- items: [],
+ type: "pro",
+ items: [],
});
describe(`${chalk.yellowBright(`${testCase}: Testing attach payment failure`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
- stripeCli = this.stripeCli;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- // Payment failure
- it("should handle payment failure", async function () {
- let customer = await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ // Payment failure
+ it("should handle payment failure", async function () {
+ let customer = await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- await attachFailedPaymentMethod({
- stripeCli,
- customer: customer!,
- });
+ await attachFailedPaymentMethod({
+ stripeCli,
+ customer: customer!,
+ });
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- // console.log(res);
+ // console.log(res);
- expect(res.checkout_url).to.exist;
- });
+ expect(res.checkout_url).to.exist;
+ });
});
diff --git a/server/tests/attach/others/others5.ts b/server/tests/attach/others/others5.ts
index 2323d8a24..aaa438495 100644
--- a/server/tests/attach/others/others5.ts
+++ b/server/tests/attach/others/others5.ts
@@ -7,240 +7,240 @@ import { expect } from "chai";
import { setupBefore } from "tests/before.js";
const checkEntitledOnProduct = async ({
- customerId,
- product,
- totalAllowance,
- finish = false,
- usageBased = false,
- timeoutMs = 8000,
+ customerId,
+ product,
+ totalAllowance,
+ finish = false,
+ usageBased = false,
+ timeoutMs = 8000,
}: {
- customerId: string;
- product: any;
- totalAllowance?: number;
- finish?: boolean;
- usageBased?: boolean;
- timeoutMs?: number;
+ customerId: string;
+ product: any;
+ totalAllowance?: number;
+ finish?: boolean;
+ usageBased?: boolean;
+ timeoutMs?: number;
}) => {
- // 1. Send events
- const allowance = totalAllowance || product.entitlements.metered1.allowance;
- // const randomNum = Math.floor(Math.random() * (allowance - 1));
- const randomNum = 3;
+ // 1. Send events
+ const allowance = totalAllowance || product.entitlements.metered1.allowance;
+ // const randomNum = Math.floor(Math.random() * (allowance - 1));
+ const randomNum = 3;
- const batchUpdates = [];
- for (let i = 0; i < randomNum; i++) {
- batchUpdates.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- })
- );
- }
+ const batchUpdates = [];
+ for (let i = 0; i < randomNum; i++) {
+ batchUpdates.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
- await Promise.all(batchUpdates);
- await timeout(timeoutMs);
- let used = randomNum;
+ await Promise.all(batchUpdates);
+ await timeout(timeoutMs);
+ let used = randomNum;
- // 2. Check entitled
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- true
- );
+ // 2. Check entitled
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ true,
+ );
- try {
- expect(allowed).to.be.true;
- expect(balanceObj!.balance).to.equal(allowance - randomNum);
+ try {
+ expect(allowed).to.be.true;
+ expect(balanceObj!.balance).to.equal(allowance - randomNum);
- if (!finish) {
- return used;
- }
- } catch (error) {
- console.group();
- console.group();
- console.log("Allowance: ", allowance, "Random num: ", randomNum);
- console.log("Expected balance to be: ", allowance - randomNum);
- console.log("Entitled res: ", { allowed, balanceObj });
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
+ if (!finish) {
+ return used;
+ }
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Allowance: ", allowance, "Random num: ", randomNum);
+ console.log("Expected balance to be: ", allowance - randomNum);
+ console.log("Entitled res: ", { allowed, balanceObj });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
- // Finish up
- const batchUpdates2 = [];
- for (let i = 0; i < allowance - randomNum; i++) {
- batchUpdates2.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- })
- );
- }
- await Promise.all(batchUpdates2);
- await timeout(timeoutMs);
- used += allowance - randomNum;
+ // Finish up
+ const batchUpdates2 = [];
+ for (let i = 0; i < allowance - randomNum; i++) {
+ batchUpdates2.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
+ await Promise.all(batchUpdates2);
+ await timeout(timeoutMs);
+ used += allowance - randomNum;
- // 3. Check entitled again
- const { allowed: allowed2, balanceObj: balanceObj2 }: any =
- await AutumnCli.entitled(customerId, features.metered1.id, true);
- try {
- if (usageBased) {
- expect(allowed2).to.be.true;
- } else {
- expect(allowed2).to.be.false;
- }
- expect(balanceObj2!.balance).to.equal(0);
- return used;
- } catch (error) {
- console.group();
- console.group();
- console.log("Expected balance to be: ", 0);
- console.log("Entitled res: ", { allowed2, balanceObj2 });
- console.groupEnd();
- console.groupEnd();
- throw error;
- }
+ // 3. Check entitled again
+ const { allowed: allowed2, balanceObj: balanceObj2 }: any =
+ await AutumnCli.entitled(customerId, features.metered1.id, true);
+ try {
+ if (usageBased) {
+ expect(allowed2).to.be.true;
+ } else {
+ expect(allowed2).to.be.false;
+ }
+ expect(balanceObj2!.balance).to.equal(0);
+ return used;
+ } catch (error) {
+ console.group();
+ console.group();
+ console.log("Expected balance to be: ", 0);
+ console.log("Entitled res: ", { allowed2, balanceObj2 });
+ console.groupEnd();
+ console.groupEnd();
+ throw error;
+ }
};
// TODO: Add test case for unlimited feature
const testCase = "others5";
describe(`${chalk.yellowBright(
- "others5: Testing /events and /entitled, for pro, one time top up"
+ "others5: Testing /events and /entitled, for pro, one time top up",
)}`, () => {
- const customerId = testCase;
+ const customerId = testCase;
- let curAllowance = 0;
- const oneTimeBillingUnits =
- products.oneTimeAddOnMetered1.prices[0].config.billing_units!;
- let oneTimeQuantity = 2 * oneTimeBillingUnits;
+ let curAllowance = 0;
+ const oneTimeBillingUnits =
+ products.oneTimeAddOnMetered1.prices[0].config.billing_units!;
+ let oneTimeQuantity = 2 * oneTimeBillingUnits;
- before(async function () {
- await setupBefore(this);
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- });
+ before(async function () {
+ await setupBefore(this);
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ });
- // it("should have correct entitlements (free)", async function () {
- // await checkEntitledOnProduct({
- // customerId: customerId,
- // product: products.free,
- // finish: true,
- // });
- // });
+ // it("should have correct entitlements (free)", async function () {
+ // await checkEntitledOnProduct({
+ // customerId: customerId,
+ // product: products.free,
+ // finish: true,
+ // });
+ // });
- it("should attach pro", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.pro.id,
- });
- });
+ it("should attach pro", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.pro.id,
+ });
+ });
- it("should have correct entitlements (pro)", async function () {
- const used = await checkEntitledOnProduct({
- customerId: customerId,
- product: products.pro,
- finish: false,
- });
+ it("should have correct entitlements (pro)", async function () {
+ const used = await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.pro,
+ finish: false,
+ });
- curAllowance = products.pro.entitlements.metered1.allowance! - used;
- });
+ curAllowance = products.pro.entitlements.metered1.allowance! - used;
+ });
- it("should attach one time top up", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.oneTimeAddOnMetered1.id,
- options: [
- {
- feature_id: features.metered1.id,
- quantity: oneTimeQuantity,
- },
- ],
- });
- });
+ it("should attach one time top up", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.oneTimeAddOnMetered1.id,
+ options: [
+ {
+ feature_id: features.metered1.id,
+ quantity: oneTimeQuantity,
+ },
+ ],
+ });
+ });
- it("should have correct entitlements (one time top up)", async function () {
- // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity;
+ it("should have correct entitlements (one time top up)", async function () {
+ // const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity;
- await checkEntitledOnProduct({
- customerId: customerId,
- product: products.oneTimeAddOnMetered1,
- finish: true,
- totalAllowance: curAllowance + oneTimeQuantity,
- timeoutMs: 15000,
- });
- });
+ await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.oneTimeAddOnMetered1,
+ finish: true,
+ totalAllowance: curAllowance + oneTimeQuantity,
+ timeoutMs: 15000,
+ });
+ });
});
describe(`${chalk.yellowBright(
- "others5: Testing /entitled & /events, for pro with overage"
+ "others5: Testing /entitled & /events, for pro with overage",
)}`, () => {
- const customerId = testCase;
+ const customerId = testCase;
- before(async function () {
- await setupBefore(this);
- await initCustomer({
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- autumn: this.autumnJs,
- attachPm: "success",
- });
- });
+ before(async function () {
+ await setupBefore(this);
+ await initCustomer({
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
+ });
- // PRO WITH OVERAGE
- it("should attach pro (with overage)", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.proWithOverage.id,
- });
- });
+ // PRO WITH OVERAGE
+ it("should attach pro (with overage)", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.proWithOverage.id,
+ });
+ });
- it("should have correct entitlements (pro with overage)", async function () {
- await checkEntitledOnProduct({
- customerId: customerId,
- product: products.proWithOverage,
- finish: true,
- totalAllowance: products.proWithOverage.entitlements.metered1.allowance!,
- usageBased: true,
- });
- });
+ it("should have correct entitlements (pro with overage)", async function () {
+ await checkEntitledOnProduct({
+ customerId: customerId,
+ product: products.proWithOverage,
+ finish: true,
+ totalAllowance: products.proWithOverage.entitlements.metered1.allowance!,
+ usageBased: true,
+ });
+ });
- it("should have correct usage-based balance (balance < 0)", async function () {
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- features.metered1.id,
- true
- );
+ it("should have correct usage-based balance (balance < 0)", async function () {
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ features.metered1.id,
+ true,
+ );
- expect(allowed).to.be.true;
- expect(balanceObj!.balance).to.equal(0);
+ expect(allowed).to.be.true;
+ expect(balanceObj!.balance).to.equal(0);
- // Sent 5 events
- const batchUpdates = [];
- for (let i = 0; i < 5; i++) {
- batchUpdates.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: features.metered1.eventName,
- })
- );
- }
+ // Sent 5 events
+ const batchUpdates = [];
+ for (let i = 0; i < 5; i++) {
+ batchUpdates.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: features.metered1.eventName,
+ }),
+ );
+ }
- await Promise.all(batchUpdates);
- await timeout(10000);
+ await Promise.all(batchUpdates);
+ await timeout(10000);
- const { allowed: allowed2, balanceObj: balanceObj2 }: any =
- await AutumnCli.entitled(customerId, features.metered1.id, true);
+ const { allowed: allowed2, balanceObj: balanceObj2 }: any =
+ await AutumnCli.entitled(customerId, features.metered1.id, true);
- expect(allowed2).to.be.true;
- expect(balanceObj2!.balance).to.equal(-5);
- expect(balanceObj2!.usage_allowed).to.be.true;
- });
+ expect(allowed2).to.be.true;
+ expect(balanceObj2!.balance).to.equal(-5);
+ expect(balanceObj2!.usage_allowed).to.be.true;
+ });
});
diff --git a/server/tests/attach/others/others6.ts b/server/tests/attach/others/others6.ts
index 4b90730c3..c16b6fe4a 100644
--- a/server/tests/attach/others/others6.ts
+++ b/server/tests/attach/others/others6.ts
@@ -14,121 +14,121 @@ import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { CusService } from "@/internal/customers/CusService.js";
export let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const testCase = "others6";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with customer ID and entity ID null`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- const email = `${customerId}@test.com`;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ const email = `${customerId}@test.com`;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- let customer = await CusService.getByEmail({
- db,
- orgId: org.id,
- env,
- email,
- });
+ let customer = await CusService.getByEmail({
+ db,
+ orgId: org.id,
+ env,
+ email,
+ });
- if (customer.length > 0) {
- await autumn.customers.delete(customer[0].internal_id);
- }
+ if (customer.length > 0) {
+ await autumn.customers.delete(customer[0].internal_id);
+ }
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ });
+ });
- let internalCustomerId = "";
- let internalEntityId = "";
- let entityId = "1";
- it("should attach create customer with no ID", async function () {
- let customer = await autumn.customers.create({
- // @ts-ignore
- id: null,
- email: `${customerId}@test.com`,
- name: customerId,
- });
+ let internalCustomerId = "";
+ let internalEntityId = "";
+ let entityId = "1";
+ it("should attach create customer with no ID", async function () {
+ let customer = await autumn.customers.create({
+ // @ts-ignore
+ id: null,
+ email: `${customerId}@test.com`,
+ name: customerId,
+ });
- expect(customer.autumn_id, "Customer ID should exist").to.exist;
+ expect(customer.autumn_id, "Customer ID should exist").to.exist;
- internalCustomerId = customer.autumn_id;
+ internalCustomerId = customer.autumn_id;
- let data = await autumn.entities.create(internalCustomerId, {
- // @ts-ignore
- id: null,
- feature_id: TestFeature.Users,
- });
+ let data = await autumn.entities.create(internalCustomerId, {
+ // @ts-ignore
+ id: null,
+ feature_id: TestFeature.Users,
+ });
- internalEntityId = data.autumn_id;
+ internalEntityId = data.autumn_id;
- expect(internalEntityId, "Entity ID should exist").to.exist;
- });
+ expect(internalEntityId, "Entity ID should exist").to.exist;
+ });
- it("should be able to attach pro product, invoice only", async function () {
- await autumn.attach({
- customer_id: internalCustomerId,
- entity_id: internalEntityId,
- product_id: pro.id,
- invoice: true,
- enable_product_immediately: true,
- });
+ it("should be able to attach pro product, invoice only", async function () {
+ await autumn.attach({
+ customer_id: internalCustomerId,
+ entity_id: internalEntityId,
+ product_id: pro.id,
+ invoice: true,
+ enable_product_immediately: true,
+ });
- const customer = await autumn.customers.get(internalCustomerId);
+ const customer = await autumn.customers.get(internalCustomerId);
- expectAttachCorrect({
- customer,
- product: pro,
- });
+ expectAttachCorrect({
+ customer,
+ product: pro,
+ });
- expect(customer.invoices.length).to.equal(1);
- expect(customer.invoices[0].status).to.equal("draft");
- });
+ expect(customer.invoices.length).to.equal(1);
+ expect(customer.invoices[0].status).to.equal("draft");
+ });
- it("should create customer with ID, and attach pro product", async function () {
- let customer = await autumn.customers.create({
- id: customerId,
- email: `${customerId}@test.com`,
- });
+ it("should create customer with ID, and attach pro product", async function () {
+ let customer = await autumn.customers.create({
+ id: customerId,
+ email: `${customerId}@test.com`,
+ });
- expect(customer.autumn_id).to.equal(internalCustomerId);
+ expect(customer.autumn_id).to.equal(internalCustomerId);
- let entity = await autumn.entities.create(customer.autumn_id, {
- id: entityId,
- feature_id: TestFeature.Users,
- });
+ let entity = await autumn.entities.create(customer.autumn_id, {
+ id: entityId,
+ feature_id: TestFeature.Users,
+ });
- internalEntityId = entity.autumn_id;
+ internalEntityId = entity.autumn_id;
- const customer2 = await autumn.customers.get(customerId);
+ const customer2 = await autumn.customers.get(customerId);
- expectAttachCorrect({
- customer: customer2,
- product: pro,
- entityId,
- });
- });
+ expectAttachCorrect({
+ customer: customer2,
+ product: pro,
+ entityId,
+ });
+ });
});
diff --git a/server/tests/attach/others/others7.ts b/server/tests/attach/others/others7.ts
index a52346f51..6647bf13b 100644
--- a/server/tests/attach/others/others7.ts
+++ b/server/tests/attach/others/others7.ts
@@ -15,66 +15,66 @@ import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
export let pro = constructProduct({
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
const testCase = "others7";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with free_trial=False`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await initCustomer({
- db,
- org,
- env,
- autumn: this.autumnJs,
- customerId,
- fingerprint: "test",
- attachPm: "success",
- });
+ await initCustomer({
+ db,
+ org,
+ env,
+ autumn: this.autumnJs,
+ customerId,
+ fingerprint: "test",
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ });
+ });
- it("should attach pro product with free_trial=False", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- free_trial: false,
- });
+ it("should attach pro product with free_trial=False", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ free_trial: false,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectAttachCorrect({
- customer,
- product: pro,
- });
+ expectAttachCorrect({
+ customer,
+ product: pro,
+ });
- expect(customer.invoices.length).to.equal(1);
- expect(customer.invoices[0].total).to.equal(getBasePrice({ product: pro }));
- });
+ expect(customer.invoices.length).to.equal(1);
+ expect(customer.invoices[0].total).to.equal(getBasePrice({ product: pro }));
+ });
});
diff --git a/server/tests/attach/others/others8.ts b/server/tests/attach/others/others8.ts
index 3b7e71b7a..a735d1571 100644
--- a/server/tests/attach/others/others8.ts
+++ b/server/tests/attach/others/others8.ts
@@ -9,9 +9,9 @@ import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -19,89 +19,89 @@ import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- }),
- constructPrepaidItem({
- isOneOff: true,
- featureId: TestFeature.Users,
- billingUnits: 1,
- price: 100,
- }),
- ],
- isAnnual: true,
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ }),
+ constructPrepaidItem({
+ isOneOff: true,
+ featureId: TestFeature.Users,
+ billingUnits: 1,
+ price: 100,
+ }),
+ ],
+ isAnnual: true,
+ type: "pro",
});
const testCase = "others8";
describe(`${chalk.yellowBright(`${testCase}: Testing annual pro with one off prepaid`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await initCustomer({
- db,
- org,
- env,
- autumn: this.autumnJs,
- customerId,
- fingerprint: "test",
- attachPm: "success",
- });
+ await initCustomer({
+ db,
+ org,
+ env,
+ autumn: this.autumnJs,
+ customerId,
+ fingerprint: "test",
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ });
+ });
- it("should attach annual pro product with one off prepaid", async function () {
- const options = [
- {
- feature_id: TestFeature.Users,
- quantity: 1,
- },
- ];
+ it("should attach annual pro product with one off prepaid", async function () {
+ const options = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 1,
+ },
+ ];
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: pro.id,
- options,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: pro.id,
+ options,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- options,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ options,
+ });
- console.log(preview);
+ console.log(preview);
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices[0];
- // expect(preview.total).to.equal(invoice.total);
- expect(invoice.total).to.equal(
- getBasePrice({ product: pro }) + options[0].quantity * 100
- );
- });
+ const invoice = customer.invoices[0];
+ // expect(preview.total).to.equal(invoice.total);
+ expect(invoice.total).to.equal(
+ getBasePrice({ product: pro }) + options[0].quantity * 100,
+ );
+ });
});
diff --git a/server/tests/attach/others/others9.ts b/server/tests/attach/others/others9.ts
index 7528706d2..c542e7408 100644
--- a/server/tests/attach/others/others9.ts
+++ b/server/tests/attach/others/others9.ts
@@ -14,14 +14,14 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
export let free = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- }),
- ],
- isAnnual: false,
- type: "free",
- isDefault: false,
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ }),
+ ],
+ isAnnual: false,
+ type: "free",
+ isDefault: false,
});
// Pro trial
@@ -31,62 +31,62 @@ export let free = constructProduct({
const testCase = "others9";
describe(`${chalk.yellowBright(`${testCase}: Testing attach free product again`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await initCustomer({
- db,
- org,
- env,
- autumn: this.autumnJs,
- customerId,
- fingerprint: "test",
- attachPm: "success",
- });
+ await initCustomer({
+ db,
+ org,
+ env,
+ autumn: this.autumnJs,
+ customerId,
+ fingerprint: "test",
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [free],
- });
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [free],
+ });
+ });
- it("should attach free product, then try again and hit error", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: free,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: true,
- });
+ it("should attach free product, then try again and hit error", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: free,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: true,
+ });
- await expectAutumnError({
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
- },
- });
- });
+ await expectAutumnError({
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
+ },
+ });
+ });
});
diff --git a/server/tests/attach/prepaid/prepaid1.ts b/server/tests/attach/prepaid/prepaid1.ts
index 43a0e3ecb..4568f7399 100644
--- a/server/tests/attach/prepaid/prepaid1.ts
+++ b/server/tests/attach/prepaid/prepaid1.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ Customer,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -28,172 +28,172 @@ import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUt
const testCase = "prepaid1";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 12.5,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- excludeBase: true,
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 12.5,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ excludeBase: true,
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proration downgrade, single use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let customer: Customer;
+ let curUnix = new Date().getTime();
+ let customer: Customer;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- customer = res.customer;
- testClockId = res.testClockId!;
- });
+ customer = res.customer;
+ testClockId = res.testClockId!;
+ });
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: 300,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 300,
+ },
+ ];
- it("should attach pro product to customer", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options,
- });
+ it("should attach pro product to customer", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- it("should reduce quantity to 200 and have correct sub item quantity + cus product quantity", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 200,
- },
- ],
- });
- });
+ it("should reduce quantity to 200 and have correct sub item quantity + cus product quantity", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 200,
+ },
+ ],
+ });
+ });
- it("should increase quantity to 400 and have correct sub item quantity + invoice..", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 400,
- },
- ],
- waitForInvoice: 5000,
- });
- });
+ it("should increase quantity to 400 and have correct sub item quantity + invoice..", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 400,
+ },
+ ],
+ waitForInvoice: 5000,
+ });
+ });
- const newQuantity = 200;
- it("should decrease quantity to 200, advance clock to next cycle and have correct balance", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: newQuantity,
- },
- ],
- });
+ const newQuantity = 200;
+ it("should decrease quantity to 200, advance clock to next cycle and have correct balance", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: newQuantity,
+ },
+ ],
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 40,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 40,
+ });
- const autumnCus = await autumn.customers.get(customerId);
- expect(autumnCus.features[TestFeature.Messages].balance).to.equal(
- newQuantity
- );
+ const autumnCus = await autumn.customers.get(customerId);
+ expect(autumnCus.features[TestFeature.Messages].balance).to.equal(
+ newQuantity,
+ );
- expect(autumnCus.invoices.length).to.equal(3);
- expect(autumnCus.invoices[0].total).to.equal((newQuantity / 100) * 12.5);
+ expect(autumnCus.invoices.length).to.equal(3);
+ expect(autumnCus.invoices[0].total).to.equal((newQuantity / 100) * 12.5);
- const cusProduct = await getMainCusProduct({
- db,
- internalCustomerId: customer.internal_id,
- productGroup: testCase,
- });
+ const cusProduct = await getMainCusProduct({
+ db,
+ internalCustomerId: customer.internal_id,
+ productGroup: testCase,
+ });
- expect(cusProduct?.options[0].quantity).to.equal(newQuantity / 100);
- expect(cusProduct?.options[0].upcoming_quantity).to.not.exist;
- });
+ expect(cusProduct?.options[0].quantity).to.equal(newQuantity / 100);
+ expect(cusProduct?.options[0].upcoming_quantity).to.not.exist;
+ });
});
diff --git a/server/tests/attach/prepaid/prepaid2.ts b/server/tests/attach/prepaid/prepaid2.ts
index 340e4823c..c08875124 100644
--- a/server/tests/attach/prepaid/prepaid2.ts
+++ b/server/tests/attach/prepaid/prepaid2.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -24,126 +24,126 @@ import { addWeeks } from "date-fns";
const testCase = "prepaid2";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 12.5,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- excludeBase: true,
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 12.5,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ excludeBase: true,
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate immediately, single use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = res.testClockId!;
- });
+ testClockId = res.testClockId!;
+ });
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: 300,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 300,
+ },
+ ];
- it("should attach pro product to customer", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options,
- });
+ it("should attach pro product to customer", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- it("should increase advance test clock, increase quantity to 400 and have correct sub item quantity + invoice..", async function () {
- const usage = Math.floor(Math.random() * 220);
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: usage,
- });
+ it("should increase advance test clock, increase quantity to 400 and have correct sub item quantity + invoice..", async function () {
+ const usage = Math.floor(Math.random() * 220);
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: usage,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 30,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 30,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 400,
- },
- ],
- usage: [
- {
- featureId: TestFeature.Messages,
- value: usage,
- },
- ],
- waitForInvoice: 5000,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 400,
+ },
+ ],
+ usage: [
+ {
+ featureId: TestFeature.Messages,
+ value: usage,
+ },
+ ],
+ waitForInvoice: 5000,
+ });
+ });
});
diff --git a/server/tests/attach/prepaid/prepaid3.ts b/server/tests/attach/prepaid/prepaid3.ts
index 9fd8b75e5..7f619d20c 100644
--- a/server/tests/attach/prepaid/prepaid3.ts
+++ b/server/tests/attach/prepaid/prepaid3.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -26,133 +26,133 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "prepaid3";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 12.5,
- config: {
- on_increase: OnIncrease.ProrateNextCycle,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- excludeBase: true,
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 12.5,
+ config: {
+ on_increase: OnIncrease.ProrateNextCycle,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ excludeBase: true,
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: upgrade quantity, prorate next cycle, single use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = res.testClockId!;
- });
+ testClockId = res.testClockId!;
+ });
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: 300,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 300,
+ },
+ ];
- it("should attach pro product to customer", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options,
- });
+ it("should attach pro product to customer", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- it("should increase advance test clock, increase quantity to 400", async function () {
- const usage = Math.floor(Math.random() * 220);
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: usage,
- });
+ it("should increase advance test clock, increase quantity to 400", async function () {
+ const usage = Math.floor(Math.random() * 220);
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: usage,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 400,
- },
- ],
- usage: [
- {
- featureId: TestFeature.Messages,
- value: usage,
- },
- ],
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 400,
+ },
+ ],
+ usage: [
+ {
+ featureId: TestFeature.Messages,
+ value: usage,
+ },
+ ],
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.invoices.length).to.equal(1);
- });
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.invoices.length).to.equal(1);
+ });
- it("should advance test clock to end of cycle and have correct invoice", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 10,
- });
- });
+ it("should advance test clock to end of cycle and have correct invoice", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 10,
+ });
+ });
});
diff --git a/server/tests/attach/prepaid/prepaid4.ts b/server/tests/attach/prepaid/prepaid4.ts
index 91b2853e0..04846b823 100644
--- a/server/tests/attach/prepaid/prepaid4.ts
+++ b/server/tests/attach/prepaid/prepaid4.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -27,122 +27,122 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "prepaid4";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 12.5,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- excludeBase: true,
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 12.5,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ excludeBase: true,
+ type: "pro",
});
describe(`${chalk.yellowBright(`attach/${testCase}: Testing prepaid reset`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = res.testClockId!;
- });
+ testClockId = res.testClockId!;
+ });
- const options = [
- {
- feature_id: TestFeature.Messages,
- quantity: 300,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 300,
+ },
+ ];
- it("should attach pro product to customer", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options,
- });
+ it("should attach pro product to customer", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
- // return;
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
+ // return;
- const usage = 100;
- it("should track usage for prepaid and have correct balance", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: usage,
- });
+ const usage = 100;
+ it("should track usage for prepaid and have correct balance", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: usage,
+ });
- await timeout(3000);
+ await timeout(3000);
- const customer = await autumn.customers.get(customerId);
- const newBalance = options[0].quantity - usage;
- expect(customer.features[TestFeature.Messages].balance).to.equal(
- newBalance
- );
- });
+ const customer = await autumn.customers.get(customerId);
+ const newBalance = options[0].quantity - usage;
+ expect(customer.features[TestFeature.Messages].balance).to.equal(
+ newBalance,
+ );
+ });
- it("should advance clock to next cycle and have correct balance", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ it("should advance clock to next cycle and have correct balance", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const customer = await autumn.customers.get(customerId);
- expect(customer.features[TestFeature.Messages].balance).to.equal(
- options[0].quantity
- );
- });
+ const customer = await autumn.customers.get(customerId);
+ expect(customer.features[TestFeature.Messages].balance).to.equal(
+ options[0].quantity,
+ );
+ });
});
diff --git a/server/tests/attach/prepaid/prepaid5.ts b/server/tests/attach/prepaid/prepaid5.ts
index 79c7dc13f..ec538d738 100644
--- a/server/tests/attach/prepaid/prepaid5.ts
+++ b/server/tests/attach/prepaid/prepaid5.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ Customer,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -15,8 +15,8 @@ import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
@@ -33,234 +33,234 @@ import { CusProductService } from "@/internal/customers/cusProducts/CusProductSe
const testCase = "prepaid5";
export let prepaidAddOn = constructProduct({
- type: "pro",
- excludeBase: true,
- id: "topup",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- billingUnits: 100,
- price: 12.5,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- isAddOn: true,
+ type: "pro",
+ excludeBase: true,
+ id: "topup",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ billingUnits: 100,
+ price: 12.5,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ isAddOn: true,
});
export let pro = constructProduct({
- type: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 250,
- }),
- ],
+ type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 250,
+ }),
+ ],
});
export let premium = constructProduct({
- type: "premium",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 1000,
- }),
- ],
+ type: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 1000,
+ }),
+ ],
});
describe(`${chalk.yellowBright(`attach/${testCase}: prepaid add on, with entities`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let customer: Customer;
+ let curUnix = new Date().getTime();
+ let customer: Customer;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- withTestClock: false,
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ withTestClock: false,
+ });
- addPrefixToProducts({
- products: [pro, premium, prepaidAddOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, prepaidAddOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium, prepaidAddOn],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium, prepaidAddOn],
+ db,
+ orgId: org.id,
+ env,
+ });
- customer = res.customer;
- // testClockId = res.testClockId!;
- });
+ customer = res.customer;
+ // testClockId = res.testClockId!;
+ });
- const entity1Id = "1";
- const entity2Id = "2";
- const entities = [
- {
- id: entity1Id,
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: entity2Id,
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entity1Id = "1";
+ const entity2Id = "2";
+ const entities = [
+ {
+ id: entity1Id,
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: entity2Id,
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product to entity1", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro product to entity1", async function () {
+ await autumn.entities.create(customerId, entities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity1Id,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity1Id,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity1Id,
- product: prepaidAddOn,
- otherProducts: [pro],
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 100,
- },
- ],
- numSubs: 2,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity1Id,
+ product: prepaidAddOn,
+ otherProducts: [pro],
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 100,
+ },
+ ],
+ numSubs: 2,
+ });
+ });
- const oldEntity2Quantity = 300;
- it("should advance test clock and attach top up to entity2", async function () {
- // await advanceTestClock({
- // stripeCli,
- // testClockId,
- // advanceTo: addWeeks(new Date(), 2).getTime(),
- // waitForSeconds: 10,
- // });
+ const oldEntity2Quantity = 300;
+ it("should advance test clock and attach top up to entity2", async function () {
+ // await advanceTestClock({
+ // stripeCli,
+ // testClockId,
+ // advanceTo: addWeeks(new Date(), 2).getTime(),
+ // waitForSeconds: 10,
+ // });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity2Id,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- numSubs: 3,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity2Id,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ numSubs: 3,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity2Id,
- product: prepaidAddOn,
- otherProducts: [premium],
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: oldEntity2Quantity,
- },
- ],
- numSubs: 4,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity2Id,
+ product: prepaidAddOn,
+ otherProducts: [premium],
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: oldEntity2Quantity,
+ },
+ ],
+ numSubs: 4,
+ });
+ });
- it("should increase prepaid add on quantity for entity1", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity1Id,
- product: prepaidAddOn,
- otherProducts: [pro],
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: 200,
- },
- ],
- numSubs: 4,
- waitForInvoice: 10000,
- });
- });
+ it("should increase prepaid add on quantity for entity1", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity1Id,
+ product: prepaidAddOn,
+ otherProducts: [pro],
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 200,
+ },
+ ],
+ numSubs: 4,
+ waitForInvoice: 10000,
+ });
+ });
- const newEntity2Quantity = 200;
- it("should decrease prepaid add on quantity for entity2", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- entityId: entity2Id,
- product: prepaidAddOn,
- otherProducts: [premium],
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Messages,
- quantity: newEntity2Quantity,
- },
- ],
- numSubs: 4,
- waitForInvoice: 5000,
- });
+ const newEntity2Quantity = 200;
+ it("should decrease prepaid add on quantity for entity2", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ entityId: entity2Id,
+ product: prepaidAddOn,
+ otherProducts: [premium],
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: newEntity2Quantity,
+ },
+ ],
+ numSubs: 4,
+ waitForInvoice: 5000,
+ });
- const entity2 = await autumn.entities.get(customerId, entity2Id);
- expect(entity2.invoices.length).to.equal(2);
- let creditProd = entity2.products.find((p: any) => p.id == prepaidAddOn.id);
- expect(creditProd).to.exist;
- const messagesItem = creditProd!.items.find(
- (i: any) => i.feature_id == TestFeature.Messages,
- );
+ const entity2 = await autumn.entities.get(customerId, entity2Id);
+ expect(entity2.invoices.length).to.equal(2);
+ let creditProd = entity2.products.find((p: any) => p.id == prepaidAddOn.id);
+ expect(creditProd).to.exist;
+ const messagesItem = creditProd!.items.find(
+ (i: any) => i.feature_id == TestFeature.Messages,
+ );
- expect(messagesItem).to.exist;
- expect(messagesItem.quantity).to.equal(oldEntity2Quantity);
- expect(messagesItem.next_cycle_quantity).to.equal(newEntity2Quantity);
- });
+ expect(messagesItem).to.exist;
+ expect(messagesItem.quantity).to.equal(oldEntity2Quantity);
+ expect(messagesItem.next_cycle_quantity).to.equal(newEntity2Quantity);
+ });
- return;
+ return;
});
diff --git a/server/tests/attach/prepaid/prepaid6.ts b/server/tests/attach/prepaid/prepaid6.ts
index b37d85219..aa4b0e999 100644
--- a/server/tests/attach/prepaid/prepaid6.ts
+++ b/server/tests/attach/prepaid/prepaid6.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ Customer,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -27,148 +27,148 @@ import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUt
import { timeout } from "@/utils/genUtils.js";
const userItem = constructPrepaidItem({
- featureId: TestFeature.Users,
- price: 10,
- billingUnits: 1,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ price: 10,
+ billingUnits: 1,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- excludeBase: true,
- type: "pro",
+ items: [userItem],
+ excludeBase: true,
+ type: "pro",
});
const testCase = "prepaid6";
describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proration downgrade, cont use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let customer: Customer;
+ let curUnix = new Date().getTime();
+ let customer: Customer;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- customer = res.customer;
- testClockId = res.testClockId!;
- });
+ customer = res.customer;
+ testClockId = res.testClockId!;
+ });
- const options = [
- {
- feature_id: TestFeature.Users,
- quantity: 4,
- },
- ];
+ const options = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 4,
+ },
+ ];
- const originalQuantity = 4;
- it("should attach pro product to customer", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options,
- });
+ const originalQuantity = 4;
+ it("should attach pro product to customer", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options,
+ });
- let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ let customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
- const usage = 3;
- const newQuantity = 3;
- it("should use 3 users, then downgrade to 3 seats", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: usage,
- });
+ const usage = 3;
+ const newQuantity = 3;
+ it("should use 3 users, then downgrade to 3 seats", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: usage,
+ });
- await timeout(3000);
+ await timeout(3000);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: [
- {
- feature_id: TestFeature.Users,
- quantity: newQuantity,
- },
- ],
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
- it("should have correct balance (0) next cycle", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice,
- ).getTime(),
- waitForSeconds: 30,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: [
+ {
+ feature_id: TestFeature.Users,
+ quantity: newQuantity,
+ },
+ ],
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
+ it("should have correct balance (0) next cycle", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- const autumnCus = await autumn.customers.get(customerId);
+ const autumnCus = await autumn.customers.get(customerId);
- expect(autumnCus.features[TestFeature.Users].balance).to.equal(0);
- let product = autumnCus.products.find((p: any) => p.id == pro.id) as any;
- let userItem = product.items.find(
- (i: any) => i.feature_id == TestFeature.Users,
- );
+ expect(autumnCus.features[TestFeature.Users].balance).to.equal(0);
+ let product = autumnCus.products.find((p: any) => p.id == pro.id) as any;
+ let userItem = product.items.find(
+ (i: any) => i.feature_id == TestFeature.Users,
+ );
- expect(userItem?.quantity).to.equal(newQuantity);
- expect(userItem?.upcoming_quantity).to.not.exist;
- expect(autumnCus.invoices[0].total).to.equal(newQuantity * userItem.price);
- });
+ expect(userItem?.quantity).to.equal(newQuantity);
+ expect(userItem?.upcoming_quantity).to.not.exist;
+ expect(autumnCus.invoices[0].total).to.equal(newQuantity * userItem.price);
+ });
});
diff --git a/server/tests/attach/renew/renew1.ts b/server/tests/attach/renew/renew1.ts
index 6aae1aff2..6539aa695 100644
--- a/server/tests/attach/renew/renew1.ts
+++ b/server/tests/attach/renew/renew1.ts
@@ -17,102 +17,102 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
const testCase = "renew1";
export let free = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- isDefault: false,
- type: "free",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ isDefault: false,
+ type: "free",
});
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Credits,
- includedUsage: 1000,
- }),
- ],
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Credits,
+ includedUsage: 1000,
+ }),
+ ],
- type: "pro",
+ type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing renew pro, force checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [free, pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free, pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free, pro],
- db,
- orgId: org.id,
- env,
- });
- });
+ await createProducts({
+ autumn,
+ products: [free, pro],
+ db,
+ orgId: org.id,
+ env,
+ });
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach free, then pro with force checkout and renew", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ it("should attach free, then pro with force checkout and renew", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- // 1. Get attach preview
- let attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: pro.id,
- });
+ // 1. Get attach preview
+ let attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- expect(attachPreview.branch).to.equal(AttachBranch.Renew);
+ expect(attachPreview.branch).to.equal(AttachBranch.Renew);
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- force_checkout: true,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ force_checkout: true,
+ });
- let customer = await autumn.customers.get(customerId);
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
- });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
+ });
});
diff --git a/server/tests/attach/updateEnts/expectUpdateEnts.ts b/server/tests/attach/updateEnts/expectUpdateEnts.ts
index 604ec6025..c86f19f9d 100644
--- a/server/tests/attach/updateEnts/expectUpdateEnts.ts
+++ b/server/tests/attach/updateEnts/expectUpdateEnts.ts
@@ -1,131 +1,131 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- Organization,
- ProductItem,
- ProductV2,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ Organization,
+ ProductItem,
+ ProductV2,
} from "@autumn/shared";
import { expect } from "chai";
import Stripe from "stripe";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import {
- expectSubItemsCorrect,
- getSubsFromCusId,
+ expectSubItemsCorrect,
+ getSubsFromCusId,
} from "tests/utils/expectUtils/expectSubUtils.js";
const runUpdateEntsTest = async ({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- newVersion,
- db,
- org,
- env,
- customItems,
- usage,
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ newVersion,
+ db,
+ org,
+ env,
+ customItems,
+ usage,
}: {
- autumn: AutumnInt;
- stripeCli: Stripe;
- customerId: string;
- customProduct: ProductV2;
- newVersion?: number;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- customItems?: ProductItem[];
- usage?: {
- featureId: string;
- value: number;
- }[];
+ autumn: AutumnInt;
+ stripeCli: Stripe;
+ customerId: string;
+ customProduct: ProductV2;
+ newVersion?: number;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ customItems?: ProductItem[];
+ usage?: {
+ featureId: string;
+ value: number;
+ }[];
}) => {
- // 1. Get subs before
+ // 1. Get subs before
- const { subs: subsBefore } = await getSubsFromCusId({
- stripeCli,
- customerId,
- productId: customProduct.id,
- db,
- org,
- env,
- });
+ const { subs: subsBefore } = await getSubsFromCusId({
+ stripeCli,
+ customerId,
+ productId: customProduct.id,
+ db,
+ org,
+ env,
+ });
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: customProduct.id,
- version: newVersion,
- is_custom: customItems ? true : undefined,
- items: customItems,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: customProduct.id,
+ version: newVersion,
+ is_custom: customItems ? true : undefined,
+ items: customItems,
+ });
- if (newVersion) {
- expect(preview.branch).to.equal(AttachBranch.NewVersion);
- } else {
- expect(preview.branch).to.equal(AttachBranch.SameCustomEnts);
- expect(preview.due_today).to.be.undefined;
- }
+ if (newVersion) {
+ expect(preview.branch).to.equal(AttachBranch.NewVersion);
+ } else {
+ expect(preview.branch).to.equal(AttachBranch.SameCustomEnts);
+ expect(preview.due_today).to.be.undefined;
+ }
- await autumn.attach({
- customer_id: customerId,
- product_id: customProduct.id,
- version: newVersion,
- is_custom: customItems ? true : undefined,
- items: customItems,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: customProduct.id,
+ version: newVersion,
+ is_custom: customItems ? true : undefined,
+ items: customItems,
+ });
- // 1. Ensure no new invoices created
- const { subs: subsAfter, cusProduct } = await getSubsFromCusId({
- stripeCli,
- customerId,
- productId: customProduct.id,
- db,
- org,
- env,
- });
+ // 1. Ensure no new invoices created
+ const { subs: subsAfter, cusProduct } = await getSubsFromCusId({
+ stripeCli,
+ customerId,
+ productId: customProduct.id,
+ db,
+ org,
+ env,
+ });
- let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
- let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
- let subIdsBefore = subsBefore.map((sub) => sub.id);
- let subIdsAfter = subsAfter.map((sub) => sub.id);
+ let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
+ let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
+ let subIdsBefore = subsBefore.map((sub) => sub.id);
+ let subIdsAfter = subsAfter.map((sub) => sub.id);
- // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end);
- // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end);
+ // let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end);
+ // let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end);
- expect(invoicesAfter).to.deep.equal(invoicesBefore);
- expect(subIdsAfter).to.deep.equal(subIdsBefore);
- // expect(periodEndsAfter).to.deep.equal(periodEndsBefore);
+ expect(invoicesAfter).to.deep.equal(invoicesBefore);
+ expect(subIdsAfter).to.deep.equal(subIdsBefore);
+ // expect(periodEndsAfter).to.deep.equal(periodEndsBefore);
- if (customItems) {
- expect(cusProduct.is_custom).to.be.true;
- }
+ if (customItems) {
+ expect(cusProduct.is_custom).to.be.true;
+ }
- const customer = await autumn.customers.get(customerId);
- expectFeaturesCorrect({
- customer,
- product: customProduct,
- usage,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectFeaturesCorrect({
+ customer,
+ product: customProduct,
+ usage,
+ });
- // 2. Expect product attached
- await expectSubItemsCorrect({
- stripeCli,
- customerId,
- product: customProduct,
- db,
- org,
- env,
- });
+ // 2. Expect product attached
+ await expectSubItemsCorrect({
+ stripeCli,
+ customerId,
+ product: customProduct,
+ db,
+ org,
+ env,
+ });
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
};
export default runUpdateEntsTest;
diff --git a/server/tests/attach/updateEnts/updateEnts1.ts b/server/tests/attach/updateEnts/updateEnts1.ts
index 982a13279..be8360071 100644
--- a/server/tests/attach/updateEnts/updateEnts1.ts
+++ b/server/tests/attach/updateEnts/updateEnts1.ts
@@ -22,154 +22,154 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "updateEnts1";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 10000,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 10000,
+ }),
+ ],
+ type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- customerId,
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- const newItem = constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 20000,
- });
+ const newItem = constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 20000,
+ });
- const customItems = replaceItems({
- items: pro.items,
- featureId: TestFeature.Words,
- newItem,
- });
+ const customItems = replaceItems({
+ items: pro.items,
+ featureId: TestFeature.Words,
+ newItem,
+ });
- let usage = 50000;
- let overage = 50000 - (newItem.included_usage as number);
+ let usage = 50000;
+ let overage = 50000 - (newItem.included_usage as number);
- it("should update overage item to have new included usage", async function () {
- const customProduct = {
- ...pro,
- items: customItems,
- };
+ it("should update overage item to have new included usage", async function () {
+ const customProduct = {
+ ...pro,
+ items: customItems,
+ };
- await autumn.track({
- customer_id: customerId,
- value: usage,
- feature_id: TestFeature.Words,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value: usage,
+ feature_id: TestFeature.Words,
+ });
- await timeout(5000);
+ await timeout(5000);
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- db,
- org,
- env,
- customItems,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
- return;
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ db,
+ org,
+ env,
+ customItems,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
+ return;
- it("should have correct invoice next cycle", async function () {
- const invoiceTotal = await getExpectedInvoiceTotal({
- org,
- env,
- customerId,
- productId: pro.id,
- stripeCli,
- db,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
+ it("should have correct invoice next cycle", async function () {
+ const invoiceTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ customerId,
+ productId: pro.id,
+ stripeCli,
+ db,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
- let curUnix = Date.now();
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(curUnix, 1).getTime(),
- });
+ let curUnix = Date.now();
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(curUnix, 1).getTime(),
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
+ waitForSeconds: 10,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices![0];
- expect(invoice.total).to.equal(
- invoiceTotal,
- "invoice total after 1 cycle should be correct"
- );
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices![0];
+ expect(invoice.total).to.equal(
+ invoiceTotal,
+ "invoice total after 1 cycle should be correct",
+ );
+ });
});
diff --git a/server/tests/attach/updateEnts/updateEnts2.ts b/server/tests/attach/updateEnts/updateEnts2.ts
index 0c701dc70..b62007d41 100644
--- a/server/tests/attach/updateEnts/updateEnts2.ts
+++ b/server/tests/attach/updateEnts/updateEnts2.ts
@@ -22,14 +22,14 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "updateEnts2";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 10000,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 10000,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
/**
@@ -45,149 +45,149 @@ export let pro = constructProduct({
*/
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing included usage) for annual plan`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro annual product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro annual product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- const newItem = constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 5000,
- });
+ const newItem = constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 5000,
+ });
- const customItems = replaceItems({
- items: pro.items,
- featureId: TestFeature.Words,
- newItem,
- });
+ const customItems = replaceItems({
+ items: pro.items,
+ featureId: TestFeature.Words,
+ newItem,
+ });
- let usage = 1200500;
+ let usage = 1200500;
- it("should attach custom pro product", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- waitForSeconds: 30,
- });
+ it("should attach custom pro product", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ waitForSeconds: 30,
+ });
- const customProduct = {
- ...pro,
- items: customItems,
- };
+ const customProduct = {
+ ...pro,
+ items: customItems,
+ };
- await autumn.track({
- customer_id: customerId,
- value: usage,
- feature_id: TestFeature.Words,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value: usage,
+ feature_id: TestFeature.Words,
+ });
- await timeout(5000);
+ await timeout(5000);
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- db,
- org,
- env,
- customItems,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ db,
+ org,
+ env,
+ customItems,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should have correct invoice usage next cycle", async function () {
- const invoiceTotal = await getExpectedInvoiceTotal({
- org,
- env,
- customerId,
- productId: pro.id,
- stripeCli,
- db,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- onlyIncludeMonthly: true,
- });
+ it("should have correct invoice usage next cycle", async function () {
+ const invoiceTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ customerId,
+ productId: pro.id,
+ stripeCli,
+ db,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ onlyIncludeMonthly: true,
+ });
- let curUnix = Date.now();
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(curUnix, 1).getTime(),
- });
+ let curUnix = Date.now();
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(curUnix, 1).getTime(),
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(curUnix, hoursToFinalizeInvoice).getTime(),
+ waitForSeconds: 10,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices![0];
- expect(invoice.total).to.equal(
- invoiceTotal,
- "invoice total after 1 cycle should be correct"
- );
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices![0];
+ expect(invoice.total).to.equal(
+ invoiceTotal,
+ "invoice total after 1 cycle should be correct",
+ );
+ });
});
diff --git a/server/tests/attach/updateEnts/updateEnts3.ts b/server/tests/attach/updateEnts/updateEnts3.ts
index 1f4d179e3..34b338ec5 100644
--- a/server/tests/attach/updateEnts/updateEnts3.ts
+++ b/server/tests/attach/updateEnts/updateEnts3.ts
@@ -20,14 +20,14 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "updateEnts3";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 10000,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 10000,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
/**
@@ -43,164 +43,164 @@ export let pro = constructProduct({
*/
describe(`${chalk.yellowBright(`${testCase}: Testing update ents (changing feature items)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- db,
- orgId: org.id,
- env,
- autumn,
- products: [pro],
- });
+ await createProducts({
+ db,
+ orgId: org.id,
+ env,
+ autumn,
+ products: [pro],
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro annual product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro annual product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- const newFeatureItem = constructFeatureItem({
- feature_id: TestFeature.Messages,
- included_usage: 500,
- });
+ const newFeatureItem = constructFeatureItem({
+ feature_id: TestFeature.Messages,
+ included_usage: 500,
+ });
- const usage = 1200500;
+ const usage = 1200500;
- const customItems = [...pro.items, newFeatureItem];
+ const customItems = [...pro.items, newFeatureItem];
- it("should attach custom pro product with new feature item", async function () {
- const customProduct = {
- ...pro,
- items: customItems,
- };
+ it("should attach custom pro product with new feature item", async function () {
+ const customProduct = {
+ ...pro,
+ items: customItems,
+ };
- await autumn.track({
- customer_id: customerId,
- value: usage,
- feature_id: TestFeature.Words,
- });
+ await autumn.track({
+ customer_id: customerId,
+ value: usage,
+ feature_id: TestFeature.Words,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ waitForSeconds: 10,
+ });
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- db,
- org,
- env,
- customItems,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ db,
+ org,
+ env,
+ customItems,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should attach custom pro product with updated feature item", async function () {
- let customItems2 = replaceItems({
- items: customItems,
- featureId: TestFeature.Messages,
- newItem: constructFeatureItem({
- feature_id: TestFeature.Messages,
- included_usage: 1000,
- }),
- });
+ it("should attach custom pro product with updated feature item", async function () {
+ let customItems2 = replaceItems({
+ items: customItems,
+ featureId: TestFeature.Messages,
+ newItem: constructFeatureItem({
+ feature_id: TestFeature.Messages,
+ included_usage: 1000,
+ }),
+ });
- const customProduct = {
- ...pro,
- items: customItems2,
- };
+ const customProduct = {
+ ...pro,
+ items: customItems2,
+ };
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- db,
- org,
- env,
- customItems: customItems2,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ db,
+ org,
+ env,
+ customItems: customItems2,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should attach custom pro product with removed feature item", async function () {
- const customItems2 = customItems.filter(
- (item) => item.feature_id != TestFeature.Messages
- );
+ it("should attach custom pro product with removed feature item", async function () {
+ const customItems2 = customItems.filter(
+ (item) => item.feature_id != TestFeature.Messages,
+ );
- const customProduct = {
- ...pro,
- items: customItems2,
- };
+ const customProduct = {
+ ...pro,
+ items: customItems2,
+ };
- await runUpdateEntsTest({
- autumn,
- stripeCli,
- customerId,
- customProduct,
- db,
- org,
- env,
- customItems: customItems2,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
- });
+ await runUpdateEntsTest({
+ autumn,
+ stripeCli,
+ customerId,
+ customProduct,
+ db,
+ org,
+ env,
+ customItems: customItems2,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/updateEnts/updateEnts4.ts b/server/tests/attach/updateEnts/updateEnts4.ts
index 05150aead..9077918e7 100644
--- a/server/tests/attach/updateEnts/updateEnts4.ts
+++ b/server/tests/attach/updateEnts/updateEnts4.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- BillingInterval,
- Organization,
- ProductItemInterval,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ BillingInterval,
+ Organization,
+ ProductItemInterval,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,89 +25,89 @@ import { constructPriceItem } from "@/internal/products/product-items/productIte
const testCase = "updateEnts4";
export let pro = constructProduct({
- items: [
- constructArrearItem({
- featureId: TestFeature.Words,
- includedUsage: 10000,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ includedUsage: 10000,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
describe(`${chalk.yellowBright(`${testCase}: Checking price changes don't result in update ents func`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro annual product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro annual product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("branch should not be same custom ents if base price updated", async function () {
- let customItems = pro.items.filter((item) => !nullish(item.feature_id));
+ it("branch should not be same custom ents if base price updated", async function () {
+ let customItems = pro.items.filter((item) => !nullish(item.feature_id));
- customItems = [
- ...customItems,
- constructPriceItem({
- price: 10,
- interval: BillingInterval.Year,
- }),
- ];
+ customItems = [
+ ...customItems,
+ constructPriceItem({
+ price: 10,
+ interval: BillingInterval.Year,
+ }),
+ ];
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: pro.id,
- is_custom: true,
- items: customItems,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: pro.id,
+ is_custom: true,
+ items: customItems,
+ });
- expect(preview.branch).to.equal(AttachBranch.SameCustom);
- });
+ expect(preview.branch).to.equal(AttachBranch.SameCustom);
+ });
});
diff --git a/server/tests/attach/updateQuantity/updateQuantity1.ts b/server/tests/attach/updateQuantity/updateQuantity1.ts
index a0dd90737..a36b48290 100644
--- a/server/tests/attach/updateQuantity/updateQuantity1.ts
+++ b/server/tests/attach/updateQuantity/updateQuantity1.ts
@@ -1,10 +1,10 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- APIVersion,
- AppEnv,
- AttachErrCode,
- Organization,
+ APIVersion,
+ AppEnv,
+ AttachErrCode,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,131 +25,131 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "updateQuantity1";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Users,
- price: 12,
- billingUnits: 1,
- }),
- ],
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Users,
+ price: 12,
+ billingUnits: 1,
+ }),
+ ],
+ type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const proOpts = [
- {
- feature_id: TestFeature.Users,
- quantity: 2,
- },
- ];
+ const proOpts = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 2,
+ },
+ ];
- it("should attach pro product (arrear prorated)", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: proOpts,
- });
- });
+ it("should attach pro product (arrear prorated)", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: proOpts,
+ });
+ });
- it("should throw error if try to attach same options", async function () {
- await expectAutumnError({
- errCode: AttachErrCode.ProductAlreadyAttached,
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- options: proOpts,
- });
- },
- });
- });
+ it("should throw error if try to attach same options", async function () {
+ await expectAutumnError({
+ errCode: AttachErrCode.ProductAlreadyAttached,
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ options: proOpts,
+ });
+ },
+ });
+ });
- const updatedOpts = [
- {
- feature_id: TestFeature.Users,
- quantity: 4,
- },
- ];
+ const updatedOpts = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 4,
+ },
+ ];
- it("should update quantity to 4 users and have usage stay the same", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 2,
- });
- await timeout(3000);
+ it("should update quantity to 4 users and have usage stay the same", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 2,
+ });
+ await timeout(3000);
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 30,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 30,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: updatedOpts,
- usage: [
- {
- featureId: TestFeature.Users,
- value: 2,
- },
- ],
- waitForInvoice: 15000,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: updatedOpts,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: 2,
+ },
+ ],
+ waitForInvoice: 15000,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade1.ts b/server/tests/attach/upgrade/upgrade1.ts
index 7cd62b131..8e7c45ee2 100644
--- a/server/tests/attach/upgrade/upgrade1.ts
+++ b/server/tests/attach/upgrade/upgrade1.ts
@@ -16,128 +16,128 @@ import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUti
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let growth = constructProduct({
- id: "growth",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "growth",
+ id: "growth",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "growth",
});
describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
- let customerId = "upgrade1";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "upgrade1";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, growth],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro, premium, growth],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach premium product", async function () {
- const wordsUsage = 100000;
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: wordsUsage,
- });
+ it("should attach premium product", async function () {
+ const wordsUsage = 100000;
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: wordsUsage,
+ });
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 10,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 10,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach growth product", async function () {
- const wordsUsage = 200000;
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: wordsUsage,
- });
+ it("should attach growth product", async function () {
+ const wordsUsage = 200000;
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: wordsUsage,
+ });
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 10,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 10,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: growth,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: growth,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade2.ts b/server/tests/attach/upgrade/upgrade2.ts
index d21111cb0..28aba97f5 100644
--- a/server/tests/attach/upgrade/upgrade2.ts
+++ b/server/tests/attach/upgrade/upgrade2.ts
@@ -18,23 +18,23 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// Shared products for attach tests
const testCase = "upgrade2";
export let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
export let proAnnual = constructProduct({
- id: "pro_annual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- isAnnual: true,
+ id: "pro_annual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ isAnnual: true,
});
export let premiumAnnual = constructProduct({
- id: "premium_annual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- isAnnual: true,
+ id: "premium_annual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ isAnnual: true,
});
/**
@@ -47,109 +47,109 @@ export let premiumAnnual = constructProduct({
*/
describe(`${chalk.yellowBright("upgrade2: Testing usage upgrades with monthly -> annual")}`, () => {
- let customerId = "upgrade2";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customerId = "upgrade2";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, proAnnual, premiumAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual, premiumAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, proAnnual, premiumAnnual],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, proAnnual, premiumAnnual],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should attach pro annual product", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: 100000,
- });
+ it("should attach pro annual product", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: 100000,
+ });
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- });
- return;
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ });
+ return;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- });
- });
- return;
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
+ return;
- it("should attach premium annual product", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: 5000000,
- });
+ it("should attach premium annual product", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: 5000000,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 10,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premiumAnnual,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premiumAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade3.ts b/server/tests/attach/upgrade/upgrade3.ts
index 728e60a47..b1de3c2ff 100644
--- a/server/tests/attach/upgrade/upgrade3.ts
+++ b/server/tests/attach/upgrade/upgrade3.ts
@@ -19,34 +19,34 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "upgrade3";
export let pro = constructProduct({
- items: [
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 12,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 12,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 20,
- }),
- ],
- type: "premium",
+ items: [
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 20,
+ }),
+ ],
+ type: "premium",
});
export let proAnnual = constructProduct({
- items: [
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 12,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 12,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
/**
@@ -61,139 +61,139 @@ export let proAnnual = constructProduct({
*/
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with arrear prorated`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium, proAnnual],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product (arrear prorated)", async function () {
- // 1. Create multiple entities
- let entities = await autumn.entities.create(customerId, [
- {
- id: "entity1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "entity2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ]);
- numUsers = 2;
+ it("should attach pro product (arrear prorated)", async function () {
+ // 1. Create multiple entities
+ let entities = await autumn.entities.create(customerId, [
+ {
+ id: "entity1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "entity2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ]);
+ numUsers = 2;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: 2,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: 2,
+ },
+ ],
+ });
+ });
- it("should create entity, then upgrade to premium product (arrear prorated)", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- });
+ it("should create entity, then upgrade to premium product (arrear prorated)", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ });
- // TODO: Check price paid for entity3
- await autumn.entities.create(customerId, [
- {
- id: "entity3",
- name: "entity3",
- feature_id: TestFeature.Users,
- },
- ]);
- numUsers += 1;
+ // TODO: Check price paid for entity3
+ await autumn.entities.create(customerId, [
+ {
+ id: "entity3",
+ name: "entity3",
+ feature_id: TestFeature.Users,
+ },
+ ]);
+ numUsers += 1;
- await timeout(3000);
+ await timeout(3000);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: numUsers,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: numUsers,
+ },
+ ],
+ });
+ });
- it("should upgrade to pro-annual product (arrear prorated)", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- });
+ it("should upgrade to pro-annual product (arrear prorated)", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: numUsers,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: numUsers,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade4.ts b/server/tests/attach/upgrade/upgrade4.ts
index 52713c7b7..3cb14195d 100644
--- a/server/tests/attach/upgrade/upgrade4.ts
+++ b/server/tests/attach/upgrade/upgrade4.ts
@@ -17,37 +17,37 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "upgrade4";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Users,
- price: 12,
- billingUnits: 1,
- }),
- ],
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Users,
+ price: 12,
+ billingUnits: 1,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Users,
- price: 20,
- billingUnits: 1,
- }),
- ],
- type: "premium",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Users,
+ price: 20,
+ billingUnits: 1,
+ }),
+ ],
+ type: "premium",
});
export let proAnnual = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Users,
- price: 12,
- billingUnits: 1,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Users,
+ price: 12,
+ billingUnits: 1,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
/**
@@ -62,118 +62,118 @@ export let proAnnual = constructProduct({
*/
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid continuous use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium, proAnnual],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const proOpts = [
- {
- feature_id: TestFeature.Users,
- quantity: 4,
- },
- ];
+ const proOpts = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 4,
+ },
+ ];
- it("should attach pro product (arrear prorated)", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: proOpts,
- });
- });
+ it("should attach pro product (arrear prorated)", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: proOpts,
+ });
+ });
- const premiumOpts = [
- {
- feature_id: TestFeature.Users,
- quantity: 6,
- },
- ];
+ const premiumOpts = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 6,
+ },
+ ];
- it("should create entity, then upgrade to premium product (arrear prorated)", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- });
+ it("should create entity, then upgrade to premium product (arrear prorated)", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- options: premiumOpts,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: premiumOpts,
+ });
+ });
- const proAnnualOpts = [
- {
- feature_id: TestFeature.Users,
- quantity: 3,
- },
- ];
+ const proAnnualOpts = [
+ {
+ feature_id: TestFeature.Users,
+ quantity: 3,
+ },
+ ];
- it("should upgrade to pro-annual product (arrear prorated)", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- });
+ it("should upgrade to pro-annual product (arrear prorated)", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- options: proAnnualOpts,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: proAnnualOpts,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade5.ts b/server/tests/attach/upgrade/upgrade5.ts
index 3db52bc01..b70c4bdc6 100644
--- a/server/tests/attach/upgrade/upgrade5.ts
+++ b/server/tests/attach/upgrade/upgrade5.ts
@@ -17,117 +17,117 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "upgrade5";
export let pro = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- price: 12,
- billingUnits: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ price: 12,
+ billingUnits: 100,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Messages,
- price: 8,
- billingUnits: 100,
- }),
- ],
- type: "premium",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Messages,
+ price: 8,
+ billingUnits: 100,
+ }),
+ ],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing upgrades with prepaid single use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
- let numUsers = 0;
+ let curUnix = new Date().getTime();
+ let numUsers = 0;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const proOpts = [
- {
- feature_id: TestFeature.Messages,
- quantity: 300,
- },
- ];
+ const proOpts = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 300,
+ },
+ ];
- it("should attach pro product (prepaid single use)", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- options: proOpts,
- });
- });
+ it("should attach pro product (prepaid single use)", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: proOpts,
+ });
+ });
- const premiumOpts = [
- {
- feature_id: TestFeature.Messages,
- quantity: 600,
- },
- ];
+ const premiumOpts = [
+ {
+ feature_id: TestFeature.Messages,
+ quantity: 600,
+ },
+ ];
- it("should upgrade to premium product (prepaid single use)", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 20,
- });
+ it("should upgrade to premium product (prepaid single use)", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 20,
+ });
- return;
+ return;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- options: premiumOpts,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ options: premiumOpts,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade6.ts b/server/tests/attach/upgrade/upgrade6.ts
index 1e173e008..4df6ed4d8 100644
--- a/server/tests/attach/upgrade/upgrade6.ts
+++ b/server/tests/attach/upgrade/upgrade6.ts
@@ -9,8 +9,8 @@ import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
- constructArrearItem,
- constructArrearProratedItem,
+ constructArrearItem,
+ constructArrearProratedItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
@@ -26,140 +26,140 @@ import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCor
const testCase = "upgrade6";
export let pro = constructProduct({
- items: [
- constructArrearItem({ featureId: TestFeature.Words }),
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 20,
- }),
- ],
- type: "pro",
+ items: [
+ constructArrearItem({ featureId: TestFeature.Words }),
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 20,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructArrearItem({ featureId: TestFeature.Words }),
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 30,
- }),
- ],
- type: "premium",
+ items: [
+ constructArrearItem({ featureId: TestFeature.Words }),
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 30,
+ }),
+ ],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing failed upgrades`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let usage = 100012;
- it("should upgrade to premium product and fail", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: usage,
- });
- await timeout(4000);
+ let usage = 100012;
+ it("should upgrade to premium product and fail", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: usage,
+ });
+ await timeout(4000);
- let cus = await CusService.get({
- db,
- orgId: org.id,
- idOrInternalId: customerId,
- env,
- });
+ let cus = await CusService.get({
+ db,
+ orgId: org.id,
+ idOrInternalId: customerId,
+ env,
+ });
- await attachFailedPaymentMethod({ stripeCli, customer: cus! });
- await timeout(2000);
+ await attachFailedPaymentMethod({ stripeCli, customer: cus! });
+ await timeout(2000);
- await expectAutumnError({
- func: async () => {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- });
- },
- errMessage: "Failed to update subscription. Your card was declined.",
- });
+ await expectAutumnError({
+ func: async () => {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ },
+ errMessage: "Failed to update subscription. Your card was declined.",
+ });
- await timeout(4000);
- let customer = await autumn.customers.get(customerId);
+ await timeout(4000);
+ let customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- });
+ expectProductAttached({
+ customer,
+ product: pro,
+ });
- expectFeaturesCorrect({
- customer,
- product: pro,
- usage: [
- {
- featureId: TestFeature.Words,
- value: usage,
- },
- ],
- });
+ expectFeaturesCorrect({
+ customer,
+ product: pro,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: usage,
+ },
+ ],
+ });
- await expectSubItemsCorrect({
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ await expectSubItemsCorrect({
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/attach/upgrade/upgrade7.ts b/server/tests/attach/upgrade/upgrade7.ts
index a94b93d7e..886479c6e 100644
--- a/server/tests/attach/upgrade/upgrade7.ts
+++ b/server/tests/attach/upgrade/upgrade7.ts
@@ -13,79 +13,79 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const testCase = "upgrade7";
export let pro = constructProduct({
- items: [],
- type: "pro",
+ items: [],
+ type: "pro",
});
export let premium = constructProduct({
- items: [],
- type: "premium",
+ items: [],
+ type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing upgrade via cancel + attach`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro product", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should cancel than attach premium product", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- cancel_immediately: true,
- });
+ it("should cancel than attach premium product", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ cancel_immediately: true,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- force_checkout: true,
- });
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ force_checkout: true,
+ });
+ });
});
diff --git a/server/tests/attach/upgradeOld/upgradeOld1.ts b/server/tests/attach/upgradeOld/upgradeOld1.ts
index 38a33eb59..ff5d6bbd4 100644
--- a/server/tests/attach/upgradeOld/upgradeOld1.ts
+++ b/server/tests/attach/upgradeOld/upgradeOld1.ts
@@ -11,65 +11,65 @@ import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import Stripe from "stripe";
describe(`${chalk.yellowBright(
- "upgradeOld1: Testing upgrade (trial to paid)",
+ "upgradeOld1: Testing upgrade (trial to paid)",
)}`, () => {
- const customerId = "upgradeOld1";
- let testClockId: string;
- let customer: Customer;
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
+ const customerId = "upgradeOld1";
+ let testClockId: string;
+ let customer: Customer;
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- });
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- it("should attach pro with trial", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.proWithTrial.id,
- });
- });
+ it("should attach pro with trial", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.proWithTrial.id,
+ });
+ });
- it("should attach premium", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 3).getTime(),
- waitForSeconds: 10,
- });
+ it("should attach premium", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 3).getTime(),
+ waitForSeconds: 10,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premium.id,
- });
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premium.id,
+ });
+ });
- it("should check product, ents and invoices", async function () {
- const res = await autumn.customers.get(customerId);
- compareMainProduct({
- sent: products.premium,
- cusRes: res,
- });
+ it("should check product, ents and invoices", async function () {
+ const res = await autumn.customers.get(customerId);
+ compareMainProduct({
+ sent: products.premium,
+ cusRes: res,
+ });
- const invoices = await res.invoices;
+ const invoices = await res.invoices;
- assert.equal(
- invoices[0].total,
- products.premium.prices[0].config.amount,
- "Invoice should be for 50.00",
- );
- });
+ assert.equal(
+ invoices[0].total,
+ products.premium.prices[0].config.amount,
+ "Invoice should be for 50.00",
+ );
+ });
});
diff --git a/server/tests/attach/upgradeOld/upgradeOld2.ts b/server/tests/attach/upgradeOld/upgradeOld2.ts
index 421b6ca35..1a8af3166 100644
--- a/server/tests/attach/upgradeOld/upgradeOld2.ts
+++ b/server/tests/attach/upgradeOld/upgradeOld2.ts
@@ -8,45 +8,45 @@ import { products } from "tests/global.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
describe(`${chalk.yellowBright(
- "upgradeOld2: Testing upgrade (paid to trial)",
+ "upgradeOld2: Testing upgrade (paid to trial)",
)}`, () => {
- const customerId = "upgradeOld2";
- let testClockId: string;
- let customer: Customer;
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
+ const customerId = "upgradeOld2";
+ let testClockId: string;
+ let customer: Customer;
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- });
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ });
- customer = customer_;
- testClockId = testClockId_;
- });
+ customer = customer_;
+ testClockId = testClockId_;
+ });
- it("should attach pro", async function () {
- this.timeout(30000);
- await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
- });
+ it("should attach pro", async function () {
+ this.timeout(30000);
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
+ });
- it("should attach premium with trial and have trial", async function () {
- this.timeout(30000);
+ it("should attach premium with trial and have trial", async function () {
+ this.timeout(30000);
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premiumWithTrial.id,
- });
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premiumWithTrial.id,
+ });
+ });
});
diff --git a/server/tests/attach/upgradeOld/upgradeOld3.ts b/server/tests/attach/upgradeOld/upgradeOld3.ts
index 23d176ed9..473166d7b 100644
--- a/server/tests/attach/upgradeOld/upgradeOld3.ts
+++ b/server/tests/attach/upgradeOld/upgradeOld3.ts
@@ -11,62 +11,62 @@ import Stripe from "stripe";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
describe(`${chalk.yellowBright("upgradeOld3: Testing upgrade (trial to trial)")}`, () => {
- const customerId = "upgradeOld3";
- let testClockId: string;
- let autumn: AutumnInt = new AutumnInt();
- let stripeCli: Stripe;
- before(async function () {
- await setupBefore(this);
- stripeCli = this.stripeCli;
- const { customer: customer_, testClockId: testClockId_ } =
- await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db: this.db,
- org: this.org,
- env: this.env,
- attachPm: "success",
- });
+ const customerId = "upgradeOld3";
+ let testClockId: string;
+ let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ before(async function () {
+ await setupBefore(this);
+ stripeCli = this.stripeCli;
+ const { customer: customer_, testClockId: testClockId_ } =
+ await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db: this.db,
+ org: this.org,
+ env: this.env,
+ attachPm: "success",
+ });
- testClockId = testClockId_;
- });
+ testClockId = testClockId_;
+ });
- it("should attach pro with trial", async function () {
- this.timeout(30000);
- await autumn.attach({
- customer_id: customerId,
- product_id: products.proWithTrial.id,
- });
+ it("should attach pro with trial", async function () {
+ this.timeout(30000);
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.proWithTrial.id,
+ });
- console.log(` ${chalk.greenBright("Attached pro with trial")}`);
- });
+ console.log(` ${chalk.greenBright("Attached pro with trial")}`);
+ });
- it("should attach premium with trial", async function () {
- const advanceTo = addDays(new Date(), 3).getTime();
+ it("should attach premium with trial", async function () {
+ const advanceTo = addDays(new Date(), 3).getTime();
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo,
- waitForSeconds: 10,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo,
+ waitForSeconds: 10,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premiumWithTrial.id,
- });
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premiumWithTrial.id,
+ });
+ });
- it("should check product and ents", async function () {
- const res = await autumn.customers.get(customerId);
- compareMainProduct({
- sent: products.premiumWithTrial,
- cusRes: res,
- status: CusProductStatus.Trialing,
- });
+ it("should check product and ents", async function () {
+ const res = await autumn.customers.get(customerId);
+ compareMainProduct({
+ sent: products.premiumWithTrial,
+ cusRes: res,
+ status: CusProductStatus.Trialing,
+ });
- const invoices = res.invoices;
+ const invoices = res.invoices;
- assert.equal(invoices![0].total, 0, "Invoice should be 0");
- });
+ assert.equal(invoices![0].total, 0, "Invoice should be 0");
+ });
});
diff --git a/server/tests/attach/upgradeOld/upgradeOld4.ts b/server/tests/attach/upgradeOld/upgradeOld4.ts
index 208e4296c..87217d5fe 100644
--- a/server/tests/attach/upgradeOld/upgradeOld4.ts
+++ b/server/tests/attach/upgradeOld/upgradeOld4.ts
@@ -5,8 +5,8 @@ import { AppEnv, Organization } from "@autumn/shared";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { products } from "tests/global.js";
import {
- attachFailedPaymentMethod,
- attachPmToCus,
+ attachFailedPaymentMethod,
+ attachPmToCus,
} from "@/external/stripe/stripeCusUtils.js";
import { Customer } from "@autumn/shared";
import { compareMainProduct } from "tests/utils/compare.js";
@@ -19,98 +19,98 @@ import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "upgradeOld4";
describe(`${chalk.yellowBright("upgradeOld4: Testing upgrade from pro -> premium")}`, () => {
- let customer: Customer;
- let customerId = testCase;
+ let customer: Customer;
+ let customerId = testCase;
- let stripeCli: Stripe;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
- let autumn: AutumnInt = new AutumnInt();
+ let stripeCli: Stripe;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
+ let autumn: AutumnInt = new AutumnInt();
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- stripeCli = this.stripeCli;
- db = this.db;
- org = this.org;
- env = this.env;
+ stripeCli = this.stripeCli;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- let { customer: customer_ } = await initCustomer({
- autumn: this.autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ let { customer: customer_ } = await initCustomer({
+ autumn: this.autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- customer = customer_;
- });
+ customer = customer_;
+ });
- it("should attach pro (trial)", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ it("should attach pro (trial)", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- let res = await autumn.customers.get(customerId);
- compareMainProduct({
- sent: products.pro,
- cusRes: res,
- });
- });
+ let res = await autumn.customers.get(customerId);
+ compareMainProduct({
+ sent: products.pro,
+ cusRes: res,
+ });
+ });
- // 1. Try force checkout...
- it("should attach premium and not be able to force checkout", async function () {
- expectAutumnError({
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premium.id,
- force_checkout: true,
- });
- },
- });
- });
+ // 1. Try force checkout...
+ it("should attach premium and not be able to force checkout", async function () {
+ expectAutumnError({
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premium.id,
+ force_checkout: true,
+ });
+ },
+ });
+ });
- it("should attach premium and not be able to upgrade (without payment method)", async function () {
- await attachFailedPaymentMethod({
- stripeCli: stripeCli,
- customer: customer,
- });
+ it("should attach premium and not be able to upgrade (without payment method)", async function () {
+ await attachFailedPaymentMethod({
+ stripeCli: stripeCli,
+ customer: customer,
+ });
- await expectAutumnError({
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.premium.id,
- force_checkout: true,
- });
- },
- });
- });
+ await expectAutumnError({
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.premium.id,
+ force_checkout: true,
+ });
+ },
+ });
+ });
- // Attach payment method
- it("should attach successful payment method", async function () {
- await attachPmToCus({
- db: this.db,
- customer: customer,
- org: this.org,
- env: this.env,
- });
- });
+ // Attach payment method
+ it("should attach successful payment method", async function () {
+ await attachPmToCus({
+ db: this.db,
+ customer: customer,
+ org: this.org,
+ env: this.env,
+ });
+ });
- it("should attach premium and have correct product and entitlements", async function () {
- await AutumnCli.attach({
- customerId: customerId,
- productId: products.premium.id,
- });
+ it("should attach premium and have correct product and entitlements", async function () {
+ await AutumnCli.attach({
+ customerId: customerId,
+ productId: products.premium.id,
+ });
- const res = await AutumnCli.getCustomer(customerId);
- compareMainProduct({
- sent: products.premium,
- cusRes: res,
- });
- });
+ const res = await AutumnCli.getCustomer(customerId);
+ compareMainProduct({
+ sent: products.premium,
+ cusRes: res,
+ });
+ });
});
diff --git a/server/tests/attach/utils.ts b/server/tests/attach/utils.ts
index 52912a03d..ccbf6dd06 100644
--- a/server/tests/attach/utils.ts
+++ b/server/tests/attach/utils.ts
@@ -1,13 +1,13 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- AppEnv,
- AttachBranch,
- BillingInterval,
- CreateEntity,
- FeatureOptions,
- Organization,
- ProductItem,
- ProductV2,
+ AppEnv,
+ AttachBranch,
+ BillingInterval,
+ CreateEntity,
+ FeatureOptions,
+ Organization,
+ ProductItem,
+ ProductV2,
} from "@autumn/shared";
import { getAttachTotal } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
@@ -156,50 +156,50 @@ import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduc
// };
export const addPrefixToProducts = ({
- products,
- prefix,
+ products,
+ prefix,
}: {
- products: ProductV2[];
- prefix: string;
+ products: ProductV2[];
+ prefix: string;
}) => {
- for (const product of products) {
- product.id = `${prefix}_${product.id}`;
- product.name = `${prefix} ${product.name}`;
- product.group = prefix;
- }
+ for (const product of products) {
+ product.id = `${prefix}_${product.id}`;
+ product.name = `${prefix} ${product.name}`;
+ product.group = prefix;
+ }
- return products;
+ return products;
};
export const replaceItems = ({
- featureId,
- interval,
- newItem,
- items,
+ featureId,
+ interval,
+ newItem,
+ items,
}: {
- featureId?: string;
- interval?: BillingInterval;
- newItem: ProductItem;
- items: ProductItem[];
+ featureId?: string;
+ interval?: BillingInterval;
+ newItem: ProductItem;
+ items: ProductItem[];
}) => {
- let newItems = structuredClone(items);
+ let newItems = structuredClone(items);
- let index;
- if (featureId) {
- index = newItems.findIndex((item) => item.feature_id == featureId);
- }
+ let index;
+ if (featureId) {
+ index = newItems.findIndex((item) => item.feature_id == featureId);
+ }
- if (interval) {
- index = newItems.findIndex(
- (item) => item.interval == (interval as any) && nullish(item.feature_id)
- );
- }
+ if (interval) {
+ index = newItems.findIndex(
+ (item) => item.interval == (interval as any) && nullish(item.feature_id),
+ );
+ }
- if (index == -1) {
- throw new Error("Item not found");
- }
+ if (index == -1) {
+ throw new Error("Item not found");
+ }
- newItems[index!] = newItem;
+ newItems[index!] = newItem;
- return newItems;
+ return newItems;
};
diff --git a/server/tests/before.ts b/server/tests/before.ts
index 3562c10b5..71450dab3 100644
--- a/server/tests/before.ts
+++ b/server/tests/before.ts
@@ -15,41 +15,41 @@ const DEFAULT_ENV = AppEnv.Sandbox;
import { Hyperbrowser } from "@hyperbrowser/sdk";
const hyperbrowser = new Hyperbrowser({
- apiKey: process.env.HYPERBROWSER_API_KEY,
+ apiKey: process.env.HYPERBROWSER_API_KEY,
});
export const setupBefore = async (instance: any) => {
- try {
- const { db, client } = initDrizzle();
+ try {
+ const { db, client } = initDrizzle();
- const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
- if (!org) {
- throw new Error("Org not found");
- }
- const env = DEFAULT_ENV;
- const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!;
- const autumn = new AutumnInt({ apiKey: autumnSecretKey });
+ const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
+ if (!org) {
+ throw new Error("Org not found");
+ }
+ const env = DEFAULT_ENV;
+ const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!;
+ const autumn = new AutumnInt({ apiKey: autumnSecretKey });
- const autumnJs = new AutumnJS({
- secretKey: autumnSecretKey,
- url: "http://localhost:8080/v1",
- });
+ const autumnJs = new AutumnJS({
+ secretKey: autumnSecretKey,
+ url: "http://localhost:8080/v1",
+ });
- const stripeCli = createStripeCli({ org, env });
- instance.org = org;
- instance.env = env;
- instance.autumn = autumn;
- instance.stripeCli = stripeCli;
- instance.autumnJs = autumnJs;
- instance.db = db;
- instance.client = client;
- } catch (error) {
- console.log("Error setting up before", error);
- throw error;
- }
+ const stripeCli = createStripeCli({ org, env });
+ instance.org = org;
+ instance.env = env;
+ instance.autumn = autumn;
+ instance.stripeCli = stripeCli;
+ instance.autumnJs = autumnJs;
+ instance.db = db;
+ instance.client = client;
+ } catch (error) {
+ console.log("Error setting up before", error);
+ throw error;
+ }
- // Return a cleanup function
- after(async () => {
- await instance.client?.end();
- });
+ // Return a cleanup function
+ after(async () => {
+ await instance.client?.end();
+ });
};
diff --git a/server/tests/browserSession.ts b/server/tests/browserSession.ts
index 98724d881..fceb4d021 100644
--- a/server/tests/browserSession.ts
+++ b/server/tests/browserSession.ts
@@ -3,7 +3,7 @@ import * as fs from "fs";
import * as path from "path";
const client = new Hyperbrowser({
- apiKey: process.env.HYPERBROWSER_API_KEY,
+ apiKey: process.env.HYPERBROWSER_API_KEY,
});
let sessionPromise: Promise | null = null;
@@ -16,171 +16,171 @@ const LOCK_FILE = path.join(TEMP_DIR, "browser-session.lock");
// Ensure temp directory exists
if (!fs.existsSync(TEMP_DIR)) {
- fs.mkdirSync(TEMP_DIR, { recursive: true });
+ fs.mkdirSync(TEMP_DIR, { recursive: true });
}
export const getBrowserSession = async () => {
- // If we already have a session in memory, return it
- if (currentSession) {
- return currentSession;
- }
+ // If we already have a session in memory, return it
+ if (currentSession) {
+ return currentSession;
+ }
- // Check if another process has already created a session
- const existingSession = loadSessionFromFile();
- if (existingSession) {
- currentSession = existingSession;
- console.log("Using existing browser session:", existingSession.id);
- return existingSession;
- }
+ // Check if another process has already created a session
+ const existingSession = loadSessionFromFile();
+ if (existingSession) {
+ currentSession = existingSession;
+ console.log("Using existing browser session:", existingSession.id);
+ return existingSession;
+ }
- // If session creation is already in progress, wait for it
- if (sessionPromise) {
- return await sessionPromise;
- }
+ // If session creation is already in progress, wait for it
+ if (sessionPromise) {
+ return await sessionPromise;
+ }
- // Try to acquire lock and create session
- sessionPromise = createSessionWithLock();
+ // Try to acquire lock and create session
+ sessionPromise = createSessionWithLock();
- try {
- currentSession = await sessionPromise;
- return currentSession;
- } catch (error) {
- // Reset promise on failure so we can retry
- sessionPromise = null;
- throw error;
- }
+ try {
+ currentSession = await sessionPromise;
+ return currentSession;
+ } catch (error) {
+ // Reset promise on failure so we can retry
+ sessionPromise = null;
+ throw error;
+ }
};
const createSessionWithLock = async (): Promise => {
- // Try to acquire lock
- const lockAcquired = await acquireLock();
+ // Try to acquire lock
+ const lockAcquired = await acquireLock();
- if (!lockAcquired) {
- // Another process is creating the session, wait for it
- console.log("Waiting for another process to create browser session...");
- return await waitForSession();
- }
+ if (!lockAcquired) {
+ // Another process is creating the session, wait for it
+ console.log("Waiting for another process to create browser session...");
+ return await waitForSession();
+ }
- try {
- // Double-check if session was created while we were acquiring lock
- const existingSession = loadSessionFromFile();
- if (existingSession) {
- console.log(
- "Session was created by another process:",
- existingSession.id
- );
- return existingSession;
- }
+ try {
+ // Double-check if session was created while we were acquiring lock
+ const existingSession = loadSessionFromFile();
+ if (existingSession) {
+ console.log(
+ "Session was created by another process:",
+ existingSession.id,
+ );
+ return existingSession;
+ }
- // Create new session
- console.log("Creating new browser session...");
- const session = await client.sessions.create();
- console.log("Browser session created successfully:", session.id);
+ // Create new session
+ console.log("Creating new browser session...");
+ const session = await client.sessions.create();
+ console.log("Browser session created successfully:", session.id);
- // Save session to file for other processes
- saveSessionToFile(session);
+ // Save session to file for other processes
+ saveSessionToFile(session);
- return session;
- } finally {
- // Always release lock
- releaseLock();
- }
+ return session;
+ } finally {
+ // Always release lock
+ releaseLock();
+ }
};
const acquireLock = async (): Promise => {
- try {
- // Try to create lock file exclusively
- fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: "wx" });
- return true;
- } catch (error) {
- // Lock file exists, another process has the lock
- return false;
- }
+ try {
+ // Try to create lock file exclusively
+ fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: "wx" });
+ return true;
+ } catch (error) {
+ // Lock file exists, another process has the lock
+ return false;
+ }
};
const releaseLock = () => {
- try {
- if (fs.existsSync(LOCK_FILE)) {
- fs.unlinkSync(LOCK_FILE);
- }
- } catch (error) {
- // Ignore errors when releasing lock
- console.warn("Warning: Could not release lock file:", error);
- }
+ try {
+ if (fs.existsSync(LOCK_FILE)) {
+ fs.unlinkSync(LOCK_FILE);
+ }
+ } catch (error) {
+ // Ignore errors when releasing lock
+ console.warn("Warning: Could not release lock file:", error);
+ }
};
const saveSessionToFile = (session: any) => {
- try {
- fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
- } catch (error) {
- console.error("Failed to save session to file:", error);
- }
+ try {
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
+ } catch (error) {
+ console.error("Failed to save session to file:", error);
+ }
};
const loadSessionFromFile = (): any | null => {
- try {
- if (fs.existsSync(SESSION_FILE)) {
- const sessionData = fs.readFileSync(SESSION_FILE, "utf-8");
- return JSON.parse(sessionData);
- }
- } catch (error) {
- // If file is corrupted or doesn't exist, ignore
- console.warn("Could not load session from file:", error);
- }
- return null;
+ try {
+ if (fs.existsSync(SESSION_FILE)) {
+ const sessionData = fs.readFileSync(SESSION_FILE, "utf-8");
+ return JSON.parse(sessionData);
+ }
+ } catch (error) {
+ // If file is corrupted or doesn't exist, ignore
+ console.warn("Could not load session from file:", error);
+ }
+ return null;
};
const waitForSession = async (): Promise => {
- // Poll for session file to appear
- for (let i = 0; i < 30; i++) {
- // Wait up to 30 seconds
- await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
+ // Poll for session file to appear
+ for (let i = 0; i < 30; i++) {
+ // Wait up to 30 seconds
+ await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
- const session = loadSessionFromFile();
- if (session) {
- console.log("Found session created by another process:", session.id);
- return session;
- }
- }
+ const session = loadSessionFromFile();
+ if (session) {
+ console.log("Found session created by another process:", session.id);
+ return session;
+ }
+ }
- throw new Error(
- "Timeout waiting for browser session to be created by another process"
- );
+ throw new Error(
+ "Timeout waiting for browser session to be created by another process",
+ );
};
// Reset session state (call this between test suites if needed)
export const resetBrowserSession = () => {
- console.log("Resetting browser session state...");
- currentSession = null;
- sessionPromise = null;
+ console.log("Resetting browser session state...");
+ currentSession = null;
+ sessionPromise = null;
- // Clean up files
- try {
- if (fs.existsSync(SESSION_FILE)) {
- fs.unlinkSync(SESSION_FILE);
- }
- if (fs.existsSync(LOCK_FILE)) {
- fs.unlinkSync(LOCK_FILE);
- }
- } catch (error) {
- console.warn("Warning: Could not clean up session files:", error);
- }
+ // Clean up files
+ try {
+ if (fs.existsSync(SESSION_FILE)) {
+ fs.unlinkSync(SESSION_FILE);
+ }
+ if (fs.existsSync(LOCK_FILE)) {
+ fs.unlinkSync(LOCK_FILE);
+ }
+ } catch (error) {
+ console.warn("Warning: Could not clean up session files:", error);
+ }
};
// Cleanup on process exit
process.on("exit", () => {
- if (currentSession) {
- console.log("Process exiting, cleaning up browser session files...");
- releaseLock();
- }
+ if (currentSession) {
+ console.log("Process exiting, cleaning up browser session files...");
+ releaseLock();
+ }
});
process.on("SIGINT", () => {
- resetBrowserSession();
- process.exit(0);
+ resetBrowserSession();
+ process.exit(0);
});
process.on("SIGTERM", () => {
- resetBrowserSession();
- process.exit(0);
+ resetBrowserSession();
+ process.exit(0);
});
diff --git a/server/tests/cli/AutumnCli.ts b/server/tests/cli/AutumnCli.ts
index 4ac9b5f3c..b87571e11 100644
--- a/server/tests/cli/AutumnCli.ts
+++ b/server/tests/cli/AutumnCli.ts
@@ -3,217 +3,217 @@ import { getAxiosInstance } from "../utils/setup.js";
import { CreateReward } from "@autumn/shared";
const handleAxiosError = (error: any) => {
- if (error.response.data) {
- throw new RecaseError({
- message: error.response.data.message,
- code: error.response.data.code,
- statusCode: error.response.data.statusCode,
- });
- }
- throw error;
+ if (error.response.data) {
+ throw new RecaseError({
+ message: error.response.data.message,
+ code: error.response.data.code,
+ statusCode: error.response.data.statusCode,
+ });
+ }
+ throw error;
};
export class AutumnCli {
- static async initStripeProducts() {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(`/v1/products/all/init_stripe`);
- return data;
- }
+ static async initStripeProducts() {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(`/v1/products/all/init_stripe`);
+ return data;
+ }
- static async getCustomer(customerId: string, params?: any) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.get(`/v1/customers/${customerId}`, {
- params,
- });
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ static async getCustomer(customerId: string, params?: any) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.get(`/v1/customers/${customerId}`, {
+ params,
+ });
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async entitled(
- customerId: string,
- featureId: string,
- getBalance: boolean = false,
- group?: string,
- ) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(`/v1/entitled`, {
- customer_id: customerId,
- feature_id: featureId,
- group,
- });
+ static async entitled(
+ customerId: string,
+ featureId: string,
+ getBalance: boolean = false,
+ group?: string,
+ ) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(`/v1/entitled`, {
+ customer_id: customerId,
+ feature_id: featureId,
+ group,
+ });
- if (getBalance) {
- return {
- allowed: data.allowed,
- balanceObj: data.balances.find(
- (b: any) => b.feature_id === featureId,
- ),
- };
- } else {
- return data as {
- allowed: boolean;
- balances: {
- feature_id: string;
- balance: number;
- required: number;
- unlimited: boolean;
- usage_allowed: boolean;
- }[];
- };
- }
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ if (getBalance) {
+ return {
+ allowed: data.allowed,
+ balanceObj: data.balances.find(
+ (b: any) => b.feature_id === featureId,
+ ),
+ };
+ } else {
+ return data as {
+ allowed: boolean;
+ balances: {
+ feature_id: string;
+ balance: number;
+ required: number;
+ unlimited: boolean;
+ usage_allowed: boolean;
+ }[];
+ };
+ }
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async attach({
- customerId,
- productId,
- productIds,
- options,
- forceCheckout,
- }: {
- customerId: string;
- productId?: string;
- productIds?: string[];
- options?: any;
- forceCheckout?: boolean;
- }) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(`/v1/attach`, {
- customer_id: customerId,
- product_id: productId,
- product_ids: productIds,
- options,
- force_checkout: forceCheckout,
- });
+ static async attach({
+ customerId,
+ productId,
+ productIds,
+ options,
+ forceCheckout,
+ }: {
+ customerId: string;
+ productId?: string;
+ productIds?: string[];
+ options?: any;
+ forceCheckout?: boolean;
+ }) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(`/v1/attach`, {
+ customer_id: customerId,
+ product_id: productId,
+ product_ids: productIds,
+ options,
+ force_checkout: forceCheckout,
+ });
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async sendEvent({
- customerId,
- eventName,
- properties,
- }: {
- customerId: string;
- eventName: string;
- properties?: any;
- }) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(`/v1/events`, {
- customer_id: customerId,
- event_name: eventName,
- properties,
- });
+ static async sendEvent({
+ customerId,
+ eventName,
+ properties,
+ }: {
+ customerId: string;
+ eventName: string;
+ properties?: any;
+ }) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(`/v1/events`, {
+ customer_id: customerId,
+ event_name: eventName,
+ properties,
+ });
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async usage({
- customerId,
- featureId,
- value,
- }: {
- customerId: string;
- featureId: string;
- value: number;
- }) {
- try {
- const axiosInstance = getAxiosInstance();
- await axiosInstance.post(`/v1/usage`, {
- customer_id: customerId,
- feature_id: featureId,
- value,
- });
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ static async usage({
+ customerId,
+ featureId,
+ value,
+ }: {
+ customerId: string;
+ featureId: string;
+ value: number;
+ }) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ await axiosInstance.post(`/v1/usage`, {
+ customer_id: customerId,
+ feature_id: featureId,
+ value,
+ });
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async expire(customerProductId: string) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(
- `/v1/customers/customer_products/${customerProductId}`,
- {
- status: "expired",
- },
- );
+ static async expire(customerProductId: string) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(
+ `/v1/customers/customer_products/${customerProductId}`,
+ {
+ status: "expired",
+ },
+ );
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async updateCusEntitlement({
- customerId,
- entitlementId,
- balance,
- }: {
- customerId: string;
- entitlementId: string;
- balance: number;
- }) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(
- `/v1/customers/${customerId}/customer_entitlements/${entitlementId}`,
- {
- balance,
- },
- );
+ static async updateCusEntitlement({
+ customerId,
+ entitlementId,
+ balance,
+ }: {
+ customerId: string;
+ entitlementId: string;
+ balance: number;
+ }) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(
+ `/v1/customers/${customerId}/customer_entitlements/${entitlementId}`,
+ {
+ balance,
+ },
+ );
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async updateBalances({
- customerId,
- balances,
- }: {
- customerId: string;
- balances: any;
- }) {
- try {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(
- `/v1/customers/${customerId}/balances`,
- {
- balances,
- },
- );
+ static async updateBalances({
+ customerId,
+ balances,
+ }: {
+ customerId: string;
+ balances: any;
+ }) {
+ try {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(
+ `/v1/customers/${customerId}/balances`,
+ {
+ balances,
+ },
+ );
- return data;
- } catch (error) {
- handleAxiosError(error);
- }
- }
+ return data;
+ } catch (error) {
+ handleAxiosError(error);
+ }
+ }
- static async getProducts() {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.get(`/v1/products?v1_schema=true`);
- return data;
- }
+ static async getProducts() {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.get(`/v1/products?v1_schema=true`);
+ return data;
+ }
- static async createCoupon(coupon: CreateReward) {
- const axiosInstance = getAxiosInstance();
- const { data } = await axiosInstance.post(`/v1/coupons`, coupon);
- return data;
- }
+ static async createCoupon(coupon: CreateReward) {
+ const axiosInstance = getAxiosInstance();
+ const { data } = await axiosInstance.post(`/v1/coupons`, coupon);
+ return data;
+ }
}
diff --git a/server/tests/contUse/entities/entity1.ts b/server/tests/contUse/entities/entity1.ts
index 3b7e31f87..dfbed09a3 100644
--- a/server/tests/contUse/entities/entity1.ts
+++ b/server/tests/contUse/entities/entity1.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -24,18 +24,18 @@ import { timeout } from "@/utils/genUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "entity1";
@@ -43,174 +43,174 @@ const testCase = "entity1";
// Pro is $20 / month, Seat is $50 / user
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create / delete entities`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += 1;
+ it("should create entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += 1;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: 1,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: 1,
+ },
+ ],
+ });
+ });
- const entities = [
- {
- id: "2",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "2",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create 2 entities and have correct invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 30,
- });
+ it("should create 2 entities and have correct invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 30,
+ });
- await autumn.entities.create(customerId, entities);
- await timeout(3000);
+ await autumn.entities.create(customerId, entities);
+ await timeout(3000);
- usage += entities.length;
+ usage += entities.length;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- itemQuantity: usage,
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ itemQuantity: usage,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices!;
- expect(invoices.length).to.equal(2);
- expect(invoices[0].total).to.equal(userItem.price! * entities.length);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices!;
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].total).to.equal(userItem.price! * entities.length);
+ });
- it("should delete 1 entity and have no new invoice", async function () {
- await autumn.entities.delete(customerId, entities[0].id);
+ it("should delete 1 entity and have no new invoice", async function () {
+ await autumn.entities.delete(customerId, entities[0].id);
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices!;
- expect(invoices.length).to.equal(2);
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices!;
+ expect(invoices.length).to.equal(2);
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 1,
- itemQuantity: usage - 1,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 1,
+ itemQuantity: usage - 1,
+ });
+ });
- const newEntities = [
- {
- id: "4",
- name: "test3",
- feature_id: TestFeature.Users,
- },
- {
- id: "5",
- name: "test4",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "4",
+ name: "test3",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "5",
+ name: "test4",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create 2 entities and have correct invoice (only pay for 1)", async function () {
- await autumn.entities.create(customerId, newEntities);
- await timeout(3000);
- usage += 1;
+ it("should create 2 entities and have correct invoice (only pay for 1)", async function () {
+ await autumn.entities.create(customerId, newEntities);
+ await timeout(3000);
+ usage += 1;
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices!;
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices!;
- expect(invoices.length).to.equal(3);
- expect(invoices[0].total).to.equal(userItem.price!);
+ expect(invoices.length).to.equal(3);
+ expect(invoices[0].total).to.equal(userItem.price!);
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- itemQuantity: usage,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ itemQuantity: usage,
+ });
+ });
});
diff --git a/server/tests/contUse/entities/entity2.ts b/server/tests/contUse/entities/entity2.ts
index cb8bcbb70..eec15289b 100644
--- a/server/tests/contUse/entities/entity2.ts
+++ b/server/tests/contUse/entities/entity2.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -18,180 +18,180 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import {
- calcProrationAndExpectInvoice,
- expectSubQuantityCorrect,
+ calcProrationAndExpectInvoice,
+ expectSubQuantityCorrect,
} from "tests/utils/expectUtils/expectContUseUtils.js";
import { addWeeks } from "date-fns";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { timeout } from "@/utils/genUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "entity2";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate now`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += 1;
+ it("should create entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += 1;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- const newEntities = [
- {
- id: "2",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test2",
- feature_id: TestFeature.Users,
- },
- ];
+ const newEntities = [
+ {
+ id: "2",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create 2 entities and have correct invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 30,
- });
+ it("should create 2 entities and have correct invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 30,
+ });
- await autumn.entities.create(customerId, newEntities);
- usage += newEntities.length;
+ await autumn.entities.create(customerId, newEntities);
+ usage += newEntities.length;
- const { stripeSubs } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- itemQuantity: usage,
- });
+ const { stripeSubs } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ itemQuantity: usage,
+ });
- await calcProrationAndExpectInvoice({
- autumn,
- stripeSubs,
- customerId,
- quantity: newEntities.length,
- unitPrice: userItem.price!,
- curUnix,
- numInvoices: 2,
- });
- });
+ await calcProrationAndExpectInvoice({
+ autumn,
+ stripeSubs,
+ customerId,
+ quantity: newEntities.length,
+ unitPrice: userItem.price!,
+ curUnix,
+ numInvoices: 2,
+ });
+ });
- it("should delete 1 entity and have correct invoice amount", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 30,
- });
+ it("should delete 1 entity and have correct invoice amount", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 30,
+ });
- await timeout(5000);
+ await timeout(5000);
- await autumn.entities.delete(customerId, newEntities[0].id);
- usage -= 1;
+ await autumn.entities.delete(customerId, newEntities[0].id);
+ usage -= 1;
- const { stripeSubs } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ const { stripeSubs } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await calcProrationAndExpectInvoice({
- autumn,
- stripeSubs,
- customerId,
- quantity: -1,
- unitPrice: userItem.price!,
- curUnix,
- numInvoices: 3,
- });
- });
+ await calcProrationAndExpectInvoice({
+ autumn,
+ stripeSubs,
+ customerId,
+ quantity: -1,
+ unitPrice: userItem.price!,
+ curUnix,
+ numInvoices: 3,
+ });
+ });
});
diff --git a/server/tests/contUse/entities/entity3.ts b/server/tests/contUse/entities/entity3.ts
index ee0d3d08a..838908131 100644
--- a/server/tests/contUse/entities/entity3.ts
+++ b/server/tests/contUse/entities/entity3.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,163 +25,163 @@ import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "entity3";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing replaceables deleted at end of cycle`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create three entities, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += firstEntities.length;
+ it("should create three entities, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += firstEntities.length;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should delete 2 entities and have no new invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 30,
- });
+ it("should delete 2 entities and have no new invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 30,
+ });
- await autumn.entities.delete(customerId, firstEntities[0].id);
- await autumn.entities.delete(customerId, firstEntities[1].id);
+ await autumn.entities.delete(customerId, firstEntities[0].id);
+ await autumn.entities.delete(customerId, firstEntities[1].id);
- const numReplaceables = 2;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables,
- itemQuantity: usage - numReplaceables,
- });
+ const numReplaceables = 2;
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables,
+ itemQuantity: usage - numReplaceables,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices!;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices!;
+ expect(invoices.length).to.equal(1);
+ });
- it("should advance clock to next cycle and have correct invoice", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- });
+ it("should advance clock to next cycle and have correct invoice", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ });
- usage -= 2; // 2 entities deleted
+ usage -= 2; // 2 entities deleted
- const customer = await autumn.customers.get(customerId);
- const invoices = customer.invoices;
+ const customer = await autumn.customers.get(customerId);
+ const invoices = customer.invoices;
- let basePrice = getBasePrice({ product: pro });
- expect(invoices.length).to.equal(2);
- expect(invoices[0].total).to.equal(basePrice); // 0 entities
+ let basePrice = getBasePrice({ product: pro });
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].total).to.equal(basePrice); // 0 entities
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- itemQuantity: usage,
- numReplaceables: 0,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ itemQuantity: usage,
+ numReplaceables: 0,
+ });
+ });
});
diff --git a/server/tests/contUse/entities/entity4.ts b/server/tests/contUse/entities/entity4.ts
index 81684fc9c..b3f9e4275 100644
--- a/server/tests/contUse/entities/entity4.ts
+++ b/server/tests/contUse/entities/entity4.ts
@@ -7,13 +7,13 @@ import { useEntityBalanceAndExpect } from "tests/utils/expectUtils/expectContUse
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusExpand,
- LimitedItem,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusExpand,
+ LimitedItem,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -24,221 +24,221 @@ import { addPrefixToProducts } from "../../attach/utils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructArrearProratedItem,
- constructFeatureItem,
+ constructArrearProratedItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
let perEntityItem = constructFeatureItem({
- featureId: TestFeature.Messages,
- entityFeatureId: TestFeature.Users,
- includedUsage: 500,
+ featureId: TestFeature.Messages,
+ entityFeatureId: TestFeature.Users,
+ includedUsage: 500,
}) as LimitedItem;
export let pro = constructProduct({
- items: [userItem, perEntityItem],
- type: "pro",
+ items: [userItem, perEntityItem],
+ type: "pro",
});
const testCase = "entity4";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing per entity features`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create one entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += firstEntities.length;
+ it("should create one entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += firstEntities.length;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should create 3 entities and have correct message balance", async function () {
- const newEntities = [
- {
- id: "2",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ it("should create 3 entities and have correct message balance", async function () {
+ const newEntities = [
+ {
+ id: "2",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- await autumn.entities.create(customerId, newEntities);
- usage += newEntities.length;
+ await autumn.entities.create(customerId, newEntities);
+ usage += newEntities.length;
- let customer = await autumn.customers.get(customerId, {
- expand: [CusExpand.Entities],
- });
+ let customer = await autumn.customers.get(customerId, {
+ expand: [CusExpand.Entities],
+ });
- let res = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
+ let res = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
- expect(res.balance).to.equal(
- (perEntityItem.included_usage as number) * usage
- );
+ expect(res.balance).to.equal(
+ (perEntityItem.included_usage as number) * usage,
+ );
- // @ts-ignore
- for (const entity of customer.entities) {
- let entRes = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: entity.id,
- });
+ // @ts-ignore
+ for (const entity of customer.entities) {
+ let entRes = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: entity.id,
+ });
- expect(entRes.balance).to.equal(perEntityItem.included_usage);
- }
- });
+ expect(entRes.balance).to.equal(perEntityItem.included_usage);
+ }
+ });
- return;
+ return;
- // 1. Use from main balance...
- it("should use from top level balance", async function () {
- let deduction = 600;
- let perEntityIncluded = perEntityItem.included_usage as number;
+ // 1. Use from main balance...
+ it("should use from top level balance", async function () {
+ let deduction = 600;
+ let perEntityIncluded = perEntityItem.included_usage as number;
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: deduction,
- });
- await timeout(5000);
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: deduction,
+ });
+ await timeout(5000);
- let { balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
+ let { balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
- expect(balance).to.equal(perEntityIncluded * usage - deduction);
- });
+ expect(balance).to.equal(perEntityIncluded * usage - deduction);
+ });
- it("should use from entity balance", async function () {
- await useEntityBalanceAndExpect({
- autumn,
- customerId,
- featureId: TestFeature.Messages,
- entityId: "2",
- });
+ it("should use from entity balance", async function () {
+ await useEntityBalanceAndExpect({
+ autumn,
+ customerId,
+ featureId: TestFeature.Messages,
+ entityId: "2",
+ });
- await useEntityBalanceAndExpect({
- autumn,
- customerId,
- featureId: TestFeature.Messages,
- entityId: "3",
- });
- });
+ await useEntityBalanceAndExpect({
+ autumn,
+ customerId,
+ featureId: TestFeature.Messages,
+ entityId: "3",
+ });
+ });
- // Delete one entity and create a new one and master balance should be same
- let deletedEntityId = "2";
- let newEntity = {
- id: "4",
- name: "test",
- feature_id: TestFeature.Users,
- };
- it("should delete one entity and create a new one", async function () {
- let { balance: masterBalanceBefore } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
+ // Delete one entity and create a new one and master balance should be same
+ let deletedEntityId = "2";
+ let newEntity = {
+ id: "4",
+ name: "test",
+ feature_id: TestFeature.Users,
+ };
+ it("should delete one entity and create a new one", async function () {
+ let { balance: masterBalanceBefore } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
- let { balance: entityBalanceBefore } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: deletedEntityId,
- });
+ let { balance: entityBalanceBefore } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: deletedEntityId,
+ });
- await autumn.entities.delete(customerId, deletedEntityId);
- await autumn.entities.create(customerId, [newEntity]);
+ await autumn.entities.delete(customerId, deletedEntityId);
+ await autumn.entities.create(customerId, [newEntity]);
- let { balance: masterBalanceAfter } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
+ let { balance: masterBalanceAfter } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
- expect(masterBalanceAfter).to.equal(masterBalanceBefore);
+ expect(masterBalanceAfter).to.equal(masterBalanceBefore);
- let { balance: entityBalanceAfter } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: newEntity.id,
- });
+ let { balance: entityBalanceAfter } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: newEntity.id,
+ });
- expect(entityBalanceAfter).to.equal(entityBalanceBefore);
- });
+ expect(entityBalanceAfter).to.equal(entityBalanceBefore);
+ });
});
diff --git a/server/tests/contUse/entities/entity5.ts b/server/tests/contUse/entities/entity5.ts
index da5fb6432..f9e904d44 100644
--- a/server/tests/contUse/entities/entity5.ts
+++ b/server/tests/contUse/entities/entity5.ts
@@ -4,12 +4,12 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- ErrCode,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ ErrCode,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -26,161 +26,161 @@ import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseU
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "entity5";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payment fail`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create one entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += firstEntities.length;
+ it("should create one entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += firstEntities.length;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should attach failed payment method", async function () {
- let fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ it("should attach failed payment method", async function () {
+ let fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- await attachFailedPaymentMethod({
- stripeCli,
- customer: fullCus,
- });
- });
+ await attachFailedPaymentMethod({
+ stripeCli,
+ customer: fullCus,
+ });
+ });
- it("should try to create entities and fail", async function () {
- await expectAutumnError({
- errMessage: "(Stripe Error) Your card was declined.",
- func: async () => {
- await autumn.entities.create(customerId, [
- {
- id: "2",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test",
- feature_id: TestFeature.Users,
- },
- ]);
- },
- });
+ it("should try to create entities and fail", async function () {
+ await expectAutumnError({
+ errMessage: "(Stripe Error) Your card was declined.",
+ func: async () => {
+ await autumn.entities.create(customerId, [
+ {
+ id: "2",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ ]);
+ },
+ });
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
+ });
- it("should track usage for users and fail", async function () {
- await expectAutumnError({
- errMessage: "(Stripe Error) Your card was declined.",
- func: async () => {
- return await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 2,
- });
- },
- });
+ it("should track usage for users and fail", async function () {
+ await expectAutumnError({
+ errMessage: "(Stripe Error) Your card was declined.",
+ func: async () => {
+ return await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 2,
+ });
+ },
+ });
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
+ });
});
diff --git a/server/tests/contUse/roles/role1.ts b/server/tests/contUse/roles/role1.ts
index 580c21864..cc3032124 100644
--- a/server/tests/contUse/roles/role1.ts
+++ b/server/tests/contUse/roles/role1.ts
@@ -1,11 +1,11 @@
// Handling per entity features!
import {
- APIVersion,
- AppEnv,
- LimitedItem,
- Organization,
- ProductItem,
+ APIVersion,
+ AppEnv,
+ LimitedItem,
+ Organization,
+ ProductItem,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -29,220 +29,220 @@ let user = TestFeature.Users;
let admin = TestFeature.Admin;
let userMessages = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- entityFeatureId: user,
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ entityFeatureId: user,
}) as LimitedItem;
let adminMessages = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 500,
- entityFeatureId: admin,
+ featureId: TestFeature.Messages,
+ includedUsage: 500,
+ entityFeatureId: admin,
}) as LimitedItem;
let adminRights = constructFeatureItem({
- featureId: TestFeature.AdminRights,
- entityFeatureId: admin,
- isBoolean: true,
+ featureId: TestFeature.AdminRights,
+ entityFeatureId: admin,
+ isBoolean: true,
}) as ProductItem;
export let pro = constructProduct({
- items: [userMessages, adminMessages, adminRights],
- type: "pro",
+ items: [userMessages, adminMessages, adminRights],
+ type: "pro",
});
const testCase = "role1";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing roles`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let userId = "user1";
- let adminId = "admin1";
- let firstEntities = [
- {
- id: userId,
- name: "test",
- feature_id: user,
- },
- {
- id: adminId,
- name: "test",
- feature_id: admin,
- },
- ];
+ let userId = "user1";
+ let adminId = "admin1";
+ let firstEntities = [
+ {
+ id: userId,
+ name: "test",
+ feature_id: user,
+ },
+ {
+ id: adminId,
+ name: "test",
+ feature_id: admin,
+ },
+ ];
- it("should create initial entities, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
+ it("should create initial entities, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should have correct check result for admin rights", async function () {
- let { allowed } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.AdminRights,
- entity_id: adminId,
- });
+ it("should have correct check result for admin rights", async function () {
+ let { allowed } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.AdminRights,
+ entity_id: adminId,
+ });
- let entity = await autumn.entities.get(customerId, adminId);
+ let entity = await autumn.entities.get(customerId, adminId);
- expect(allowed).to.equal(true);
- expect(entity.features[TestFeature.AdminRights]).exist;
+ expect(allowed).to.equal(true);
+ expect(entity.features[TestFeature.AdminRights]).exist;
- let { allowed: userAllowed } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.AdminRights,
- entity_id: userId,
- });
- let userEntity = await autumn.entities.get(customerId, userId);
+ let { allowed: userAllowed } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.AdminRights,
+ entity_id: userId,
+ });
+ let userEntity = await autumn.entities.get(customerId, userId);
- expect(userAllowed).to.equal(false);
- expect(userEntity.features[TestFeature.AdminRights]).not.exist;
- });
+ expect(userAllowed).to.equal(false);
+ expect(userEntity.features[TestFeature.AdminRights]).not.exist;
+ });
- it("should have correct total balance", async function () {
- let { balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- });
+ it("should have correct total balance", async function () {
+ let { balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ });
- let totalIncluded =
- userMessages.included_usage + adminMessages.included_usage;
+ let totalIncluded =
+ userMessages.included_usage + adminMessages.included_usage;
- expect(balance).to.equal(totalIncluded);
- });
+ expect(balance).to.equal(totalIncluded);
+ });
- it("should have correct per entity balance", async function () {
- let { balance: userBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: userId,
- });
+ it("should have correct per entity balance", async function () {
+ let { balance: userBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: userId,
+ });
- let userEntity = await autumn.entities.get(customerId, userId);
+ let userEntity = await autumn.entities.get(customerId, userId);
- expect(userBalance).to.equal(userMessages.included_usage);
- expect(userEntity.features[TestFeature.Messages].included_usage).to.equal(
- userMessages.included_usage
- );
+ expect(userBalance).to.equal(userMessages.included_usage);
+ expect(userEntity.features[TestFeature.Messages].included_usage).to.equal(
+ userMessages.included_usage,
+ );
- let { balance: adminBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: adminId,
- });
+ let { balance: adminBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: adminId,
+ });
- let adminEntity = await autumn.entities.get(customerId, adminId);
+ let adminEntity = await autumn.entities.get(customerId, adminId);
- expect(adminBalance).to.equal(adminMessages.included_usage);
- expect(adminEntity.features[TestFeature.Messages].included_usage).to.equal(
- adminMessages.included_usage
- );
- });
+ expect(adminBalance).to.equal(adminMessages.included_usage);
+ expect(adminEntity.features[TestFeature.Messages].included_usage).to.equal(
+ adminMessages.included_usage,
+ );
+ });
- let userUsage = Math.random() * 50;
- let expectedUserBalance = new Decimal(userMessages.included_usage)
- .minus(userUsage)
- .toNumber();
- it("should have correct user usage", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: userUsage,
- entity_id: userId,
- });
- await timeout(2000);
+ let userUsage = Math.random() * 50;
+ let expectedUserBalance = new Decimal(userMessages.included_usage)
+ .minus(userUsage)
+ .toNumber();
+ it("should have correct user usage", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: userUsage,
+ entity_id: userId,
+ });
+ await timeout(2000);
- let { balance: userBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: userId,
- });
+ let { balance: userBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: userId,
+ });
- let { balance: adminBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: adminId,
- });
+ let { balance: adminBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: adminId,
+ });
- expect(adminBalance).to.equal(adminMessages.included_usage);
- expect(userBalance).to.equal(expectedUserBalance);
- });
+ expect(adminBalance).to.equal(adminMessages.included_usage);
+ expect(userBalance).to.equal(expectedUserBalance);
+ });
- let adminUsage = Math.random() * 50;
- let expectedAdminBalance = new Decimal(adminMessages.included_usage)
- .minus(adminUsage)
- .toNumber();
- it("Should have correct admin usage", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: adminUsage,
- entity_id: adminId,
- });
- await timeout(2000);
+ let adminUsage = Math.random() * 50;
+ let expectedAdminBalance = new Decimal(adminMessages.included_usage)
+ .minus(adminUsage)
+ .toNumber();
+ it("Should have correct admin usage", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: adminUsage,
+ entity_id: adminId,
+ });
+ await timeout(2000);
- let { balance: adminBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: adminId,
- });
+ let { balance: adminBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: adminId,
+ });
- let { balance: userBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: userId,
- });
+ let { balance: userBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: userId,
+ });
- expect(adminBalance).to.equal(expectedAdminBalance);
- expect(userBalance).to.equal(expectedUserBalance);
- });
+ expect(adminBalance).to.equal(expectedAdminBalance);
+ expect(userBalance).to.equal(expectedUserBalance);
+ });
});
diff --git a/server/tests/contUse/roles/role2.ts b/server/tests/contUse/roles/role2.ts
index 5b76dd181..5cd93d1ee 100644
--- a/server/tests/contUse/roles/role2.ts
+++ b/server/tests/contUse/roles/role2.ts
@@ -2,11 +2,11 @@ import chalk from "chalk";
import Stripe from "stripe";
import {
- APIVersion,
- AppEnv,
- CreateEntity,
- LimitedItem,
- Organization,
+ APIVersion,
+ AppEnv,
+ CreateEntity,
+ LimitedItem,
+ Organization,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -31,163 +31,163 @@ let user = TestFeature.Users;
let admin = TestFeature.Admin;
let userMessages = constructArrearItem({
- featureId: TestFeature.Messages,
- price: 0.5,
- entityFeatureId: user,
+ featureId: TestFeature.Messages,
+ price: 0.5,
+ entityFeatureId: user,
}) as LimitedItem;
export let pro = constructProduct({
- items: [userMessages],
- type: "pro",
+ items: [userMessages],
+ type: "pro",
});
const testCase = "role2";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing overages for per entity`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let testClockId: string;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- });
+ testClockId = res.testClockId!;
+ });
- let user1 = "user1";
- let user2 = "user2";
+ let user1 = "user1";
+ let user2 = "user2";
- let firstEntities: CreateEntity[] = [
- {
- id: user1,
- name: "test",
- feature_id: user,
- },
- {
- id: user2,
- name: "test",
- feature_id: user,
- },
- ];
+ let firstEntities: CreateEntity[] = [
+ {
+ id: user1,
+ name: "test",
+ feature_id: user,
+ },
+ {
+ id: user2,
+ name: "test",
+ feature_id: user,
+ },
+ ];
- it("should create initial entities, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
+ it("should create initial entities, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- entities: firstEntities,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities: firstEntities,
+ });
- let customer = await autumn.customers.get(customerId);
- expect(customer.features[TestFeature.Messages].included_usage).to.equal(
- userMessages.included_usage * firstEntities.length
- );
- });
+ let customer = await autumn.customers.get(customerId);
+ expect(customer.features[TestFeature.Messages].included_usage).to.equal(
+ userMessages.included_usage * firstEntities.length,
+ );
+ });
- let user1Usage = 125000;
- let user2Usage = 150000;
- it("should track correct usage for seat messages", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: user1Usage,
- entity_id: user1,
- });
+ let user1Usage = 125000;
+ let user2Usage = 150000;
+ it("should track correct usage for seat messages", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: user1Usage,
+ entity_id: user1,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: user2Usage,
- entity_id: user2,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: user2Usage,
+ entity_id: user2,
+ });
- await timeout(4000);
+ await timeout(4000);
- let includedUsage = userMessages.included_usage;
+ let includedUsage = userMessages.included_usage;
- let { balance: userBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: user1,
- });
+ let { balance: userBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: user1,
+ });
- expect(userBalance).to.equal(includedUsage - user1Usage);
+ expect(userBalance).to.equal(includedUsage - user1Usage);
- let { balance: user2Balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: user2,
- });
+ let { balance: user2Balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: user2,
+ });
- expect(user2Balance).to.equal(includedUsage - user2Usage);
- });
+ expect(user2Balance).to.equal(includedUsage - user2Usage);
+ });
- it("should have correct invoice next cycle", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ it("should have correct invoice next cycle", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
- let includedUsage = userMessages.included_usage;
- let user1Overage = user1Usage - includedUsage;
- let user2Overage = user2Usage - includedUsage;
+ let includedUsage = userMessages.included_usage;
+ let user1Overage = user1Usage - includedUsage;
+ let user2Overage = user2Usage - includedUsage;
- let totalUsage = user1Overage + user2Overage + includedUsage;
+ let totalUsage = user1Overage + user2Overage + includedUsage;
- let expectedInvoiceTotal = await getExpectedInvoiceTotal({
- customerId,
- productId: pro.id,
- usage: [{ featureId: TestFeature.Messages, value: totalUsage }],
- stripeCli,
- db,
- org,
- env,
- expectExpired: true,
- });
+ let expectedInvoiceTotal = await getExpectedInvoiceTotal({
+ customerId,
+ productId: pro.id,
+ usage: [{ featureId: TestFeature.Messages, value: totalUsage }],
+ stripeCli,
+ db,
+ org,
+ env,
+ expectExpired: true,
+ });
- let customer = await autumn.customers.get(customerId);
- expect(customer.invoices[0].total).to.equal(expectedInvoiceTotal);
- });
+ let customer = await autumn.customers.get(customerId);
+ expect(customer.invoices[0].total).to.equal(expectedInvoiceTotal);
+ });
});
diff --git a/server/tests/contUse/roles/role3.ts b/server/tests/contUse/roles/role3.ts
index be2dfb033..92302ec09 100644
--- a/server/tests/contUse/roles/role3.ts
+++ b/server/tests/contUse/roles/role3.ts
@@ -2,11 +2,11 @@ import chalk from "chalk";
import Stripe from "stripe";
import {
- APIVersion,
- AppEnv,
- CreateEntity,
- LimitedItem,
- Organization,
+ APIVersion,
+ AppEnv,
+ CreateEntity,
+ LimitedItem,
+ Organization,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -31,233 +31,233 @@ let user = TestFeature.Users;
let admin = TestFeature.Admin;
let userMessages = constructArrearItem({
- featureId: TestFeature.Messages,
- price: 0.5,
- entityFeatureId: user,
+ featureId: TestFeature.Messages,
+ price: 0.5,
+ entityFeatureId: user,
}) as LimitedItem;
let adminMessages = constructArrearItem({
- featureId: TestFeature.Messages,
- includedUsage: 0,
- price: 0.1,
- entityFeatureId: admin,
+ featureId: TestFeature.Messages,
+ includedUsage: 0,
+ price: 0.1,
+ entityFeatureId: admin,
}) as LimitedItem;
export let pro = constructProduct({
- items: [userMessages, adminMessages],
- type: "pro",
+ items: [userMessages, adminMessages],
+ type: "pro",
});
const testCase = "role3";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing overages for per entity, diff roles`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let testClockId: string;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- });
+ testClockId = res.testClockId!;
+ });
- let user1 = "user1";
- let user2 = "user2";
- let admin1 = "admin1";
- let admin2 = "admin2";
- let firstEntities: CreateEntity[] = [
- {
- id: user1,
- name: "test",
- feature_id: user,
- },
- {
- id: user2,
- name: "test",
- feature_id: user,
- },
- {
- id: admin1,
- name: "test",
- feature_id: admin,
- },
- {
- id: admin2,
- name: "test",
- feature_id: admin,
- },
- ];
+ let user1 = "user1";
+ let user2 = "user2";
+ let admin1 = "admin1";
+ let admin2 = "admin2";
+ let firstEntities: CreateEntity[] = [
+ {
+ id: user1,
+ name: "test",
+ feature_id: user,
+ },
+ {
+ id: user2,
+ name: "test",
+ feature_id: user,
+ },
+ {
+ id: admin1,
+ name: "test",
+ feature_id: admin,
+ },
+ {
+ id: admin2,
+ name: "test",
+ feature_id: admin,
+ },
+ ];
- it("should create initial entities, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
+ it("should create initial entities, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- entities: firstEntities,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities: firstEntities,
+ });
+ });
- let user1Usage = 125000;
- let user2Usage = 150000;
+ let user1Usage = 125000;
+ let user2Usage = 150000;
- // total: 275000, included: 10000, overage: 255000
- it("should track correct usage for seat messages", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: user1Usage,
- entity_id: user1,
- });
+ // total: 275000, included: 10000, overage: 255000
+ it("should track correct usage for seat messages", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: user1Usage,
+ entity_id: user1,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: user2Usage,
- entity_id: user2,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: user2Usage,
+ entity_id: user2,
+ });
- await timeout(4000);
+ await timeout(4000);
- let includedUsage = userMessages.included_usage;
+ let includedUsage = userMessages.included_usage;
- let { balance: userBalance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: user1,
- });
+ let { balance: userBalance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: user1,
+ });
- expect(userBalance).to.equal(includedUsage - user1Usage);
+ expect(userBalance).to.equal(includedUsage - user1Usage);
- let { balance: user2Balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: user2,
- });
+ let { balance: user2Balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: user2,
+ });
- expect(user2Balance).to.equal(includedUsage - user2Usage);
+ expect(user2Balance).to.equal(includedUsage - user2Usage);
- const { balance: admin1Balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: admin1,
- });
+ const { balance: admin1Balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: admin1,
+ });
- expect(admin1Balance).to.equal(adminMessages.included_usage);
+ expect(admin1Balance).to.equal(adminMessages.included_usage);
- const { balance: admin2Balance } = await autumn.check({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- entity_id: admin2,
- });
+ const { balance: admin2Balance } = await autumn.check({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ entity_id: admin2,
+ });
- expect(admin2Balance).to.equal(adminMessages.included_usage);
- });
+ expect(admin2Balance).to.equal(adminMessages.included_usage);
+ });
- let admin1Usage = 130000;
- let admin2Usage = 140000;
- // total: 270000, included: 0, overage: 270000
- it("should track correct usage for admin messages", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: admin1Usage,
- entity_id: admin1,
- });
+ let admin1Usage = 130000;
+ let admin2Usage = 140000;
+ // total: 270000, included: 0, overage: 270000
+ it("should track correct usage for admin messages", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: admin1Usage,
+ entity_id: admin1,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: admin2Usage,
- entity_id: admin2,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: admin2Usage,
+ entity_id: admin2,
+ });
- await timeout(4000);
- });
+ await timeout(4000);
+ });
- it("should have correct invoice next cycle", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1).getTime(),
- // addHours(
- // addMonths(new Date(), 1),
- // hoursToFinalizeInvoice
- // ).getTime(),
- waitForSeconds: 30,
- });
+ it("should have correct invoice next cycle", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1).getTime(),
+ // addHours(
+ // addMonths(new Date(), 1),
+ // hoursToFinalizeInvoice
+ // ).getTime(),
+ waitForSeconds: 30,
+ });
- return;
+ return;
- let includedUsage = userMessages.included_usage;
- let user1Overage = user1Usage - includedUsage;
- let user2Overage = user2Usage - includedUsage;
- let totalUserUsage = user1Overage + user2Overage + includedUsage;
+ let includedUsage = userMessages.included_usage;
+ let user1Overage = user1Usage - includedUsage;
+ let user2Overage = user2Usage - includedUsage;
+ let totalUserUsage = user1Overage + user2Overage + includedUsage;
- let admin1Overage = admin1Usage - adminMessages.included_usage;
- let admin2Overage = admin2Usage - adminMessages.included_usage;
- let totalAdminUsage =
- admin1Overage + admin2Overage + adminMessages.included_usage;
+ let admin1Overage = admin1Usage - adminMessages.included_usage;
+ let admin2Overage = admin2Usage - adminMessages.included_usage;
+ let totalAdminUsage =
+ admin1Overage + admin2Overage + adminMessages.included_usage;
- let expectedInvoiceTotal = await getExpectedInvoiceTotal({
- customerId,
- productId: pro.id,
- usage: [
- {
- featureId: TestFeature.Messages,
- entityFeatureId: user,
- value: totalUserUsage,
- },
- {
- featureId: TestFeature.Messages,
- entityFeatureId: admin,
- value: totalAdminUsage,
- },
- ],
- stripeCli,
- db,
- org,
- env,
- expectExpired: true,
- });
+ let expectedInvoiceTotal = await getExpectedInvoiceTotal({
+ customerId,
+ productId: pro.id,
+ usage: [
+ {
+ featureId: TestFeature.Messages,
+ entityFeatureId: user,
+ value: totalUserUsage,
+ },
+ {
+ featureId: TestFeature.Messages,
+ entityFeatureId: admin,
+ value: totalAdminUsage,
+ },
+ ],
+ stripeCli,
+ db,
+ org,
+ env,
+ expectExpired: true,
+ });
- let customer = await autumn.customers.get(customerId);
- expect(customer.invoices[0].total).to.equal(expectedInvoiceTotal);
- });
+ let customer = await autumn.customers.get(customerId);
+ expect(customer.invoices[0].total).to.equal(expectedInvoiceTotal);
+ });
});
diff --git a/server/tests/contUse/track/track1.ts b/server/tests/contUse/track/track1.ts
index 5316a3160..8b7701cc5 100644
--- a/server/tests/contUse/track/track1.ts
+++ b/server/tests/contUse/track/track1.ts
@@ -1,12 +1,12 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- entities,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ entities,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,159 +25,159 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "track1";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- it("should attach pro", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ let usage = 0;
+ it("should attach pro", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should create track +3 usage and have correct invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 5,
- });
+ it("should create track +3 usage and have correct invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 5,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 3,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 3,
+ });
- await timeout(15000);
+ await timeout(15000);
- usage += 3;
+ usage += 3;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
- expect(invoices[0].total).to.equal(userItem.price! * 2);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].total).to.equal(userItem.price! * 2);
+ });
- it("should track -3 and have no new invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: -3,
- });
+ it("should track -3 and have no new invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: -3,
+ });
- await timeout(5000);
+ await timeout(5000);
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 3,
- itemQuantity: usage - 3,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 3,
+ itemQuantity: usage - 3,
+ });
+ });
- it("should track +3 and have no new invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 3,
- });
+ it("should track +3 and have no new invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 3,
+ });
- await timeout(5000);
+ await timeout(5000);
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
+ });
});
diff --git a/server/tests/contUse/track/track2.ts b/server/tests/contUse/track/track2.ts
index f71521f09..ec4815c0f 100644
--- a/server/tests/contUse/track/track2.ts
+++ b/server/tests/contUse/track/track2.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -22,119 +22,119 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "track2";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use (without overage)`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- it("should attach pro", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ let usage = 0;
+ it("should attach pro", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should track +1 and have no new invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 1,
- });
+ it("should track +1 and have no new invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 1,
+ });
- usage += 1;
+ usage += 1;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
- it("should track -1 and have no new invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: -1,
- });
+ it("should track -1 and have no new invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: -1,
+ });
- usage -= 1;
+ usage -= 1;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
+ });
});
diff --git a/server/tests/contUse/track/track3.ts b/server/tests/contUse/track/track3.ts
index 8766e4111..59eccc379 100644
--- a/server/tests/contUse/track/track3.ts
+++ b/server/tests/contUse/track/track3.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -25,194 +25,194 @@ import { expectUpcomingItemsCorrect } from "tests/utils/expectUtils/expectContUs
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.ProrateNextCycle,
- on_decrease: OnDecrease.ProrateNextCycle,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.ProrateNextCycle,
+ on_decrease: OnDecrease.ProrateNextCycle,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "track3";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing track usage for cont use, prorate next cycle`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- it("should attach pro", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ let usage = 0;
+ it("should attach pro", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should create track +3 usage and have correct invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 5,
- });
+ it("should create track +3 usage and have correct invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 5,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 3,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 3,
+ });
- await timeout(15000);
+ await timeout(15000);
- usage += 3;
+ usage += 3;
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- curUnix,
- expectedNumItems: 1,
- unitPrice: userItem.price!,
- quantity: 2,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ curUnix,
+ expectedNumItems: 1,
+ unitPrice: userItem.price!,
+ quantity: 2,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
- it("should track -1 and have no new invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 5,
- });
+ it("should track -1 and have no new invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 5,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: -1,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: -1,
+ });
- usage -= 1;
+ usage -= 1;
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- unitPrice: userItem.price!,
- curUnix,
- expectedNumItems: 2,
- quantity: -1,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ unitPrice: userItem.price!,
+ curUnix,
+ expectedNumItems: 2,
+ quantity: -1,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
- it("should track -1 and have no new invoice", async function () {
- let quantity = 2;
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: quantity,
- });
+ it("should track -1 and have no new invoice", async function () {
+ let quantity = 2;
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: quantity,
+ });
- usage += quantity;
+ usage += quantity;
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- unitPrice: userItem.price!,
- curUnix,
- expectedNumItems: 3,
- quantity,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ unitPrice: userItem.price!,
+ curUnix,
+ expectedNumItems: 3,
+ quantity,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
});
diff --git a/server/tests/contUse/track/track4.ts b/server/tests/contUse/track/track4.ts
index e2ddbfe35..9dcf3e50b 100644
--- a/server/tests/contUse/track/track4.ts
+++ b/server/tests/contUse/track/track4.ts
@@ -4,11 +4,11 @@ import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -27,194 +27,194 @@ import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseU
import { expectUpcomingItemsCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.ProrateNextCycle,
- on_decrease: OnDecrease.ProrateNextCycle,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.ProrateNextCycle,
+ on_decrease: OnDecrease.ProrateNextCycle,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "track4";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing set usage for cont use, prorate next cycle`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- it("should attach pro", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ let usage = 0;
+ it("should attach pro", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- it("should create set usage to 3 and have correct invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- waitForSeconds: 15,
- });
+ it("should create set usage to 3 and have correct invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ waitForSeconds: 15,
+ });
- await autumn.usage({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 3,
- });
+ await autumn.usage({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 3,
+ });
- await timeout(15000);
+ await timeout(15000);
- usage += 3;
+ usage += 3;
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- curUnix,
- expectedNumItems: 1,
- unitPrice: userItem.price!,
- quantity: 2,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ curUnix,
+ expectedNumItems: 1,
+ unitPrice: userItem.price!,
+ quantity: 2,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
- it("should set usage to 2 and have no new invoice", async function () {
- let newUsage = 2;
+ it("should set usage to 2 and have no new invoice", async function () {
+ let newUsage = 2;
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 15,
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 15,
+ });
- await autumn.usage({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: newUsage,
- });
+ await autumn.usage({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: newUsage,
+ });
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage: newUsage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage: newUsage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- unitPrice: userItem.price!,
- curUnix,
- expectedNumItems: 2,
- quantity: -1,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ unitPrice: userItem.price!,
+ curUnix,
+ expectedNumItems: 2,
+ quantity: -1,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
- it("should set usage to 4 and have no new invoice", async function () {
- let newUsage = 4;
- await autumn.usage({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: newUsage,
- });
+ it("should set usage to 4 and have no new invoice", async function () {
+ let newUsage = 4;
+ await autumn.usage({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: newUsage,
+ });
- usage = newUsage;
+ usage = newUsage;
- let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- });
+ let { stripeSubs, cusProduct, fullCus } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ });
- await expectUpcomingItemsCorrect({
- stripeCli,
- fullCus,
- stripeSubs,
- unitPrice: userItem.price!,
- curUnix,
- expectedNumItems: 3,
- quantity: 2,
- });
+ await expectUpcomingItemsCorrect({
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ unitPrice: userItem.price!,
+ curUnix,
+ expectedNumItems: 3,
+ quantity: 2,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(1);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(1);
+ });
});
diff --git a/server/tests/contUse/track/track5.ts b/server/tests/contUse/track/track5.ts
index 52b95d4e1..c169e902d 100644
--- a/server/tests/contUse/track/track5.ts
+++ b/server/tests/contUse/track/track5.ts
@@ -15,8 +15,8 @@ import { AppEnv, OnDecrease, OnIncrease, Organization } from "@autumn/shared";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
@@ -28,231 +28,231 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
const seatsItem = constructArrearProratedItem({
- featureId: features.seats.id,
- pricePerUnit: 20,
- includedUsage: 3,
- config: {
- on_increase: OnIncrease.ProrateNextCycle,
- on_decrease: OnDecrease.ProrateNextCycle,
- },
+ featureId: features.seats.id,
+ pricePerUnit: 20,
+ includedUsage: 3,
+ config: {
+ on_increase: OnIncrease.ProrateNextCycle,
+ on_decrease: OnDecrease.ProrateNextCycle,
+ },
});
const seatsProduct = constructProduct({
- type: "pro",
- items: [seatsItem],
+ type: "pro",
+ items: [seatsItem],
});
const testCase = "track5";
const includedUsage = seatsItem.included_usage as number;
const simulateOneCycle = async ({
- customerId,
- db,
- org,
- env,
- stripeCli,
- curUnix,
- usageValues,
- autumn,
- testClockId,
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ curUnix,
+ usageValues,
+ autumn,
+ testClockId,
}: {
- customerId: string;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- stripeCli: Stripe;
- curUnix: number;
- usageValues: number[];
- autumn: AutumnInt;
- testClockId: string;
+ customerId: string;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ stripeCli: Stripe;
+ curUnix: number;
+ usageValues: number[];
+ autumn: AutumnInt;
+ testClockId: string;
}) => {
- const { subs } = await getSubsFromCusId({
- customerId,
- db,
- org,
- env,
- stripeCli,
- productId: seatsProduct.id,
- });
+ const { subs } = await getSubsFromCusId({
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ productId: seatsProduct.id,
+ });
- let sub = subs[0];
+ let sub = subs[0];
- let accruedPrice = 0;
- for (const usageValue of usageValues) {
- let daysToAdvance = Math.round(Math.random() * 10) + 1;
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(curUnix, daysToAdvance).getTime(),
- waitForSeconds: 10,
- });
+ let accruedPrice = 0;
+ for (const usageValue of usageValues) {
+ let daysToAdvance = Math.round(Math.random() * 10) + 1;
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(curUnix, daysToAdvance).getTime(),
+ waitForSeconds: 10,
+ });
- let customer = await autumn.customers.get(customerId);
- let prevBalance = customer.features[seatsItem.feature_id!].balance!;
- let prevUsage = includedUsage - prevBalance;
+ let customer = await autumn.customers.get(customerId);
+ let prevBalance = customer.features[seatsItem.feature_id!].balance!;
+ let prevUsage = includedUsage - prevBalance;
- let usageDiff = usageValue - prevUsage;
+ let usageDiff = usageValue - prevUsage;
- let value1 = Math.floor(usageDiff / 2);
- let value2 = usageDiff - value1;
+ let value1 = Math.floor(usageDiff / 2);
+ let value2 = usageDiff - value1;
- await autumn.track({
- customer_id: customerId,
- feature_id: seatsItem.feature_id!,
- value: value1,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: seatsItem.feature_id!,
+ value: value1,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: seatsItem.feature_id!,
- value: value2,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: seatsItem.feature_id!,
+ value: value2,
+ });
- let newBalance = includedUsage - usageValue;
- let prevOverage = Math.max(0, -prevBalance);
- let newOverage = Math.max(0, -newBalance);
+ let newBalance = includedUsage - usageValue;
+ let prevOverage = Math.max(0, -prevBalance);
+ let newOverage = Math.max(0, -newBalance);
- let newPrice = (newOverage - prevOverage) * seatsItem.price!;
+ let newPrice = (newOverage - prevOverage) * seatsItem.price!;
- const { start, end } = subToPeriodStartEnd({ sub });
- let proratedPrice = calculateProrationAmount({
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- amount: newPrice,
- allowNegative: true,
- });
+ const { start, end } = subToPeriodStartEnd({ sub });
+ let proratedPrice = calculateProrationAmount({
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ amount: newPrice,
+ allowNegative: true,
+ });
- accruedPrice = new Decimal(accruedPrice).plus(proratedPrice).toNumber();
- }
+ accruedPrice = new Decimal(accruedPrice).plus(proratedPrice).toNumber();
+ }
- let customer = await autumn.customers.get(customerId);
- let balance = customer.features[seatsItem.feature_id!].balance!;
+ let customer = await autumn.customers.get(customerId);
+ let balance = customer.features[seatsItem.feature_id!].balance!;
- let overage = Math.min(0, includedUsage - balance);
- let usagePrice = overage * seatsItem.price!;
- let basePrice = getBasePrice({ product: seatsProduct });
+ let overage = Math.min(0, includedUsage - balance);
+ let usagePrice = overage * seatsItem.price!;
+ let basePrice = getBasePrice({ product: seatsProduct });
- const totalPrice = new Decimal(accruedPrice)
- .plus(usagePrice)
- .plus(basePrice)
- .toDecimalPlaces(2)
- .toNumber();
+ const totalPrice = new Decimal(accruedPrice)
+ .plus(usagePrice)
+ .plus(basePrice)
+ .toDecimalPlaces(2)
+ .toNumber();
- const { start, end } = subToPeriodStartEnd({ sub });
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(end * 1000, hoursToFinalizeInvoice).getTime(),
- waitForSeconds: 30,
- });
+ const { start, end } = subToPeriodStartEnd({ sub });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(end * 1000, hoursToFinalizeInvoice).getTime(),
+ waitForSeconds: 30,
+ });
- let cusAfter = await autumn.customers.get(customerId);
- let invoices = cusAfter.invoices;
- let invoice = invoices[0];
+ let cusAfter = await autumn.customers.get(customerId);
+ let invoices = cusAfter.invoices;
+ let invoice = invoices[0];
- expect(invoice.total).to.approximately(
- totalPrice,
- 0.01,
- `Invoice total should be ${totalPrice} +/- 0.01`
- );
+ expect(invoice.total).to.approximately(
+ totalPrice,
+ 0.01,
+ `Invoice total should be ${totalPrice} +/- 0.01`,
+ );
- return {
- curUnix,
- };
+ return {
+ curUnix,
+ };
};
describe(`${chalk.yellowBright("conUse/track5: Testing update cont use through /usage")}`, () => {
- const customerId = testCase;
+ const customerId = testCase;
- let stripeCli: Stripe;
+ let stripeCli: Stripe;
- let testClockId = "";
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
- let autumn = new AutumnInt({ version: defaultApiVersion });
- let curUnix = Date.now();
+ let testClockId = "";
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
+ let autumn = new AutumnInt({ version: defaultApiVersion });
+ let curUnix = Date.now();
- before(async function () {
- await setupBefore(this);
- org = this.org;
- env = this.env;
- db = this.db;
+ before(async function () {
+ await setupBefore(this);
+ org = this.org;
+ env = this.env;
+ db = this.db;
- let res = await initCustomer({
- customerId,
- org,
- env,
- db,
- autumn: this.autumnJs,
- attachPm: "success",
- });
+ let res = await initCustomer({
+ customerId,
+ org,
+ env,
+ db,
+ autumn: this.autumnJs,
+ attachPm: "success",
+ });
- addPrefixToProducts({
- products: [seatsProduct],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [seatsProduct],
+ prefix: testCase,
+ });
- await createProducts({
- products: [seatsProduct],
- orgId: org.id,
- env,
- db,
- autumn,
- });
+ await createProducts({
+ products: [seatsProduct],
+ orgId: org.id,
+ env,
+ db,
+ autumn,
+ });
- testClockId = res.testClockId;
+ testClockId = res.testClockId;
- db = this.db;
- org = this.org;
- env = this.env;
- stripeCli = this.stripeCli;
- });
+ db = this.db;
+ org = this.org;
+ env = this.env;
+ stripeCli = this.stripeCli;
+ });
- it("should attach in arrear prorated seats", async () => {
- await attachAndExpectCorrect({
- customerId,
- product: seatsProduct,
- db,
- org,
- env,
- autumn,
- stripeCli,
- });
- });
+ it("should attach in arrear prorated seats", async () => {
+ await attachAndExpectCorrect({
+ customerId,
+ product: seatsProduct,
+ db,
+ org,
+ env,
+ autumn,
+ stripeCli,
+ });
+ });
- // return;
+ // return;
- it("simulate first cycle and have correct invoice / balance", async () => {
- let res = await simulateOneCycle({
- customerId,
- db,
- org,
- env,
- stripeCli,
- curUnix,
- usageValues: [8, 2],
- autumn,
- testClockId,
- });
+ it("simulate first cycle and have correct invoice / balance", async () => {
+ let res = await simulateOneCycle({
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ curUnix,
+ usageValues: [8, 2],
+ autumn,
+ testClockId,
+ });
- curUnix = res.curUnix;
- });
+ curUnix = res.curUnix;
+ });
- it("simulate second cycle and have correct invoice / balance", async () => {
- let res = await simulateOneCycle({
- customerId,
- db,
- org,
- env,
- stripeCli,
- curUnix,
- usageValues: [12, 3],
- autumn,
- testClockId,
- });
+ it("simulate second cycle and have correct invoice / balance", async () => {
+ let res = await simulateOneCycle({
+ customerId,
+ db,
+ org,
+ env,
+ stripeCli,
+ curUnix,
+ usageValues: [12, 3],
+ autumn,
+ testClockId,
+ });
- curUnix = res.curUnix;
- });
+ curUnix = res.curUnix;
+ });
});
diff --git a/server/tests/contUse/track/track6.ts b/server/tests/contUse/track/track6.ts
index 722947a01..488b2c807 100644
--- a/server/tests/contUse/track/track6.ts
+++ b/server/tests/contUse/track/track6.ts
@@ -14,103 +14,103 @@ import { timeout } from "@/utils/genUtils.js";
import { expect } from "chai";
const userItem = constructFeatureItem({
- featureId: TestFeature.Users,
- includedUsage: 5,
+ featureId: TestFeature.Users,
+ includedUsage: 5,
}) as LimitedItem;
export let free = constructProduct({
- items: [userItem],
- type: "free",
- isDefault: false,
+ items: [userItem],
+ type: "free",
+ isDefault: false,
});
const testCase = "track6";
describe(`${chalk.yellowBright(`${testCase}: Testing track cont use, race condition`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should track 5 events in a row and have correct balance", async function () {
- let startingBalance = userItem.included_usage;
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
+ it("should track 5 events in a row and have correct balance", async function () {
+ let startingBalance = userItem.included_usage;
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
- const promises = [];
- for (let i = 0; i < 2; i++) {
- console.log("--------------------------------");
- console.log(`Cycle ${i}`);
- console.log(`Starting balance: ${startingBalance}`);
- const values = [];
- for (let i = 0; i < 10; i++) {
- const randomVal =
- Math.floor(Math.random() * 5) * (Math.random() < 0.3 ? -1 : 1);
- promises.push(
- autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: randomVal,
- })
- );
- startingBalance -= randomVal;
- values.push(randomVal);
- }
+ const promises = [];
+ for (let i = 0; i < 2; i++) {
+ console.log("--------------------------------");
+ console.log(`Cycle ${i}`);
+ console.log(`Starting balance: ${startingBalance}`);
+ const values = [];
+ for (let i = 0; i < 10; i++) {
+ const randomVal =
+ Math.floor(Math.random() * 5) * (Math.random() < 0.3 ? -1 : 1);
+ promises.push(
+ autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: randomVal,
+ }),
+ );
+ startingBalance -= randomVal;
+ values.push(randomVal);
+ }
- console.log(`New balance: ${startingBalance}`);
+ console.log(`New balance: ${startingBalance}`);
- const results = await Promise.all(promises);
+ const results = await Promise.all(promises);
- await timeout(10000);
+ await timeout(10000);
- let customer = await autumn.customers.get(customerId);
- let userFeature = customer.features[TestFeature.Users];
- if (userFeature.balance != startingBalance) {
- for (let i = 0; i < values.length; i++) {
- console.log(`Value: ${values[i]}, Event ID: ${results[i].id}`);
- }
- }
- expect(userFeature.balance).to.equal(startingBalance);
- }
- });
+ let customer = await autumn.customers.get(customerId);
+ let userFeature = customer.features[TestFeature.Users];
+ if (userFeature.balance != startingBalance) {
+ for (let i = 0; i < values.length; i++) {
+ console.log(`Value: ${values[i]}, Event ID: ${results[i].id}`);
+ }
+ }
+ expect(userFeature.balance).to.equal(startingBalance);
+ }
+ });
});
diff --git a/server/tests/contUse/update/updateContUse1.ts b/server/tests/contUse/update/updateContUse1.ts
index dac3f0eee..6e3817970 100644
--- a/server/tests/contUse/update/updateContUse1.ts
+++ b/server/tests/contUse/update/updateContUse1.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -23,187 +23,187 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "updateContUse1";
describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing update contUse, add included usage`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "test2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test3",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "test2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += 3;
+ it("should create entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += 3;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- let extraUsage = 2;
- let newItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: (userItem.included_usage as number) + extraUsage,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
- });
+ let extraUsage = 2;
+ let newItem = constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: (userItem.included_usage as number) + extraUsage,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ });
- return;
+ return;
- it("should update product with extra included usage", async function () {
- let customItems = replaceItems({
- featureId: TestFeature.Users,
- items: pro.items,
- newItem,
- });
+ it("should update product with extra included usage", async function () {
+ let customItems = replaceItems({
+ featureId: TestFeature.Users,
+ items: pro.items,
+ newItem,
+ });
- usage += extraUsage;
+ usage += extraUsage;
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- is_custom: true,
- items: customItems,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ is_custom: true,
+ items: customItems,
+ });
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: extraUsage,
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: extraUsage,
+ });
- // Will have 1 invoice because price is replaced...
- });
+ // Will have 1 invoice because price is replaced...
+ });
- const entities = [
- {
- id: "4",
- name: "test4",
- feature_id: TestFeature.Users,
- },
- {
- id: "5",
- name: "test5",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "4",
+ name: "test4",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "5",
+ name: "test5",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create 2 entities and have no invoice", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- waitForSeconds: 10,
- });
+ it("should create 2 entities and have no invoice", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ waitForSeconds: 10,
+ });
- await autumn.entities.create(customerId, entities);
+ await autumn.entities.create(customerId, entities);
- // Usage won't change since using replaceables...
- // usage += entities.length;
+ // Usage won't change since using replaceables...
+ // usage += entities.length;
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
- });
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
+ });
});
diff --git a/server/tests/contUse/update/updateContUse2.ts b/server/tests/contUse/update/updateContUse2.ts
index f07e18987..80ab6863a 100644
--- a/server/tests/contUse/update/updateContUse2.ts
+++ b/server/tests/contUse/update/updateContUse2.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -23,160 +23,160 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "updateContUse2";
describe(`${chalk.yellowBright(`contUse/update/${testCase}: Testing update cont use, remove included usage`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let usage = 0;
- let firstEntities = [
- {
- id: "1",
- name: "test",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "test2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "test3",
- feature_id: TestFeature.Users,
- },
- ];
+ let usage = 0;
+ let firstEntities = [
+ {
+ id: "1",
+ name: "test",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "test2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "test3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should create entity, then attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += 3;
+ it("should create entity, then attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += 3;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- let reduceUsageBy = 1;
- let newItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: (userItem.included_usage as number) - reduceUsageBy,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
- });
+ let reduceUsageBy = 1;
+ let newItem = constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: (userItem.included_usage as number) - reduceUsageBy,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ });
- it("should update product with reduced included usage", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 1).getTime(),
- waitForSeconds: 5,
- });
+ it("should update product with reduced included usage", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 1).getTime(),
+ waitForSeconds: 5,
+ });
- const customItems = replaceItems({
- featureId: TestFeature.Users,
- items: pro.items,
- newItem,
- });
+ const customItems = replaceItems({
+ featureId: TestFeature.Users,
+ items: pro.items,
+ newItem,
+ });
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: pro.id,
- is_custom: true,
- items: customItems,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: pro.id,
+ is_custom: true,
+ items: customItems,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- is_custom: true,
- items: customItems,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ is_custom: true,
+ items: customItems,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(2);
- expect(invoices[0].total).to.equal(preview.due_today.total);
+ const customer = await autumn.customers.get(customerId);
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(2);
+ expect(invoices[0].total).to.equal(preview.due_today.total);
- // Usage stays the same...
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
- });
- return;
+ // Usage stays the same...
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
+ });
+ return;
});
diff --git a/server/tests/contUse/update/updateContUse3.ts b/server/tests/contUse/update/updateContUse3.ts
index 0f7f370d0..c8c6bfdc0 100644
--- a/server/tests/contUse/update/updateContUse3.ts
+++ b/server/tests/contUse/update/updateContUse3.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -21,118 +21,118 @@ import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseU
import { attachNewContUseAndExpectCorrect } from "tests/utils/expectUtils/expectContUse/expectUpdateContUse.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "updateContUse3";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage when no entities created`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
- });
+ it("should attach pro", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ });
- let extraUsage = 2;
- let newItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: (userItem.included_usage as number) + extraUsage,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
- });
+ let extraUsage = 2;
+ let newItem = constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: (userItem.included_usage as number) + extraUsage,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ });
- it("should update product with extra included usage", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Users,
- value: 1,
- });
+ it("should update product with extra included usage", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Users,
+ value: 1,
+ });
- let customItems = replaceItems({
- featureId: TestFeature.Users,
- items: pro.items,
- newItem,
- });
+ let customItems = replaceItems({
+ featureId: TestFeature.Users,
+ items: pro.items,
+ newItem,
+ });
- await attachNewContUseAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- customItems,
- numInvoices: 2,
- });
+ await attachNewContUseAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ customItems,
+ numInvoices: 2,
+ });
- await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage: 1,
- numReplaceables: 0,
- });
- });
+ await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage: 1,
+ numReplaceables: 0,
+ });
+ });
});
diff --git a/server/tests/contUse/update/updateContUse4.ts b/server/tests/contUse/update/updateContUse4.ts
index 12d47174e..3483181a7 100644
--- a/server/tests/contUse/update/updateContUse4.ts
+++ b/server/tests/contUse/update/updateContUse4.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -26,223 +26,223 @@ import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js"
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
const testCase = "updateContUse4";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage, prorate now`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const firstEntities = [
- {
- id: "1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ];
+ const firstEntities = [
+ {
+ id: "1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let usage = 0;
- it("should attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += firstEntities.length;
+ let usage = 0;
+ it("should attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += firstEntities.length;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- let extraUsage = 2;
- let newItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: (userItem.included_usage as number) + extraUsage,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
- });
+ let extraUsage = 2;
+ let newItem = constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: (userItem.included_usage as number) + extraUsage,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
+ });
- it("should update product with extra included usage", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- waitForSeconds: 15,
- });
+ it("should update product with extra included usage", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ waitForSeconds: 15,
+ });
- let customItems = replaceItems({
- featureId: TestFeature.Users,
- items: pro.items,
- newItem,
- });
+ let customItems = replaceItems({
+ featureId: TestFeature.Users,
+ items: pro.items,
+ newItem,
+ });
- const { invoices } = await attachNewContUseAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- customItems,
- numInvoices: 2,
- });
+ const { invoices } = await attachNewContUseAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ customItems,
+ numInvoices: 2,
+ });
- const { stripeSubs } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
+ const { stripeSubs } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
- // Do own calculation too..
- let sub = stripeSubs[0];
- let amount = -userItem.price!;
- const { start, end } = subToPeriodStartEnd({ sub });
- let proratedAmount = calculateProrationAmount({
- amount,
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- allowNegative: true,
- });
- proratedAmount = Number(proratedAmount.toFixed(2));
+ // Do own calculation too..
+ let sub = stripeSubs[0];
+ let amount = -userItem.price!;
+ const { start, end } = subToPeriodStartEnd({ sub });
+ let proratedAmount = calculateProrationAmount({
+ amount,
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ allowNegative: true,
+ });
+ proratedAmount = Number(proratedAmount.toFixed(2));
- expect(invoices[0].total).to.equal(
- proratedAmount,
- "invoice is equal to calculated prorated amount"
- );
- });
+ expect(invoices[0].total).to.equal(
+ proratedAmount,
+ "invoice is equal to calculated prorated amount",
+ );
+ });
- const reducedUsage = 3;
- const newItem2 = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: (newItem.included_usage as number) - reducedUsage,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
- });
+ const reducedUsage = 3;
+ const newItem2 = constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: (newItem.included_usage as number) - reducedUsage,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
+ });
- it("should update product with reduced included usage", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 1).getTime(),
- waitForSeconds: 15,
- });
+ it("should update product with reduced included usage", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 1).getTime(),
+ waitForSeconds: 15,
+ });
- let customItems = replaceItems({
- featureId: TestFeature.Users,
- items: pro.items,
- newItem: newItem2,
- });
+ let customItems = replaceItems({
+ featureId: TestFeature.Users,
+ items: pro.items,
+ newItem: newItem2,
+ });
- const { invoices } = await attachNewContUseAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- customItems,
- numInvoices: 3,
- });
+ const { invoices } = await attachNewContUseAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ customItems,
+ numInvoices: 3,
+ });
- const { stripeSubs } = await expectSubQuantityCorrect({
- stripeCli,
- productId: pro.id,
- db,
- org,
- env,
- customerId,
- usage,
- numReplaceables: 0,
- });
+ const { stripeSubs } = await expectSubQuantityCorrect({
+ stripeCli,
+ productId: pro.id,
+ db,
+ org,
+ env,
+ customerId,
+ usage,
+ numReplaceables: 0,
+ });
- // Do own calculation too..
- let sub = stripeSubs[0];
- let amount = Math.min(reducedUsage, usage) * userItem.price!;
- const { start, end } = subToPeriodStartEnd({ sub });
- let proratedAmount = calculateProrationAmount({
- amount,
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- allowNegative: true,
- });
- proratedAmount = Number(proratedAmount.toFixed(2));
+ // Do own calculation too..
+ let sub = stripeSubs[0];
+ let amount = Math.min(reducedUsage, usage) * userItem.price!;
+ const { start, end } = subToPeriodStartEnd({ sub });
+ let proratedAmount = calculateProrationAmount({
+ amount,
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ allowNegative: true,
+ });
+ proratedAmount = Number(proratedAmount.toFixed(2));
- expect(invoices[0].total).to.equal(
- proratedAmount,
- "invoice is equal to calculated prorated amount"
- );
- });
+ expect(invoices[0].total).to.equal(
+ proratedAmount,
+ "invoice is equal to calculated prorated amount",
+ );
+ });
});
diff --git a/server/tests/contUse/update/updateContUse5.ts b/server/tests/contUse/update/updateContUse5.ts
index c8abe2737..006f4bb31 100644
--- a/server/tests/contUse/update/updateContUse5.ts
+++ b/server/tests/contUse/update/updateContUse5.ts
@@ -1,11 +1,11 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
@@ -21,143 +21,143 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addWeeks } from "date-fns";
let userItem = constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 1,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 1,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
export let pro = constructProduct({
- items: [userItem],
- type: "pro",
+ items: [userItem],
+ type: "pro",
});
export let proAnnual = constructProduct({
- items: [
- constructArrearProratedItem({
- featureId: TestFeature.Users,
- pricePerUnit: 50,
- includedUsage: 2,
- config: {
- on_increase: OnIncrease.BillImmediately,
- on_decrease: OnDecrease.None,
- },
- }),
- ],
- type: "pro",
- isAnnual: true,
+ items: [
+ constructArrearProratedItem({
+ featureId: TestFeature.Users,
+ pricePerUnit: 50,
+ includedUsage: 2,
+ config: {
+ on_increase: OnIncrease.BillImmediately,
+ on_decrease: OnDecrease.None,
+ },
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "updateContUse5";
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse included usage, prorate now`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, proAnnual],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, proAnnual],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const firstEntities = [
- {
- id: "1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "entity3",
- feature_id: TestFeature.Users,
- },
- ];
+ const firstEntities = [
+ {
+ id: "1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "entity3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- let usage = 0;
- it("should attach pro", async function () {
- await autumn.entities.create(customerId, firstEntities);
- usage += firstEntities.length;
+ let usage = 0;
+ it("should attach pro", async function () {
+ await autumn.entities.create(customerId, firstEntities);
+ usage += firstEntities.length;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
- it("should upgrade to pro annual", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(curUnix, 2).getTime(),
- waitForSeconds: 5,
- });
- return;
+ it("should upgrade to pro annual", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(curUnix, 2).getTime(),
+ waitForSeconds: 5,
+ });
+ return;
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: proAnnual,
- stripeCli,
- db,
- org,
- env,
- usage: [
- {
- featureId: TestFeature.Users,
- value: usage,
- },
- ],
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: proAnnual,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage: [
+ {
+ featureId: TestFeature.Users,
+ value: usage,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/core/cancel/cancel2.test.ts b/server/tests/core/cancel/cancel2.test.ts
index 5c72f4734..7d93590aa 100644
--- a/server/tests/core/cancel/cancel2.test.ts
+++ b/server/tests/core/cancel/cancel2.test.ts
@@ -7,21 +7,21 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
- priceToInvoiceAmount,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
+ priceToInvoiceAmount,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructArrearProratedItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructArrearProratedItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
@@ -32,148 +32,148 @@ import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUt
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const wordsUsage = 300000;
const ops = [
- {
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const testCase = "cancel2";
describe(`${chalk.yellowBright("cancel2: Testing cancel at period end (with usage)")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- const cus1 = await autumn.customers.get(customerId);
- const prod = cus1.products.find((p) => p.id === premium.id);
- const proration = {
- start: prod?.current_period_start!,
- end: prod?.current_period_end!,
- };
+ it("should track usage cancel, advance test clock and have correct invoice", async function () {
+ const cus1 = await autumn.customers.get(customerId);
+ const prod = cus1.products.find((p) => p.id === premium.id);
+ const proration = {
+ start: prod?.current_period_start!,
+ end: prod?.current_period_end!,
+ };
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: wordsUsage,
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: wordsUsage,
+ });
- await autumn.cancel({
- customer_id: customerId,
- product_id: premium.id,
- cancel_immediately: false,
- });
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: premium.id,
+ cancel_immediately: false,
+ });
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const wordsAmount = await getExpectedInvoiceTotal({
- db,
- org,
- env,
- onlyIncludeArrear: true,
- usage: [
- {
- featureId: TestFeature.Words,
- value: wordsUsage,
- },
- ],
- stripeCli,
- customerId,
- productId: premium.id,
- expectExpired: true,
- });
+ const wordsAmount = await getExpectedInvoiceTotal({
+ db,
+ org,
+ env,
+ onlyIncludeArrear: true,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: wordsUsage,
+ },
+ ],
+ stripeCli,
+ customerId,
+ productId: premium.id,
+ expectExpired: true,
+ });
- const cus = await autumn.customers.get(customerId);
- const prods = cus.products.filter((p) => p.group == premium.group);
- expect(prods.length).to.equal(0);
+ const cus = await autumn.customers.get(customerId);
+ const prods = cus.products.filter((p) => p.group == premium.group);
+ expect(prods.length).to.equal(0);
- expect(cus.invoices.length).to.equal(2);
- expect(cus.invoices[0].total).to.equal(wordsAmount);
- });
+ expect(cus.invoices.length).to.equal(2);
+ expect(cus.invoices[0].total).to.equal(wordsAmount);
+ });
});
diff --git a/server/tests/core/cancel/cancel3.test.ts b/server/tests/core/cancel/cancel3.test.ts
index 1337bff5d..b7246f900 100644
--- a/server/tests/core/cancel/cancel3.test.ts
+++ b/server/tests/core/cancel/cancel3.test.ts
@@ -7,144 +7,144 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
- priceToInvoiceAmount,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
+ priceToInvoiceAmount,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructArrearProratedItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructArrearProratedItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let free = constructProduct({
- id: "free",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- }),
- ],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ }),
+ ],
+ type: "free",
+ isDefault: false,
});
const ops = [
- {
- product: free,
- results: [{ product: free, status: CusProductStatus.Active }],
- skipSubCheck: true,
- },
+ {
+ product: free,
+ results: [{ product: free, status: CusProductStatus.Active }],
+ skipSubCheck: true,
+ },
];
const testCase = "cancel3";
describe(`${chalk.yellowBright("cancel3: Cancelling free product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [free],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [free],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: op.skipSubCheck,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: op.skipSubCheck,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- const cus1 = await autumn.customers.get(customerId);
+ it("should track usage cancel, advance test clock and have correct invoice", async function () {
+ const cus1 = await autumn.customers.get(customerId);
- await autumn.cancel({
- customer_id: customerId,
- product_id: free.id,
- cancel_immediately: true,
- });
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: free.id,
+ cancel_immediately: true,
+ });
- const cus = await autumn.customers.get(customerId);
- const prods = cus.products.filter((p) => p.group == free.group);
- expect(prods.length).to.equal(0);
- });
+ const cus = await autumn.customers.get(customerId);
+ const prods = cus.products.filter((p) => p.group == free.group);
+ expect(prods.length).to.equal(0);
+ });
});
diff --git a/server/tests/core/cancel/cancel4.test.ts b/server/tests/core/cancel/cancel4.test.ts
index 93e0bfe08..7d3871e79 100644
--- a/server/tests/core/cancel/cancel4.test.ts
+++ b/server/tests/core/cancel/cancel4.test.ts
@@ -1,152 +1,148 @@
-import chalk from "chalk";
-import { setupBefore } from "tests/before.js";
-import { Stripe } from "stripe";
-import { createProducts } from "tests/utils/productUtils.js";
-import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
-import { TestFeature } from "tests/setup/v2Features.js";
-import { AutumnInt } from "@/external/autumn/autumnCli.js";
-import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ type AppEnv,
+ CusProductStatus,
+ type Organization,
} from "@autumn/shared";
-import {
- constructArrearItem,
- constructFeatureItem,
-} from "@/utils/scriptUtils/constructItem.js";
-import { DrizzleCli } from "@/db/initDrizzle.js";
-import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
+import chalk from "chalk";
+import type { Stripe } from "stripe";
+import { setupBefore } from "tests/before.js";
+import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
+import { createProducts } from "tests/utils/productUtils.js";
+import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
+import type { DrizzleCli } from "@/db/initDrizzle.js";
+import { AutumnInt } from "@/external/autumn/autumnCli.js";
+import {
+ constructArrearItem,
+ constructFeatureItem,
+} from "@/utils/scriptUtils/constructItem.js";
+import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
+import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
-let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+const premium = constructProduct({
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
-let addOn = constructProduct({
- id: "free_add_on",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "free",
- isAddOn: true,
- isDefault: false,
+const addOn = constructProduct({
+ id: "free_add_on",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "free",
+ isAddOn: true,
+ isDefault: false,
});
const ops = [
- {
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- skipSubCheck: true,
- },
- {
- product: addOn,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- },
+ {
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ skipSubCheck: true,
+ },
+ {
+ product: addOn,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ },
];
const testCase = "cancel4";
describe(`${chalk.yellowBright("cancel4: Cancelling free add on product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ const customerId = testCase;
+ const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium, addOn],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium, addOn],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
+ });
- testClockId = testClockId1!;
- });
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ it("should run operations", async () => {
+ await autumn.entities.create(customerId, entities);
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: op.skipSubCheck,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: op.skipSubCheck,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ it("should track usage cancel, advance test clock and have correct invoice", async () => {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: addOn.id,
+ cancel_immediately: true,
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: addOn.id,
- cancel_immediately: true,
- });
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: premium,
+ });
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: premium,
- });
-
- const products = cus.products.filter((p) => p.group == addOn.group);
- expect(products.length).to.equal(1);
- });
+ const products = cus.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(1);
+ });
});
diff --git a/server/tests/core/cancel/cancel5.test.ts b/server/tests/core/cancel/cancel5.test.ts
index ee6f2971f..9a292ea5e 100644
--- a/server/tests/core/cancel/cancel5.test.ts
+++ b/server/tests/core/cancel/cancel5.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -27,100 +27,100 @@ import { expect } from "chai";
const testCase = "cancel1";
describe(`${chalk.yellowBright("cancel1: Testing cancel for trial products")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: products.pro.id,
- });
+ it("should attach pro", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: products.pro.id,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- productId: products.pro.id,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ productId: products.pro.id,
+ });
+ });
- let sub: Stripe.Subscription | undefined;
+ let sub: Stripe.Subscription | undefined;
- it("should cancel pro product through stripe CLI", async function () {
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ it("should cancel pro product through stripe CLI", async function () {
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- sub = await cusProductToSub({
- cusProduct: fullCus.customer_products?.[0],
- stripeCli,
- });
+ sub = await cusProductToSub({
+ cusProduct: fullCus.customer_products?.[0],
+ stripeCli,
+ });
- await stripeCli.subscriptions.update(sub!.id, {
- cancel_at_period_end: true,
- });
+ await stripeCli.subscriptions.update(sub!.id, {
+ cancel_at_period_end: true,
+ });
- await timeout(4000);
+ await timeout(4000);
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- productId: products.pro.id,
- isCanceled: true,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ productId: products.pro.id,
+ isCanceled: true,
+ });
- expectProductAttached({
- customer,
- productId: products.free.id,
- status: CusProductStatus.Scheduled,
- });
- });
- return;
+ expectProductAttached({
+ customer,
+ productId: products.free.id,
+ status: CusProductStatus.Scheduled,
+ });
+ });
+ return;
- it("should renew pro produce through stripe CLI and have it update correctly", async function () {
- await stripeCli.subscriptions.update(sub!.id, {
- cancel_at_period_end: false,
- });
+ it("should renew pro produce through stripe CLI and have it update correctly", async function () {
+ await stripeCli.subscriptions.update(sub!.id, {
+ cancel_at_period_end: false,
+ });
- await timeout(4000);
+ await timeout(4000);
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- productId: products.pro.id,
- status: CusProductStatus.Active,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ productId: products.pro.id,
+ status: CusProductStatus.Active,
+ });
- expect(customer.products.length).to.equal(1);
- });
+ expect(customer.products.length).to.equal(1);
+ });
});
diff --git a/server/tests/core/cancel/mergedCancel1.test.ts b/server/tests/core/cancel/mergedCancel1.test.ts
index b26471fe9..3da442a29 100644
--- a/server/tests/core/cancel/mergedCancel1.test.ts
+++ b/server/tests/core/cancel/mergedCancel1.test.ts
@@ -7,16 +7,16 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- AttachScenario,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ AttachScenario,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -26,175 +26,175 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- skipSubCheck: true,
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ skipSubCheck: true,
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const cancels = [
- {
- entityId: "1",
- product: premium,
- },
- {
- entityId: "2",
- product: premium,
- shouldBeCanceled: true,
- },
+ {
+ entityId: "1",
+ product: premium,
+ },
+ {
+ entityId: "2",
+ product: premium,
+ shouldBeCanceled: true,
+ },
];
const renewals = [
- {
- entityId: "1",
- product: premium,
- },
+ {
+ entityId: "1",
+ product: premium,
+ },
- {
- entityId: "2",
- product: premium,
- },
+ {
+ entityId: "2",
+ product: premium,
+ },
];
const testCase = "mergedCancel1";
describe(`${chalk.yellowBright("mergedCancel1: Merged cancel")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- skipSubCheck: op.skipSubCheck,
- entityId: op.entityId,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ skipSubCheck: op.skipSubCheck,
+ entityId: op.entityId,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- for (const cancel of cancels) {
- await autumn.cancel({
- customer_id: customerId,
- product_id: cancel.product.id,
- entity_id: cancel.entityId,
- cancel_immediately: false,
- });
+ it("should track usage cancel, advance test clock and have correct invoice", async function () {
+ for (const cancel of cancels) {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: cancel.product.id,
+ entity_id: cancel.entityId,
+ cancel_immediately: false,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled: cancel.shouldBeCanceled,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled: cancel.shouldBeCanceled,
+ });
+ }
+ });
- it("should renew both entities", async function () {
- for (const renewal of renewals) {
- const checkout = await autumn.checkout({
- customer_id: customerId,
- product_id: renewal.product.id,
- entity_id: renewal.entityId,
- });
+ it("should renew both entities", async function () {
+ for (const renewal of renewals) {
+ const checkout = await autumn.checkout({
+ customer_id: customerId,
+ product_id: renewal.product.id,
+ entity_id: renewal.entityId,
+ });
- expect(checkout.product.scenario).to.equal(AttachScenario.Renew);
- expect(checkout.total).to.equal(0);
+ expect(checkout.product.scenario).to.equal(AttachScenario.Renew);
+ expect(checkout.total).to.equal(0);
- const attach = await autumn.attach({
- customer_id: customerId,
- product_id: renewal.product.id,
- entity_id: renewal.entityId,
- });
+ const attach = await autumn.attach({
+ customer_id: customerId,
+ product_id: renewal.product.id,
+ entity_id: renewal.entityId,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ }
+ });
});
diff --git a/server/tests/core/cancel/mergedCancel2.test.ts b/server/tests/core/cancel/mergedCancel2.test.ts
index bb96e4d27..2508bd6e2 100644
--- a/server/tests/core/cancel/mergedCancel2.test.ts
+++ b/server/tests/core/cancel/mergedCancel2.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -25,136 +25,136 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j
// Results: Canceled sub
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const cancels = [
- {
- entityId: "1",
- product: premium,
- },
- {
- entityId: "2",
- product: premium,
- cancelImmediately: true,
- shouldBeCanceled: true,
- },
+ {
+ entityId: "1",
+ product: premium,
+ },
+ {
+ entityId: "2",
+ product: premium,
+ cancelImmediately: true,
+ shouldBeCanceled: true,
+ },
];
const testCase = "mergedCancel2";
describe(`${chalk.yellowBright("mergedCancel2: Testing cancel immediately")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- for (const cancel of cancels) {
- await autumn.cancel({
- customer_id: customerId,
- product_id: cancel.product.id,
- entity_id: cancel.entityId,
- cancel_immediately: cancel.cancelImmediately ?? false,
- });
+ it("should track usage cancel, advance test clock and have correct invoice", async function () {
+ for (const cancel of cancels) {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: cancel.product.id,
+ entity_id: cancel.entityId,
+ cancel_immediately: cancel.cancelImmediately ?? false,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled: cancel.shouldBeCanceled,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled: cancel.shouldBeCanceled,
+ });
+ }
+ });
});
diff --git a/server/tests/core/cancel/mergedCancel3.test.ts b/server/tests/core/cancel/mergedCancel3.test.ts
index 108aa5141..6329b1f08 100644
--- a/server/tests/core/cancel/mergedCancel3.test.ts
+++ b/server/tests/core/cancel/mergedCancel3.test.ts
@@ -7,11 +7,11 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- AttachScenario,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ AttachScenario,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -26,140 +26,140 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j
// Results: No sub
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const cancels = [
- {
- entityId: "1",
- product: premium,
- cancelImmediately: true,
- },
- {
- entityId: "2",
- product: premium,
- cancelImmediately: true,
- shouldBeCanceled: true,
- skipSubCheck: true,
- },
+ {
+ entityId: "1",
+ product: premium,
+ cancelImmediately: true,
+ },
+ {
+ entityId: "2",
+ product: premium,
+ cancelImmediately: true,
+ shouldBeCanceled: true,
+ skipSubCheck: true,
+ },
];
const testCase = "mergedCancel3";
describe(`${chalk.yellowBright("mergedCancel3: Testing cancel immediately")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- } catch (error) {
- console.log(`Operation failed: ${op.product.id}, index: ${index}`);
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ } catch (error) {
+ console.log(`Operation failed: ${op.product.id}, index: ${index}`);
+ throw error;
+ }
+ }
+ });
- it("should track usage cancel, advance test clock and have correct invoice", async function () {
- for (const cancel of cancels) {
- await autumn.cancel({
- customer_id: customerId,
- product_id: cancel.product.id,
- entity_id: cancel.entityId,
- cancel_immediately: cancel.cancelImmediately ?? false,
- });
+ it("should track usage cancel, advance test clock and have correct invoice", async function () {
+ for (const cancel of cancels) {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: cancel.product.id,
+ entity_id: cancel.entityId,
+ cancel_immediately: cancel.cancelImmediately ?? false,
+ });
- if (cancel.skipSubCheck) continue;
+ if (cancel.skipSubCheck) continue;
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled: cancel.shouldBeCanceled,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled: cancel.shouldBeCanceled,
+ });
+ }
+ });
});
diff --git a/server/tests/core/multiAttach/multiAttach1.test.ts b/server/tests/core/multiAttach/multiAttach1.test.ts
index 2e47ef650..800938772 100644
--- a/server/tests/core/multiAttach/multiAttach1.test.ts
+++ b/server/tests/core/multiAttach/multiAttach1.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -20,173 +20,173 @@ import { addDays } from "date-fns";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
let growth = constructProduct({
- id: "growth",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 100 }),
- ],
- type: "growth",
+ id: "growth",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 100 }),
+ ],
+ type: "growth",
});
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "multiAttach1";
describe(`${chalk.yellowBright("multiAttach1: Testing multi attach for trial products and update product quantities mid trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({
- version: APIVersion.v1_4,
- orgConfig: { entity_product: true },
- });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({
+ version: APIVersion.v1_4,
+ orgConfig: { entity_product: true },
+ });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: growth.id,
- quantity: 2,
- product: growth,
- status: CusProductStatus.Trialing,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: growth.id,
+ quantity: 2,
+ product: growth,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- it("should advance clock and update premium & growth while trialing", async function () {
- const newProducts = [
- {
- product_id: premium.id,
- quantity: 1,
- },
- {
- product_id: growth.id,
- quantity: 5,
- },
- ];
+ it("should advance clock and update premium & growth while trialing", async function () {
+ const newProducts = [
+ {
+ product_id: premium.id,
+ quantity: 1,
+ },
+ {
+ product_id: growth.id,
+ quantity: 5,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- {
- product: premium,
- quantity: 1,
- status: CusProductStatus.Trialing,
- },
+ const results = [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: premium,
+ quantity: 1,
+ status: CusProductStatus.Trialing,
+ },
- {
- product: growth,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- ];
+ {
+ product: growth,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 3).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 3).getTime(),
+ });
- await expectMultiAttachCorrect({
- customerId,
- products: newProducts,
- results,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: newProducts,
+ results,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiAttach2.test.ts b/server/tests/core/multiAttach/multiAttach2.test.ts
index ae3013489..b654dde60 100644
--- a/server/tests/core/multiAttach/multiAttach2.test.ts
+++ b/server/tests/core/multiAttach/multiAttach2.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -20,170 +20,170 @@ import { addDays } from "date-fns";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
let growth = constructProduct({
- id: "growth",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 100 }),
- ],
- type: "growth",
+ id: "growth",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 100 }),
+ ],
+ type: "growth",
});
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "multiAttach2";
describe(`${chalk.yellowBright("multiAttach2: Testing multi attach for trial products and update product quantities mid trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: growth.id,
- quantity: 2,
- product: growth,
- status: CusProductStatus.Trialing,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: growth.id,
+ quantity: 2,
+ product: growth,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- it("should advance clock and update premium & growth while trialing", async function () {
- const newProducts = [
- {
- product_id: premium.id,
- quantity: 1,
- },
- {
- product_id: growth.id,
- quantity: 5,
- },
- ];
+ it("should advance clock and update premium & growth while trialing", async function () {
+ const newProducts = [
+ {
+ product_id: premium.id,
+ quantity: 1,
+ },
+ {
+ product_id: growth.id,
+ quantity: 5,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- {
- product: premium,
- quantity: 1,
- status: CusProductStatus.Trialing,
- },
+ const results = [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: premium,
+ quantity: 1,
+ status: CusProductStatus.Trialing,
+ },
- {
- product: growth,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- ];
+ {
+ product: growth,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 3).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 3).getTime(),
+ });
- await expectMultiAttachCorrect({
- customerId,
- products: newProducts,
- results,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: newProducts,
+ results,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiAttach3.test.ts b/server/tests/core/multiAttach/multiAttach3.test.ts
index 64f164134..9ed9c6b1d 100644
--- a/server/tests/core/multiAttach/multiAttach3.test.ts
+++ b/server/tests/core/multiAttach/multiAttach3.test.ts
@@ -7,20 +7,20 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
@@ -28,229 +28,229 @@ import { addDays } from "date-fns";
import { expect } from "chai";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
+ trial: true,
});
const testCase = "multiAttach3";
describe(`${chalk.yellowBright("multiAttach3: Testing multi attach for trial products transfer to entity, then cancel products on entities...")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({
- version: APIVersion.v1_4,
- orgConfig: { entity_product: true },
- });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({
+ version: APIVersion.v1_4,
+ orgConfig: { entity_product: true },
+ });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Trialing,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- {
- product: premium,
- quantity: 3,
- status: CusProductStatus.Trialing,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Trialing,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Trialing,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: premium,
+ quantity: 3,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Trialing,
+ },
+ ];
- it("should transfer to entity and have correct sub", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity and have correct sub", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "2",
- product_id: pro.id,
- });
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: premium.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "2",
+ product_id: pro.id,
+ });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: premium.id,
+ });
- await expectResultsCorrect({
- autumn,
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ autumn,
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should cancel one entity's sub at end of cycle and have correct schedule...", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
+ it("should cancel one entity's sub at end of cycle and have correct schedule...", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should cancel one entity's sub immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- cancel_immediately: true,
- // @ts-ignore
- prorate: false,
- });
+ it("should cancel one entity's sub immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ cancel_immediately: true,
+ // @ts-ignore
+ prorate: false,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should advance test clock to end of trial and have correct sub", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 8).getTime(),
- waitForSeconds: 30,
- });
+ it("should advance test clock to end of trial and have correct sub", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 8).getTime(),
+ waitForSeconds: 30,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- const latestInvoice = customer.invoices[0];
+ const customer = await autumn.customers.get(customerId);
+ const latestInvoice = customer.invoices[0];
- // Should only have paid for 4 pro and 2 premium...
- const invoiceTotal =
- getBasePrice({ product: pro }) * 4 +
- getBasePrice({ product: premium }) * 2;
+ // Should only have paid for 4 pro and 2 premium...
+ const invoiceTotal =
+ getBasePrice({ product: pro }) * 4 +
+ getBasePrice({ product: premium }) * 2;
- expect(invoiceTotal).to.equal(latestInvoice.total);
+ expect(invoiceTotal).to.equal(latestInvoice.total);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiAttach4.test.ts b/server/tests/core/multiAttach/multiAttach4.test.ts
index 09ff1ce34..acdf573b6 100644
--- a/server/tests/core/multiAttach/multiAttach4.test.ts
+++ b/server/tests/core/multiAttach/multiAttach4.test.ts
@@ -7,20 +7,20 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
@@ -28,242 +28,242 @@ import { addDays } from "date-fns";
import { expect } from "chai";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 5000,
- }),
- ],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 5000,
+ }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "multiAttach4";
describe(`${chalk.yellowBright("multiAttach4: Testing multi attach for annual products...")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- {
- product_id: proAnnual.id,
- quantity: 4,
- product: proAnnual,
- status: CusProductStatus.Active,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: proAnnual.id,
+ quantity: 4,
+ product: proAnnual,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- return;
+ return;
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 4,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 2,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 4,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 2,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ ];
- it("should transfer to entity and have correct sub", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity and have correct sub", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "2",
- product_id: pro.id,
- });
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: premium.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "2",
+ product_id: pro.id,
+ });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: premium.id,
+ });
- await expectResultsCorrect({
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should cancel one entity's sub at end of cycle and have correct schedule...", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
+ it("should cancel one entity's sub at end of cycle and have correct schedule...", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should cancel one entity's sub immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- cancel_immediately: true,
- // @ts-ignore
- prorate: false,
- });
+ it("should cancel one entity's sub immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ cancel_immediately: true,
+ // @ts-ignore
+ prorate: false,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should advance test clock to end of trial and have correct sub", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 8).getTime(),
- waitForSeconds: 30,
- });
+ it("should advance test clock to end of trial and have correct sub", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 8).getTime(),
+ waitForSeconds: 30,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- const latestInvoice = customer.invoices[0];
+ const customer = await autumn.customers.get(customerId);
+ const latestInvoice = customer.invoices[0];
- // Should only have paid for 4 pro and 2 premium...
- const invoiceTotal =
- getBasePrice({ product: pro }) * 4 +
- getBasePrice({ product: premium }) * 2;
+ // Should only have paid for 4 pro and 2 premium...
+ const invoiceTotal =
+ getBasePrice({ product: pro }) * 4 +
+ getBasePrice({ product: premium }) * 2;
- expect(invoiceTotal).to.equal(latestInvoice.total);
+ expect(invoiceTotal).to.equal(latestInvoice.total);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiAttach5.test.ts b/server/tests/core/multiAttach/multiAttach5.test.ts
index b1cde7e14..aaa9a798c 100644
--- a/server/tests/core/multiAttach/multiAttach5.test.ts
+++ b/server/tests/core/multiAttach/multiAttach5.test.ts
@@ -7,21 +7,21 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
- organizations,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
+ organizations,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
@@ -35,284 +35,284 @@ import { eq } from "drizzle-orm";
import { CacheManager } from "@/external/caching/CacheManager.js";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
});
const testCase = "multiAttach5";
describe(`${chalk.yellowBright("multiAttach5: Testing multi attach and get customer")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await OrgService.update({
- db,
- orgId: org.id,
- updates: {
- config: {
- ...org.config,
- entity_product: true,
- },
- },
- });
+ await OrgService.update({
+ db,
+ orgId: org.id,
+ updates: {
+ config: {
+ ...org.config,
+ entity_product: true,
+ },
+ },
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 4,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 4,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should transfer to entity 1 and 2", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity 1 and 2", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: pro.id,
+ });
- await autumn.transfer(customerId, {
- to_entity_id: "2",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "2",
+ product_id: pro.id,
+ });
- const results = [
- {
- product_id: pro.id,
- quantity: 4,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- {
- product_id: pro.id,
- quantity: 1,
- product: pro,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- {
- product_id: pro.id,
- quantity: 1,
- product: pro,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product_id: pro.id,
+ quantity: 4,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: pro.id,
+ quantity: 1,
+ product: pro,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: pro.id,
+ quantity: 1,
+ product: pro,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectResultsCorrect({
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
- const entity1 = await autumn.entities.get(customerId, "1");
- const entity2 = await autumn.entities.get(customerId, "2");
- expectFeaturesCorrect({
- customer: entity1,
- product: pro,
- });
- expectFeaturesCorrect({
- customer: entity2,
- product: pro,
- });
- });
+ const entity1 = await autumn.entities.get(customerId, "1");
+ const entity2 = await autumn.entities.get(customerId, "2");
+ expectFeaturesCorrect({
+ customer: entity1,
+ product: pro,
+ });
+ expectFeaturesCorrect({
+ customer: entity2,
+ product: pro,
+ });
+ });
- it("should try to reduce quantity of pro to less than number of entities and fail", async function () {
- await expectAutumnError({
- func: async () => {
- await autumn.attach({
- customer_id: customerId,
- products: [
- {
- product_id: pro.id,
- quantity: 1,
- },
- ],
- });
- },
- });
- });
+ it("should try to reduce quantity of pro to less than number of entities and fail", async function () {
+ await expectAutumnError({
+ func: async () => {
+ await autumn.attach({
+ customer_id: customerId,
+ products: [
+ {
+ product_id: pro.id,
+ quantity: 1,
+ },
+ ],
+ });
+ },
+ });
+ });
- it("should update pro product quantity and have correct amount", async function () {
- await expectMultiAttachCorrect({
- customerId,
- products: [
- {
- product_id: pro.id,
- quantity: 6,
- },
- ],
- results: [
- {
- product: pro,
- quantity: 6,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- ],
- db,
- org,
- env,
- });
- });
+ it("should update pro product quantity and have correct amount", async function () {
+ await expectMultiAttachCorrect({
+ customerId,
+ products: [
+ {
+ product_id: pro.id,
+ quantity: 6,
+ },
+ ],
+ results: [
+ {
+ product: pro,
+ quantity: 6,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ ],
+ db,
+ org,
+ env,
+ });
+ });
- it("should decrease pro product quantity and have correct amount", async function () {
- await expectMultiAttachCorrect({
- customerId,
- products: [
- {
- product_id: pro.id,
- quantity: 5,
- },
- ],
- results: [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- ],
- db,
- org,
- env,
- });
- });
+ it("should decrease pro product quantity and have correct amount", async function () {
+ await expectMultiAttachCorrect({
+ customerId,
+ products: [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ },
+ ],
+ results: [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ ],
+ db,
+ org,
+ env,
+ });
+ });
- after(async function () {
- await OrgService.update({
- db,
- orgId: org.id,
- updates: {
- config: {
- ...org.config,
- entity_product: false,
- },
- },
- });
- await CacheManager.disconnect();
- });
+ after(async function () {
+ await OrgService.update({
+ db,
+ orgId: org.id,
+ updates: {
+ config: {
+ ...org.config,
+ entity_product: false,
+ },
+ },
+ });
+ await CacheManager.disconnect();
+ });
- return;
+ return;
});
diff --git a/server/tests/core/multiAttach/multiAttach6.test.ts b/server/tests/core/multiAttach/multiAttach6.test.ts
index 9f5ff2387..3d4cc3621 100644
--- a/server/tests/core/multiAttach/multiAttach6.test.ts
+++ b/server/tests/core/multiAttach/multiAttach6.test.ts
@@ -7,19 +7,19 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- FullCusProduct,
- nullish,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ FullCusProduct,
+ nullish,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
@@ -30,251 +30,251 @@ import { CacheManager } from "@/external/caching/CacheManager.js";
import { CusService } from "@/internal/customers/CusService.js";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
});
const testCase = "multiAttach6";
describe(`${chalk.yellowBright("multiAttach6: Testing multi attach and get customer")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await OrgService.update({
- db,
- orgId: org.id,
- updates: {
- config: {
- ...org.config,
- entity_product: true,
- },
- },
- });
+ await OrgService.update({
+ db,
+ orgId: org.id,
+ updates: {
+ config: {
+ ...org.config,
+ entity_product: true,
+ },
+ },
+ });
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 4,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 4,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should transfer to entity 1 and 2", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity 1 and 2", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: pro.id,
+ });
- await autumn.transfer(customerId, {
- to_entity_id: "2",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "2",
+ product_id: pro.id,
+ });
- const results = [
- {
- product_id: pro.id,
- quantity: 4,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- {
- product_id: pro.id,
- quantity: 1,
- product: pro,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- {
- product_id: pro.id,
- quantity: 1,
- product: pro,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product_id: pro.id,
+ quantity: 4,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: pro.id,
+ quantity: 1,
+ product: pro,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: pro.id,
+ quantity: 1,
+ product: pro,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectResultsCorrect({
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should try to reduce quantity of pro to 0 and have no top level cus product...", async function () {
- await autumn.attach({
- customer_id: customerId,
- products: [
- {
- product_id: pro.id,
- quantity: 2,
- },
- ],
- });
+ it("should try to reduce quantity of pro to 0 and have no top level cus product...", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ products: [
+ {
+ product_id: pro.id,
+ quantity: 2,
+ },
+ ],
+ });
- const results = [
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "2",
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 2,
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "2",
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 2,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectResultsCorrect({
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ customerId,
+ results,
+ });
- const fullCus = await CusService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: customerId,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: customerId,
+ });
- const proProduct = fullCus.customer_products.find(
- (p: FullCusProduct) =>
- p.product_id === pro.id && nullish(p.internal_entity_id)
- );
- expect(proProduct).to.be.undefined;
- });
+ const proProduct = fullCus.customer_products.find(
+ (p: FullCusProduct) =>
+ p.product_id === pro.id && nullish(p.internal_entity_id),
+ );
+ expect(proProduct).to.be.undefined;
+ });
- it("should increase pro product quantity and have correct amount", async function () {
- await autumn.attach({
- customer_id: customerId,
- products: [
- {
- product_id: pro.id,
- quantity: 4,
- },
- ],
- });
- });
+ it("should increase pro product quantity and have correct amount", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ products: [
+ {
+ product_id: pro.id,
+ quantity: 4,
+ },
+ ],
+ });
+ });
- after(async function () {
- await OrgService.update({
- db,
- orgId: org.id,
- updates: {
- config: {
- ...org.config,
- entity_product: false,
- },
- },
- });
- await CacheManager.disconnect();
- });
+ after(async function () {
+ await OrgService.update({
+ db,
+ orgId: org.id,
+ updates: {
+ config: {
+ ...org.config,
+ entity_product: false,
+ },
+ },
+ });
+ await CacheManager.disconnect();
+ });
- return;
+ return;
});
diff --git a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts
index 16a6abb6a..1aee9cd7f 100644
--- a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts
+++ b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts
@@ -7,178 +7,178 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
- advanceTestClock,
- completeCheckoutForm,
+ advanceTestClock,
+ completeCheckoutForm,
} from "tests/utils/stripeUtils.js";
import { addDays, addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "multiInvoice1";
describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 3,
- product: pro,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Trialing,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 3,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- attachParams: {
- invoice: true,
- enable_product_immediately: true,
- },
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ attachParams: {
+ invoice: true,
+ enable_product_immediately: true,
+ },
+ db,
+ org,
+ env,
+ });
+ });
- it("should update premium & pro while trialing", async function () {
- const newProducts = [
- {
- product_id: premium.id,
- quantity: 2,
- },
- {
- product_id: pro.id,
- quantity: 4,
- },
- ];
+ it("should update premium & pro while trialing", async function () {
+ const newProducts = [
+ {
+ product_id: premium.id,
+ quantity: 2,
+ },
+ {
+ product_id: pro.id,
+ quantity: 4,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 4,
- status: CusProductStatus.Trialing,
- },
- {
- product: premium,
- quantity: 2,
- status: CusProductStatus.Trialing,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 4,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: premium,
+ quantity: 2,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- await expectMultiAttachCorrect({
- customerId,
- products: newProducts,
- results,
- db,
- org,
- env,
- attachParams: {
- invoice: true,
- enable_product_immediately: true,
- },
- });
- });
+ await expectMultiAttachCorrect({
+ customerId,
+ products: newProducts,
+ results,
+ db,
+ org,
+ env,
+ attachParams: {
+ invoice: true,
+ enable_product_immediately: true,
+ },
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiReward/multiReward1.test.ts b/server/tests/core/multiAttach/multiReward/multiReward1.test.ts
index 8352c4a33..6136b34ed 100644
--- a/server/tests/core/multiAttach/multiReward/multiReward1.test.ts
+++ b/server/tests/core/multiAttach/multiReward/multiReward1.test.ts
@@ -5,147 +5,147 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
import {
- multiRewardPremium,
- multiRewardPro,
- premiumReward,
- proReward,
- setupMultiRewardBefore,
+ multiRewardPremium,
+ multiRewardPro,
+ premiumReward,
+ proReward,
+ setupMultiRewardBefore,
} from "./multiRewardUtils.test.js";
const testCase = "multiReward1";
describe(`${chalk.yellowBright("multiReward1: Testing multi attach with rewards")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- await setupMultiRewardBefore({
- orgId: org.id,
- db,
- env,
- });
+ await setupMultiRewardBefore({
+ orgId: org.id,
+ db,
+ env,
+ });
- // addPrefixToProducts({
- // products: [pro, premium, growth],
- // prefix: testCase,
- // });
+ // addPrefixToProducts({
+ // products: [pro, premium, growth],
+ // prefix: testCase,
+ // });
- // await createProducts({
- // autumn: autumnJs,
- // products: [pro, premium, growth],
- // db,
- // orgId: org.id,
- // env,
- // customerId,
- // });
+ // await createProducts({
+ // autumn: autumnJs,
+ // products: [pro, premium, growth],
+ // db,
+ // orgId: org.id,
+ // env,
+ // customerId,
+ // });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: multiRewardPro.id,
- quantity: 3,
- product: multiRewardPro,
- status: CusProductStatus.Active,
- },
- {
- product_id: multiRewardPremium.id,
- quantity: 3,
- product: multiRewardPremium,
- status: CusProductStatus.Active,
- },
- ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- rewards: [proReward.id, premiumReward.id],
- expectedRewards: [proReward.id, premiumReward.id],
- });
- });
- return;
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: multiRewardPro.id,
+ quantity: 3,
+ product: multiRewardPro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: multiRewardPremium.id,
+ quantity: 3,
+ product: multiRewardPremium,
+ status: CusProductStatus.Active,
+ },
+ ];
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ rewards: [proReward.id, premiumReward.id],
+ expectedRewards: [proReward.id, premiumReward.id],
+ });
+ });
+ return;
- // it("should advance clock and update premium & growth while trialing", async function () {
- // const newProducts = [
- // {
- // product_id: premium.id,
- // quantity: 1,
- // },
- // {
- // product_id: growth.id,
- // quantity: 5,
- // },
- // ];
+ // it("should advance clock and update premium & growth while trialing", async function () {
+ // const newProducts = [
+ // {
+ // product_id: premium.id,
+ // quantity: 1,
+ // },
+ // {
+ // product_id: growth.id,
+ // quantity: 5,
+ // },
+ // ];
- // const results = [
- // {
- // product: pro,
- // quantity: 5,
- // status: CusProductStatus.Trialing,
- // },
- // {
- // product: premium,
- // quantity: 1,
- // status: CusProductStatus.Trialing,
- // },
+ // const results = [
+ // {
+ // product: pro,
+ // quantity: 5,
+ // status: CusProductStatus.Trialing,
+ // },
+ // {
+ // product: premium,
+ // quantity: 1,
+ // status: CusProductStatus.Trialing,
+ // },
- // {
- // product: growth,
- // quantity: 5,
- // status: CusProductStatus.Trialing,
- // },
- // ];
+ // {
+ // product: growth,
+ // quantity: 5,
+ // status: CusProductStatus.Trialing,
+ // },
+ // ];
- // await advanceTestClock({
- // stripeCli,
- // testClockId,
- // advanceTo: addDays(new Date(), 3).getTime(),
- // });
+ // await advanceTestClock({
+ // stripeCli,
+ // testClockId,
+ // advanceTo: addDays(new Date(), 3).getTime(),
+ // });
- // await expectMultiAttachCorrect({
- // customerId,
- // products: newProducts,
- // results,
- // db,
- // org,
- // env,
- // });
- // });
+ // await expectMultiAttachCorrect({
+ // customerId,
+ // products: newProducts,
+ // results,
+ // db,
+ // org,
+ // env,
+ // });
+ // });
});
diff --git a/server/tests/core/multiAttach/multiReward/multiReward2.test.ts b/server/tests/core/multiAttach/multiReward/multiReward2.test.ts
index 22edf2df7..e79f3d607 100644
--- a/server/tests/core/multiAttach/multiReward/multiReward2.test.ts
+++ b/server/tests/core/multiAttach/multiReward/multiReward2.test.ts
@@ -5,20 +5,20 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
import {
- multiRewardPremium,
- multiRewardPro,
- premiumReward,
- proReward,
- setupMultiRewardBefore,
+ multiRewardPremium,
+ multiRewardPro,
+ premiumReward,
+ proReward,
+ setupMultiRewardBefore,
} from "./multiRewardUtils.test.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
@@ -26,164 +26,164 @@ import { createStripeCli } from "@/external/stripe/utils.js";
const testCase = "multiReward2";
describe(`${chalk.yellowBright("multiReward2: Testing multi attach with rewards -- delete reward and prorate")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- await setupMultiRewardBefore({
- orgId: org.id,
- db,
- env,
- });
+ await setupMultiRewardBefore({
+ orgId: org.id,
+ db,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: multiRewardPro.id,
- quantity: 3,
- product: multiRewardPro,
- status: CusProductStatus.Active,
- },
- {
- product_id: multiRewardPremium.id,
- quantity: 3,
- product: multiRewardPremium,
- status: CusProductStatus.Active,
- },
- ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- rewards: [proReward.id, premiumReward.id],
- expectedRewards: [proReward.id, premiumReward.id],
- });
- });
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: multiRewardPro.id,
+ quantity: 3,
+ product: multiRewardPro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: multiRewardPremium.id,
+ quantity: 3,
+ product: multiRewardPremium,
+ status: CusProductStatus.Active,
+ },
+ ];
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ rewards: [proReward.id, premiumReward.id],
+ expectedRewards: [proReward.id, premiumReward.id],
+ });
+ });
- it("should delete discounts from subscription and prorate correctly", async function () {
- const fullCus = await CusService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: customerId,
- });
+ it("should delete discounts from subscription and prorate correctly", async function () {
+ const fullCus = await CusService.getFull({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: customerId,
+ });
- const cusProduct = fullCus.customer_products.find(
- (cp) => cp.product.id === multiRewardPro.id
- );
- const sub = await cusProductToSub({ cusProduct, stripeCli });
+ const cusProduct = fullCus.customer_products.find(
+ (cp) => cp.product.id === multiRewardPro.id,
+ );
+ const sub = await cusProductToSub({ cusProduct, stripeCli });
- await stripeCli.subscriptions.update(sub!.id, {
- discounts: null,
- });
- });
+ await stripeCli.subscriptions.update(sub!.id, {
+ discounts: null,
+ });
+ });
- it("should update pro quantity and have correct checkout amount", async function () {
- const productsList = [
- {
- product_id: multiRewardPro.id,
- quantity: 5,
- },
- ];
+ it("should update pro quantity and have correct checkout amount", async function () {
+ const productsList = [
+ {
+ product_id: multiRewardPro.id,
+ quantity: 5,
+ },
+ ];
- const results = [
- {
- product: multiRewardPro,
- quantity: 5,
- status: CusProductStatus.Active,
- },
- {
- product: multiRewardPremium,
- quantity: 3,
- status: CusProductStatus.Active,
- },
- ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results,
- db,
- org,
- env,
- expectedRewards: [],
- });
- });
- return;
+ const results = [
+ {
+ product: multiRewardPro,
+ quantity: 5,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: multiRewardPremium,
+ quantity: 3,
+ status: CusProductStatus.Active,
+ },
+ ];
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results,
+ db,
+ org,
+ env,
+ expectedRewards: [],
+ });
+ });
+ return;
- // it("should advance clock and update premium & growth while trialing", async function () {
- // const newProducts = [
- // {
- // product_id: premium.id,
- // quantity: 1,
- // },
- // {
- // product_id: growth.id,
- // quantity: 5,
- // },
- // ];
+ // it("should advance clock and update premium & growth while trialing", async function () {
+ // const newProducts = [
+ // {
+ // product_id: premium.id,
+ // quantity: 1,
+ // },
+ // {
+ // product_id: growth.id,
+ // quantity: 5,
+ // },
+ // ];
- // const results = [
- // {
- // product: pro,
- // quantity: 5,
- // status: CusProductStatus.Trialing,
- // },
- // {
- // product: premium,
- // quantity: 1,
- // status: CusProductStatus.Trialing,
- // },
+ // const results = [
+ // {
+ // product: pro,
+ // quantity: 5,
+ // status: CusProductStatus.Trialing,
+ // },
+ // {
+ // product: premium,
+ // quantity: 1,
+ // status: CusProductStatus.Trialing,
+ // },
- // {
- // product: growth,
- // quantity: 5,
- // status: CusProductStatus.Trialing,
- // },
- // ];
+ // {
+ // product: growth,
+ // quantity: 5,
+ // status: CusProductStatus.Trialing,
+ // },
+ // ];
- // await advanceTestClock({
- // stripeCli,
- // testClockId,
- // advanceTo: addDays(new Date(), 3).getTime(),
- // });
+ // await advanceTestClock({
+ // stripeCli,
+ // testClockId,
+ // advanceTo: addDays(new Date(), 3).getTime(),
+ // });
- // await expectMultiAttachCorrect({
- // customerId,
- // products: newProducts,
- // results,
- // db,
- // org,
- // env,
- // });
- // });
+ // await expectMultiAttachCorrect({
+ // customerId,
+ // products: newProducts,
+ // results,
+ // db,
+ // org,
+ // env,
+ // });
+ // });
});
diff --git a/server/tests/core/multiAttach/multiReward/multiReward3.test.ts b/server/tests/core/multiAttach/multiReward/multiReward3.test.ts
index 3b7c40da4..1bbea8cfe 100644
--- a/server/tests/core/multiAttach/multiReward/multiReward3.test.ts
+++ b/server/tests/core/multiAttach/multiReward/multiReward3.test.ts
@@ -5,22 +5,22 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { expectMultiAttachCorrect } from "tests/utils/expectUtils/expectMultiAttach.js";
import {
- multiRewardPremium,
- multiRewardPro,
- premiumReward,
- premiumTrial,
- proReward,
- proTrial,
- setupMultiRewardBefore,
+ multiRewardPremium,
+ multiRewardPro,
+ premiumReward,
+ premiumTrial,
+ proReward,
+ proTrial,
+ setupMultiRewardBefore,
} from "./multiRewardUtils.test.js";
import { CusService } from "@/internal/customers/CusService.js";
import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
@@ -34,139 +34,139 @@ import { Decimal } from "decimal.js";
const testCase = "multiReward3";
describe(`${chalk.yellowBright("multiReward3: Testing multi attach with rewards -- advance clock and update pro quantity")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- await setupMultiRewardBefore({
- orgId: org.id,
- db,
- env,
- });
+ await setupMultiRewardBefore({
+ orgId: org.id,
+ db,
+ env,
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: proTrial.id,
- quantity: 3,
- product: proTrial,
- status: CusProductStatus.Trialing,
- },
- {
- product_id: premiumTrial.id,
- quantity: 3,
- product: premiumTrial,
- status: CusProductStatus.Trialing,
- },
- ];
- await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- rewards: [proReward.id, premiumReward.id],
- expectedRewards: [proReward.id, premiumReward.id],
- });
- });
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: proTrial.id,
+ quantity: 3,
+ product: proTrial,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product_id: premiumTrial.id,
+ quantity: 3,
+ product: premiumTrial,
+ status: CusProductStatus.Trialing,
+ },
+ ];
+ await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ rewards: [proReward.id, premiumReward.id],
+ expectedRewards: [proReward.id, premiumReward.id],
+ });
+ });
- let checkoutRes: any;
+ let checkoutRes: any;
- it("should advance clock and update pro quantity", async function () {
- const productsList = [
- {
- product_id: proTrial.id,
- quantity: 5,
- product: proTrial,
- status: CusProductStatus.Trialing,
- },
- ];
+ it("should advance clock and update pro quantity", async function () {
+ const productsList = [
+ {
+ product_id: proTrial.id,
+ quantity: 5,
+ product: proTrial,
+ status: CusProductStatus.Trialing,
+ },
+ ];
- const results = [
- {
- product: proTrial,
- quantity: 5,
- status: CusProductStatus.Trialing,
- },
- {
- product: premiumTrial,
- quantity: 3,
- status: CusProductStatus.Trialing,
- },
- ];
- const res = await expectMultiAttachCorrect({
- customerId,
- products: productsList,
- results,
- db,
- org,
- env,
- rewards: [proReward.id, premiumReward.id],
- expectedRewards: [proReward.id, premiumReward.id],
- });
+ const results = [
+ {
+ product: proTrial,
+ quantity: 5,
+ status: CusProductStatus.Trialing,
+ },
+ {
+ product: premiumTrial,
+ quantity: 3,
+ status: CusProductStatus.Trialing,
+ },
+ ];
+ const res = await expectMultiAttachCorrect({
+ customerId,
+ products: productsList,
+ results,
+ db,
+ org,
+ env,
+ rewards: [proReward.id, premiumReward.id],
+ expectedRewards: [proReward.id, premiumReward.id],
+ });
- checkoutRes = res.checkoutRes;
- });
+ checkoutRes = res.checkoutRes;
+ });
- it("should advance to trial end and have correct quantity", async function () {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 12).getTime(),
- waitForSeconds: 30,
- });
+ it("should advance to trial end and have correct quantity", async function () {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 12).getTime(),
+ waitForSeconds: 30,
+ });
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- // sub: curSub,
- // cusProduct: curMainProduct,
- // results: productsList,
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ // sub: curSub,
+ // cusProduct: curMainProduct,
+ // results: productsList,
+ });
- const customer = await autumn.customers.get(customerId);
- const latestInvoice = customer.invoices[0];
+ const customer = await autumn.customers.get(customerId);
+ const latestInvoice = customer.invoices[0];
- const checkoutNextCycleTotal = checkoutRes.next_cycle?.total;
- const premiumPrice = new Decimal(getBasePrice({ product: premiumTrial }))
- .mul(3)
- .mul(0.2)
- .toNumber();
+ const checkoutNextCycleTotal = checkoutRes.next_cycle?.total;
+ const premiumPrice = new Decimal(getBasePrice({ product: premiumTrial }))
+ .mul(3)
+ .mul(0.2)
+ .toNumber();
- console.log("Premium price: ", premiumPrice);
- console.log("Checkout next cycle total: ", checkoutNextCycleTotal);
- expect(latestInvoice.total).to.equal(
- checkoutRes.next_cycle?.total + premiumPrice
- );
- });
+ console.log("Premium price: ", premiumPrice);
+ console.log("Checkout next cycle total: ", checkoutNextCycleTotal);
+ expect(latestInvoice.total).to.equal(
+ checkoutRes.next_cycle?.total + premiumPrice,
+ );
+ });
});
diff --git a/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts b/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts
index abc6e5f73..97ae7bc58 100644
--- a/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts
+++ b/server/tests/core/multiAttach/multiReward/multiRewardUtils.test.ts
@@ -5,155 +5,155 @@ import { RewardService } from "@/internal/rewards/RewardService.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import {
- APIVersion,
- AppEnv,
- CouponDurationType,
- CreateReward,
- ProductItemInterval,
- RewardType,
+ APIVersion,
+ AppEnv,
+ CouponDurationType,
+ CreateReward,
+ ProductItemInterval,
+ RewardType,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
export let premiumTrial = constructProduct({
- id: "multiReward_premiumTrial",
- group: "multiReward",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
- trial: true,
+ id: "multiReward_premiumTrial",
+ group: "multiReward",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
+ trial: true,
});
export let proTrial = constructProduct({
- id: "multiReward_proTrial",
- group: "multiReward",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 300 }),
- ],
- type: "pro",
- trial: true,
+ id: "multiReward_proTrial",
+ group: "multiReward",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 300 }),
+ ],
+ type: "pro",
+ trial: true,
});
export let multiRewardPremium = constructProduct({
- id: "multiReward_premium",
- group: "multiReward",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "multiReward_premium",
+ group: "multiReward",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
export let multiRewardPro = constructProduct({
- id: "multiReward_pro",
- group: "multiReward",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 300 }),
- ],
- type: "pro",
+ id: "multiReward_pro",
+ group: "multiReward",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 300 }),
+ ],
+ type: "pro",
});
export const proReward: CreateReward = {
- id: "pro_reward",
- name: "pro_reward",
- promo_codes: [{ code: "pro_reward" }],
- type: RewardType.PercentageDiscount,
- discount_config: {
- discount_value: 50,
- duration_type: CouponDurationType.Months,
- duration_value: 3,
- should_rollover: true,
- apply_to_all: false,
- price_ids: [proTrial.id],
- },
+ id: "pro_reward",
+ name: "pro_reward",
+ promo_codes: [{ code: "pro_reward" }],
+ type: RewardType.PercentageDiscount,
+ discount_config: {
+ discount_value: 50,
+ duration_type: CouponDurationType.Months,
+ duration_value: 3,
+ should_rollover: true,
+ apply_to_all: false,
+ price_ids: [proTrial.id],
+ },
};
export const premiumReward: CreateReward = {
- id: "premium_reward",
- name: "premium_reward",
- promo_codes: [{ code: "premium_reward" }],
- type: RewardType.PercentageDiscount,
- discount_config: {
- discount_value: 80,
- duration_type: CouponDurationType.Months,
- duration_value: 3,
- should_rollover: true,
- apply_to_all: false,
- },
+ id: "premium_reward",
+ name: "premium_reward",
+ promo_codes: [{ code: "premium_reward" }],
+ type: RewardType.PercentageDiscount,
+ discount_config: {
+ discount_value: 80,
+ duration_type: CouponDurationType.Months,
+ duration_value: 3,
+ should_rollover: true,
+ apply_to_all: false,
+ },
};
export const setupMultiRewardBefore = async ({
- orgId,
- db,
- env,
+ orgId,
+ db,
+ env,
}: {
- orgId: string;
- db: DrizzleCli;
- env: AppEnv;
+ orgId: string;
+ db: DrizzleCli;
+ env: AppEnv;
}) => {
- const autumn = new AutumnInt({ version: APIVersion.v1_2 });
- for (const product of [
- proTrial,
- premiumTrial,
- multiRewardPro,
- multiRewardPremium,
- ]) {
- // let res = await autumn.products.get(product.id);
+ const autumn = new AutumnInt({ version: APIVersion.v1_2 });
+ for (const product of [
+ proTrial,
+ premiumTrial,
+ multiRewardPro,
+ multiRewardPremium,
+ ]) {
+ // let res = await autumn.products.get(product.id);
- // if (res.code === "product_not_found") {
- // try {
- // await autumn.products.create(product);
- // } catch (error) {}
- // }
+ // if (res.code === "product_not_found") {
+ // try {
+ // await autumn.products.create(product);
+ // } catch (error) {}
+ // }
- try {
- await autumn.products.delete(product.id);
- } catch (error) {
- // console.log("Error deleting product:", error);
- }
+ try {
+ await autumn.products.delete(product.id);
+ } catch (error) {
+ // console.log("Error deleting product:", error);
+ }
- try {
- await autumn.products.create(product);
- } catch (error) {}
- }
+ try {
+ await autumn.products.create(product);
+ } catch (error) {}
+ }
- const products = await ProductService.listFull({
- db,
- orgId,
- env,
- });
+ const products = await ProductService.listFull({
+ db,
+ orgId,
+ env,
+ });
- const proTrialPrice = products.find((p) => p.id === proTrial.id)?.prices[0];
- const premiumTrialPrice = products.find((p) => p.id === premiumTrial.id)
- ?.prices[0];
- const proProduct = products.find((p) => p.id === multiRewardPro.id);
- const premiumProduct = products.find((p) => p.id === multiRewardPremium.id);
+ const proTrialPrice = products.find((p) => p.id === proTrial.id)?.prices[0];
+ const premiumTrialPrice = products.find((p) => p.id === premiumTrial.id)
+ ?.prices[0];
+ const proProduct = products.find((p) => p.id === multiRewardPro.id);
+ const premiumProduct = products.find((p) => p.id === multiRewardPremium.id);
- const proPriceIds = [proTrialPrice!.id, proProduct!.prices[0]!.id];
- const premiumPriceIds = [
- premiumTrialPrice!.id,
- premiumProduct!.prices[0]!.id,
- ];
+ const proPriceIds = [proTrialPrice!.id, proProduct!.prices[0]!.id];
+ const premiumPriceIds = [
+ premiumTrialPrice!.id,
+ premiumProduct!.prices[0]!.id,
+ ];
- for (const reward of [proReward, premiumReward]) {
- let rewardRes = null;
- // try {
- // rewardRes = await autumn.rewards.get(reward.id);
- // } catch (error) {}
+ for (const reward of [proReward, premiumReward]) {
+ let rewardRes = null;
+ // try {
+ // rewardRes = await autumn.rewards.get(reward.id);
+ // } catch (error) {}
- try {
- await autumn.rewards.delete(reward.id);
- } catch (error) {}
+ try {
+ await autumn.rewards.delete(reward.id);
+ } catch (error) {}
- if (!rewardRes) {
- try {
- await autumn.rewards.create({
- ...reward,
- discount_config: {
- ...reward.discount_config,
- price_ids:
- reward.id == proReward.id ? proPriceIds : premiumPriceIds,
- },
- });
- } catch (error) {}
- }
- }
+ if (!rewardRes) {
+ try {
+ await autumn.rewards.create({
+ ...reward,
+ discount_config: {
+ ...reward.discount_config,
+ price_ids:
+ reward.id == proReward.id ? proPriceIds : premiumPriceIds,
+ },
+ });
+ } catch (error) {}
+ }
+ }
};
diff --git a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts
index ceb8d0a3c..119017152 100644
--- a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts
+++ b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade1.test.ts
@@ -7,20 +7,20 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
@@ -30,199 +30,199 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { updateOrgConfig } from "@/internal/orgs/orgUtils.js";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
});
const testCase = "multiUpgrade1";
describe(`${chalk.yellowBright("multiUpgrade1: Testing multi attach and upgrade")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({
- version: APIVersion.v1_4,
- orgConfig: { entity_product: true },
- });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({
+ version: APIVersion.v1_4,
+ orgConfig: { entity_product: true },
+ });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
+ before(async function () {
+ await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- autumn,
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ autumn,
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 3,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 3,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ ];
- it("should transfer to entity and have correct sub", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity and have correct sub", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: pro.id,
+ });
- await expectResultsCorrect({
- autumn,
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ autumn,
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should upgrade entity's sub to premium", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ it("should upgrade entity's sub to premium", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- const entity = await autumn.entities.get(customerId, "1");
- const invoices = entity.invoices[0];
+ const entity = await autumn.entities.get(customerId, "1");
+ const invoices = entity.invoices[0];
- expect(invoices.total).to.equal(checkoutRes.total);
+ expect(invoices.total).to.equal(checkoutRes.total);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
- const results = [
- {
- product: pro,
- quantity: 4,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 4,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 4,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 4,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectResultsCorrect({
- autumn,
- customerId,
- results,
- });
- });
+ await expectResultsCorrect({
+ autumn,
+ customerId,
+ results,
+ });
+ });
});
diff --git a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts
index 80c176c44..630c9d052 100644
--- a/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts
+++ b/server/tests/core/multiAttach/multiUpgrade/multiUpgrade2.test.ts
@@ -7,204 +7,204 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import {
- expectMultiAttachCorrect,
- expectResultsCorrect,
+ expectMultiAttachCorrect,
+ expectResultsCorrect,
} from "tests/utils/expectUtils/expectMultiAttach.js";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
- id: "premium",
- items: [
- constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
- ],
- type: "premium",
+ id: "premium",
+ items: [
+ constructFeatureItem({ featureId: TestFeature.Words, includedUsage: 200 }),
+ ],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [
- constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 300,
- }),
- ],
- type: "pro",
+ id: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Words,
+ includedUsage: 300,
+ }),
+ ],
+ type: "pro",
});
const testCase = "multiUpgrade2";
describe(`${chalk.yellowBright("multiUpgrade2: Testing multi attach and update quantities downward")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({
- version: APIVersion.v1_4,
- orgConfig: { entity_product: true },
- });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({
+ version: APIVersion.v1_4,
+ orgConfig: { entity_product: true },
+ });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should run multi attach through checkout and have correct sub", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 5,
- product: pro,
- status: CusProductStatus.Active,
- },
- {
- product_id: premium.id,
- quantity: 3,
- product: premium,
- status: CusProductStatus.Active,
- },
- ];
+ it("should run multi attach through checkout and have correct sub", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 5,
+ product: pro,
+ status: CusProductStatus.Active,
+ },
+ {
+ product_id: premium.id,
+ quantity: 3,
+ product: premium,
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- autumn,
- customerId,
- products: productsList,
- results: productsList,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ autumn,
+ customerId,
+ products: productsList,
+ results: productsList,
+ db,
+ org,
+ env,
+ });
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 5,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 3,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 5,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 3,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ ];
- it("should transfer to entity and have correct sub", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should transfer to entity and have correct sub", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.transfer(customerId, {
- to_entity_id: "1",
- product_id: pro.id,
- });
+ await autumn.transfer(customerId, {
+ to_entity_id: "1",
+ product_id: pro.id,
+ });
- await expectResultsCorrect({
- autumn,
- customerId,
- results,
- });
+ await expectResultsCorrect({
+ autumn,
+ customerId,
+ results,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should update premium and pro quantities downward", async function () {
- const productsList = [
- {
- product_id: pro.id,
- quantity: 6,
- },
- {
- product_id: premium.id,
- quantity: 1,
- },
- ];
+ it("should update premium and pro quantities downward", async function () {
+ const productsList = [
+ {
+ product_id: pro.id,
+ quantity: 6,
+ },
+ {
+ product_id: premium.id,
+ quantity: 1,
+ },
+ ];
- const results = [
- {
- product: pro,
- quantity: 6,
- status: CusProductStatus.Active,
- },
- {
- product: premium,
- quantity: 1,
- status: CusProductStatus.Active,
- },
- {
- product: pro,
- quantity: 1,
- entityId: "1",
- status: CusProductStatus.Active,
- },
- ];
+ const results = [
+ {
+ product: pro,
+ quantity: 6,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: premium,
+ quantity: 1,
+ status: CusProductStatus.Active,
+ },
+ {
+ product: pro,
+ quantity: 1,
+ entityId: "1",
+ status: CusProductStatus.Active,
+ },
+ ];
- await expectMultiAttachCorrect({
- autumn,
- customerId,
- products: productsList,
- results,
- db,
- org,
- env,
- });
- });
+ await expectMultiAttachCorrect({
+ autumn,
+ customerId,
+ products: productsList,
+ results,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/core/reset1.ts b/server/tests/core/reset1.ts
index e86609172..ca0056cb7 100644
--- a/server/tests/core/reset1.ts
+++ b/server/tests/core/reset1.ts
@@ -4,13 +4,13 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- Customer,
- LimitedItem,
- Organization,
- ProductItemInterval,
- RolloverDuration,
+ APIVersion,
+ AppEnv,
+ Customer,
+ LimitedItem,
+ Organization,
+ ProductItemInterval,
+ RolloverDuration,
} from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -28,121 +28,121 @@ import { UTCDate } from "@date-fns/utc";
import { addDays, addMonths } from "date-fns";
const messagesItem = constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 400,
- interval: ProductItemInterval.Day,
- intervalCount: 3,
+ featureId: TestFeature.Messages,
+ includedUsage: 400,
+ interval: ProductItemInterval.Day,
+ intervalCount: 3,
}) as LimitedItem;
const wordsItem = constructFeatureItem({
- featureId: TestFeature.Words,
- includedUsage: 100,
- interval: ProductItemInterval.Month,
- intervalCount: 4,
+ featureId: TestFeature.Words,
+ includedUsage: 100,
+ interval: ProductItemInterval.Month,
+ intervalCount: 4,
}) as LimitedItem;
export let free = constructProduct({
- items: [messagesItem, wordsItem],
- type: "free",
- isDefault: false,
+ items: [messagesItem, wordsItem],
+ type: "free",
+ isDefault: false,
});
const testCase = "reset1";
describe(`${chalk.yellowBright(`${testCase}: Testing custom reset intervals`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let customer: Customer;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let customer: Customer;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [free],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [free],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const res = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const res = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = res.testClockId!;
- customer = res.customer;
- });
+ testClockId = res.testClockId!;
+ customer = res.customer;
+ });
- it("should attach free product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: free.id,
- });
- });
+ it("should attach free product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: free.id,
+ });
+ });
- let messageUsage = 250;
- let curBalance = messagesItem.included_usage;
+ let messageUsage = 250;
+ let curBalance = messagesItem.included_usage;
- it("should reset messages feature and have correct next reset at", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Messages,
- value: messageUsage,
- });
+ it("should reset messages feature and have correct next reset at", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Messages,
+ value: messageUsage,
+ });
- await timeout(3000);
+ await timeout(3000);
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Messages,
- });
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Messages,
+ });
- const cus = await autumn.customers.get(customerId);
- const msgesFeature = cus.features[TestFeature.Messages];
- expect(msgesFeature.next_reset_at).to.exist;
- expect(msgesFeature.next_reset_at).to.approximately(
- addDays(new Date(), 3).getTime(),
- 1000 * 30
- );
- });
+ const cus = await autumn.customers.get(customerId);
+ const msgesFeature = cus.features[TestFeature.Messages];
+ expect(msgesFeature.next_reset_at).to.exist;
+ expect(msgesFeature.next_reset_at).to.approximately(
+ addDays(new Date(), 3).getTime(),
+ 1000 * 30,
+ );
+ });
- it("should reset words feature and have correct next reset at", async function () {
- await resetAndGetCusEnt({
- db,
- customer,
- productGroup: free.group,
- featureId: TestFeature.Words,
- });
+ it("should reset words feature and have correct next reset at", async function () {
+ await resetAndGetCusEnt({
+ db,
+ customer,
+ productGroup: free.group,
+ featureId: TestFeature.Words,
+ });
- const cus = await autumn.customers.get(customerId);
- const wordsFeature = cus.features[TestFeature.Words];
- expect(wordsFeature.next_reset_at).to.exist;
- expect(wordsFeature.next_reset_at).to.approximately(
- addMonths(new Date(), 4).getTime(),
- 1000 * 30 * 60 // account for timezone differences
- );
- });
+ const cus = await autumn.customers.get(customerId);
+ const wordsFeature = cus.features[TestFeature.Words];
+ expect(wordsFeature.next_reset_at).to.exist;
+ expect(wordsFeature.next_reset_at).to.approximately(
+ addMonths(new Date(), 4).getTime(),
+ 1000 * 30 * 60, // account for timezone differences
+ );
+ });
});
diff --git a/server/tests/global.ts b/server/tests/global.ts
index 3e8ea98ff..2cfc62a85 100644
--- a/server/tests/global.ts
+++ b/server/tests/global.ts
@@ -3,994 +3,994 @@ import dotenv from "dotenv";
dotenv.config();
import {
- AggregateType,
- AllowanceType,
- AppEnv,
- BillingInterval,
- CouponDurationType,
- EntInterval,
- type Feature,
- FeatureType,
- FeatureUsageType,
- RewardReceivedBy,
- RewardTriggerEvent,
- RewardType,
+ AggregateType,
+ AllowanceType,
+ AppEnv,
+ BillingInterval,
+ CouponDurationType,
+ EntInterval,
+ type Feature,
+ FeatureType,
+ FeatureUsageType,
+ RewardReceivedBy,
+ RewardTriggerEvent,
+ RewardType,
} from "@autumn/shared";
import { initDrizzle } from "@/db/initDrizzle.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import {
- initEntitlement,
- initFeature,
- initFreeTrial,
- initPrice,
- initProduct,
- initReward,
- initRewardProgram,
+ initEntitlement,
+ initFeature,
+ initFreeTrial,
+ initPrice,
+ initProduct,
+ initReward,
+ initRewardProgram,
} from "./utils/init.js";
export const features: Record = {
- boolean1: initFeature({
- id: "boolean1",
- type: FeatureType.Boolean,
- }),
- metered1: initFeature({
- id: "metered1",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Sum,
- groupBy: "user_id",
- eventName: "metered_1",
- usageType: FeatureUsageType.Single,
- }),
- infinite1: initFeature({
- id: "infinite1",
- type: FeatureType.Metered,
- usageType: FeatureUsageType.Single,
- }),
- metered2: initFeature({
- id: "metered2",
- type: FeatureType.Metered,
- aggregateType: AggregateType.Count,
- eventName: "metered_2",
- usageType: FeatureUsageType.Single,
- }),
+ boolean1: initFeature({
+ id: "boolean1",
+ type: FeatureType.Boolean,
+ }),
+ metered1: initFeature({
+ id: "metered1",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Sum,
+ groupBy: "user_id",
+ eventName: "metered_1",
+ usageType: FeatureUsageType.Single,
+ }),
+ infinite1: initFeature({
+ id: "infinite1",
+ type: FeatureType.Metered,
+ usageType: FeatureUsageType.Single,
+ }),
+ metered2: initFeature({
+ id: "metered2",
+ type: FeatureType.Metered,
+ aggregateType: AggregateType.Count,
+ eventName: "metered_2",
+ usageType: FeatureUsageType.Single,
+ }),
- // GPU SYSTEM
- gpu1: initFeature({
- id: "gpu1",
- type: FeatureType.Metered,
- groupBy: "user_id",
- usageType: FeatureUsageType.Single,
- }),
- gpu2: initFeature({
- id: "gpu2",
- type: FeatureType.Metered,
- groupBy: "user_id",
- usageType: FeatureUsageType.Single,
- }),
+ // GPU SYSTEM
+ gpu1: initFeature({
+ id: "gpu1",
+ type: FeatureType.Metered,
+ groupBy: "user_id",
+ usageType: FeatureUsageType.Single,
+ }),
+ gpu2: initFeature({
+ id: "gpu2",
+ type: FeatureType.Metered,
+ groupBy: "user_id",
+ usageType: FeatureUsageType.Single,
+ }),
- // In arrear prorated
- seats: initFeature({
- id: "seats",
- type: FeatureType.Metered,
- usageType: FeatureUsageType.Continuous,
- }),
+ // In arrear prorated
+ seats: initFeature({
+ id: "seats",
+ type: FeatureType.Metered,
+ usageType: FeatureUsageType.Continuous,
+ }),
};
export const creditSystems = {
- gpuCredits: initFeature({
- id: "gpuCredits",
- type: FeatureType.CreditSystem,
- creditSchema: [
- {
- metered_feature_id: features.gpu1.id,
- feature_amount: 1,
- credit_amount: 0.01,
- },
- {
- metered_feature_id: features.gpu2.id,
- feature_amount: 1,
- credit_amount: 0.0213,
- },
- ],
- usageType: FeatureUsageType.Single,
- }),
+ gpuCredits: initFeature({
+ id: "gpuCredits",
+ type: FeatureType.CreditSystem,
+ creditSchema: [
+ {
+ metered_feature_id: features.gpu1.id,
+ feature_amount: 1,
+ credit_amount: 0.01,
+ },
+ {
+ metered_feature_id: features.gpu2.id,
+ feature_amount: 1,
+ credit_amount: 0.0213,
+ },
+ ],
+ usageType: FeatureUsageType.Single,
+ }),
};
export const products = {
- free: initProduct({
- id: "free",
- isDefault: true,
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 5,
- interval: EntInterval.Month,
- }),
- },
- prices: [],
- freeTrial: null,
- }),
+ free: initProduct({
+ id: "free",
+ isDefault: true,
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 5,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ }),
- pro: initProduct({
- id: "pro",
- entitlements: {
- boolean1: initEntitlement({
- feature: features.boolean1,
- }),
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 10,
- interval: EntInterval.Month,
- }),
- infinite1: initEntitlement({
- feature: features.infinite1,
- allowanceType: AllowanceType.Unlimited,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- }),
- ],
- freeTrial: null,
- }),
+ pro: initProduct({
+ id: "pro",
+ entitlements: {
+ boolean1: initEntitlement({
+ feature: features.boolean1,
+ }),
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 10,
+ interval: EntInterval.Month,
+ }),
+ infinite1: initEntitlement({
+ feature: features.infinite1,
+ allowanceType: AllowanceType.Unlimited,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ }),
+ ],
+ freeTrial: null,
+ }),
- oneTimeAddOnMetered1: initProduct({
- id: "one-time-add-on-metered-1",
- isAddOn: true,
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- billingInterval: BillingInterval.OneOff,
- feature: features.metered1,
- }),
- ],
- freeTrial: null,
- }),
+ oneTimeAddOnMetered1: initProduct({
+ id: "one-time-add-on-metered-1",
+ isAddOn: true,
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ billingInterval: BillingInterval.OneOff,
+ feature: features.metered1,
+ }),
+ ],
+ freeTrial: null,
+ }),
- monthlyAddOnMetered1: initProduct({
- id: "monthly-add-on-metered-1",
- isAddOn: true,
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 0,
- interval: EntInterval.Month,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- }),
- ],
- freeTrial: null,
- }),
+ monthlyAddOnMetered1: initProduct({
+ id: "monthly-add-on-metered-1",
+ isAddOn: true,
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 0,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ }),
+ ],
+ freeTrial: null,
+ }),
- proWithOverage: initProduct({
- id: "pro-with-overage",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 10,
- interval: EntInterval.Month,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- billingInterval: BillingInterval.Month,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- }),
- ],
- freeTrial: null,
- }),
+ proWithOverage: initProduct({
+ id: "pro-with-overage",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 10,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ billingInterval: BillingInterval.Month,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ }),
+ ],
+ freeTrial: null,
+ }),
- proOnlyUsage: initProduct({
- id: "pro-only-usage",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 0,
- allowanceType: AllowanceType.Fixed,
- interval: EntInterval.Month,
- }),
- },
+ proOnlyUsage: initProduct({
+ id: "pro-only-usage",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 0,
+ allowanceType: AllowanceType.Fixed,
+ interval: EntInterval.Month,
+ }),
+ },
- prices: [
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- }),
- ],
- freeTrial: null,
- }),
+ prices: [
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ }),
+ ],
+ freeTrial: null,
+ }),
- proWithTrial: initProduct({
- id: "pro-with-trial",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 10,
- interval: EntInterval.Month,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- }),
- ],
- freeTrial: initFreeTrial({
- length: 7,
- uniqueFingerprint: true,
- }),
- }),
+ proWithTrial: initProduct({
+ id: "pro-with-trial",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 10,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ }),
+ ],
+ freeTrial: initFreeTrial({
+ length: 7,
+ uniqueFingerprint: true,
+ }),
+ }),
- premium: initProduct({
- id: "premium",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 100,
- interval: EntInterval.Month,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 50,
- }),
- ],
- freeTrial: null,
- }),
+ premium: initProduct({
+ id: "premium",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 100,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 50,
+ }),
+ ],
+ freeTrial: null,
+ }),
- premiumWithTrial: initProduct({
- id: "premium-with-trial",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 100,
- interval: EntInterval.Month,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 50,
- }),
- ],
- freeTrial: initFreeTrial({
- length: 7,
- uniqueFingerprint: true,
- }),
- }),
+ premiumWithTrial: initProduct({
+ id: "premium-with-trial",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 100,
+ interval: EntInterval.Month,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 50,
+ }),
+ ],
+ freeTrial: initFreeTrial({
+ length: 7,
+ uniqueFingerprint: true,
+ }),
+ }),
- monthlyWithOneTime: initProduct({
- id: "mothlyWithOneTime",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- metered2: initEntitlement({
- feature: features.metered2,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- }),
- initPrice({
- type: "in_advance",
- billingInterval: BillingInterval.OneOff,
- feature: features.metered1,
- amount: 100,
- }),
- initPrice({
- type: "in_advance",
- billingInterval: BillingInterval.OneOff,
- feature: features.metered2,
- amount: 200,
- }),
- ],
- freeTrial: null,
- }),
+ monthlyWithOneTime: initProduct({
+ id: "mothlyWithOneTime",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ metered2: initEntitlement({
+ feature: features.metered2,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ }),
+ initPrice({
+ type: "in_advance",
+ billingInterval: BillingInterval.OneOff,
+ feature: features.metered1,
+ amount: 100,
+ }),
+ initPrice({
+ type: "in_advance",
+ billingInterval: BillingInterval.OneOff,
+ feature: features.metered2,
+ amount: 200,
+ }),
+ ],
+ freeTrial: null,
+ }),
- freeAddOn: initProduct({
- id: "freeAddOn",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 100,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [],
- freeTrial: null,
- isAddOn: true,
- }),
+ freeAddOn: initProduct({
+ id: "freeAddOn",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 100,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ isAddOn: true,
+ }),
- proAddOn: initProduct({
- id: "proAddOn",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 100,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "fixed_cycle",
- billingInterval: BillingInterval.OneOff,
- amount: 100,
- }),
- ],
- freeTrial: null,
- isAddOn: true,
- }),
+ proAddOn: initProduct({
+ id: "proAddOn",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 100,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "fixed_cycle",
+ billingInterval: BillingInterval.OneOff,
+ amount: 100,
+ }),
+ ],
+ freeTrial: null,
+ isAddOn: true,
+ }),
};
export const oneTimeProducts = {
- oneTimeMetered1: initProduct({
- id: "oneTimeMetered1",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 500,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- billingInterval: BillingInterval.OneOff,
- feature: features.metered1,
- amount: 100,
- }),
- ],
- freeTrial: null,
- }),
- oneTimeMetered2: initProduct({
- id: "oneTimeMetered2",
- entitlements: {
- metered2: initEntitlement({
- feature: features.metered2,
- allowance: 0,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "in_advance",
- billingInterval: BillingInterval.OneOff,
- feature: features.metered2,
- amount: 0.01,
- }),
- ],
- freeTrial: null,
- }),
+ oneTimeMetered1: initProduct({
+ id: "oneTimeMetered1",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 500,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ billingInterval: BillingInterval.OneOff,
+ feature: features.metered1,
+ amount: 100,
+ }),
+ ],
+ freeTrial: null,
+ }),
+ oneTimeMetered2: initProduct({
+ id: "oneTimeMetered2",
+ entitlements: {
+ metered2: initEntitlement({
+ feature: features.metered2,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_advance",
+ billingInterval: BillingInterval.OneOff,
+ feature: features.metered2,
+ amount: 0.01,
+ }),
+ ],
+ freeTrial: null,
+ }),
};
export const advanceProducts = {
- // GPU SYSTEM
- gpuSystemStarter: initProduct({
- id: "gpu-system-starter",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 500,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 20,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 5,
- }),
- ],
- freeTrial: null,
- }),
+ // GPU SYSTEM
+ gpuSystemStarter: initProduct({
+ id: "gpu-system-starter",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 500,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 20,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 5,
+ }),
+ ],
+ freeTrial: null,
+ }),
- gpuSystemPro: initProduct({
- id: "gpu-system-pro",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 5000,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 100,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 1,
- }),
- ],
- freeTrial: null,
- }),
+ gpuSystemPro: initProduct({
+ id: "gpu-system-pro",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 5000,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 100,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ ],
+ freeTrial: null,
+ }),
- // Quarterly
- gpuStarterQuarter: initProduct({
- id: "gpuStarterQuarter",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 500,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 20,
- billingInterval: BillingInterval.Quarter,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 5,
- }),
- ],
- freeTrial: null,
- }),
+ // Quarterly
+ gpuStarterQuarter: initProduct({
+ id: "gpuStarterQuarter",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 500,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 20,
+ billingInterval: BillingInterval.Quarter,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 5,
+ }),
+ ],
+ freeTrial: null,
+ }),
- gpuProQuarter: initProduct({
- id: "gpuProQuarter",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 5000,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 1,
- }),
- initPrice({
- type: "fixed_cycle",
- amount: 1000,
- billingInterval: BillingInterval.Quarter,
- }),
- ],
+ gpuProQuarter: initProduct({
+ id: "gpuProQuarter",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 5000,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ initPrice({
+ type: "fixed_cycle",
+ amount: 1000,
+ billingInterval: BillingInterval.Quarter,
+ }),
+ ],
- freeTrial: null,
- }),
+ freeTrial: null,
+ }),
- gpuStarterAnnual: initProduct({
- id: "gpu-starter-annual",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 500,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "fixed_cycle",
- amount: 200,
- billingInterval: BillingInterval.Year,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 1,
- }),
- ],
+ gpuStarterAnnual: initProduct({
+ id: "gpu-starter-annual",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 500,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "fixed_cycle",
+ amount: 200,
+ billingInterval: BillingInterval.Year,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ ],
- freeTrial: null,
- }),
+ freeTrial: null,
+ }),
- gpuProAnnual: initProduct({
- id: "gpuProAnnual",
- entitlements: {
- gpuCredits: initEntitlement({
- allowance: 5000,
- feature: creditSystems.gpuCredits,
- }),
- },
- prices: [
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: creditSystems.gpuCredits,
- amount: 0.01,
- oneTier: true,
- billingUnits: 1,
- }),
- initPrice({
- type: "fixed_cycle",
- amount: 1000,
- billingInterval: BillingInterval.Year,
- }),
- ],
+ gpuProAnnual: initProduct({
+ id: "gpuProAnnual",
+ entitlements: {
+ gpuCredits: initEntitlement({
+ allowance: 5000,
+ feature: creditSystems.gpuCredits,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: creditSystems.gpuCredits,
+ amount: 0.01,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ initPrice({
+ type: "fixed_cycle",
+ amount: 1000,
+ billingInterval: BillingInterval.Year,
+ }),
+ ],
- freeTrial: null,
- }),
+ freeTrial: null,
+ }),
- proratedArrearSeats: initProduct({
- id: "prorated-arrear-seats",
- entitlements: {
- seats: initEntitlement({
- feature: features.seats,
- allowance: 3,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 20,
- }),
- initPrice({
- type: "in_arrear_prorated",
- billingInterval: BillingInterval.Month,
- feature: features.seats,
- amount: 10,
- oneTier: true,
- billingUnits: 1,
- }),
- ],
+ proratedArrearSeats: initProduct({
+ id: "prorated-arrear-seats",
+ entitlements: {
+ seats: initEntitlement({
+ feature: features.seats,
+ allowance: 3,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 20,
+ }),
+ initPrice({
+ type: "in_arrear_prorated",
+ billingInterval: BillingInterval.Month,
+ feature: features.seats,
+ amount: 10,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ ],
- freeTrial: null,
- }),
+ freeTrial: null,
+ }),
- proratedArrearSeatsWithReset: initProduct({
- id: "prorated-arrear-seats-with-reset",
- entitlements: {
- seats: initEntitlement({
- feature: features.seats,
- allowance: 3,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 20,
- }),
- initPrice({
- type: "in_arrear_prorated",
- billingInterval: BillingInterval.Month,
- feature: features.seats,
- amount: 10,
- oneTier: true,
- billingUnits: 1,
- }),
- ],
+ proratedArrearSeatsWithReset: initProduct({
+ id: "prorated-arrear-seats-with-reset",
+ entitlements: {
+ seats: initEntitlement({
+ feature: features.seats,
+ allowance: 3,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 20,
+ }),
+ initPrice({
+ type: "in_arrear_prorated",
+ billingInterval: BillingInterval.Month,
+ feature: features.seats,
+ amount: 10,
+ oneTier: true,
+ billingUnits: 1,
+ }),
+ ],
- freeTrial: null,
- }),
+ freeTrial: null,
+ }),
};
export const attachProducts = {
- // 1. pro1Starter
- starterGroup1: initProduct({
- id: "starterGroup1",
- group: "g1",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- interval: EntInterval.Month,
- allowance: 10,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 10,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- amount: 0.5,
- }),
- ],
- freeTrial: null,
- }),
- proGroup1: initProduct({
- id: "proGroup1",
- group: "g1",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- interval: EntInterval.Month,
- allowance: 10,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 30,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- amount: 1.0,
- }),
- ],
- freeTrial: null,
- }),
- premiumGroup1: initProduct({
- id: "premiumGroup1",
- group: "g1",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered1,
- interval: EntInterval.Month,
- allowance: 100,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 50,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered1,
- amount: 2.0,
- }),
- ],
- freeTrial: null,
- }),
+ // 1. pro1Starter
+ starterGroup1: initProduct({
+ id: "starterGroup1",
+ group: "g1",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ interval: EntInterval.Month,
+ allowance: 10,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 10,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ amount: 0.5,
+ }),
+ ],
+ freeTrial: null,
+ }),
+ proGroup1: initProduct({
+ id: "proGroup1",
+ group: "g1",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ interval: EntInterval.Month,
+ allowance: 10,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 30,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ amount: 1.0,
+ }),
+ ],
+ freeTrial: null,
+ }),
+ premiumGroup1: initProduct({
+ id: "premiumGroup1",
+ group: "g1",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered1,
+ interval: EntInterval.Month,
+ allowance: 100,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 50,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered1,
+ amount: 2.0,
+ }),
+ ],
+ freeTrial: null,
+ }),
- // 2. pro2Starter
- freeGroup2: initProduct({
- id: "freeGroup2",
- group: "g2",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered2,
- allowance: 10,
- }),
- },
- prices: [],
- freeTrial: null,
- }),
- starterGroup2: initProduct({
- id: "starterGroup2",
- group: "g2",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered2,
- interval: EntInterval.Month,
- allowance: 10,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 20,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered2,
- amount: 0.3,
- }),
- ],
- freeTrial: null,
- }),
+ // 2. pro2Starter
+ freeGroup2: initProduct({
+ id: "freeGroup2",
+ group: "g2",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered2,
+ allowance: 10,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ }),
+ starterGroup2: initProduct({
+ id: "starterGroup2",
+ group: "g2",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered2,
+ interval: EntInterval.Month,
+ allowance: 10,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 20,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered2,
+ amount: 0.3,
+ }),
+ ],
+ freeTrial: null,
+ }),
- proGroup2: initProduct({
- id: "proGroup2",
- group: "g2",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered2,
- interval: EntInterval.Month,
- allowance: 10,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 40,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered2,
- amount: 0.6,
- }),
- ],
- freeTrial: null,
- }),
+ proGroup2: initProduct({
+ id: "proGroup2",
+ group: "g2",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered2,
+ interval: EntInterval.Month,
+ allowance: 10,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 40,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered2,
+ amount: 0.6,
+ }),
+ ],
+ freeTrial: null,
+ }),
- premiumGroup2: initProduct({
- id: "premiumGroup2",
- group: "g2",
- entitlements: {
- metered1: initEntitlement({
- feature: features.metered2,
- interval: EntInterval.Month,
- allowance: 10,
- }),
- },
- prices: [
- initPrice({
- type: "monthly",
- amount: 60,
- }),
- initPrice({
- type: "in_arrears",
- billingInterval: BillingInterval.Month,
- feature: features.metered2,
- amount: 0.9,
- }),
- ],
- freeTrial: null,
- }),
+ premiumGroup2: initProduct({
+ id: "premiumGroup2",
+ group: "g2",
+ entitlements: {
+ metered1: initEntitlement({
+ feature: features.metered2,
+ interval: EntInterval.Month,
+ allowance: 10,
+ }),
+ },
+ prices: [
+ initPrice({
+ type: "monthly",
+ amount: 60,
+ }),
+ initPrice({
+ type: "in_arrears",
+ billingInterval: BillingInterval.Month,
+ feature: features.metered2,
+ amount: 0.9,
+ }),
+ ],
+ freeTrial: null,
+ }),
};
// Entity products
export const entityProducts = {
- entityFree: initProduct({
- id: "entityFree",
- entitlements: {
- seats: initEntitlement({
- feature: features.seats,
- allowance: 1,
- interval: EntInterval.Lifetime,
- }),
- },
- prices: [],
- freeTrial: null,
- }),
+ entityFree: initProduct({
+ id: "entityFree",
+ entitlements: {
+ seats: initEntitlement({
+ feature: features.seats,
+ allowance: 1,
+ interval: EntInterval.Lifetime,
+ }),
+ },
+ prices: [],
+ freeTrial: null,
+ }),
- entityPro: initProduct({
- id: "entityPro",
- entitlements: {
- seats: initEntitlement({
- feature: features.seats,
- allowance: 0,
- interval: EntInterval.Lifetime,
- carryFromPrevious: true,
- }),
- metered1: initEntitlement({
- feature: features.metered1,
- allowance: 500,
- interval: EntInterval.Month,
- entityFeatureId: features.seats.id,
- carryFromPrevious: true,
- }),
- },
- prices: [
- // initPrice({
- // type: "monthly",
- // amount: 10,
- // }),
- initPrice({
- type: "in_arrear_prorated",
- billingInterval: BillingInterval.Month,
- feature: features.seats,
- amount: 100,
- oneTier: true,
- billingUnits: 1,
- // Carry over usage
- }),
- ],
- freeTrial: null,
- }),
+ entityPro: initProduct({
+ id: "entityPro",
+ entitlements: {
+ seats: initEntitlement({
+ feature: features.seats,
+ allowance: 0,
+ interval: EntInterval.Lifetime,
+ carryFromPrevious: true,
+ }),
+ metered1: initEntitlement({
+ feature: features.metered1,
+ allowance: 500,
+ interval: EntInterval.Month,
+ entityFeatureId: features.seats.id,
+ carryFromPrevious: true,
+ }),
+ },
+ prices: [
+ // initPrice({
+ // type: "monthly",
+ // amount: 10,
+ // }),
+ initPrice({
+ type: "in_arrear_prorated",
+ billingInterval: BillingInterval.Month,
+ feature: features.seats,
+ amount: 100,
+ oneTier: true,
+ billingUnits: 1,
+ // Carry over usage
+ }),
+ ],
+ freeTrial: null,
+ }),
};
export const rewards = {
- rolloverAll: initReward({
- id: "rolloverAll",
- type: RewardType.InvoiceCredits,
- discountValue: 1000,
- durationType: CouponDurationType.Forever,
- applyToAll: true,
- }),
- rolloverUsage: initReward({
- id: "rolloverUsage",
- type: RewardType.InvoiceCredits,
- discountValue: 1000,
- durationType: CouponDurationType.Forever,
- onlyUsagePrices: true,
- productIds: [products.proWithOverage.id],
- }),
- monthOff: initReward({
- id: "monthOff",
- type: RewardType.PercentageDiscount,
- discountValue: 100,
- applyToAll: true,
- durationType: CouponDurationType.Months,
- durationValue: 1,
- }),
- paidProductWithConfig: initReward({
- id: "paidProductWithConfig",
- type: RewardType.FreeProduct,
- freeProductId: products.pro.id,
- freeProductConfig: {
- durationType: CouponDurationType.Months,
- durationValue: 1,
- },
- }),
- paidProductAddOn: initReward({
- id: "paidProductAddOn",
- type: RewardType.FreeProduct,
- freeProductId: products.proAddOn.id,
- }),
- freeProduct: initReward({
- id: "freeProduct",
- type: RewardType.FreeProduct,
- freeProductId: products.freeAddOn.id,
- }),
+ rolloverAll: initReward({
+ id: "rolloverAll",
+ type: RewardType.InvoiceCredits,
+ discountValue: 1000,
+ durationType: CouponDurationType.Forever,
+ applyToAll: true,
+ }),
+ rolloverUsage: initReward({
+ id: "rolloverUsage",
+ type: RewardType.InvoiceCredits,
+ discountValue: 1000,
+ durationType: CouponDurationType.Forever,
+ onlyUsagePrices: true,
+ productIds: [products.proWithOverage.id],
+ }),
+ monthOff: initReward({
+ id: "monthOff",
+ type: RewardType.PercentageDiscount,
+ discountValue: 100,
+ applyToAll: true,
+ durationType: CouponDurationType.Months,
+ durationValue: 1,
+ }),
+ paidProductWithConfig: initReward({
+ id: "paidProductWithConfig",
+ type: RewardType.FreeProduct,
+ freeProductId: products.pro.id,
+ freeProductConfig: {
+ durationType: CouponDurationType.Months,
+ durationValue: 1,
+ },
+ }),
+ paidProductAddOn: initReward({
+ id: "paidProductAddOn",
+ type: RewardType.FreeProduct,
+ freeProductId: products.proAddOn.id,
+ }),
+ freeProduct: initReward({
+ id: "freeProduct",
+ type: RewardType.FreeProduct,
+ freeProductId: products.freeAddOn.id,
+ }),
};
export const referralPrograms = {
- freeProduct: initRewardProgram({
- id: "freeProduct",
- internalRewardId: rewards.freeProduct.id,
- when: RewardTriggerEvent.Checkout,
- receivedBy: RewardReceivedBy.All,
- productIds: [products.pro.id, products.proWithTrial.id],
- }),
- onCheckout: initRewardProgram({
- id: "onCheckout",
- internalRewardId: rewards.monthOff.id,
- when: RewardTriggerEvent.Checkout,
- productIds: [products.pro.id, products.proWithTrial.id],
- }),
- immediate: initRewardProgram({
- id: "immediate",
- internalRewardId: rewards.monthOff.id,
- when: RewardTriggerEvent.CustomerCreation,
- }),
- paidProductImmediateAll: initRewardProgram({
- id: "paidProduct-immediate-all",
- internalRewardId: rewards.paidProductWithConfig.id,
- when: RewardTriggerEvent.CustomerCreation,
- receivedBy: RewardReceivedBy.All,
- productIds: [products.pro.id],
- maxRedemptions: 100,
- }),
- paidProductImmediateReferrer: initRewardProgram({
- id: "paidProduct-immediate-referrer",
- internalRewardId: rewards.paidProductWithConfig.id,
- when: RewardTriggerEvent.CustomerCreation,
- receivedBy: RewardReceivedBy.Referrer,
- productIds: [products.pro.id],
- maxRedemptions: 100,
- }),
+ freeProduct: initRewardProgram({
+ id: "freeProduct",
+ internalRewardId: rewards.freeProduct.id,
+ when: RewardTriggerEvent.Checkout,
+ receivedBy: RewardReceivedBy.All,
+ productIds: [products.pro.id, products.proWithTrial.id],
+ }),
+ onCheckout: initRewardProgram({
+ id: "onCheckout",
+ internalRewardId: rewards.monthOff.id,
+ when: RewardTriggerEvent.Checkout,
+ productIds: [products.pro.id, products.proWithTrial.id],
+ }),
+ immediate: initRewardProgram({
+ id: "immediate",
+ internalRewardId: rewards.monthOff.id,
+ when: RewardTriggerEvent.CustomerCreation,
+ }),
+ paidProductImmediateAll: initRewardProgram({
+ id: "paidProduct-immediate-all",
+ internalRewardId: rewards.paidProductWithConfig.id,
+ when: RewardTriggerEvent.CustomerCreation,
+ receivedBy: RewardReceivedBy.All,
+ productIds: [products.pro.id],
+ maxRedemptions: 100,
+ }),
+ paidProductImmediateReferrer: initRewardProgram({
+ id: "paidProduct-immediate-referrer",
+ internalRewardId: rewards.paidProductWithConfig.id,
+ when: RewardTriggerEvent.CustomerCreation,
+ receivedBy: RewardReceivedBy.Referrer,
+ productIds: [products.pro.id],
+ maxRedemptions: 100,
+ }),
- paidProductCheckoutAll: initRewardProgram({
- id: "paidProduct-checkout-all",
- internalRewardId: rewards.paidProductWithConfig.id,
- when: RewardTriggerEvent.Checkout,
- receivedBy: RewardReceivedBy.All,
- productIds: [products.premium.id],
- }),
- paidProductCheckoutReferrer: initRewardProgram({
- id: "paidProduct-checkout-referrer",
- internalRewardId: rewards.paidProductWithConfig.id,
- when: RewardTriggerEvent.Checkout,
- receivedBy: RewardReceivedBy.Referrer,
- productIds: [products.premium.id],
- }),
+ paidProductCheckoutAll: initRewardProgram({
+ id: "paidProduct-checkout-all",
+ internalRewardId: rewards.paidProductWithConfig.id,
+ when: RewardTriggerEvent.Checkout,
+ receivedBy: RewardReceivedBy.All,
+ productIds: [products.premium.id],
+ }),
+ paidProductCheckoutReferrer: initRewardProgram({
+ id: "paidProduct-checkout-referrer",
+ internalRewardId: rewards.paidProductWithConfig.id,
+ when: RewardTriggerEvent.Checkout,
+ receivedBy: RewardReceivedBy.Referrer,
+ productIds: [products.premium.id],
+ }),
- paidAddOnAll: initRewardProgram({
- id: "paidAddOn-all",
- internalRewardId: rewards.paidProductAddOn.id,
- when: RewardTriggerEvent.CustomerCreation,
- receivedBy: RewardReceivedBy.All,
- productIds: [products.proAddOn.id],
- }),
- paidAddOnReferrer: initRewardProgram({
- id: "paidAddOn-referrer",
- internalRewardId: rewards.paidProductAddOn.id,
- when: RewardTriggerEvent.CustomerCreation,
- receivedBy: RewardReceivedBy.Referrer,
- productIds: [products.proAddOn.id],
- }),
+ paidAddOnAll: initRewardProgram({
+ id: "paidAddOn-all",
+ internalRewardId: rewards.paidProductAddOn.id,
+ when: RewardTriggerEvent.CustomerCreation,
+ receivedBy: RewardReceivedBy.All,
+ productIds: [products.proAddOn.id],
+ }),
+ paidAddOnReferrer: initRewardProgram({
+ id: "paidAddOn-referrer",
+ internalRewardId: rewards.paidProductAddOn.id,
+ when: RewardTriggerEvent.CustomerCreation,
+ receivedBy: RewardReceivedBy.Referrer,
+ productIds: [products.proAddOn.id],
+ }),
- paidAddOnCheckoutAll: initRewardProgram({
- id: "paidAddOn-checkout-all",
- internalRewardId: rewards.paidProductAddOn.id,
- when: RewardTriggerEvent.Checkout,
- receivedBy: RewardReceivedBy.All,
- productIds: [products.premium.id],
- }),
- paidAddOnCheckoutReferrer: initRewardProgram({
- id: "paidAddOn-checkout-referrer",
- internalRewardId: rewards.paidProductAddOn.id,
- when: RewardTriggerEvent.Checkout,
- receivedBy: RewardReceivedBy.Referrer,
- productIds: [products.premium.id],
- }),
+ paidAddOnCheckoutAll: initRewardProgram({
+ id: "paidAddOn-checkout-all",
+ internalRewardId: rewards.paidProductAddOn.id,
+ when: RewardTriggerEvent.Checkout,
+ receivedBy: RewardReceivedBy.All,
+ productIds: [products.premium.id],
+ }),
+ paidAddOnCheckoutReferrer: initRewardProgram({
+ id: "paidAddOn-checkout-referrer",
+ internalRewardId: rewards.paidProductAddOn.id,
+ when: RewardTriggerEvent.Checkout,
+ receivedBy: RewardReceivedBy.Referrer,
+ productIds: [products.premium.id],
+ }),
};
const ORG_SLUG = process.env.TESTS_ORG!;
before(async function () {
- try {
- this.env = AppEnv.Sandbox;
- const { db, client } = initDrizzle();
- this.db = db;
- this.client = client;
+ try {
+ this.env = AppEnv.Sandbox;
+ const { db, client } = initDrizzle();
+ this.db = db;
+ this.client = client;
- this.org = await OrgService.getBySlug({
- db: this.db,
- slug: ORG_SLUG,
- });
+ this.org = await OrgService.getBySlug({
+ db: this.db,
+ slug: ORG_SLUG,
+ });
- const dbFeatures = await FeatureService.list({
- db: this.db,
- orgId: this.org.id,
- env: this.env,
- });
+ const dbFeatures = await FeatureService.list({
+ db: this.db,
+ orgId: this.org.id,
+ env: this.env,
+ });
- const cleanFeatures = (features: Record) => {
- for (const featureId in features) {
- const feature = features[featureId as keyof typeof features];
- const dbFeature = dbFeatures.find((f: any) => f.id === feature.id);
- if (!dbFeature) {
- // throw new Error(`Feature ${feature.id} not found`);
- continue;
- }
- features[featureId as keyof typeof features].internal_id =
- dbFeature.internal_id;
- if (feature.type === FeatureType.Metered) {
- // Ignore this for now
- // @ts-expect-error eventName is manually set
- features[featureId as keyof typeof features].eventName =
- dbFeature.config?.filters[0].value[0];
- }
- }
- };
+ const cleanFeatures = (features: Record) => {
+ for (const featureId in features) {
+ const feature = features[featureId as keyof typeof features];
+ const dbFeature = dbFeatures.find((f: any) => f.id === feature.id);
+ if (!dbFeature) {
+ // throw new Error(`Feature ${feature.id} not found`);
+ continue;
+ }
+ features[featureId as keyof typeof features].internal_id =
+ dbFeature.internal_id;
+ if (feature.type === FeatureType.Metered) {
+ // Ignore this for now
+ // @ts-expect-error eventName is manually set
+ features[featureId as keyof typeof features].eventName =
+ dbFeature.config?.filters[0].value[0];
+ }
+ }
+ };
- cleanFeatures(features);
- cleanFeatures(creditSystems);
- } catch (error) {
- console.error(error);
- }
+ cleanFeatures(features);
+ cleanFeatures(creditSystems);
+ } catch (error) {
+ console.error(error);
+ }
});
after(async function () {
- await this.client?.end();
+ await this.client?.end();
});
diff --git a/server/tests/interval/multiSub/multiSubInterval1.test.ts b/server/tests/interval/multiSub/multiSubInterval1.test.ts
index f7ebdd78b..ba0fc1c8a 100644
--- a/server/tests/interval/multiSub/multiSubInterval1.test.ts
+++ b/server/tests/interval/multiSub/multiSubInterval1.test.ts
@@ -18,128 +18,128 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "multiSubInterval1";
describe(`${chalk.yellowBright("multiSubInterval1: Should attach pro and pro annual to entity mid cycle and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro and advance test clock", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro and advance test clock", async function () {
+ await autumn.entities.create(customerId, entities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ });
+ });
- it("should attach pro to entity 2 and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: entities[1].id,
- });
+ it("should attach pro to entity 2 and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: entities[1].id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addMonths(new Date(), 1).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addMonths(new Date(), 1).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: entities[1].id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: entities[1].id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: pro.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: pro.id,
+ });
- const subItem = sub!.items.data[0];
- expect(subItem.current_period_end * 1000).to.approximately(
- checkoutRes.next_cycle?.starts_at!,
- toMilliseconds.days(1) // +- 1 day
- );
- });
+ const subItem = sub!.items.data[0];
+ expect(subItem.current_period_end * 1000).to.approximately(
+ checkoutRes.next_cycle?.starts_at!,
+ toMilliseconds.days(1), // +- 1 day
+ );
+ });
});
diff --git a/server/tests/interval/multiSub/multiSubInterval2.test.ts b/server/tests/interval/multiSub/multiSubInterval2.test.ts
index 6432dbdcc..afc2f2483 100644
--- a/server/tests/interval/multiSub/multiSubInterval2.test.ts
+++ b/server/tests/interval/multiSub/multiSubInterval2.test.ts
@@ -8,8 +8,8 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -21,134 +21,134 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "multiSubInterval2";
describe(`${chalk.yellowBright("multiSubInterval2: Should attach pro and pro annual to entity mid cycle and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro and advance test clock", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro and advance test clock", async function () {
+ await autumn.entities.create(customerId, entities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1.5).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1.5).getTime(),
+ });
+ });
- it("should attach pro annual to entity 2 and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: proAnnual.id,
- entity_id: entities[1].id,
- });
+ it("should attach pro annual to entity 2 and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ entity_id: entities[1].id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addYears(new Date(), 1).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addYears(new Date(), 1).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: proAnnual.id,
- entity_id: entities[1].id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ entity_id: entities[1].id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: proAnnual.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: proAnnual.id,
+ });
- const periodEndExists = sub!.items.data.some(
- (item) =>
- Math.abs(
- differenceInDays(
- item.current_period_end * 1000,
- checkoutRes.next_cycle?.starts_at!
- )
- ) < 1
- );
+ const periodEndExists = sub!.items.data.some(
+ (item) =>
+ Math.abs(
+ differenceInDays(
+ item.current_period_end * 1000,
+ checkoutRes.next_cycle?.starts_at!,
+ ),
+ ) < 1,
+ );
- expect(periodEndExists).to.be.true;
- });
+ expect(periodEndExists).to.be.true;
+ });
});
diff --git a/server/tests/interval/multiSub/multiSubInterval3.test.ts b/server/tests/interval/multiSub/multiSubInterval3.test.ts
index 5977408a7..e872d85b9 100644
--- a/server/tests/interval/multiSub/multiSubInterval3.test.ts
+++ b/server/tests/interval/multiSub/multiSubInterval3.test.ts
@@ -8,8 +8,8 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -21,137 +21,137 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [
- constructArrearItem({ featureId: TestFeature.Credits }),
- constructFeatureItem({ featureId: TestFeature.Words }),
- ],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [
+ constructArrearItem({ featureId: TestFeature.Credits }),
+ constructFeatureItem({ featureId: TestFeature.Words }),
+ ],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "multiSubInterval3";
describe(`${chalk.yellowBright("multiSubInterval3: Should attach pro and pro annual (with monthly usage price) to entity mid cycle and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "entity1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "entity2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "entity1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "entity2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro and advance test clock", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro and advance test clock", async function () {
+ await autumn.entities.create(customerId, entities);
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addMonths(new Date(), 1.5).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addMonths(new Date(), 1.5).getTime(),
+ });
+ });
- it("should attach pro annual to entity 2 and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: proAnnual.id,
- entity_id: entities[1].id,
- });
+ it("should attach pro annual to entity 2 and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ entity_id: entities[1].id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addYears(new Date(), 1).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addYears(new Date(), 1).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: proAnnual.id,
- entity_id: entities[1].id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ entity_id: entities[1].id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: proAnnual.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: proAnnual.id,
+ });
- const periodEndExists = sub!.items.data.some(
- (item) =>
- Math.abs(
- differenceInDays(
- item.current_period_end * 1000,
- checkoutRes.next_cycle?.starts_at!
- )
- ) < 1
- );
+ const periodEndExists = sub!.items.data.some(
+ (item) =>
+ Math.abs(
+ differenceInDays(
+ item.current_period_end * 1000,
+ checkoutRes.next_cycle?.starts_at!,
+ ),
+ ) < 1,
+ );
- expect(periodEndExists).to.be.true;
- });
+ expect(periodEndExists).to.be.true;
+ });
});
diff --git a/server/tests/interval/upgrade/interval1.test.ts b/server/tests/interval/upgrade/interval1.test.ts
index 2cb046642..fdca5c069 100644
--- a/server/tests/interval/upgrade/interval1.test.ts
+++ b/server/tests/interval/upgrade/interval1.test.ts
@@ -18,111 +18,111 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "interval1";
describe(`${chalk.yellowBright("interval1: Should upgrade from pro to pro annual mid cycle and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro and advance test clock", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should attach pro and advance test clock", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(new Date(), 2).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(new Date(), 2).getTime(),
+ });
+ });
- it("should upgrade to pro annual and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: proAnnual.id,
- });
+ it("should upgrade to pro annual and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addYears(new Date(), 1).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addYears(new Date(), 1).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: proAnnual.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: proAnnual.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: proAnnual.id,
+ });
- const subItem = sub!.items.data[0];
- expect(subItem.current_period_end * 1000).to.approximately(
- checkoutRes.next_cycle?.starts_at!,
- toMilliseconds.days(1) // +- 1 day
- );
- });
+ const subItem = sub!.items.data[0];
+ expect(subItem.current_period_end * 1000).to.approximately(
+ checkoutRes.next_cycle?.starts_at!,
+ toMilliseconds.days(1), // +- 1 day
+ );
+ });
});
diff --git a/server/tests/interval/upgrade/interval2.test.ts b/server/tests/interval/upgrade/interval2.test.ts
index 9ae2cbc74..956d388e0 100644
--- a/server/tests/interval/upgrade/interval2.test.ts
+++ b/server/tests/interval/upgrade/interval2.test.ts
@@ -18,111 +18,111 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let proAnnual = constructProduct({
- id: "proAnnual",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- isAnnual: true,
+ id: "proAnnual",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ isAnnual: true,
});
const testCase = "interval2";
describe(`${chalk.yellowBright("interval2: Should upgrade from pro to pro annual after 1.5 cycles and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, proAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, proAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, proAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, proAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro and advance test clock", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should attach pro and advance test clock", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(addMonths(new Date(), 1), 2).getTime(),
- });
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(addMonths(new Date(), 1), 2).getTime(),
+ });
+ });
- it("should upgrade to pro annual and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: proAnnual.id,
- });
+ it("should upgrade to pro annual and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addYears(new Date(), 1).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addYears(new Date(), 1).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: proAnnual.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: proAnnual.id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: proAnnual.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: proAnnual.id,
+ });
- const subItem = sub!.items.data[0];
- expect(subItem.current_period_end * 1000).to.approximately(
- checkoutRes.next_cycle?.starts_at!,
- toMilliseconds.days(1) // +- 1 day
- );
- });
+ const subItem = sub!.items.data[0];
+ expect(subItem.current_period_end * 1000).to.approximately(
+ checkoutRes.next_cycle?.starts_at!,
+ toMilliseconds.days(1), // +- 1 day
+ );
+ });
});
diff --git a/server/tests/interval/upgrade/interval3.test.ts b/server/tests/interval/upgrade/interval3.test.ts
index b65006108..9b2917205 100644
--- a/server/tests/interval/upgrade/interval3.test.ts
+++ b/server/tests/interval/upgrade/interval3.test.ts
@@ -18,112 +18,112 @@ import { toMilliseconds } from "@/utils/timeUtils.js";
import { getCusSub } from "@/utils/scriptUtils/testUtils/cusTestUtils.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
let premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const testCase = "interval3";
describe(`${chalk.yellowBright("interval3: Should upgrade from pro trial to premium trial and have correct next cycle at")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro and advance test clock", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: pro,
- stripeCli,
- db,
- org,
- env,
- });
+ it("should attach pro and advance test clock", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: pro,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 3).getTime(),
- });
- });
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 3).getTime(),
+ });
+ });
- it("should upgrade to premium and have correct next cycle at", async function () {
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: premium.id,
- });
+ it("should upgrade to premium and have correct next cycle at", async function () {
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- expect(checkoutRes.next_cycle).to.exist;
- expect(checkoutRes.next_cycle?.starts_at).to.approximately(
- addDays(curUnix, 7).getTime(),
- toMilliseconds.days(1) // +- 1 day
- );
+ expect(checkoutRes.next_cycle).to.exist;
+ expect(checkoutRes.next_cycle?.starts_at).to.approximately(
+ addDays(curUnix, 7).getTime(),
+ toMilliseconds.days(1), // +- 1 day
+ );
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- const sub = await getCusSub({
- db,
- org,
- customerId,
- productId: premium.id,
- });
+ const sub = await getCusSub({
+ db,
+ org,
+ customerId,
+ productId: premium.id,
+ });
- const subItem = sub!.items.data[0];
- expect(subItem.current_period_end * 1000).to.approximately(
- checkoutRes.next_cycle?.starts_at!,
- toMilliseconds.days(1) // +- 1 day
- );
- });
+ const subItem = sub!.items.data[0];
+ expect(subItem.current_period_end * 1000).to.approximately(
+ checkoutRes.next_cycle?.starts_at!,
+ toMilliseconds.days(1), // +- 1 day
+ );
+ });
});
diff --git a/server/tests/merged/add/mergedAdd1.test.ts b/server/tests/merged/add/mergedAdd1.test.ts
index ad4d86be2..bae12984a 100644
--- a/server/tests/merged/add/mergedAdd1.test.ts
+++ b/server/tests/merged/add/mergedAdd1.test.ts
@@ -10,8 +10,8 @@ import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils.test.js";
@@ -23,148 +23,148 @@ import { timeout } from "@/utils/genUtils.js";
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, () => {
- let customerId = "mergedAdd1";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "mergedAdd1";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro product", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "1",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "1",
+ });
- const expectedTotal = await getAttachPreviewTotal({
- customerId,
- productId: pro.id,
- entityId: "2",
- });
+ const expectedTotal = await getAttachPreviewTotal({
+ customerId,
+ productId: pro.id,
+ entityId: "2",
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices;
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices;
- expect(invoice[0].total).to.equal(expectedTotal);
+ expect(invoice[0].total).to.equal(expectedTotal);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should track usage and have correct invoice end of month", async function () {
- const value1 = 110000;
- const value2 = 310000;
- const values = [value1, value2];
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: value1,
- entity_id: "1",
- });
+ it("should track usage and have correct invoice end of month", async function () {
+ const value1 = 110000;
+ const value2 = 310000;
+ const values = [value1, value2];
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: value1,
+ entity_id: "1",
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: value2,
- entity_id: "2",
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: value2,
+ entity_id: "2",
+ });
- await timeout(3000);
+ await timeout(3000);
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- let total = 0;
- for (let i = 0; i < entities.length; i++) {
- const expectedTotal = await getExpectedInvoiceTotal({
- customerId,
- productId: pro.id,
- usage: [{ featureId: TestFeature.Words, value: values[i] }],
- onlyIncludeUsage: true,
- stripeCli,
- db,
- org,
- env,
- });
- total += expectedTotal;
- }
+ let total = 0;
+ for (let i = 0; i < entities.length; i++) {
+ const expectedTotal = await getExpectedInvoiceTotal({
+ customerId,
+ productId: pro.id,
+ usage: [{ featureId: TestFeature.Words, value: values[i] }],
+ onlyIncludeUsage: true,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ total += expectedTotal;
+ }
- const basePrice = getBasePrice({ product: pro });
+ const basePrice = getBasePrice({ product: pro });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices;
- expect(invoice[0].total).to.equal(basePrice * 2 + total);
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices;
+ expect(invoice[0].total).to.equal(basePrice * 2 + total);
+ });
});
diff --git a/server/tests/merged/add/mergedAdd2.test.ts b/server/tests/merged/add/mergedAdd2.test.ts
index b3f21d47f..e2c79d5fe 100644
--- a/server/tests/merged/add/mergedAdd2.test.ts
+++ b/server/tests/merged/add/mergedAdd2.test.ts
@@ -13,160 +13,160 @@ import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUti
// UNCOMMENT FROM HERE
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const testCase = "mergedAdd2";
describe(`${chalk.yellowBright(`${testCase}: Testing merged subs, downgrade`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium, pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium, pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium, pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium, pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro product", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "2",
- });
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
- // await autumn.attach({
- // customer_id: customerId,
- // product_id: pro.id,
- // entity_id: "2",
- // });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "2",
+ });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
+ // await autumn.attach({
+ // customer_id: customerId,
+ // product_id: pro.id,
+ // entity_id: "2",
+ // });
- // const customer = await autumn.customers.get(customerId);
- // const invoice = customer.invoices;
+ // const customer = await autumn.customers.get(customerId);
+ // const invoice = customer.invoices;
- // await expectSubToBeCorrect({
- // db,
- // customerId,
- // org,
- // env,
- // });
- });
+ // await expectSubToBeCorrect({
+ // db,
+ // customerId,
+ // org,
+ // env,
+ // });
+ });
- return;
+ return;
- it("should track usage and have correct invoice end of month", async function () {
- const value1 = 110000;
- const value2 = 310000;
- const values = [value1, value2];
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: value1,
- entity_id: "1",
- });
+ it("should track usage and have correct invoice end of month", async function () {
+ const value1 = 110000;
+ const value2 = 310000;
+ const values = [value1, value2];
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: value1,
+ entity_id: "1",
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: value2,
- entity_id: "2",
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: value2,
+ entity_id: "2",
+ });
- // await timeout(3000);
+ // await timeout(3000);
- // await advanceToNextInvoice({
- // stripeCli,
- // testClockId,
- // });
+ // await advanceToNextInvoice({
+ // stripeCli,
+ // testClockId,
+ // });
- // let total = 0;
- // for (let i = 0; i < entities.length; i++) {
- // const expectedTotal = await getExpectedInvoiceTotal({
- // customerId,
- // productId: pro.id,
- // usage: [{ featureId: TestFeature.Words, value: values[i] }],
- // onlyIncludeUsage: true,
- // stripeCli,
- // db,
- // org,
- // env,
- // });
- // total += expectedTotal;
- // }
+ // let total = 0;
+ // for (let i = 0; i < entities.length; i++) {
+ // const expectedTotal = await getExpectedInvoiceTotal({
+ // customerId,
+ // productId: pro.id,
+ // usage: [{ featureId: TestFeature.Words, value: values[i] }],
+ // onlyIncludeUsage: true,
+ // stripeCli,
+ // db,
+ // org,
+ // env,
+ // });
+ // total += expectedTotal;
+ // }
- // const basePrice = getBasePrice({ product: pro });
+ // const basePrice = getBasePrice({ product: pro });
- // const customer = await autumn.customers.get(customerId);
- // const invoice = customer.invoices;
- // expect(invoice[0].total).to.equal(basePrice * 2 + total);
- });
+ // const customer = await autumn.customers.get(customerId);
+ // const invoice = customer.invoices;
+ // expect(invoice[0].total).to.equal(basePrice * 2 + total);
+ });
});
// const expectedTotal = await getAttachPreviewTotal({
diff --git a/server/tests/merged/add/mergedAdd3.test.ts b/server/tests/merged/add/mergedAdd3.test.ts
index 8626ab2aa..f5e9de185 100644
--- a/server/tests/merged/add/mergedAdd3.test.ts
+++ b/server/tests/merged/add/mergedAdd3.test.ts
@@ -7,15 +7,15 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- entities,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ entities,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -24,132 +24,132 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
// UNCOMMENT FROM HERE
let premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "3",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "3",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedAdd3";
describe(`${chalk.yellowBright(`${testCase}: Testing scheduled, and merged add to subscription`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium, pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium, pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium, pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium, pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "Entity 3",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "Entity 3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn1.test.ts b/server/tests/merged/addOn/mergedAddOn1.test.ts
index 041c68d28..6c7e1c4de 100644
--- a/server/tests/merged/addOn/mergedAddOn1.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn1.test.ts
@@ -3,22 +3,22 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -31,197 +31,197 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- constructArrearItem({
- featureId: TestFeature.Words,
- billingUnits: 100,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ constructArrearItem({
+ featureId: TestFeature.Words,
+ billingUnits: 100,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [pro],
- },
+ {
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [pro],
+ },
- // Update quantity
- {
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 5,
- },
- ],
- otherProducts: [pro],
- },
+ // Update quantity
+ {
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 5,
+ },
+ ],
+ otherProducts: [pro],
+ },
];
const testCase = "mergedAddOn1";
describe(`${chalk.yellowBright("mergedAddOn1: Adding an add on")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- it("should cancel add on product and have correct sub items", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: addOn.id,
- cancel_immediately: false,
- });
+ it("should cancel add on product and have correct sub items", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: addOn.id,
+ cancel_immediately: false,
+ });
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: addOn,
- status: CusProductStatus.Active,
- });
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Active,
- });
- });
+ expectProductAttached({
+ customer,
+ product: addOn,
+ status: CusProductStatus.Active,
+ });
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Active,
+ });
+ });
- // return;
- it("should advance to next invoice and have no add on product", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ // return;
+ it("should advance to next invoice and have no add on product", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Active,
- });
- const products = customer.products.filter((p) => p.group === addOn.group);
- expect(products.length).to.equal(1);
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Active,
+ });
+ const products = customer.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(1);
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn2.test.ts b/server/tests/merged/addOn/mergedAddOn2.test.ts
index e0310bce5..70ca3fab5 100644
--- a/server/tests/merged/addOn/mergedAddOn2.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn2.test.ts
@@ -3,22 +3,22 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
- constructPrepaidItem,
+ constructArrearItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -29,171 +29,171 @@ import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [pro],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [pro],
+ },
- {
- entityId: "2",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 5,
- },
- ],
- otherProducts: [pro],
- },
- {
- entityId: "2",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 2,
- },
- ],
- otherProducts: [pro],
- },
+ {
+ entityId: "2",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 5,
+ },
+ ],
+ otherProducts: [pro],
+ },
+ {
+ entityId: "2",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 2,
+ },
+ ],
+ otherProducts: [pro],
+ },
];
const testCase = "mergedAddOn2";
describe(`${chalk.yellowBright("mergedAddOn2: testing add ons between multiple entities")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn3.test.ts b/server/tests/merged/addOn/mergedAddOn3.test.ts
index 8b6784dfb..f0cb484ed 100644
--- a/server/tests/merged/addOn/mergedAddOn3.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn3.test.ts
@@ -3,23 +3,23 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -30,182 +30,182 @@ import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- // {
- // entityId: "2",
- // product: pro,
- // results: [{ product: pro, status: CusProductStatus.Active }],
- // },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [pro],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ // {
+ // entityId: "2",
+ // product: pro,
+ // results: [{ product: pro, status: CusProductStatus.Active }],
+ // },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [pro],
+ },
- // {
- // entityId: "2",
- // product: addOn,
- // results: [
- // { product: pro, status: CusProductStatus.Active },
- // { product: addOn, status: CusProductStatus.Active },
- // ],
- // options: [
- // {
- // feature_id: TestFeature.Credits,
- // quantity: billingUnits * 5,
- // },
- // ],
- // otherProducts: [pro],
- // },
+ // {
+ // entityId: "2",
+ // product: addOn,
+ // results: [
+ // { product: pro, status: CusProductStatus.Active },
+ // { product: addOn, status: CusProductStatus.Active },
+ // ],
+ // options: [
+ // {
+ // feature_id: TestFeature.Credits,
+ // quantity: billingUnits * 5,
+ // },
+ // ],
+ // otherProducts: [pro],
+ // },
];
const testCase = "mergedAddOn3";
describe(`${chalk.yellowBright("mergedAddOn3: testing add ons between multiple entities")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- it("should cancel add on product immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: addOn.id,
- entity_id: "1",
- cancel_immediately: true,
- });
+ it("should cancel add on product immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: addOn.id,
+ entity_id: "1",
+ cancel_immediately: true,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Active,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Active,
+ });
- const products = customer.products.filter((p) => p.group === addOn.group);
- expect(products.length).to.equal(1);
+ const products = customer.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(1);
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn4.test.ts b/server/tests/merged/addOn/mergedAddOn4.test.ts
index 51d0bd956..998c83754 100644
--- a/server/tests/merged/addOn/mergedAddOn4.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn4.test.ts
@@ -3,23 +3,23 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -30,183 +30,183 @@ import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [premium],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [premium],
+ },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- otherProducts: [premium],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ otherProducts: [premium],
+ },
];
const testCase = "mergedAddOn4";
describe(`${chalk.yellowBright("mergedAddOn4: testing cancelling add on immediately while there's scheduled product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- it("should cancel add on product immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: addOn.id,
- entity_id: "1",
- cancel_immediately: true,
- });
+ it("should cancel add on product immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: addOn.id,
+ entity_id: "1",
+ cancel_immediately: true,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Scheduled,
- });
- expectProductAttached({
- customer,
- product: premium,
- status: CusProductStatus.Active,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Scheduled,
+ });
+ expectProductAttached({
+ customer,
+ product: premium,
+ status: CusProductStatus.Active,
+ });
- const products = customer.products.filter((p) => p.group === addOn.group);
- expect(products.length).to.equal(2);
+ const products = customer.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(2);
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn5.test.ts b/server/tests/merged/addOn/mergedAddOn5.test.ts
index a1fca5874..6633347cd 100644
--- a/server/tests/merged/addOn/mergedAddOn5.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn5.test.ts
@@ -3,23 +3,23 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -31,210 +31,210 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [premium],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [premium],
+ },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- otherProducts: [premium],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ otherProducts: [premium],
+ },
];
const testCase = "mergedAddOn5";
describe(`${chalk.yellowBright("mergedAddOn5: testing cancelling add on immediately while there's scheduled product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- it("should cancel add on product immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: addOn.id,
- entity_id: "1",
- });
+ it("should cancel add on product immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: addOn.id,
+ entity_id: "1",
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Scheduled,
- });
- expectProductAttached({
- customer,
- product: premium,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Scheduled,
+ });
+ expectProductAttached({
+ customer,
+ product: premium,
+ });
- expectProductAttached({
- customer,
- product: addOn,
- });
+ expectProductAttached({
+ customer,
+ product: addOn,
+ });
- const products = customer.products.filter((p) => p.group === addOn.group);
- expect(products.length).to.equal(3);
+ const products = customer.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(3);
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
+ });
- it("should advance test clock and have correct products / sub", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ it("should advance test clock and have correct products / sub", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Active,
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Active,
+ });
- const products = customer.products.filter((p) => p.group === addOn.group);
- expect(products.length).to.equal(1);
+ const products = customer.products.filter((p) => p.group === addOn.group);
+ expect(products.length).to.equal(1);
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/addOn/mergedAddOn6.test.ts b/server/tests/merged/addOn/mergedAddOn6.test.ts
index 093a7528a..5a56e84d2 100644
--- a/server/tests/merged/addOn/mergedAddOn6.test.ts
+++ b/server/tests/merged/addOn/mergedAddOn6.test.ts
@@ -3,23 +3,23 @@ import { setupBefore } from "tests/before.js";
import { Stripe } from "stripe";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -31,223 +31,223 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
let premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Credits })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Credits })],
+ type: "pro",
});
const billingUnits = 100;
const addOn = constructRawProduct({
- id: "addOn",
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits,
- price: 10,
- }),
- ],
- isAddOn: true,
+ id: "addOn",
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits,
+ price: 10,
+ }),
+ ],
+ isAddOn: true,
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [premium],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [premium],
+ },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: addOn,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 5,
- },
- ],
- otherProducts: [premium],
- },
- {
- entityId: "3",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "3",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- otherProducts: [pro],
- },
- {
- entityId: "1",
- product: addOn,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: addOn, status: CusProductStatus.Active },
- ],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 5,
- },
- ],
- otherProducts: [pro],
- },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: addOn,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 5,
+ },
+ ],
+ otherProducts: [premium],
+ },
+ {
+ entityId: "3",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "3",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ otherProducts: [pro],
+ },
+ {
+ entityId: "1",
+ product: addOn,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: addOn, status: CusProductStatus.Active },
+ ],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 5,
+ },
+ ],
+ otherProducts: [pro],
+ },
];
const testCase = "mergedAddOn6";
describe(`${chalk.yellowBright("mergedAddOn6: testing update add on quantities on many entities")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, addOn, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, addOn, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, addOn, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, addOn, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "Entity 3",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "Entity 3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- options: op.options,
- otherProducts: op.otherProducts,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ options: op.options,
+ otherProducts: op.otherProducts,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- // const entity = await autumn.entities.get(customerId, op.entityId);
- const cus = await autumn.customers.get(customerId);
- expectProductAttached({
- customer: cus,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
- return;
+ for (const result of op.results) {
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ const cus = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer: cus,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
+ return;
- it("should update prepaid quantity for entity 1 and 2", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: addOn,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: "1",
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- });
- });
+ it("should update prepaid quantity for entity 1 and 2", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: addOn,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: "1",
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ });
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade1.ts b/server/tests/merged/downgrade/mergedDowngrade1.ts
index 3e0fd79b5..aa246b8a6 100644
--- a/server/tests/merged/downgrade/mergedDowngrade1.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade1.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -26,181 +26,181 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
// UNCOMMENT FROM HERE
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const init = [
- { entityId: "1", product: premium }, // upgrade to premium
- { entityId: "2", product: premium }, // upgrade to premium
+ { entityId: "1", product: premium }, // upgrade to premium
+ { entityId: "2", product: premium }, // upgrade to premium
];
const ops1 = [
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
];
// Renew
const ops2 = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
describe(`${chalk.yellowBright("mergedDowngrade1: Testing merged subs, downgrade 2 pros")}`, () => {
- let customerId = "mergedDowngrade1";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "mergedDowngrade1";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro product to both entities", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro product to both entities", async function () {
+ await autumn.entities.create(customerId, entities);
- for (const op of init) {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
- }
- });
+ for (const op of init) {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
+ }
+ });
- it("should downgrade both entities to pro and have correct sub + schedule", async function () {
- for (const op of ops1) {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: op.entityId,
- });
+ it("should downgrade both entities to pro and have correct sub + schedule", async function () {
+ for (const op of ops1) {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ }
+ });
- it("should renew both entities and have correct sub + schedule", async function () {
- for (const op of ops2) {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
+ it("should renew both entities and have correct sub + schedule", async function () {
+ for (const op of ops2) {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ }
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade2.ts b/server/tests/merged/downgrade/mergedDowngrade2.ts
index 8ae090351..51705b595 100644
--- a/server/tests/merged/downgrade/mergedDowngrade2.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade2.ts
@@ -7,14 +7,14 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -31,198 +31,198 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// Free, Pro
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- shouldBeCanceled: true,
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ shouldBeCanceled: true,
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
];
const testCase = "mergedDowngrade2";
describe(`${chalk.yellowBright("mergedDowngrade2: Testing merged subs, downgrade free 1, add premium 2")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, free],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, free],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled: op.shouldBeCanceled,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
- // return;
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled: op.shouldBeCanceled,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
+ // return;
- it("should advance test clock and have correct products for entity 1 & 2", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ it("should advance test clock and have correct products for entity 1 & 2", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const results = [
- { entityId: "1", product: free, status: CusProductStatus.Active },
- { entityId: "2", product: pro, status: CusProductStatus.Active },
- ];
+ const results = [
+ { entityId: "1", product: free, status: CusProductStatus.Active },
+ { entityId: "2", product: pro, status: CusProductStatus.Active },
+ ];
- for (const result of results) {
- const entity = await autumn.entities.get(customerId, result.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
+ for (const result of results) {
+ const entity = await autumn.entities.get(customerId, result.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
- const products = entity.products.filter(
- (p: any) => p.group == result.product.group
- );
- expect(products.length).to.equal(1);
- }
+ const products = entity.products.filter(
+ (p: any) => p.group == result.product.group,
+ );
+ expect(products.length).to.equal(1);
+ }
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
- it("should attach premium to entity 1 (which is free) and have correct products", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "1",
- });
- });
+ it("should attach premium to entity 1 (which is free) and have correct products", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "1",
+ });
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade3.ts b/server/tests/merged/downgrade/mergedDowngrade3.ts
index 6de123e58..6f09dc040 100644
--- a/server/tests/merged/downgrade/mergedDowngrade3.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade3.ts
@@ -7,14 +7,14 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -27,146 +27,146 @@ import { expect } from "chai";
// Free, Premium
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedDowngrade3";
describe(`${chalk.yellowBright("mergedDowngrade3: Testing merged subs, pro 1, pro 2, downgrade free pro 1, upgrade pro 2 ")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, free],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, free],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, free],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, free],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade4.ts b/server/tests/merged/downgrade/mergedDowngrade4.ts
index 1630f6e22..4d56be7ca 100644
--- a/server/tests/merged/downgrade/mergedDowngrade4.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade4.ts
@@ -7,14 +7,14 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -30,172 +30,172 @@ import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtil
// PremiumAnnual, Pro
let premiumAnnual = constructProduct({
- id: "premiumAnnual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- isAnnual: true,
+ id: "premiumAnnual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ isAnnual: true,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: premiumAnnual,
- results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
+ {
+ entityId: "1",
+ product: premiumAnnual,
+ results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
];
const testCase = "mergedDowngrade4";
describe(`${chalk.yellowBright("mergedDowngrade4: Testing advance clock, schedule activates")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, premiumAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, premiumAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, premiumAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, premiumAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
- it("should advance test clock and have correct premium downgraded for entity 2", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ it("should advance test clock and have correct premium downgraded for entity 2", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- // 1. Check that only
- const results = [
- {
- entityId: "1",
- product: premiumAnnual,
- status: CusProductStatus.Active,
- },
- { entityId: "2", product: pro, status: CusProductStatus.Active },
- ];
+ // 1. Check that only
+ const results = [
+ {
+ entityId: "1",
+ product: premiumAnnual,
+ status: CusProductStatus.Active,
+ },
+ { entityId: "2", product: pro, status: CusProductStatus.Active },
+ ];
- for (const result of results) {
- const entity = await autumn.entities.get(customerId, result.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
+ for (const result of results) {
+ const entity = await autumn.entities.get(customerId, result.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
- const products = entity.products.filter(
- (p: any) => p.group == result.product.group
- );
- expect(products.length).to.equal(1);
- }
- });
+ const products = entity.products.filter(
+ (p: any) => p.group == result.product.group,
+ );
+ expect(products.length).to.equal(1);
+ }
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade5.test.ts b/server/tests/merged/downgrade/mergedDowngrade5.test.ts
index bf58de750..6ff73aea2 100644
--- a/server/tests/merged/downgrade/mergedDowngrade5.test.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade5.test.ts
@@ -7,14 +7,14 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -30,184 +30,184 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// Pro, Free
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
const premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: free,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- shouldBeCanceled: true,
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- // {
- // entityId: "2",
- // product: free,
- // results: [{ product: free, status: CusProductStatus.Active }],
- // },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: free,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ shouldBeCanceled: true,
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ // {
+ // entityId: "2",
+ // product: free,
+ // results: [{ product: free, status: CusProductStatus.Active }],
+ // },
];
const testCase = "mergedDowngrade5";
describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, free, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, free, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, free, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, free, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- shouldBeCanceled: op.shouldBeCanceled,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ shouldBeCanceled: op.shouldBeCanceled,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- // it("should advance test clock and have correct premium downgraded for entity 2", async function () {
- // await advanceToNextInvoice({
- // stripeCli,
- // testClockId,
- // });
+ // it("should advance test clock and have correct premium downgraded for entity 2", async function () {
+ // await advanceToNextInvoice({
+ // stripeCli,
+ // testClockId,
+ // });
- // // 1. Check that only
- // const results = [
- // {
- // entityId: "1",
- // product: premiumAnnual,
- // status: CusProductStatus.Active,
- // },
- // { entityId: "2", product: premium, status: CusProductStatus.Active },
- // ];
+ // // 1. Check that only
+ // const results = [
+ // {
+ // entityId: "1",
+ // product: premiumAnnual,
+ // status: CusProductStatus.Active,
+ // },
+ // { entityId: "2", product: premium, status: CusProductStatus.Active },
+ // ];
- // for (const result of results) {
- // const entity = await autumn.entities.get(customerId, result.entityId);
- // expectProductAttached({
- // customer: entity,
- // product: result.product,
- // status: result.status,
- // });
+ // for (const result of results) {
+ // const entity = await autumn.entities.get(customerId, result.entityId);
+ // expectProductAttached({
+ // customer: entity,
+ // product: result.product,
+ // status: result.status,
+ // });
- // const products = entity.products.filter(
- // (p: any) => p.group == result.product.group
- // );
- // expect(products.length).to.equal(1);
- // }
- // });
+ // const products = entity.products.filter(
+ // (p: any) => p.group == result.product.group
+ // );
+ // expect(products.length).to.equal(1);
+ // }
+ // });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade6.test.ts b/server/tests/merged/downgrade/mergedDowngrade6.test.ts
index 94cc72dd2..666c60fa3 100644
--- a/server/tests/merged/downgrade/mergedDowngrade6.test.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade6.test.ts
@@ -7,14 +7,14 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import {
- constructArrearItem,
- constructFeatureItem,
+ constructArrearItem,
+ constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
@@ -29,159 +29,159 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// Free
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
const premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const growth = constructProduct({
- id: "growth",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "growth",
+ id: "growth",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "growth",
});
const ops = [
- {
- entityId: "1",
- product: growth,
- results: [{ product: growth, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: growth,
- results: [{ product: growth, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: growth, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: growth, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: premium,
- results: [
- { product: growth, status: CusProductStatus.Active },
- { product: premium, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: growth, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- },
+ {
+ entityId: "1",
+ product: growth,
+ results: [{ product: growth, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: growth,
+ results: [{ product: growth, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: growth, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: growth, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: premium,
+ results: [
+ { product: growth, status: CusProductStatus.Active },
+ { product: premium, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: growth, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ },
];
const testCase = "mergedDowngrade6";
describe(`${chalk.yellowBright("mergedDowngrade6: Testing downgrade changes")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, free, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, free, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, free, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, free, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade8.ts b/server/tests/merged/downgrade/mergedDowngrade8.ts
index e51469ed0..e499150e3 100644
--- a/server/tests/merged/downgrade/mergedDowngrade8.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade8.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -21,22 +21,22 @@ import { expect } from "chai";
// UNCOMMENT FROM HERE
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let premiumAnnual = constructProduct({
- id: "premiumAnnual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- isAnnual: true,
+ id: "premiumAnnual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ isAnnual: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
// const init = [
@@ -45,140 +45,140 @@ let pro = constructProduct({
// ];
const ops = [
- {
- entityId: "1",
- product: premiumAnnual,
- results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premiumAnnual, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: premiumAnnual,
- results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premiumAnnual,
+ results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premiumAnnual, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: premiumAnnual,
+ results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedDowngrade8";
describe(`${chalk.yellowBright("mergedDowngrade8: Testing merged subs, downgrade 2 monthly + annual")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, premiumAnnual],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro, premium, premiumAnnual],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, premiumAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, premiumAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- entity_id: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ entity_id: op.entityId,
+ });
- const entity = await autumn.entities.get(customerId, op.entityId);
- for (const result of op.results) {
- expectProductAttached({
- customer: entity,
- product: result.product,
- entityId: op.entityId,
- });
- }
- expect(
- entity.products.filter((p: any) => p.group == premium.group).length
- ).to.equal(op.results.length);
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ entityId: op.entityId,
+ });
+ }
+ expect(
+ entity.products.filter((p: any) => p.group == premium.group).length,
+ ).to.equal(op.results.length);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
});
diff --git a/server/tests/merged/downgrade/mergedDowngrade9.ts b/server/tests/merged/downgrade/mergedDowngrade9.ts
index e522dd2ac..36a5c8316 100644
--- a/server/tests/merged/downgrade/mergedDowngrade9.ts
+++ b/server/tests/merged/downgrade/mergedDowngrade9.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -23,22 +23,22 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// UNCOMMENT FROM HERE
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let premiumAnnual = constructProduct({
- id: "premiumAnnual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- isAnnual: true,
+ id: "premiumAnnual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ isAnnual: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
// const init = [
@@ -47,187 +47,187 @@ let pro = constructProduct({
// ];
const ops = [
- {
- entityId: "1",
- product: premiumAnnual,
- results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premiumAnnual, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
+ {
+ entityId: "1",
+ product: premiumAnnual,
+ results: [{ product: premiumAnnual, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premiumAnnual, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
];
const testCase = "mergedDowngrade9";
describe(`${chalk.yellowBright("mergedDowngrade9: Testing merged subs, downgrade 2 monthly + annual & advance test clock")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, premiumAnnual],
- prefix: customerId,
- });
+ addPrefixToProducts({
+ products: [pro, premium, premiumAnnual],
+ prefix: customerId,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, premiumAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, premiumAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- // await autumn.attach({
- // customer_id: customerId,
- // product_id: op.product.id,
- // entity_id: op.entityId,
- // });
- // const entity = await autumn.entities.get(customerId, op.entityId);
- // for (const result of op.results) {
- // expectProductAttached({
- // customer: entity,
- // product: result.product,
- // entityId: op.entityId,
- // });
- // }
- // expect(
- // entity.products.filter((p: any) => p.group == premium.group).length
- // ).to.equal(op.results.length);
- // await expectSubToBeCorrect({
- // db,
- // customerId,
- // org,
- // env,
- // });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ // await autumn.attach({
+ // customer_id: customerId,
+ // product_id: op.product.id,
+ // entity_id: op.entityId,
+ // });
+ // const entity = await autumn.entities.get(customerId, op.entityId);
+ // for (const result of op.results) {
+ // expectProductAttached({
+ // customer: entity,
+ // product: result.product,
+ // entityId: op.entityId,
+ // });
+ // }
+ // expect(
+ // entity.products.filter((p: any) => p.group == premium.group).length
+ // ).to.equal(op.results.length);
+ // await expectSubToBeCorrect({
+ // db,
+ // customerId,
+ // org,
+ // env,
+ // });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
- it("should advance test clock and have correct products for entity 1 & 2", async function () {
- const results = [
- {
- entityId: "1",
- products: [
- { product: premiumAnnual, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- products: [{ product: pro, status: CusProductStatus.Active }],
- },
- ];
+ it("should advance test clock and have correct products for entity 1 & 2", async function () {
+ const results = [
+ {
+ entityId: "1",
+ products: [
+ { product: premiumAnnual, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ products: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ ];
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- for (const result of results) {
- const entity = await autumn.entities.get(customerId, result.entityId);
- for (const product of result.products) {
- expectProductAttached({
- customer: entity,
- product: product.product,
- status: product.status,
- });
- }
- const products = entity.products.filter(
- (p: any) => p.group == premium.group
- );
- expect(products.length).to.equal(result.products.length);
- }
- });
+ for (const result of results) {
+ const entity = await autumn.entities.get(customerId, result.entityId);
+ for (const product of result.products) {
+ expectProductAttached({
+ customer: entity,
+ product: product.product,
+ status: product.status,
+ });
+ }
+ const products = entity.products.filter(
+ (p: any) => p.group == premium.group,
+ );
+ expect(products.length).to.equal(result.products.length);
+ }
+ });
- it("should attach premium to entity 2 and have correct products", async function () {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "2",
- });
- });
+ it("should attach premium to entity 2 and have correct products", async function () {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "2",
+ });
+ });
});
diff --git a/server/tests/merged/group/mergedGroup1.test.ts b/server/tests/merged/group/mergedGroup1.test.ts
index ec6eb2bdf..79c70961e 100644
--- a/server/tests/merged/group/mergedGroup1.test.ts
+++ b/server/tests/merged/group/mergedGroup1.test.ts
@@ -7,16 +7,16 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils.test.js";
@@ -30,143 +30,143 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
// UNCOMMENT FROM HERE
let g1Pro = constructProduct({
- id: "mergedGroups1_g1Pro",
- group: "mergedG1_1",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "mergedGroups1_g1Pro",
+ group: "mergedG1_1",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let g1Premium = constructProduct({
- id: "mergedGroups1_g1Premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- group: "mergedG1_1",
- type: "premium",
+ id: "mergedGroups1_g1Premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ group: "mergedG1_1",
+ type: "premium",
});
let g2Pro = constructProduct({
- id: "mergedGroups1_g2Pro",
- group: "mergedG1_2",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "mergedGroups1_g2Pro",
+ group: "mergedG1_2",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let g2Premium = constructProduct({
- id: "mergedGroups1_g2Premium",
- group: "mergedG1_2",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "mergedGroups1_g2Premium",
+ group: "mergedG1_2",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
// Ops
const ops = [
- {
- product: g1Pro,
- results: [{ product: g1Pro, status: CusProductStatus.Active }],
- },
- {
- product: g2Pro,
- results: [
- { product: g1Pro, status: CusProductStatus.Active },
- { product: g2Pro, status: CusProductStatus.Active },
- ],
- otherProducts: [g1Pro],
- },
- {
- product: g1Premium,
- results: [
- { product: g1Premium, status: CusProductStatus.Active },
- { product: g2Pro, status: CusProductStatus.Active },
- ],
- otherProducts: [g2Pro],
- },
- {
- product: g1Pro,
- results: [
- { product: g1Premium, status: CusProductStatus.Active },
- { product: g2Pro, status: CusProductStatus.Active },
- { product: g1Pro, status: CusProductStatus.Scheduled },
- ],
- // otherProducts: [g2Pro],
- },
+ {
+ product: g1Pro,
+ results: [{ product: g1Pro, status: CusProductStatus.Active }],
+ },
+ {
+ product: g2Pro,
+ results: [
+ { product: g1Pro, status: CusProductStatus.Active },
+ { product: g2Pro, status: CusProductStatus.Active },
+ ],
+ otherProducts: [g1Pro],
+ },
+ {
+ product: g1Premium,
+ results: [
+ { product: g1Premium, status: CusProductStatus.Active },
+ { product: g2Pro, status: CusProductStatus.Active },
+ ],
+ otherProducts: [g2Pro],
+ },
+ {
+ product: g1Pro,
+ results: [
+ { product: g1Premium, status: CusProductStatus.Active },
+ { product: g2Pro, status: CusProductStatus.Active },
+ { product: g1Pro, status: CusProductStatus.Scheduled },
+ ],
+ // otherProducts: [g2Pro],
+ },
];
describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups")}`, () => {
- let customerId = "mergedGroup1";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "mergedGroup1";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await createProducts({
- autumn: autumnJs,
- products: [g1Pro, g2Pro, g1Premium, g2Premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [g1Pro, g2Pro, g1Premium, g2Premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- otherProducts: op.otherProducts,
- db,
- org,
- env,
- });
+ it("should attach pro product", async function () {
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ otherProducts: op.otherProducts,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- for (const result of op.results) {
- expectProductAttached({
- customer,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ const customer = await autumn.customers.get(customerId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- it("should cancel scheduled product (g1Pro)", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: g1Pro.id,
- cancel_immediately: true,
- });
+ it("should cancel scheduled product (g1Pro)", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: g1Pro.id,
+ cancel_immediately: true,
+ });
- await expectSubToBeCorrect({
- customerId,
- db,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ customerId,
+ db,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/group/mergedGroup2.test.ts b/server/tests/merged/group/mergedGroup2.test.ts
index 895b5c9c1..55dfb132c 100644
--- a/server/tests/merged/group/mergedGroup2.test.ts
+++ b/server/tests/merged/group/mergedGroup2.test.ts
@@ -7,16 +7,16 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { expectSubToBeCorrect } from "../mergeUtils.test.js";
@@ -30,129 +30,129 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
// UNCOMMENT FROM HERE
let g1Pro = constructProduct({
- id: "mergedGroups2_g1Pro",
- group: "mergedG2_1",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "mergedGroups2_g1Pro",
+ group: "mergedG2_1",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let g2Pro = constructProduct({
- id: "mergedGroups2_g2Pro",
- group: "mergedG2_2",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "mergedGroups2_g2Pro",
+ group: "mergedG2_2",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let g1Premium = constructProduct({
- id: "mergedGroups2_g1Premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- group: "mergedG2_1",
+ id: "mergedGroups2_g1Premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ group: "mergedG2_1",
});
let g2Premium = constructProduct({
- id: "mergedGroups2_g2Premium",
- group: "mergedG2_2",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "mergedGroups2_g2Premium",
+ group: "mergedG2_2",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
// Ops
const ops = [
- {
- product: g1Premium,
- results: [{ product: g1Premium, status: CusProductStatus.Active }],
- },
- {
- product: g2Premium,
- results: [
- { product: g1Premium, status: CusProductStatus.Active },
- { product: g2Premium, status: CusProductStatus.Active },
- ],
- otherProducts: [g1Premium],
- },
- {
- product: g1Pro,
- results: [
- { product: g1Premium, status: CusProductStatus.Active },
- { product: g2Premium, status: CusProductStatus.Active },
- { product: g1Pro, status: CusProductStatus.Scheduled },
- ],
- // otherProducts: [g2Premium],
- },
+ {
+ product: g1Premium,
+ results: [{ product: g1Premium, status: CusProductStatus.Active }],
+ },
+ {
+ product: g2Premium,
+ results: [
+ { product: g1Premium, status: CusProductStatus.Active },
+ { product: g2Premium, status: CusProductStatus.Active },
+ ],
+ otherProducts: [g1Premium],
+ },
+ {
+ product: g1Pro,
+ results: [
+ { product: g1Premium, status: CusProductStatus.Active },
+ { product: g2Premium, status: CusProductStatus.Active },
+ { product: g1Pro, status: CusProductStatus.Scheduled },
+ ],
+ // otherProducts: [g2Premium],
+ },
];
describe(`${chalk.yellowBright("mergedGroup2: Testing products from diff groups")}`, () => {
- let customerId = "mergedGroup2";
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = "mergedGroup2";
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- await createProducts({
- autumn: autumnJs,
- products: [g1Pro, g2Pro, g1Premium, g2Premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [g1Pro, g2Pro, g1Premium, g2Premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach pro product", async function () {
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- otherProducts: op.otherProducts,
- db,
- org,
- env,
- });
+ it("should attach pro product", async function () {
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ otherProducts: op.otherProducts,
+ db,
+ org,
+ env,
+ });
- const customer = await autumn.customers.get(customerId);
- for (const result of op.results) {
- expectProductAttached({
- customer,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ const customer = await autumn.customers.get(customerId);
+ for (const result of op.results) {
+ expectProductAttached({
+ customer,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
- return;
- it("should cancel scheduled product (g1Pro)", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: g1Pro.id,
- cancel_immediately: true,
- });
- });
+ return;
+ it("should cancel scheduled product (g1Pro)", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: g1Pro.id,
+ cancel_immediately: true,
+ });
+ });
});
diff --git a/server/tests/merged/mergeUtils.test.ts b/server/tests/merged/mergeUtils.test.ts
index dab02adb0..e20aca2a4 100644
--- a/server/tests/merged/mergeUtils.test.ts
+++ b/server/tests/merged/mergeUtils.test.ts
@@ -2,126 +2,126 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import {
- cusProductToEnts,
- cusProductToPrices,
- cusProductToProduct,
+ cusProductToEnts,
+ cusProductToPrices,
+ cusProductToProduct,
} from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import {
- getPriceEntitlement,
- getPriceOptions,
+ getPriceEntitlement,
+ getPriceOptions,
} from "@/internal/products/prices/priceUtils.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- APIVersion,
- AppEnv,
- FullCusProduct,
- Organization,
+ APIVersion,
+ AppEnv,
+ FullCusProduct,
+ Organization,
} from "@autumn/shared";
import { expect } from "chai";
export const cusProductToSubIds = ({
- cusProducts,
+ cusProducts,
}: {
- cusProducts: FullCusProduct[];
+ cusProducts: FullCusProduct[];
}) => {
- return [...new Set(cusProducts.flatMap((cp) => cp.subscription_ids || []))];
+ return [...new Set(cusProducts.flatMap((cp) => cp.subscription_ids || []))];
};
export const cpToPrice = ({
- cp,
- type,
+ cp,
+ type,
}: {
- cp: FullCusProduct;
- type: "base" | "arrear" | "cont" | "prepaid";
+ cp: FullCusProduct;
+ type: "base" | "arrear" | "cont" | "prepaid";
}) => {
- const prices = cusProductToPrices({ cusProduct: cp });
- return prices.find((p) => isFixedPrice({ price: p }));
+ const prices = cusProductToPrices({ cusProduct: cp });
+ return prices.find((p) => isFixedPrice({ price: p }));
};
export const expectSubToBeCorrect = async ({
- db,
- customerId,
- org,
- env,
+ db,
+ customerId,
+ org,
+ env,
}: {
- db: DrizzleCli;
- customerId: string;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
}) => {
- const stripeCli = createStripeCli({ org, env });
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- // 1. Only 1 sub ID available
- const cusProducts = fullCus.customer_products;
- const subIds = cusProductToSubIds({ cusProducts });
- expect(subIds.length, "should only have 1 sub ID available").to.equal(1);
+ // 1. Only 1 sub ID available
+ const cusProducts = fullCus.customer_products;
+ const subIds = cusProductToSubIds({ cusProducts });
+ expect(subIds.length, "should only have 1 sub ID available").to.equal(1);
- // Get the items that should be in the sub
- const supposedSubItems = [];
+ // Get the items that should be in the sub
+ const supposedSubItems = [];
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const ents = cusProductToEnts({ cusProduct });
- const product = cusProductToProduct({ cusProduct });
- for (const price of prices) {
- const relatedEnt = getPriceEntitlement(price, ents);
- const options = getPriceOptions(price, cusProduct.options);
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const ents = cusProductToEnts({ cusProduct });
+ const product = cusProductToProduct({ cusProduct });
+ for (const price of prices) {
+ const relatedEnt = getPriceEntitlement(price, ents);
+ const options = getPriceOptions(price, cusProduct.options);
- const res = priceToStripeItem({
- price,
- relatedEnt,
- product,
- org,
- options,
- existingUsage: 0,
- withEntity: true,
- isCheckout: false,
- apiVersion: APIVersion.v1_4,
- });
+ const res = priceToStripeItem({
+ price,
+ relatedEnt,
+ product,
+ org,
+ options,
+ existingUsage: 0,
+ withEntity: true,
+ isCheckout: false,
+ apiVersion: APIVersion.v1_4,
+ });
- const lineItem: any = res?.lineItem;
- if (lineItem && res?.lineItem) {
- const existingIndex = supposedSubItems.findIndex(
- (si: any) => si.price === lineItem.price
- );
- if (existingIndex !== -1) {
- // @ts-ignore
- supposedSubItems[existingIndex].quantity += lineItem.quantity!;
- } else {
- supposedSubItems.push(res.lineItem);
- }
- }
- }
- }
+ const lineItem: any = res?.lineItem;
+ if (lineItem && res?.lineItem) {
+ const existingIndex = supposedSubItems.findIndex(
+ (si: any) => si.price === lineItem.price,
+ );
+ if (existingIndex !== -1) {
+ // @ts-ignore
+ supposedSubItems[existingIndex].quantity += lineItem.quantity!;
+ } else {
+ supposedSubItems.push(res.lineItem);
+ }
+ }
+ }
+ }
- const sub = await stripeCli.subscriptions.retrieve(subIds[0]);
+ const sub = await stripeCli.subscriptions.retrieve(subIds[0]);
- const actualItems = sub.items.data.map((item: any) => ({
- price: item.price.id,
- quantity: item.quantity || 0,
- }));
+ const actualItems = sub.items.data.map((item: any) => ({
+ price: item.price.id,
+ quantity: item.quantity || 0,
+ }));
- // Check for missing items and quantity mismatches
- for (const expectedItem of supposedSubItems) {
- const actualItem = actualItems.find(
- (item: any) => item.price === (expectedItem as any).price
- );
+ // Check for missing items and quantity mismatches
+ for (const expectedItem of supposedSubItems) {
+ const actualItem = actualItems.find(
+ (item: any) => item.price === (expectedItem as any).price,
+ );
- if (!actualItem) {
- console.log("Missing item:", expectedItem);
- }
+ if (!actualItem) {
+ console.log("Missing item:", expectedItem);
+ }
- expect(actualItem).to.exist;
- expect(actualItem?.quantity).to.equal((expectedItem as any).quantity);
- }
+ expect(actualItem).to.exist;
+ expect(actualItem?.quantity).to.equal((expectedItem as any).quantity);
+ }
- // Check that number of sub items between the two are the same
- expect(actualItems.length).to.equal(supposedSubItems.length);
+ // Check that number of sub items between the two are the same
+ expect(actualItems.length).to.equal(supposedSubItems.length);
};
diff --git a/server/tests/merged/mergeUtils/expectSubCorrect.ts b/server/tests/merged/mergeUtils/expectSubCorrect.ts
index d2bcbb9fc..1fe1b0f64 100644
--- a/server/tests/merged/mergeUtils/expectSubCorrect.ts
+++ b/server/tests/merged/mergeUtils/expectSubCorrect.ts
@@ -2,24 +2,24 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- FullCustomer,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ FullCustomer,
+ Organization,
} from "@autumn/shared";
import { cusProductToSubIds } from "../mergeUtils.test.js";
import { expect } from "chai";
import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js";
import {
- cusProductToPrices,
- cusProductToEnts,
- cusProductToProduct,
+ cusProductToPrices,
+ cusProductToEnts,
+ cusProductToProduct,
} from "@autumn/shared";
import {
- formatPrice,
- getPriceEntitlement,
- getPriceOptions,
+ formatPrice,
+ getPriceEntitlement,
+ getPriceOptions,
} from "@/internal/products/prices/priceUtils.js";
import { logSubItems } from "@/utils/scriptUtils/logUtils/logSubItems.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
@@ -29,10 +29,10 @@ import Stripe from "stripe";
import { getUniqueUpcomingSchedulePairs } from "@/internal/customers/cusProducts/cusProductUtils/getUpcomingSchedules.js";
import { formatUnixToDateTime, nullish } from "@/utils/genUtils.js";
import {
- cusProductInPhase,
- logPhaseItems,
- logPhases,
- similarUnix,
+ cusProductInPhase,
+ logPhaseItems,
+ logPhases,
+ similarUnix,
} from "@/internal/customers/attach/mergeUtils/phaseUtils/phaseUtils.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
@@ -42,457 +42,457 @@ import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProd
import { notNullish } from "@shared/utils/utils.js";
const compareActualItems = async ({
- actualItems,
- expectedItems,
- type,
- fullCus,
- db,
- phaseStartsAt,
+ actualItems,
+ expectedItems,
+ type,
+ fullCus,
+ db,
+ phaseStartsAt,
}: {
- actualItems: any[];
- expectedItems: any[];
- type: "sub" | "schedule";
- fullCus: FullCustomer;
- phaseStartsAt?: number;
- db: DrizzleCli;
+ actualItems: any[];
+ expectedItems: any[];
+ type: "sub" | "schedule";
+ fullCus: FullCustomer;
+ phaseStartsAt?: number;
+ db: DrizzleCli;
}) => {
- for (const expectedItem of expectedItems) {
- const actualItem = actualItems.find(
- (item: any) => item.price === (expectedItem as any).price
- );
+ for (const expectedItem of expectedItems) {
+ const actualItem = actualItems.find(
+ (item: any) => item.price === (expectedItem as any).price,
+ );
- if (!actualItem) {
- // Search for price by stripe id
- const price = await PriceService.getByStripeId({
- db,
- stripePriceId: expectedItem.price,
- });
- console.log(`(${type}) Missing item:`, expectedItem);
- // if (price) {
- // console.log(`Autumn price:`, `${price.id} - ${formatPrice({ price })}`);
- // }
+ if (!actualItem) {
+ // Search for price by stripe id
+ const price = await PriceService.getByStripeId({
+ db,
+ stripePriceId: expectedItem.price,
+ });
+ console.log(`(${type}) Missing item:`, expectedItem);
+ // if (price) {
+ // console.log(`Autumn price:`, `${price.id} - ${formatPrice({ price })}`);
+ // }
- // Actual items
- console.log(`(${type}) Actual items (${actualItems.length}):`);
- await logPhaseItems({
- db,
- items: actualItems,
- });
+ // Actual items
+ console.log(`(${type}) Actual items (${actualItems.length}):`);
+ await logPhaseItems({
+ db,
+ items: actualItems,
+ });
- console.log(`(${type}) Expected items (${expectedItems.length}):`);
- await logPhaseItems({
- db,
- items: expectedItems,
- });
- }
+ console.log(`(${type}) Expected items (${expectedItems.length}):`);
+ await logPhaseItems({
+ db,
+ items: expectedItems,
+ });
+ }
- expect(actualItem).to.exist;
+ expect(actualItem).to.exist;
- if (actualItem?.quantity !== (expectedItem as any).quantity) {
- if (phaseStartsAt) {
- console.log(`Phase starts at: ${formatUnixToDateTime(phaseStartsAt)}`);
- }
+ if (actualItem?.quantity !== (expectedItem as any).quantity) {
+ if (phaseStartsAt) {
+ console.log(`Phase starts at: ${formatUnixToDateTime(phaseStartsAt)}`);
+ }
- console.log("Actual items:");
- await logPhaseItems({
- db,
- items: actualItems,
- });
+ console.log("Actual items:");
+ await logPhaseItems({
+ db,
+ items: actualItems,
+ });
- console.log("Expected items:");
- await logPhaseItems({
- db,
- items: expectedItems,
- });
+ console.log("Expected items:");
+ await logPhaseItems({
+ db,
+ items: expectedItems,
+ });
- console.log(
- `Item quantity mismatch: ${actualItem?.quantity} !== ${expectedItem.quantity}`
- );
+ console.log(
+ `Item quantity mismatch: ${actualItem?.quantity} !== ${expectedItem.quantity}`,
+ );
- const price = await PriceService.getByStripeId({
- db,
- stripePriceId: expectedItem.price,
- });
- if (price) {
- console.log(
- `Autumn price:`,
- `${price?.product.name} - ${formatPrice({ price })}`
- );
- }
+ const price = await PriceService.getByStripeId({
+ db,
+ stripePriceId: expectedItem.price,
+ });
+ if (price) {
+ console.log(
+ `Autumn price:`,
+ `${price?.product.name} - ${formatPrice({ price })}`,
+ );
+ }
- console.log("--------------------------------");
- }
+ console.log("--------------------------------");
+ }
- expect(actualItem?.quantity).to.equal(
- (expectedItem as any).quantity,
- `actual items quantity should be equals to ${expectedItem.quantity}`
- );
- }
+ expect(actualItem?.quantity).to.equal(
+ (expectedItem as any).quantity,
+ `actual items quantity should be equals to ${expectedItem.quantity}`,
+ );
+ }
- expect(actualItems.length).to.equal(expectedItems.length);
+ expect(actualItems.length).to.equal(expectedItems.length);
};
export const expectSubToBeCorrect = async ({
- db,
- customerId,
- org,
- env,
+ db,
+ customerId,
+ org,
+ env,
- entityId,
- shouldBeCanceled,
- shouldBeTrialing = false,
- flags,
- subId,
- rewards,
+ entityId,
+ shouldBeCanceled,
+ shouldBeTrialing = false,
+ flags,
+ subId,
+ rewards,
}: {
- db: DrizzleCli;
- customerId: string;
- org: Organization;
- env: AppEnv;
+ db: DrizzleCli;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
- entityId?: string;
- shouldBeCanceled?: boolean;
- shouldBeTrialing?: boolean;
- flags?: {
- checkNotTrialing?: boolean;
- };
- subId?: string;
- rewards?: string[];
+ entityId?: string;
+ shouldBeCanceled?: boolean;
+ shouldBeTrialing?: boolean;
+ flags?: {
+ checkNotTrialing?: boolean;
+ };
+ subId?: string;
+ rewards?: string[];
}) => {
- const stripeCli = createStripeCli({ org, env });
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- withEntities: true,
- });
+ const stripeCli = createStripeCli({ org, env });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ withEntities: true,
+ });
- // 1. Only 1 sub ID available
- let cusProducts = fullCus.customer_products;
- if (!subId) {
- const subIds = cusProductToSubIds({ cusProducts });
- subId = subIds[0];
- expect(subIds.length, "should only have 1 sub ID available").to.equal(1);
- } else {
- cusProducts = cusProducts.filter((cp) =>
- cp.subscription_ids?.includes(subId!)
- );
- }
+ // 1. Only 1 sub ID available
+ let cusProducts = fullCus.customer_products;
+ if (!subId) {
+ const subIds = cusProductToSubIds({ cusProducts });
+ subId = subIds[0];
+ expect(subIds.length, "should only have 1 sub ID available").to.equal(1);
+ } else {
+ cusProducts = cusProducts.filter((cp) =>
+ cp.subscription_ids?.includes(subId!),
+ );
+ }
- // Get the items that should be in the sub
- const supposedSubItems = [];
+ // Get the items that should be in the sub
+ const supposedSubItems = [];
- const scheduleUnixes = getUniqueUpcomingSchedulePairs({
- cusProducts,
- now: Date.now(),
- });
+ const scheduleUnixes = getUniqueUpcomingSchedulePairs({
+ cusProducts,
+ now: Date.now(),
+ });
- const supposedPhases: any[] = scheduleUnixes.map((unix) => {
- return {
- start_date: unix, // milliseconds
- items: [],
- };
- });
+ const supposedPhases: any[] = scheduleUnixes.map((unix) => {
+ return {
+ start_date: unix, // milliseconds
+ items: [],
+ };
+ });
- // console.log(`\n\nChecking sub correct`);
- let printCusProduct = false;
- if (printCusProduct) {
- console.log(`\n\nChecking sub correct`);
- }
+ // console.log(`\n\nChecking sub correct`);
+ let printCusProduct = false;
+ if (printCusProduct) {
+ console.log(`\n\nChecking sub correct`);
+ }
- for (const cusProduct of cusProducts) {
- const prices = cusProductToPrices({ cusProduct });
- const ents = cusProductToEnts({ cusProduct });
- const product = cusProductToProduct({ cusProduct });
+ for (const cusProduct of cusProducts) {
+ const prices = cusProductToPrices({ cusProduct });
+ const ents = cusProductToEnts({ cusProduct });
+ const product = cusProductToProduct({ cusProduct });
- // Add to schedules
- const scheduleIndexes: number[] = [];
- const apiVersion = cusProduct.api_version || defaultApiVersion;
+ // Add to schedules
+ const scheduleIndexes: number[] = [];
+ const apiVersion = cusProduct.api_version || defaultApiVersion;
- if (isFreeProduct(product.prices)) {
- expect(cusProduct.subscription_ids, "free product should have no subs").to
- .be.empty;
- continue;
- }
+ if (isFreeProduct(product.prices)) {
+ expect(cusProduct.subscription_ids, "free product should have no subs").to
+ .be.empty;
+ continue;
+ }
- if (printCusProduct) {
- console.log(
- `Cus product: ${cusProduct.product.name}, Status: ${cusProduct.status}, Entity ID: ${cusProduct.entity_id}`
- );
- console.log(`Starts at: ${formatUnixToDateTime(cusProduct.starts_at)}`);
- }
+ if (printCusProduct) {
+ console.log(
+ `Cus product: ${cusProduct.product.name}, Status: ${cusProduct.status}, Entity ID: ${cusProduct.entity_id}`,
+ );
+ console.log(`Starts at: ${formatUnixToDateTime(cusProduct.starts_at)}`);
+ }
- scheduleUnixes.forEach((unix, index) => {
- if (
- cusProduct.status === CusProductStatus.Scheduled &&
- cusProductInPhase({ phaseStartMillis: unix, cusProduct })
- ) {
- return scheduleIndexes.push(index);
- }
+ scheduleUnixes.forEach((unix, index) => {
+ if (
+ cusProduct.status === CusProductStatus.Scheduled &&
+ cusProductInPhase({ phaseStartMillis: unix, cusProduct })
+ ) {
+ return scheduleIndexes.push(index);
+ }
- if (cusProduct.status === CusProductStatus.Scheduled) return;
+ if (cusProduct.status === CusProductStatus.Scheduled) return;
- if (cusProduct.product.is_add_on) {
- // 1. If it's canceled
- if (cusProduct.canceled && (cusProduct.ended_at || 0) > unix) {
- return scheduleIndexes.push(index);
- } else if (!cusProduct.canceled) {
- return scheduleIndexes.push(index);
- }
+ if (cusProduct.product.is_add_on) {
+ // 1. If it's canceled
+ if (cusProduct.canceled && (cusProduct.ended_at || 0) > unix) {
+ return scheduleIndexes.push(index);
+ } else if (!cusProduct.canceled) {
+ return scheduleIndexes.push(index);
+ }
- return;
- }
+ return;
+ }
- // 2. If main product, check that schedule is AFTER this phase
- const curScheduledProduct = cusProducts.find(
- (cp) =>
- cp.product.group === product.group &&
- cp.status === CusProductStatus.Scheduled &&
- (cp.internal_entity_id
- ? cp.internal_entity_id == cusProduct.internal_entity_id
- : nullish(cp.internal_entity_id))
- );
+ // 2. If main product, check that schedule is AFTER this phase
+ const curScheduledProduct = cusProducts.find(
+ (cp) =>
+ cp.product.group === product.group &&
+ cp.status === CusProductStatus.Scheduled &&
+ (cp.internal_entity_id
+ ? cp.internal_entity_id == cusProduct.internal_entity_id
+ : nullish(cp.internal_entity_id)),
+ );
- if (!curScheduledProduct) return scheduleIndexes.push(index);
+ if (!curScheduledProduct) return scheduleIndexes.push(index);
- // If scheduled product NOT in phase, add main product to schedule
- if (
- !cusProductInPhase({
- phaseStartMillis: unix,
- cusProduct: curScheduledProduct,
- })
- ) {
- scheduleIndexes.push(index);
- }
- });
+ // If scheduled product NOT in phase, add main product to schedule
+ if (
+ !cusProductInPhase({
+ phaseStartMillis: unix,
+ cusProduct: curScheduledProduct,
+ })
+ ) {
+ scheduleIndexes.push(index);
+ }
+ });
- if (printCusProduct) {
- console.log(`Schedule indexes:`, scheduleIndexes);
- console.log("--------------------------------");
- }
+ if (printCusProduct) {
+ console.log(`Schedule indexes:`, scheduleIndexes);
+ console.log("--------------------------------");
+ }
- // const hasScheduledProduct =
- cusProduct.status !== CusProductStatus.Scheduled &&
- !cusProduct.product.is_add_on &&
- cusProducts.some(
- (cp) =>
- cp.product.group === product.group &&
- ACTIVE_STATUSES.includes(cp.status)
- );
+ // const hasScheduledProduct =
+ cusProduct.status !== CusProductStatus.Scheduled &&
+ !cusProduct.product.is_add_on &&
+ cusProducts.some(
+ (cp) =>
+ cp.product.group === product.group &&
+ ACTIVE_STATUSES.includes(cp.status),
+ );
- const addToSub = cusProduct.status !== CusProductStatus.Scheduled;
+ const addToSub = cusProduct.status !== CusProductStatus.Scheduled;
- for (const price of prices) {
- const relatedEnt = getPriceEntitlement(price, ents);
- const options = getPriceOptions(price, cusProduct.options);
- let existingUsage = getExistingUsageFromCusProducts({
- entitlement: relatedEnt,
- cusProducts,
- entities: fullCus.entities,
- carryExistingUsages: true,
- internalEntityId: cusProduct.internal_entity_id || undefined,
- });
+ for (const price of prices) {
+ const relatedEnt = getPriceEntitlement(price, ents);
+ const options = getPriceOptions(price, cusProduct.options);
+ let existingUsage = getExistingUsageFromCusProducts({
+ entitlement: relatedEnt,
+ cusProducts,
+ entities: fullCus.entities,
+ carryExistingUsages: true,
+ internalEntityId: cusProduct.internal_entity_id || undefined,
+ });
- const res = priceToStripeItem({
- price,
- relatedEnt,
- product,
- org,
- options,
- existingUsage,
- withEntity: !!entityId,
- isCheckout: false,
- apiVersion,
- productOptions: cusProduct.quantity
- ? {
- product_id: product.id,
- quantity: cusProduct.quantity,
- }
- : undefined,
- });
+ const res = priceToStripeItem({
+ price,
+ relatedEnt,
+ product,
+ org,
+ options,
+ existingUsage,
+ withEntity: !!entityId,
+ isCheckout: false,
+ apiVersion,
+ productOptions: cusProduct.quantity
+ ? {
+ product_id: product.id,
+ quantity: cusProduct.quantity,
+ }
+ : undefined,
+ });
- if (res?.lineItem && nullish(res.lineItem.quantity)) {
- res.lineItem.quantity = 0;
- }
+ if (res?.lineItem && nullish(res.lineItem.quantity)) {
+ res.lineItem.quantity = 0;
+ }
- // console.log("API VERSION:", apiVersion);
- // console.log("LINE ITEM:", res?.lineItem);
- if (options?.upcoming_quantity && res?.lineItem) {
- res.lineItem.quantity = options.upcoming_quantity;
- }
+ // console.log("API VERSION:", apiVersion);
+ // console.log("LINE ITEM:", res?.lineItem);
+ if (options?.upcoming_quantity && res?.lineItem) {
+ res.lineItem.quantity = options.upcoming_quantity;
+ }
- const lineItem: any = res?.lineItem;
- if (lineItem && res?.lineItem) {
- if (addToSub) {
- const existingIndex = supposedSubItems.findIndex(
- (si: any) => si.price === lineItem.price
- );
+ const lineItem: any = res?.lineItem;
+ if (lineItem && res?.lineItem) {
+ if (addToSub) {
+ const existingIndex = supposedSubItems.findIndex(
+ (si: any) => si.price === lineItem.price,
+ );
- if (existingIndex !== -1) {
- // @ts-ignore
- supposedSubItems[existingIndex].quantity += lineItem.quantity;
- } else {
- supposedSubItems.push({
- ...res.lineItem,
- priceStr: `${product.id}-${formatPrice({ price })}`,
- });
- }
- }
+ if (existingIndex !== -1) {
+ // @ts-ignore
+ supposedSubItems[existingIndex].quantity += lineItem.quantity;
+ } else {
+ supposedSubItems.push({
+ ...res.lineItem,
+ priceStr: `${product.id}-${formatPrice({ price })}`,
+ });
+ }
+ }
- for (const scheduleIndex of scheduleIndexes) {
- const phase = supposedPhases[scheduleIndex];
- const existingIndex = phase.items.findIndex(
- (item: any) => item.price === lineItem.price
- );
+ for (const scheduleIndex of scheduleIndexes) {
+ const phase = supposedPhases[scheduleIndex];
+ const existingIndex = phase.items.findIndex(
+ (item: any) => item.price === lineItem.price,
+ );
- if (existingIndex !== -1) {
- phase.items[existingIndex].quantity += lineItem.quantity!;
- } else {
- phase.items.push({
- price: lineItem.price,
- quantity: lineItem.quantity!,
- });
- }
- }
- }
- }
- }
+ if (existingIndex !== -1) {
+ phase.items[existingIndex].quantity += lineItem.quantity!;
+ } else {
+ phase.items.push({
+ price: lineItem.price,
+ quantity: lineItem.quantity!,
+ });
+ }
+ }
+ }
+ }
+ }
- const sub = await stripeCli.subscriptions.retrieve(subId, {
- expand: ["discounts.coupon"],
- });
+ const sub = await stripeCli.subscriptions.retrieve(subId, {
+ expand: ["discounts.coupon"],
+ });
- const actualItems = sub.items.data.map((item: any) => ({
- price: item.price.id,
- quantity: item.quantity || 0,
- }));
+ const actualItems = sub.items.data.map((item: any) => ({
+ price: item.price.id,
+ quantity: item.quantity || 0,
+ }));
- const subCouponIds = sub.discounts?.map(
- (discount: any) => discount.coupon.id
- );
- if (rewards) {
- for (const reward of rewards) {
- const corresponding = subCouponIds.find(
- (subCouponId: any) => subCouponId === reward
- );
- expect(corresponding, `reward ${reward} should be in sub`).to.exist;
- }
- expect(subCouponIds.length).to.equal(rewards.length);
- }
+ const subCouponIds = sub.discounts?.map(
+ (discount: any) => discount.coupon.id,
+ );
+ if (rewards) {
+ for (const reward of rewards) {
+ const corresponding = subCouponIds.find(
+ (subCouponId: any) => subCouponId === reward,
+ );
+ expect(corresponding, `reward ${reward} should be in sub`).to.exist;
+ }
+ expect(subCouponIds.length).to.equal(rewards.length);
+ }
- await compareActualItems({
- actualItems,
- expectedItems: supposedSubItems,
- type: "sub",
- fullCus,
- db,
- });
+ await compareActualItems({
+ actualItems,
+ expectedItems: supposedSubItems,
+ type: "sub",
+ fullCus,
+ db,
+ });
- if (shouldBeTrialing) {
- expect(sub.status, "sub should be trialing").to.equal("trialing");
- }
+ if (shouldBeTrialing) {
+ expect(sub.status, "sub should be trialing").to.equal("trialing");
+ }
- if (flags?.checkNotTrialing) {
- expect(sub.status, "sub should not be trialing").to.not.equal("trialing");
- }
+ if (flags?.checkNotTrialing) {
+ expect(sub.status, "sub should not be trialing").to.not.equal("trialing");
+ }
- // Should be canceled
- const cusSubShouldBeCanceled = cusProducts.every((cp) => {
- if (cp.subscription_ids?.includes(subId!)) {
- // 1. Get scheduled product
- const { curScheduledProduct } = getExistingCusProducts({
- cusProducts,
- product: cp.product,
- internalEntityId: cp.internal_entity_id,
- });
+ // Should be canceled
+ const cusSubShouldBeCanceled = cusProducts.every((cp) => {
+ if (cp.subscription_ids?.includes(subId!)) {
+ // 1. Get scheduled product
+ const { curScheduledProduct } = getExistingCusProducts({
+ cusProducts,
+ product: cp.product,
+ internalEntityId: cp.internal_entity_id,
+ });
- if (curScheduledProduct) {
- const scheduledProduct = cusProductToProduct({
- cusProduct: curScheduledProduct,
- });
- if (!isFreeProduct(scheduledProduct.prices)) {
- return false;
- }
- }
+ if (curScheduledProduct) {
+ const scheduledProduct = cusProductToProduct({
+ cusProduct: curScheduledProduct,
+ });
+ if (!isFreeProduct(scheduledProduct.prices)) {
+ return false;
+ }
+ }
- return cp.canceled;
- }
+ return cp.canceled;
+ }
- return true;
- });
+ return true;
+ });
- // console.log("Sub should be canceled:", cusSubShouldBeCanceled);
+ // console.log("Sub should be canceled:", cusSubShouldBeCanceled);
- const finalShouldBeCanceled = notNullish(shouldBeCanceled)
- ? shouldBeCanceled!
- : cusSubShouldBeCanceled;
+ const finalShouldBeCanceled = notNullish(shouldBeCanceled)
+ ? shouldBeCanceled!
+ : cusSubShouldBeCanceled;
- // console.log("Final should be canceled:", finalShouldBeCanceled);
+ // console.log("Final should be canceled:", finalShouldBeCanceled);
- if (finalShouldBeCanceled) {
- expect(sub.schedule, "sub should NOT have a schedule").to.be.null;
- // expect(sub.cancel_at, "sub should be canceled").to.exist;
- expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
- return;
- }
+ if (finalShouldBeCanceled) {
+ expect(sub.schedule, "sub should NOT have a schedule").to.be.null;
+ // expect(sub.cancel_at, "sub should be canceled").to.exist;
+ expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
+ return;
+ }
- const schedule =
- supposedPhases.length > 0
- ? await stripeCli.subscriptionSchedules.retrieve(sub.schedule as string, {
- expand: ["phases.items.price"],
- })
- : null;
+ const schedule =
+ supposedPhases.length > 0
+ ? await stripeCli.subscriptionSchedules.retrieve(sub.schedule as string, {
+ expand: ["phases.items.price"],
+ })
+ : null;
- // console.log("--------------------------------");
- // console.log("Supposed phases:");
- // await logPhases({
- // phases: supposedPhases,
- // db,
- // });
+ // console.log("--------------------------------");
+ // console.log("Supposed phases:");
+ // await logPhases({
+ // phases: supposedPhases,
+ // db,
+ // });
- // console.log("--------------------------------");
- // console.log("Actual phases:");
+ // console.log("--------------------------------");
+ // console.log("Actual phases:");
- // await logPhases({
- // phases: (schedule?.phases as any) || [],
- // db,
- // });
+ // await logPhases({
+ // phases: (schedule?.phases as any) || [],
+ // db,
+ // });
- for (let i = 0; i < supposedPhases.length; i++) {
- const supposedPhase = supposedPhases[i];
+ for (let i = 0; i < supposedPhases.length; i++) {
+ const supposedPhase = supposedPhases[i];
- if (supposedPhase.items.length === 0) continue;
+ if (supposedPhase.items.length === 0) continue;
- const actualPhase = schedule?.phases?.[i + 1];
- expect(schedule?.phases.length).to.be.greaterThan(i + 1);
+ const actualPhase = schedule?.phases?.[i + 1];
+ expect(schedule?.phases.length).to.be.greaterThan(i + 1);
- expect(
- similarUnix({
- unix1: supposedPhase.start_date,
- unix2: actualPhase!.start_date * 1000,
- })
- ).to.be.true;
+ expect(
+ similarUnix({
+ unix1: supposedPhase.start_date,
+ unix2: actualPhase!.start_date * 1000,
+ }),
+ ).to.be.true;
- const actualItems =
- actualPhase?.items.map((item) => ({
- price: (item.price as Stripe.Price).id,
- quantity: item.quantity,
- })) || [];
+ const actualItems =
+ actualPhase?.items.map((item) => ({
+ price: (item.price as Stripe.Price).id,
+ quantity: item.quantity,
+ })) || [];
- await compareActualItems({
- actualItems,
- expectedItems: supposedPhase.items,
- type: "schedule",
- fullCus,
- db,
- phaseStartsAt: supposedPhase.start_date,
- });
- }
+ await compareActualItems({
+ actualItems,
+ expectedItems: supposedPhase.items,
+ type: "schedule",
+ fullCus,
+ db,
+ phaseStartsAt: supposedPhase.start_date,
+ });
+ }
- expect(sub.cancel_at, "sub should not be canceled").to.be.null;
- // if (shouldBeCanceled) {
- // expect(sub.cancel_at, "sub should be canceled").to.exist;
- // } else {
- // }
+ expect(sub.cancel_at, "sub should not be canceled").to.be.null;
+ // if (shouldBeCanceled) {
+ // expect(sub.cancel_at, "sub should be canceled").to.exist;
+ // } else {
+ // }
};
diff --git a/server/tests/merged/prepaid/mergedPrepaid1.ts b/server/tests/merged/prepaid/mergedPrepaid1.ts
index faf159b97..2f47404be 100644
--- a/server/tests/merged/prepaid/mergedPrepaid1.ts
+++ b/server/tests/merged/prepaid/mergedPrepaid1.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -19,157 +19,157 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
const billingUnits = 100;
const creditItem = constructPrepaidItem({
- featureId: TestFeature.Credits,
- includedUsage: 100,
- price: 10,
- billingUnits,
+ featureId: TestFeature.Credits,
+ includedUsage: 100,
+ price: 10,
+ billingUnits,
});
let premium = constructProduct({
- id: "premium",
- items: [creditItem],
- type: "premium",
+ id: "premium",
+ items: [creditItem],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [creditItem],
- type: "pro",
+ id: "pro",
+ items: [creditItem],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 4,
- },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 4,
+ },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ },
- // Update prepaid quantity (increase)
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 5,
- },
- ],
- },
- // Update prepaid quantity (decrease)
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 1,
- },
- ],
- },
+ // Update prepaid quantity (increase)
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 5,
+ },
+ ],
+ },
+ // Update prepaid quantity (decrease)
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 1,
+ },
+ ],
+ },
];
const testCase = "mergedPrepaid1";
describe(`${chalk.yellowBright("mergedPrepaid1: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- options: op.options,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ options: op.options,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
});
diff --git a/server/tests/merged/prepaid/mergedPrepaid2.ts b/server/tests/merged/prepaid/mergedPrepaid2.ts
index 796810ea0..113ca25d3 100644
--- a/server/tests/merged/prepaid/mergedPrepaid2.ts
+++ b/server/tests/merged/prepaid/mergedPrepaid2.ts
@@ -7,12 +7,12 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -22,179 +22,179 @@ import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtil
const billingUnits = 100;
const creditItem = constructPrepaidItem({
- featureId: TestFeature.Credits,
- includedUsage: 100,
- price: 10,
- billingUnits,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.None,
- },
+ featureId: TestFeature.Credits,
+ includedUsage: 100,
+ price: 10,
+ billingUnits,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.None,
+ },
});
let premium = constructProduct({
- id: "premium",
- items: [creditItem],
- type: "premium",
+ id: "premium",
+ items: [creditItem],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [creditItem],
- type: "pro",
+ id: "pro",
+ items: [creditItem],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 4,
- },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 4,
+ },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ },
- // Update prepaid quantity (increase)
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 2,
- },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 1,
- },
- ],
- },
- // // Update prepaid quantity (decrease)
- // {
- // entityId: "2",
- // product: pro,
- // results: [{ product: pro, status: CusProductStatus.Active }],
- // options: [
- // {
- // feature_id: TestFeature.Credits,
- // quantity: billingUnits * 1,
- // },
- // ],
- // },
+ // Update prepaid quantity (increase)
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 2,
+ },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 1,
+ },
+ ],
+ },
+ // // Update prepaid quantity (decrease)
+ // {
+ // entityId: "2",
+ // product: pro,
+ // results: [{ product: pro, status: CusProductStatus.Active }],
+ // options: [
+ // {
+ // feature_id: TestFeature.Credits,
+ // quantity: billingUnits * 1,
+ // },
+ // ],
+ // },
];
const testCase = "mergedPrepaid2";
describe(`${chalk.yellowBright("mergedPrepaid2: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- options: op.options,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ options: op.options,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
- it("should have correct balances after update", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
- });
+ it("should have correct balances after update", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
+ });
});
diff --git a/server/tests/merged/prepaid/mergedPrepaid3.ts b/server/tests/merged/prepaid/mergedPrepaid3.ts
index e33541b3e..5bedfb78f 100644
--- a/server/tests/merged/prepaid/mergedPrepaid3.ts
+++ b/server/tests/merged/prepaid/mergedPrepaid3.ts
@@ -8,12 +8,12 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- OnDecrease,
- OnIncrease,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ OnDecrease,
+ OnIncrease,
+ Organization,
} from "@autumn/shared";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -25,170 +25,170 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
const billingUnits = 100;
const creditItem = constructPrepaidItem({
- featureId: TestFeature.Credits,
- includedUsage: 100,
- price: 10,
- billingUnits,
- config: {
- on_increase: OnIncrease.ProrateImmediately,
- on_decrease: OnDecrease.ProrateImmediately,
- },
+ featureId: TestFeature.Credits,
+ includedUsage: 100,
+ price: 10,
+ billingUnits,
+ config: {
+ on_increase: OnIncrease.ProrateImmediately,
+ on_decrease: OnDecrease.ProrateImmediately,
+ },
});
let premium = constructProduct({
- id: "premium",
- items: [creditItem],
- type: "premium",
+ id: "premium",
+ items: [creditItem],
+ type: "premium",
});
let pro = constructProduct({
- id: "pro",
- items: [creditItem],
- type: "pro",
+ id: "pro",
+ items: [creditItem],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 4,
- },
- ],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 3,
- },
- ],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 4,
+ },
+ ],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 3,
+ },
+ ],
+ },
- // Update prepaid quantity (increase)
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: billingUnits * 2,
- },
- ],
- },
+ // Update prepaid quantity (increase)
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: billingUnits * 2,
+ },
+ ],
+ },
];
const testCase = "mergedPrepaid3";
describe(`${chalk.yellowBright("mergedPrepaid3: Testing merged subs, upgrade 1 & 2 to premium, downgrade 1 to pro")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- options: op.options,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ options: op.options,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
- it("should have correct products after update", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ it("should have correct products after update", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const entity1 = await autumn.entities.get(customerId, "1");
- expectProductAttached({
- customer: entity1,
- product: pro,
- entityId: "1",
- });
+ const entity1 = await autumn.entities.get(customerId, "1");
+ expectProductAttached({
+ customer: entity1,
+ product: pro,
+ entityId: "1",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/separate/separate1.test.ts b/server/tests/merged/separate/separate1.test.ts
index 1e269c896..3f15a213c 100644
--- a/server/tests/merged/separate/separate1.test.ts
+++ b/server/tests/merged/separate/separate1.test.ts
@@ -16,163 +16,163 @@ import { CusService } from "@/internal/customers/CusService.js";
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "premium",
});
const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
];
const ops = [
- {
- product: pro,
- entityId: "1",
- },
- {
- product: pro,
- entityId: "2",
- },
+ {
+ product: pro,
+ entityId: "1",
+ },
+ {
+ product: pro,
+ entityId: "2",
+ },
];
const testCase = "separate1";
describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions because of invoice checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let subIds: string[] = [];
- it("should attach pro product", async function () {
- await autumn.entities.create(customerId, entities);
- for (const op of ops) {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- invoice: true,
- entity_id: op.entityId,
- });
+ let subIds: string[] = [];
+ it("should attach pro product", async function () {
+ await autumn.entities.create(customerId, entities);
+ for (const op of ops) {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ invoice: true,
+ entity_id: op.entityId,
+ });
- await completeInvoiceCheckout({
- url: res.checkout_url,
- });
- }
+ await completeInvoiceCheckout({
+ url: res.checkout_url,
+ });
+ }
- const fullCus = await CusService.getFull({
- idOrInternalId: customerId,
- db,
- orgId: org.id,
- env,
- });
- const cusProducts = fullCus.customer_products;
- const entity1Prod = cusProducts.find((cp) => cp.entity_id === "1");
- const entity2Prod = cusProducts.find((cp) => cp.entity_id === "2");
+ const fullCus = await CusService.getFull({
+ idOrInternalId: customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
+ const cusProducts = fullCus.customer_products;
+ const entity1Prod = cusProducts.find((cp) => cp.entity_id === "1");
+ const entity2Prod = cusProducts.find((cp) => cp.entity_id === "2");
- const entity1SubId = entity1Prod?.subscription_ids?.[0];
- const entity2SubId = entity2Prod?.subscription_ids?.[0];
+ const entity1SubId = entity1Prod?.subscription_ids?.[0];
+ const entity2SubId = entity2Prod?.subscription_ids?.[0];
- expect(entity1SubId).to.not.equal(entity2SubId);
+ expect(entity1SubId).to.not.equal(entity2SubId);
- subIds.push(entity1SubId!);
- subIds.push(entity2SubId!);
+ subIds.push(entity1SubId!);
+ subIds.push(entity2SubId!);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: entity1SubId,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: entity1SubId,
+ });
+ });
- it("should upgrade both entities to premium", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ it("should upgrade both entities to premium", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "2",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "2",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: subIds[0],
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: subIds[0],
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: subIds[1],
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: subIds[1],
+ });
+ });
});
diff --git a/server/tests/merged/separate/separate2.test.ts b/server/tests/merged/separate/separate2.test.ts
index f19906e30..df3ce5951 100644
--- a/server/tests/merged/separate/separate2.test.ts
+++ b/server/tests/merged/separate/separate2.test.ts
@@ -7,19 +7,19 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import {
- constructProduct,
- constructRawProduct,
+ constructProduct,
+ constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import {
- constructFeatureItem,
- constructPrepaidItem,
+ constructFeatureItem,
+ constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expect } from "chai";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { completeInvoiceCheckout } from "tests/utils/stripeUtils/completeInvoiceCheckout.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
@@ -28,207 +28,207 @@ import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
export let pro = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "pro",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "pro",
});
export let premium = constructProduct({
- items: [
- constructFeatureItem({
- featureId: TestFeature.Messages,
- includedUsage: 100,
- }),
- ],
- type: "premium",
+ items: [
+ constructFeatureItem({
+ featureId: TestFeature.Messages,
+ includedUsage: 100,
+ }),
+ ],
+ type: "premium",
});
const addOnBillingUnits = 100;
export const addOn = constructRawProduct({
- id: "creditsAddOn",
- isAddOn: true,
- items: [
- constructPrepaidItem({
- featureId: TestFeature.Credits,
- billingUnits: addOnBillingUnits,
- includedUsage: 0,
- price: 10,
- }),
- ],
+ id: "creditsAddOn",
+ isAddOn: true,
+ items: [
+ constructPrepaidItem({
+ featureId: TestFeature.Credits,
+ billingUnits: addOnBillingUnits,
+ includedUsage: 0,
+ price: 10,
+ }),
+ ],
});
const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
];
const ops = [
- {
- product: pro,
- entityId: "1",
- },
- {
- product: pro,
- entityId: "2",
- },
+ {
+ product: pro,
+ entityId: "1",
+ },
+ {
+ product: pro,
+ entityId: "2",
+ },
];
const testCase = "separate2";
describe(`${chalk.yellowBright(`${testCase}: Testing separate subscriptions because of force checkout`)}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
- let testClockId: string;
- let db: DrizzleCli, org: Organization, env: AppEnv;
- let stripeCli: Stripe;
- let curUnix = new Date().getTime();
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
+ let testClockId: string;
+ let db: DrizzleCli, org: Organization, env: AppEnv;
+ let stripeCli: Stripe;
+ let curUnix = new Date().getTime();
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, addOn],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, addOn],
+ prefix: testCase,
+ });
- await createProducts({
- autumn,
- products: [pro, premium, addOn],
- customerId,
- db,
- orgId: org.id,
- env,
- });
+ await createProducts({
+ autumn,
+ products: [pro, premium, addOn],
+ customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- // attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ // attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- let subIds: string[] = [];
- it("should attach pro product", async function () {
- await autumn.entities.create(customerId, entities);
- for (const op of ops) {
- const res = await autumn.attach({
- customer_id: customerId,
- product_id: op.product.id,
- force_checkout: true,
- entity_id: op.entityId,
- });
+ let subIds: string[] = [];
+ it("should attach pro product", async function () {
+ await autumn.entities.create(customerId, entities);
+ for (const op of ops) {
+ const res = await autumn.attach({
+ customer_id: customerId,
+ product_id: op.product.id,
+ force_checkout: true,
+ entity_id: op.entityId,
+ });
- expect(res.checkout_url).to.exist;
+ expect(res.checkout_url).to.exist;
- await completeCheckoutForm(res.checkout_url);
- }
+ await completeCheckoutForm(res.checkout_url);
+ }
- const fullCus = await CusService.getFull({
- idOrInternalId: customerId,
- db,
- orgId: org.id,
- env,
- });
- const cusProducts = fullCus.customer_products;
- const entity1Prod = cusProducts.find((cp) => cp.entity_id === "1");
- const entity2Prod = cusProducts.find((cp) => cp.entity_id === "2");
+ const fullCus = await CusService.getFull({
+ idOrInternalId: customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
+ const cusProducts = fullCus.customer_products;
+ const entity1Prod = cusProducts.find((cp) => cp.entity_id === "1");
+ const entity2Prod = cusProducts.find((cp) => cp.entity_id === "2");
- const entity1SubId = entity1Prod?.subscription_ids?.[0];
- const entity2SubId = entity2Prod?.subscription_ids?.[0];
+ const entity1SubId = entity1Prod?.subscription_ids?.[0];
+ const entity2SubId = entity2Prod?.subscription_ids?.[0];
- expect(entity1SubId).to.not.equal(entity2SubId);
+ expect(entity1SubId).to.not.equal(entity2SubId);
- subIds.push(entity1SubId!);
- subIds.push(entity2SubId!);
+ subIds.push(entity1SubId!);
+ subIds.push(entity2SubId!);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: entity1SubId,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: entity1SubId,
+ });
+ });
- it("should upgrade both entities to premium", async function () {
- for (const id of ["1", "2"]) {
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: id,
- });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: subIds[0]!,
- });
+ it("should upgrade both entities to premium", async function () {
+ for (const id of ["1", "2"]) {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: id,
+ });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: subIds[0]!,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: subIds[1],
- });
- }
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: subIds[1],
+ });
+ }
+ });
- it("should attach add on to entity 2 and correct sub", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: addOn.id,
- entity_id: "2",
- options: [
- {
- feature_id: TestFeature.Credits,
- quantity: addOnBillingUnits * 2,
- },
- ],
- });
+ it("should attach add on to entity 2 and correct sub", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: addOn.id,
+ entity_id: "2",
+ options: [
+ {
+ feature_id: TestFeature.Credits,
+ quantity: addOnBillingUnits * 2,
+ },
+ ],
+ });
- const fullCus = await CusService.getFull({
- idOrInternalId: customerId,
- db,
- orgId: org.id,
- env,
- });
- const cusProducts = fullCus.customer_products;
- const addOnProd = cusProducts.find((cp) => cp.product.id === addOn.id);
+ const fullCus = await CusService.getFull({
+ idOrInternalId: customerId,
+ db,
+ orgId: org.id,
+ env,
+ });
+ const cusProducts = fullCus.customer_products;
+ const addOnProd = cusProducts.find((cp) => cp.product.id === addOn.id);
- expect(addOnProd).to.exist;
- const addOnSubId = addOnProd?.subscription_ids?.[0];
- expect(addOnSubId).to.equal(subIds[1]);
+ expect(addOnProd).to.exist;
+ const addOnSubId = addOnProd?.subscription_ids?.[0];
+ expect(addOnSubId).to.equal(subIds[1]);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- subId: subIds[1],
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ subId: subIds[1],
+ });
+ });
});
diff --git a/server/tests/merged/trial/mergedTrial1.test.ts b/server/tests/merged/trial/mergedTrial1.test.ts
index 0a47d39af..a37c8f814 100644
--- a/server/tests/merged/trial/mergedTrial1.test.ts
+++ b/server/tests/merged/trial/mergedTrial1.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -27,115 +27,115 @@ import { addDays } from "date-fns";
// Results: Canceled sub
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const testCase = "mergedTrial1";
describe(`${chalk.yellowBright("mergedTrial1: Testing trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach first trial, and advance clock past trial", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach first trial, and advance clock past trial", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 2).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 2).getTime(),
+ });
- const entity1 = await autumn.entities.get(customerId, "1");
- const premium1 = entity1.products.find((p: any) => p.id == premium.id);
+ const entity1 = await autumn.entities.get(customerId, "1");
+ const premium1 = entity1.products.find((p: any) => p.id == premium.id);
- const checkout = await autumn.checkout({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "2",
- });
+ const checkout = await autumn.checkout({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "2",
+ });
- const nextCycle = checkout.next_cycle;
- expect(nextCycle?.starts_at);
- expect(nextCycle?.starts_at).to.approximately(
- premium1?.current_period_end,
- 60000
- ); // 1 min
+ const nextCycle = checkout.next_cycle;
+ expect(nextCycle?.starts_at);
+ expect(nextCycle?.starts_at).to.approximately(
+ premium1?.current_period_end,
+ 60000,
+ ); // 1 min
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "2",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "2",
+ });
- const entity2 = await autumn.entities.get(customerId, "2");
- const premium2 = entity2.products.find((p: any) => p.id == premium.id);
- expect(premium2?.status).to.equal(CusProductStatus.Trialing);
- expect(premium2?.current_period_end).to.approximately(
- premium1?.current_period_end,
- 60000
- ); // 1 min
- });
+ const entity2 = await autumn.entities.get(customerId, "2");
+ const premium2 = entity2.products.find((p: any) => p.id == premium.id);
+ expect(premium2?.status).to.equal(CusProductStatus.Trialing);
+ expect(premium2?.current_period_end).to.approximately(
+ premium1?.current_period_end,
+ 60000,
+ ); // 1 min
+ });
});
diff --git a/server/tests/merged/trial/mergedTrial2.test.ts b/server/tests/merged/trial/mergedTrial2.test.ts
index 36a95b751..16fe3a195 100644
--- a/server/tests/merged/trial/mergedTrial2.test.ts
+++ b/server/tests/merged/trial/mergedTrial2.test.ts
@@ -8,10 +8,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -21,135 +21,135 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- // {
- // entityId: "2",
- // product: premium,
- // results: [{ product: premium, status: CusProductStatus.Active }],
- // },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ // {
+ // entityId: "2",
+ // product: premium,
+ // results: [{ product: premium, status: CusProductStatus.Active }],
+ // },
];
const testCase = "mergedTrial2";
describe(`${chalk.yellowBright("mergedTrial2: Testing add second trial product after first trial ends")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach first trial, and advance clock past trial", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach first trial, and advance clock past trial", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- entity_id: "1",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ entity_id: "1",
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 8).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 8).getTime(),
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "2",
- });
- // const entity1 = await autumn.entities.get(customerId, "1");
- // const premium1 = entity1.products.find((p: any) => p.id == premium.id);
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "2",
+ });
+ // const entity1 = await autumn.entities.get(customerId, "1");
+ // const premium1 = entity1.products.find((p: any) => p.id == premium.id);
- // const checkout = await autumn.checkout({
- // customer_id: customerId,
- // product_id: premium.id,
- // entity_id: "2",
- // });
+ // const checkout = await autumn.checkout({
+ // customer_id: customerId,
+ // product_id: premium.id,
+ // entity_id: "2",
+ // });
- // const nextCycle = checkout.next_cycle;
- // expect(nextCycle?.starts_at);
- // expect(nextCycle?.starts_at).to.approximately(
- // premium1?.current_period_end,
- // 60000
- // ); // 1 min
+ // const nextCycle = checkout.next_cycle;
+ // expect(nextCycle?.starts_at);
+ // expect(nextCycle?.starts_at).to.approximately(
+ // premium1?.current_period_end,
+ // 60000
+ // ); // 1 min
- // await autumn.attach({
- // customer_id: customerId,
- // product_id: premium.id,
- // entity_id: "2",
- // });
+ // await autumn.attach({
+ // customer_id: customerId,
+ // product_id: premium.id,
+ // entity_id: "2",
+ // });
- // const entity2 = await autumn.entities.get(customerId, "2");
- // const premium2 = entity2.products.find((p: any) => p.id == premium.id);
- // expect(premium2?.status).to.equal(CusProductStatus.Active);
- // expect(premium2?.current_period_end).to.equal(premium1?.current_period_end);
- });
+ // const entity2 = await autumn.entities.get(customerId, "2");
+ // const premium2 = entity2.products.find((p: any) => p.id == premium.id);
+ // expect(premium2?.status).to.equal(CusProductStatus.Active);
+ // expect(premium2?.current_period_end).to.equal(premium1?.current_period_end);
+ });
});
diff --git a/server/tests/merged/trial/mergedTrial3.test.ts b/server/tests/merged/trial/mergedTrial3.test.ts
index 5e005c664..efd89f68f 100644
--- a/server/tests/merged/trial/mergedTrial3.test.ts
+++ b/server/tests/merged/trial/mergedTrial3.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -21,133 +21,133 @@ import { addDays } from "date-fns";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- // {
- // entityId: "2",
- // product: premium,
- // results: [{ product: premium, status: CusProductStatus.Active }],
- // },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ // {
+ // entityId: "2",
+ // product: premium,
+ // results: [{ product: premium, status: CusProductStatus.Active }],
+ // },
];
const testCase = "mergedTrial3";
describe(`${chalk.yellowBright("mergedTrial3: Testing upgrade to product with trial in merged state")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach first trial, and advance clock past trial", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach first trial, and advance clock past trial", async function () {
+ await autumn.entities.create(customerId, entities);
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "1",
- });
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "1",
+ });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 8).getTime(),
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 8).getTime(),
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "1",
- checkNotTrialing: true,
- });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "2",
- checkNotTrialing: true,
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "1",
+ checkNotTrialing: true,
+ });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "2",
+ checkNotTrialing: true,
+ });
+ });
});
diff --git a/server/tests/merged/trial/mergedTrial4.test.ts b/server/tests/merged/trial/mergedTrial4.test.ts
index a26111639..527400b25 100644
--- a/server/tests/merged/trial/mergedTrial4.test.ts
+++ b/server/tests/merged/trial/mergedTrial4.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -22,123 +22,123 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedTrial4";
describe(`${chalk.yellowBright("mergedTrial4: Testing cancel immediately on merged sub trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro trial for entity 1 and entity 2", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro trial for entity 1 and entity 2", async function () {
+ await autumn.entities.create(customerId, entities);
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- }
- });
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ }
+ });
- it("should cancel one of subs immediately and have sub still trialing", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- cancel_immediately: true,
- });
+ it("should cancel one of subs immediately and have sub still trialing", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ cancel_immediately: true,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeTrialing: true,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeTrialing: true,
+ });
+ });
});
diff --git a/server/tests/merged/trial/mergedTrial5.test.ts b/server/tests/merged/trial/mergedTrial5.test.ts
index 08ceac0a9..17591a6db 100644
--- a/server/tests/merged/trial/mergedTrial5.test.ts
+++ b/server/tests/merged/trial/mergedTrial5.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -22,166 +22,166 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "3",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "3",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedTrial5";
describe(`${chalk.yellowBright("mergedTrial5: Testing cancel at end of cycle and cancel immediately on merged sub trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- {
- id: "3",
- name: "Entity 3",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "3",
+ name: "Entity 3",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should attach pro trial for entity 1 and entity 2", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should attach pro trial for entity 1 and entity 2", async function () {
+ await autumn.entities.create(customerId, entities);
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- }
- });
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ }
+ });
- it("should cancel one sub end of cycle", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "2",
- });
+ it("should cancel one sub end of cycle", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "2",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeTrialing: true,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeTrialing: true,
+ });
+ });
- it("should cancel one sub immediately", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "3",
- cancel_immediately: true,
- });
+ it("should cancel one sub immediately", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "3",
+ cancel_immediately: true,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeTrialing: true,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeTrialing: true,
+ });
+ });
- it("should cancel last sub at end of cycle", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- entity_id: "1",
- });
+ it("should cancel last sub at end of cycle", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ entity_id: "1",
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeTrialing: true,
- shouldBeCanceled: true,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeTrialing: true,
+ shouldBeCanceled: true,
+ });
+ });
});
diff --git a/server/tests/merged/trial/trial1.test.ts b/server/tests/merged/trial/trial1.test.ts
index 64bfda29f..41bdd607c 100644
--- a/server/tests/merged/trial/trial1.test.ts
+++ b/server/tests/merged/trial/trial1.test.ts
@@ -7,11 +7,11 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -29,136 +29,136 @@ import { expectProductAttached } from "tests/utils/expectUtils/expectProductAtta
// Results: Canceled sub
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const ops = [
- {
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Trialing }],
- },
- // {
- // entityId: "2",
- // product: premium,
- // results: [{ product: premium, status: CusProductStatus.Active }],
- // },
+ {
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Trialing }],
+ },
+ // {
+ // entityId: "2",
+ // product: premium,
+ // results: [{ product: premium, status: CusProductStatus.Active }],
+ // },
];
const testCase = "trial1";
describe(`${chalk.yellowBright("trial1: Testing main trial branch, upgrade from pro trial -> premium trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach first trial, and advance clock past trial", async function () {
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- });
- }
+ it("should attach first trial, and advance clock past trial", async function () {
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ }
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Trialing,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should advance test clock to before trial ends and attach premium", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 2).getTime(),
- });
- // return;
+ it("should advance test clock to before trial ends and attach premium", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 2).getTime(),
+ });
+ // return;
- const attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: premium.id,
- });
+ const attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- expect(attachPreview?.branch).to.equal(AttachBranch.MainIsTrial);
+ expect(attachPreview?.branch).to.equal(AttachBranch.MainIsTrial);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: premium,
- status: CusProductStatus.Trialing,
- });
- const product = customer.products.find((p) => p.id === premium.id)!;
- expect(product.current_period_end).to.be.approximately(
- addDays(curUnix, 7).getTime(),
- 1000 * 60 * 30 // 30 minutes
- );
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ });
+ const product = customer.products.find((p) => p.id === premium.id)!;
+ expect(product.current_period_end).to.be.approximately(
+ addDays(curUnix, 7).getTime(),
+ 1000 * 60 * 30, // 30 minutes
+ );
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ });
+ });
});
diff --git a/server/tests/merged/trial/trial2.test.ts b/server/tests/merged/trial/trial2.test.ts
index 3ee2d6dee..748fd66fc 100644
--- a/server/tests/merged/trial/trial2.test.ts
+++ b/server/tests/merged/trial/trial2.test.ts
@@ -7,11 +7,11 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -31,147 +31,147 @@ import { Decimal } from "decimal.js";
// Premium Trial
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const ops = [
- {
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Trialing }],
- },
- // {
- // entityId: "2",
- // product: premium,
- // results: [{ product: premium, status: CusProductStatus.Active }],
- // },
+ {
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Trialing }],
+ },
+ // {
+ // entityId: "2",
+ // product: premium,
+ // results: [{ product: premium, status: CusProductStatus.Active }],
+ // },
];
const testCase = "trial2";
describe(`${chalk.yellowBright("trial2: Testing main trial branch, upgrade from pro trial -> trial finished -> premium trial")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach first trial", async function () {
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- });
- }
+ it("should attach first trial", async function () {
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ }
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Trialing,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ });
+ });
- it("should advance test clock to past trial ends and attach premium", async function () {
- curUnix = await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addDays(new Date(), 8).getTime(),
- });
+ it("should advance test clock to past trial ends and attach premium", async function () {
+ curUnix = await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addDays(new Date(), 8).getTime(),
+ });
- const attachPreview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: premium.id,
- });
+ const attachPreview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: premium.id,
- });
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- expect(attachPreview?.branch).to.equal(AttachBranch.Upgrade);
+ expect(attachPreview?.branch).to.equal(AttachBranch.Upgrade);
- await autumn.attach({
- customer_id: customerId,
- product_id: premium.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: premium.id,
+ });
- await timeout(5000);
+ await timeout(5000);
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: premium,
- status: CusProductStatus.Trialing,
- });
- const product = customer.products.find((p) => p.id === premium.id)!;
- expect(product.current_period_end).to.be.approximately(
- addDays(curUnix, 7).getTime(),
- 1000 * 60 * 30 // 30 minutes
- );
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: premium,
+ status: CusProductStatus.Trialing,
+ });
+ const product = customer.products.find((p) => p.id === premium.id)!;
+ expect(product.current_period_end).to.be.approximately(
+ addDays(curUnix, 7).getTime(),
+ 1000 * 60 * 30, // 30 minutes
+ );
- expect(customer.invoices[0].total).to.equal(
- new Decimal(checkoutRes.total).toDP(2).toNumber()
- );
+ expect(customer.invoices[0].total).to.equal(
+ new Decimal(checkoutRes.total).toDP(2).toNumber(),
+ );
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeTrialing: true,
- });
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeTrialing: true,
+ });
+ });
});
diff --git a/server/tests/merged/trial/trial3.test.ts b/server/tests/merged/trial/trial3.test.ts
index 28506e111..8af537ff9 100644
--- a/server/tests/merged/trial/trial3.test.ts
+++ b/server/tests/merged/trial/trial3.test.ts
@@ -7,11 +7,11 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- FullCusProduct,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ FullCusProduct,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
@@ -29,155 +29,155 @@ import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtil
// Premium Trial
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
- trial: true,
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
+ trial: true,
});
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- trial: true,
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ trial: true,
});
const ops = [
- {
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Trialing }],
- },
- // {
- // entityId: "2",
- // product: premium,
- // results: [{ product: premium, status: CusProductStatus.Active }],
- // },
+ {
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Trialing }],
+ },
+ // {
+ // entityId: "2",
+ // product: premium,
+ // results: [{ product: premium, status: CusProductStatus.Active }],
+ // },
];
const testCase = "trial3";
describe(`${chalk.yellowBright("trial3: Testing cancel trial product")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- it("should attach first trial, and advance clock past trial", async function () {
- for (const op of ops) {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- });
- }
+ it("should attach first trial, and advance clock past trial", async function () {
+ for (const op of ops) {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ });
+ }
- const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product: pro,
- status: CusProductStatus.Trialing,
- });
- });
+ const customer = await autumn.customers.get(customerId);
+ expectProductAttached({
+ customer,
+ product: pro,
+ status: CusProductStatus.Trialing,
+ });
+ });
- let cusProduct: FullCusProduct;
+ let cusProduct: FullCusProduct;
- it("should have canceled trial product at the end of cycle", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should have canceled trial product at the end of cycle", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
- cusProduct = fullCus.customer_products.find(
- (p) => p.product.id === pro.id
- )!;
- const sub = await cusProductToSub({
- cusProduct,
- stripeCli,
- });
- // console.log(`cancel at period end: ${sub?.cancel_at_period_end}`);
- // console.log(`cancel at: ${sub?.cancel_at}`);
- // console.log(`canceled at: ${sub?.canceled_at}`);
- const canceled = sub?.canceled_at || sub?.cancel_at;
- expect(canceled).to.exist;
- });
+ cusProduct = fullCus.customer_products.find(
+ (p) => p.product.id === pro.id,
+ )!;
+ const sub = await cusProductToSub({
+ cusProduct,
+ stripeCli,
+ });
+ // console.log(`cancel at period end: ${sub?.cancel_at_period_end}`);
+ // console.log(`cancel at: ${sub?.cancel_at}`);
+ // console.log(`canceled at: ${sub?.canceled_at}`);
+ const canceled = sub?.canceled_at || sub?.cancel_at;
+ expect(canceled).to.exist;
+ });
- it("should have sub not canceled if renew product", async function () {
- await autumn.attach({
- customer_id: customerId,
- product_id: pro.id,
- });
+ it("should have sub not canceled if renew product", async function () {
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: pro.id,
+ });
- const sub = await cusProductToSub({
- cusProduct,
- stripeCli,
- });
- expect(sub?.cancel_at_period_end).to.equal(false);
- });
- it("should be canceled completely", async function () {
- await autumn.cancel({
- customer_id: customerId,
- product_id: pro.id,
- cancel_immediately: true,
- });
+ const sub = await cusProductToSub({
+ cusProduct,
+ stripeCli,
+ });
+ expect(sub?.cancel_at_period_end).to.equal(false);
+ });
+ it("should be canceled completely", async function () {
+ await autumn.cancel({
+ customer_id: customerId,
+ product_id: pro.id,
+ cancel_immediately: true,
+ });
- const customer = await autumn.customers.get(customerId);
- const proProduct = customer.products.find((p) => p.id === pro.id)!;
- expect(proProduct).to.not.exist;
+ const customer = await autumn.customers.get(customerId);
+ const proProduct = customer.products.find((p) => p.id === pro.id)!;
+ expect(proProduct).to.not.exist;
- const sub = await cusProductToSub({
- cusProduct,
- stripeCli,
- });
- expect(sub?.status).to.equal("canceled");
- });
+ const sub = await cusProductToSub({
+ cusProduct,
+ stripeCli,
+ });
+ expect(sub?.status).to.equal("canceled");
+ });
});
diff --git a/server/tests/merged/upgrade/mergedUpgrade1.test.ts b/server/tests/merged/upgrade/mergedUpgrade1.test.ts
index 93c76a29f..17e574fc8 100644
--- a/server/tests/merged/upgrade/mergedUpgrade1.test.ts
+++ b/server/tests/merged/upgrade/mergedUpgrade1.test.ts
@@ -7,16 +7,16 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
- addPrefixToProducts,
- getBasePrice,
+ addPrefixToProducts,
+ getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import { expect } from "chai";
import { advanceToNextInvoice } from "tests/utils/testAttachUtils/testAttachUtils.js";
@@ -26,184 +26,184 @@ import { addWeeks } from "date-fns";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
let premium = constructProduct({
- id: "premium",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
let premiumAnnual = constructProduct({
- id: "premiumAnnual",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "premium",
- isAnnual: true,
+ id: "premiumAnnual",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "premium",
+ isAnnual: true,
});
let pro = constructProduct({
- id: "pro",
- items: [constructArrearItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructArrearItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedUpgrade1";
describe(`${chalk.yellowBright("mergedUpgrade1: Testing merged subs, upgrade 1 & 2 to pro, add premium 2")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, premium, premiumAnnual],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, premium, premiumAnnual],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, premium, premiumAnnual],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, premium, premiumAnnual],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- try {
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entityId: op.entityId,
- });
- } catch (error) {
- console.log(
- `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`
- );
- throw error;
- }
- }
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ try {
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: op.entityId,
+ });
+ } catch (error) {
+ console.log(
+ `Operation failed: ${op.entityId} ${op.product.id}, index: ${index}`,
+ );
+ throw error;
+ }
+ }
+ });
- const entity1Val = 100000;
- const entity2Val = 300000;
+ const entity1Val = 100000;
+ const entity2Val = 300000;
- it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async function () {
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: entity1Val,
- entity_id: "1",
- });
+ it("should advance test clock and upgrade entity 1 to premium, and have correct invoice", async function () {
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: entity1Val,
+ entity_id: "1",
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: TestFeature.Words,
- value: entity2Val,
- entity_id: "2",
- });
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: TestFeature.Words,
+ value: entity2Val,
+ entity_id: "2",
+ });
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addWeeks(Date.now(), 2).getTime(),
- waitForSeconds: 30,
- });
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addWeeks(Date.now(), 2).getTime(),
+ waitForSeconds: 30,
+ });
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: premium,
- stripeCli,
- db,
- org,
- env,
- entityId: "1",
- });
- });
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: premium,
+ stripeCli,
+ db,
+ org,
+ env,
+ entityId: "1",
+ });
+ });
- it("should advance to next invoice and have correct invoice", async function () {
- await advanceToNextInvoice({
- stripeCli,
- testClockId,
- });
+ it("should advance to next invoice and have correct invoice", async function () {
+ await advanceToNextInvoice({
+ stripeCli,
+ testClockId,
+ });
- const expectedTotal = await getExpectedInvoiceTotal({
- org,
- env,
- customerId,
- productId: pro.id,
- stripeCli,
- db,
- onlyIncludeUsage: true,
- usage: [
- {
- featureId: TestFeature.Words,
- value: entity2Val,
- },
- ],
- });
+ const expectedTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ customerId,
+ productId: pro.id,
+ stripeCli,
+ db,
+ onlyIncludeUsage: true,
+ usage: [
+ {
+ featureId: TestFeature.Words,
+ value: entity2Val,
+ },
+ ],
+ });
- const customer = await autumn.customers.get(customerId);
- const invoice = customer.invoices[0];
- const basePrice =
- getBasePrice({ product: pro }) + getBasePrice({ product: premium });
- expect(invoice.total).to.equal(basePrice + expectedTotal);
- });
+ const customer = await autumn.customers.get(customerId);
+ const invoice = customer.invoices[0];
+ const basePrice =
+ getBasePrice({ product: pro }) + getBasePrice({ product: premium });
+ expect(invoice.total).to.equal(basePrice + expectedTotal);
+ });
});
diff --git a/server/tests/merged/upgrade/mergedUpgrade2.test.ts b/server/tests/merged/upgrade/mergedUpgrade2.test.ts
index 280c34515..9fd56a92f 100644
--- a/server/tests/merged/upgrade/mergedUpgrade2.test.ts
+++ b/server/tests/merged/upgrade/mergedUpgrade2.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -21,140 +21,140 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
const premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const growth = constructProduct({
- id: "growth",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "growth",
+ id: "growth",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "growth",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: growth,
- results: [{ product: growth, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: growth,
+ results: [{ product: growth, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedUpgrade2";
describe(`${chalk.yellowBright("mergedUpgrade2: Upgrading when there's a scheduled downgrade")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, free, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, free, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, free, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, free, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/merged/upgrade/mergedUpgrade3.test.ts b/server/tests/merged/upgrade/mergedUpgrade3.test.ts
index e77279d83..a858a8ae8 100644
--- a/server/tests/merged/upgrade/mergedUpgrade3.test.ts
+++ b/server/tests/merged/upgrade/mergedUpgrade3.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -21,149 +21,149 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// UNCOMMENT FROM HERE
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
const premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const growth = constructProduct({
- id: "growth",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "growth",
+ id: "growth",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "growth",
});
const ops = [
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: pro,
- results: [
- { product: premium, status: CusProductStatus.Active },
- { product: pro, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "2",
- product: growth,
- results: [{ product: growth, status: CusProductStatus.Active }],
- // waitForInvoice: 5000,
- },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [
+ { product: premium, status: CusProductStatus.Active },
+ { product: pro, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "2",
+ product: growth,
+ results: [{ product: growth, status: CusProductStatus.Active }],
+ // waitForInvoice: 5000,
+ },
];
const testCase = "mergedUpgrade3";
describe(`${chalk.yellowBright("mergedUpgrade3: Upgrading when there's a scheduled downgrade")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, free, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, free, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, free, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, free, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/merged/upgrade/mergedUpgrade4.test.ts b/server/tests/merged/upgrade/mergedUpgrade4.test.ts
index 29e927de3..cfde4874a 100644
--- a/server/tests/merged/upgrade/mergedUpgrade4.test.ts
+++ b/server/tests/merged/upgrade/mergedUpgrade4.test.ts
@@ -7,10 +7,10 @@ import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
- APIVersion,
- AppEnv,
- CusProductStatus,
- Organization,
+ APIVersion,
+ AppEnv,
+ CusProductStatus,
+ Organization,
} from "@autumn/shared";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
@@ -24,140 +24,140 @@ import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js"
// Premium
let pro = constructProduct({
- id: "pro",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "pro",
+ id: "pro",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "pro",
});
let free = constructProduct({
- id: "free",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "free",
- isDefault: false,
+ id: "free",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "free",
+ isDefault: false,
});
const premium = constructProduct({
- id: "premium",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "premium",
+ id: "premium",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "premium",
});
const growth = constructProduct({
- id: "growth",
- items: [constructFeatureItem({ featureId: TestFeature.Words })],
- type: "growth",
+ id: "growth",
+ items: [constructFeatureItem({ featureId: TestFeature.Words })],
+ type: "growth",
});
const ops = [
- {
- entityId: "1",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "2",
- product: pro,
- results: [{ product: pro, status: CusProductStatus.Active }],
- },
- {
- entityId: "1",
- product: free,
- results: [
- { product: pro, status: CusProductStatus.Active },
- { product: free, status: CusProductStatus.Scheduled },
- ],
- },
- {
- entityId: "1",
- product: premium,
- results: [{ product: premium, status: CusProductStatus.Active }],
- },
+ {
+ entityId: "1",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "2",
+ product: pro,
+ results: [{ product: pro, status: CusProductStatus.Active }],
+ },
+ {
+ entityId: "1",
+ product: free,
+ results: [
+ { product: pro, status: CusProductStatus.Active },
+ { product: free, status: CusProductStatus.Scheduled },
+ ],
+ },
+ {
+ entityId: "1",
+ product: premium,
+ results: [{ product: premium, status: CusProductStatus.Active }],
+ },
];
const testCase = "mergedUpgrade4";
describe(`${chalk.yellowBright("mergedUpgrade4: Upgrading when there's a cancel")}`, () => {
- let customerId = testCase;
- let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
+ let customerId = testCase;
+ let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
- let stripeCli: Stripe;
- let testClockId: string;
- let curUnix: number;
- let db: DrizzleCli;
- let org: Organization;
- let env: AppEnv;
+ let stripeCli: Stripe;
+ let testClockId: string;
+ let curUnix: number;
+ let db: DrizzleCli;
+ let org: Organization;
+ let env: AppEnv;
- before(async function () {
- await setupBefore(this);
- const { autumnJs } = this;
- db = this.db;
- org = this.org;
- env = this.env;
+ before(async function () {
+ await setupBefore(this);
+ const { autumnJs } = this;
+ db = this.db;
+ org = this.org;
+ env = this.env;
- stripeCli = this.stripeCli;
+ stripeCli = this.stripeCli;
- addPrefixToProducts({
- products: [pro, free, premium, growth],
- prefix: testCase,
- });
+ addPrefixToProducts({
+ products: [pro, free, premium, growth],
+ prefix: testCase,
+ });
- await createProducts({
- autumn: autumnJs,
- products: [pro, free, premium, growth],
- db,
- orgId: org.id,
- env,
- customerId,
- });
+ await createProducts({
+ autumn: autumnJs,
+ products: [pro, free, premium, growth],
+ db,
+ orgId: org.id,
+ env,
+ customerId,
+ });
- const { testClockId: testClockId1 } = await initCustomer({
- autumn: autumnJs,
- customerId,
- db,
- org,
- env,
- attachPm: "success",
- });
+ const { testClockId: testClockId1 } = await initCustomer({
+ autumn: autumnJs,
+ customerId,
+ db,
+ org,
+ env,
+ attachPm: "success",
+ });
- testClockId = testClockId1!;
- });
+ testClockId = testClockId1!;
+ });
- const entities = [
- {
- id: "1",
- name: "Entity 1",
- feature_id: TestFeature.Users,
- },
- {
- id: "2",
- name: "Entity 2",
- feature_id: TestFeature.Users,
- },
- ];
+ const entities = [
+ {
+ id: "1",
+ name: "Entity 1",
+ feature_id: TestFeature.Users,
+ },
+ {
+ id: "2",
+ name: "Entity 2",
+ feature_id: TestFeature.Users,
+ },
+ ];
- it("should run operations", async function () {
- await autumn.entities.create(customerId, entities);
+ it("should run operations", async function () {
+ await autumn.entities.create(customerId, entities);
- for (let index = 0; index < ops.length; index++) {
- const op = ops[index];
- await attachAndExpectCorrect({
- autumn,
- customerId,
- product: op.product,
- stripeCli,
- db,
- org,
- env,
- entities,
- entityId: op.entityId,
- });
+ for (let index = 0; index < ops.length; index++) {
+ const op = ops[index];
+ await attachAndExpectCorrect({
+ autumn,
+ customerId,
+ product: op.product,
+ stripeCli,
+ db,
+ org,
+ env,
+ entities,
+ entityId: op.entityId,
+ });
- for (const result of op.results) {
- const entity = await autumn.entities.get(customerId, op.entityId);
- expectProductAttached({
- customer: entity,
- product: result.product,
- status: result.status,
- });
- }
- }
- });
+ for (const result of op.results) {
+ const entity = await autumn.entities.get(customerId, op.entityId);
+ expectProductAttached({
+ customer: entity,
+ product: result.product,
+ status: result.status,
+ });
+ }
+ }
+ });
});
diff --git a/server/tests/setup/v2Features.ts b/server/tests/setup/v2Features.ts
index 7622d0ad4..580f28474 100644
--- a/server/tests/setup/v2Features.ts
+++ b/server/tests/setup/v2Features.ts
@@ -2,81 +2,81 @@ import dotenv from "dotenv";
dotenv.config();
import {
- constructBooleanFeature,
- constructCreditSystem,
- constructMeteredFeature,
+ constructBooleanFeature,
+ constructCreditSystem,
+ constructMeteredFeature,
} from "@/internal/features/utils/constructFeatureUtils.js";
import { AppEnv, FeatureUsageType, UsageModel } from "@autumn/shared";
export enum TestFeature {
- Messages = "messages", // single use (prepaid)
- Users = "users", // cont use
- Admin = "admin", // cont use
- AdminRights = "admin_rights", // cont use
- Words = "words", // single use (pay per use)
+ Messages = "messages", // single use (prepaid)
+ Users = "users", // cont use
+ Admin = "admin", // cont use
+ AdminRights = "admin_rights", // cont use
+ Words = "words", // single use (pay per use)
- Action1 = "action1", // single use (pay per use)
- Action2 = "action2", // single use (pay per use)
- Credits = "credits", // credit system
+ Action1 = "action1", // single use (pay per use)
+ Action2 = "action2", // single use (pay per use)
+ Credits = "credits", // credit system
}
const orgId = process.env.TESTS_ORG_ID!;
export const features = {
- [TestFeature.AdminRights]: constructBooleanFeature({
- featureId: TestFeature.AdminRights,
- orgId,
- env: AppEnv.Sandbox,
- }),
- [TestFeature.Messages]: constructMeteredFeature({
- featureId: TestFeature.Messages,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Single,
- }),
- [TestFeature.Admin]: constructMeteredFeature({
- featureId: TestFeature.Admin,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Continuous,
- }),
- [TestFeature.Users]: constructMeteredFeature({
- featureId: TestFeature.Users,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Continuous,
- }),
- [TestFeature.Words]: constructMeteredFeature({
- featureId: TestFeature.Words,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Single,
- }),
- [TestFeature.Action1]: constructMeteredFeature({
- featureId: TestFeature.Action1,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Single,
- }),
- [TestFeature.Action2]: constructMeteredFeature({
- featureId: TestFeature.Action2,
- orgId,
- env: AppEnv.Sandbox,
- usageType: FeatureUsageType.Single,
- }),
- [TestFeature.Credits]: constructCreditSystem({
- featureId: TestFeature.Credits,
- orgId,
- env: AppEnv.Sandbox,
- schema: [
- {
- metered_feature_id: TestFeature.Action1,
- credit_cost: 0.2,
- },
- {
- metered_feature_id: TestFeature.Action2,
- credit_cost: 0.6,
- },
- ],
- }),
+ [TestFeature.AdminRights]: constructBooleanFeature({
+ featureId: TestFeature.AdminRights,
+ orgId,
+ env: AppEnv.Sandbox,
+ }),
+ [TestFeature.Messages]: constructMeteredFeature({
+ featureId: TestFeature.Messages,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Single,
+ }),
+ [TestFeature.Admin]: constructMeteredFeature({
+ featureId: TestFeature.Admin,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Continuous,
+ }),
+ [TestFeature.Users]: constructMeteredFeature({
+ featureId: TestFeature.Users,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Continuous,
+ }),
+ [TestFeature.Words]: constructMeteredFeature({
+ featureId: TestFeature.Words,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Single,
+ }),
+ [TestFeature.Action1]: constructMeteredFeature({
+ featureId: TestFeature.Action1,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Single,
+ }),
+ [TestFeature.Action2]: constructMeteredFeature({
+ featureId: TestFeature.Action2,
+ orgId,
+ env: AppEnv.Sandbox,
+ usageType: FeatureUsageType.Single,
+ }),
+ [TestFeature.Credits]: constructCreditSystem({
+ featureId: TestFeature.Credits,
+ orgId,
+ env: AppEnv.Sandbox,
+ schema: [
+ {
+ metered_feature_id: TestFeature.Action1,
+ credit_cost: 0.2,
+ },
+ {
+ metered_feature_id: TestFeature.Action2,
+ credit_cost: 0.6,
+ },
+ ],
+ }),
};
diff --git a/server/tests/utils/advancedUsageUtils.ts b/server/tests/utils/advancedUsageUtils.ts
index 8c76d9e4a..e24660c9f 100644
--- a/server/tests/utils/advancedUsageUtils.ts
+++ b/server/tests/utils/advancedUsageUtils.ts
@@ -12,152 +12,152 @@ const PRECISION = 10;
const CREDIT_MULTIPLIER = 100000;
export const getCreditsUsed = (
- creditSystem: Feature,
- meteredFeatureId: string,
- value: number,
+ creditSystem: Feature,
+ meteredFeatureId: string,
+ value: number,
) => {
- let schemaItem = creditSystem.config.schema.find(
- (item: any) => item.metered_feature_id === meteredFeatureId,
- );
+ let schemaItem = creditSystem.config.schema.find(
+ (item: any) => item.metered_feature_id === meteredFeatureId,
+ );
- return new Decimal(value).mul(schemaItem.credit_amount).toNumber();
+ return new Decimal(value).mul(schemaItem.credit_amount).toNumber();
};
export const checkCreditBalance = async ({
- customerId,
- featureId,
- totalCreditsUsed,
- originalAllowance,
+ customerId,
+ featureId,
+ totalCreditsUsed,
+ originalAllowance,
}: {
- customerId: string;
- featureId: string;
- totalCreditsUsed: number;
- originalAllowance: number;
+ customerId: string;
+ featureId: string;
+ totalCreditsUsed: number;
+ originalAllowance: number;
}) => {
- // Check entitled
- const { allowed, balanceObj }: any = await AutumnCli.entitled(
- customerId,
- featureId,
- true,
- );
+ // Check entitled
+ const { allowed, balanceObj }: any = await AutumnCli.entitled(
+ customerId,
+ featureId,
+ true,
+ );
- try {
- assert.equal(allowed, true);
- assert.equal(
- balanceObj.balance,
- new Decimal(originalAllowance).minus(totalCreditsUsed).toNumber(),
- );
- } catch (error) {
- console.group();
- console.log(" - Total credits used: ", totalCreditsUsed);
- console.log(" - Original allowance: ", originalAllowance);
- console.log(
- " - Expected balance: ",
- originalAllowance - totalCreditsUsed,
- );
- console.log(" - Actual balance: ", balanceObj.balance);
- console.groupEnd();
- throw error;
- }
+ try {
+ assert.equal(allowed, true);
+ assert.equal(
+ balanceObj.balance,
+ new Decimal(originalAllowance).minus(totalCreditsUsed).toNumber(),
+ );
+ } catch (error) {
+ console.group();
+ console.log(" - Total credits used: ", totalCreditsUsed);
+ console.log(" - Original allowance: ", originalAllowance);
+ console.log(
+ " - Expected balance: ",
+ originalAllowance - totalCreditsUsed,
+ );
+ console.log(" - Actual balance: ", balanceObj.balance);
+ console.groupEnd();
+ throw error;
+ }
};
export const checkUsageInvoiceAmount = async ({
- invoices,
- totalUsage,
- product,
- featureId,
- invoiceIndex,
- includeBase = true,
+ invoices,
+ totalUsage,
+ product,
+ featureId,
+ invoiceIndex,
+ includeBase = true,
}: {
- invoices: any;
- totalUsage: number;
- product: any;
- featureId: string;
- invoiceIndex?: number;
- includeBase?: boolean;
+ invoices: any;
+ totalUsage: number;
+ product: any;
+ featureId: string;
+ invoiceIndex?: number;
+ includeBase?: boolean;
}) => {
- const featureEntitlement: any = Object.values(product.entitlements).find(
- (entitlement: any) => entitlement.feature_id === featureId,
- );
+ const featureEntitlement: any = Object.values(product.entitlements).find(
+ (entitlement: any) => entitlement.feature_id === featureId,
+ );
- let meteredPrice = product.prices[product.prices.length - 1];
- let overage = new Decimal(totalUsage)
- .minus(featureEntitlement.allowance)
- .toNumber();
- const overagePrice = getPriceForOverage(meteredPrice, overage);
+ let meteredPrice = product.prices[product.prices.length - 1];
+ let overage = new Decimal(totalUsage)
+ .minus(featureEntitlement.allowance)
+ .toNumber();
+ const overagePrice = getPriceForOverage(meteredPrice, overage);
- let basePrice = 0;
- if (includeBase && product.prices.length > 1) {
- basePrice = product.prices[0].config.amount;
- }
+ let basePrice = 0;
+ if (includeBase && product.prices.length > 1) {
+ basePrice = product.prices[0].config.amount;
+ }
- let totalPrice = new Decimal(overagePrice.toFixed(2))
- .plus(basePrice)
- .toNumber();
+ let totalPrice = new Decimal(overagePrice.toFixed(2))
+ .plus(basePrice)
+ .toNumber();
- try {
- for (let i = 0; i < invoices.length; i++) {
- let invoice = invoices[i];
- if (invoice.total == totalPrice) {
- invoiceIndex = i;
- assert.equal(invoice.product_ids[0], product.id);
- return;
- }
- }
- assert.fail("No invoice found with correct total price");
- } catch (error) {
- console.group();
- console.log("Check usage invoice amount failed");
- console.log("- Base price: ", basePrice);
- console.log("- Overage price: ", overagePrice);
- console.log(
- `Expected to find invoice with total of ${totalPrice} and product id ${product.id}`,
- );
- // console.log("Instead got: ", invoices[invoiceIndex || 0].total);
- console.log("Last 3 invoices", invoices.slice(-3));
- console.group();
- throw error;
- }
+ try {
+ for (let i = 0; i < invoices.length; i++) {
+ let invoice = invoices[i];
+ if (invoice.total == totalPrice) {
+ invoiceIndex = i;
+ assert.equal(invoice.product_ids[0], product.id);
+ return;
+ }
+ }
+ assert.fail("No invoice found with correct total price");
+ } catch (error) {
+ console.group();
+ console.log("Check usage invoice amount failed");
+ console.log("- Base price: ", basePrice);
+ console.log("- Overage price: ", overagePrice);
+ console.log(
+ `Expected to find invoice with total of ${totalPrice} and product id ${product.id}`,
+ );
+ // console.log("Instead got: ", invoices[invoiceIndex || 0].total);
+ console.log("Last 3 invoices", invoices.slice(-3));
+ console.group();
+ throw error;
+ }
};
export const sendGPUEvents = async ({
- customerId,
- eventCount,
- groupObj = {},
+ customerId,
+ eventCount,
+ groupObj = {},
}: {
- customerId: string;
- eventCount: number;
- groupObj?: any;
+ customerId: string;
+ eventCount: number;
+ groupObj?: any;
}) => {
- let totalCreditsUsed = 0;
- const batchEvents = [];
- for (let i = 0; i < eventCount; i++) {
- let randomVal = new Decimal(Math.random().toFixed(PRECISION))
- .mul(CREDIT_MULTIPLIER)
- .toNumber();
- let gpuId = i % 2 == 0 ? features.gpu1.id : features.gpu2.id;
+ let totalCreditsUsed = 0;
+ const batchEvents = [];
+ for (let i = 0; i < eventCount; i++) {
+ let randomVal = new Decimal(Math.random().toFixed(PRECISION))
+ .mul(CREDIT_MULTIPLIER)
+ .toNumber();
+ let gpuId = i % 2 == 0 ? features.gpu1.id : features.gpu2.id;
- let creditsUsed = getCreditsUsed(
- creditSystems.gpuCredits,
- gpuId,
- randomVal,
- );
+ let creditsUsed = getCreditsUsed(
+ creditSystems.gpuCredits,
+ gpuId,
+ randomVal,
+ );
- totalCreditsUsed = new Decimal(totalCreditsUsed)
- .plus(creditsUsed)
- .toNumber();
+ totalCreditsUsed = new Decimal(totalCreditsUsed)
+ .plus(creditsUsed)
+ .toNumber();
- batchEvents.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: gpuId,
- properties: { value: randomVal, ...groupObj },
- }),
- );
- }
+ batchEvents.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: gpuId,
+ properties: { value: randomVal, ...groupObj },
+ }),
+ );
+ }
- await Promise.all(batchEvents);
- await timeout(10000);
+ await Promise.all(batchEvents);
+ await timeout(10000);
- return { creditsUsed: totalCreditsUsed };
+ return { creditsUsed: totalCreditsUsed };
};
diff --git a/server/tests/utils/autumnUtils.ts b/server/tests/utils/autumnUtils.ts
index 79d0c3c77..3323d86c2 100644
--- a/server/tests/utils/autumnUtils.ts
+++ b/server/tests/utils/autumnUtils.ts
@@ -2,18 +2,18 @@ import { Autumn } from "@/external/autumn/autumnCli.js";
import { expect } from "chai";
export const checkBalance = async ({
- autumn,
- featureId,
- customerId,
- expectedBalance,
+ autumn,
+ featureId,
+ customerId,
+ expectedBalance,
}: {
- autumn: Autumn;
- featureId: string;
- customerId: string;
- expectedBalance: number;
+ autumn: Autumn;
+ featureId: string;
+ customerId: string;
+ expectedBalance: number;
}) => {
- let { entitlements } = await autumn.customers.get(customerId);
- let entitlement = entitlements.find((e: any) => e.feature_id == featureId);
+ let { entitlements } = await autumn.customers.get(customerId);
+ let entitlement = entitlements.find((e: any) => e.feature_id == featureId);
- expect(entitlement.balance).to.equal(expectedBalance);
-}
\ No newline at end of file
+ expect(entitlement.balance).to.equal(expectedBalance);
+};
diff --git a/server/tests/utils/compare.ts b/server/tests/utils/compare.ts
index eae49607c..fb640d9b6 100644
--- a/server/tests/utils/compare.ts
+++ b/server/tests/utils/compare.ts
@@ -1,12 +1,12 @@
import {
- AllowanceType,
- CusProductStatus,
- Entitlement,
- Feature,
- FeatureOptions,
- FeatureType,
- FullProduct,
- UsagePriceConfig,
+ AllowanceType,
+ CusProductStatus,
+ Entitlement,
+ Feature,
+ FeatureOptions,
+ FeatureType,
+ FullProduct,
+ UsagePriceConfig,
} from "@autumn/shared";
import { expect } from "chai";
import { AutumnCli } from "tests/cli/AutumnCli.js";
@@ -14,191 +14,191 @@ import { creditSystems } from "tests/global.js";
import { Decimal } from "decimal.js";
export const checkProductIsScheduled = ({
- cusRes,
- product,
+ cusRes,
+ product,
}: {
- cusRes: any;
- product: any;
+ cusRes: any;
+ product: any;
}) => {
- const { products, add_ons, entitlements } = cusRes;
- const prod = products.find((p: any) => p.id === product.id);
- try {
- expect(prod).to.exist;
- expect(prod.status).to.equal(CusProductStatus.Scheduled);
- } catch (error) {
- console.group();
- console.log(`Expected product ${product.id} to be scheduled`);
- console.log("Received: ", cusRes.products);
- console.groupEnd();
- throw error;
- }
+ const { products, add_ons, entitlements } = cusRes;
+ const prod = products.find((p: any) => p.id === product.id);
+ try {
+ expect(prod).to.exist;
+ expect(prod.status).to.equal(CusProductStatus.Scheduled);
+ } catch (error) {
+ console.group();
+ console.log(`Expected product ${product.id} to be scheduled`);
+ console.log("Received: ", cusRes.products);
+ console.groupEnd();
+ throw error;
+ }
};
export const compareMainProduct = ({
- sent,
- cusRes,
- status = CusProductStatus.Active,
- optionsList = [],
+ sent,
+ cusRes,
+ status = CusProductStatus.Active,
+ optionsList = [],
}: {
- sent: any;
- cusRes: any;
- status?: CusProductStatus;
- optionsList?: FeatureOptions[];
+ sent: any;
+ cusRes: any;
+ status?: CusProductStatus;
+ optionsList?: FeatureOptions[];
}) => {
- const { products, add_ons, entitlements } = cusRes;
- const prod = products.find(
- (p: any) => p.id === sent.id && p.status == status && !sent.is_add_on,
- );
+ const { products, add_ons, entitlements } = cusRes;
+ const prod = products.find(
+ (p: any) => p.id === sent.id && p.status == status && !sent.is_add_on,
+ );
- try {
- expect(prod).to.exist;
- expect(sent.id).to.equal(prod.id);
- } catch (error) {
- console.log(`Failed to compare main product ${sent.id}`);
- console.log("Sent: ", sent);
- console.log("Received: ", cusRes);
- throw error;
- }
+ try {
+ expect(prod).to.exist;
+ expect(sent.id).to.equal(prod.id);
+ } catch (error) {
+ console.log(`Failed to compare main product ${sent.id}`);
+ console.log("Sent: ", sent);
+ console.log("Received: ", cusRes);
+ throw error;
+ }
- // Check entitlements
- let sentEntitlements = Object.values(sent.entitlements) as Entitlement[];
- let recEntitlements = entitlements;
+ // Check entitlements
+ let sentEntitlements = Object.values(sent.entitlements) as Entitlement[];
+ let recEntitlements = entitlements;
- // expect(sentEntitlements.length).to.equal(recEntitlements.length);
- for (const entitlement of sentEntitlements) {
- // Corresponding entitlement in received
- const recEntitlement = recEntitlements.find((e: any) => {
- if (e.feature_id !== entitlement.feature_id) return false;
- if (entitlement.interval && e.interval !== entitlement.interval)
- return false;
- return true;
- });
+ // expect(sentEntitlements.length).to.equal(recEntitlements.length);
+ for (const entitlement of sentEntitlements) {
+ // Corresponding entitlement in received
+ const recEntitlement = recEntitlements.find((e: any) => {
+ if (e.feature_id !== entitlement.feature_id) return false;
+ if (entitlement.interval && e.interval !== entitlement.interval)
+ return false;
+ return true;
+ });
- // If options list provideed, and feature
- let options = optionsList.find(
- (o: any) => o.feature_id === entitlement.feature_id,
- );
+ // If options list provideed, and feature
+ let options = optionsList.find(
+ (o: any) => o.feature_id === entitlement.feature_id,
+ );
- let expectedBalance = entitlement.allowance;
- if (options?.quantity) {
- // Get price from sent
- const price = sent.prices.find(
- (p: any) => p.config.feature_id === entitlement.feature_id,
- );
- const config = price.config as UsagePriceConfig;
- expectedBalance = new Decimal(expectedBalance || 0)
- .add(options.quantity * (config.billing_units || 1))
- .toNumber();
- }
+ let expectedBalance = entitlement.allowance;
+ if (options?.quantity) {
+ // Get price from sent
+ const price = sent.prices.find(
+ (p: any) => p.config.feature_id === entitlement.feature_id,
+ );
+ const config = price.config as UsagePriceConfig;
+ expectedBalance = new Decimal(expectedBalance || 0)
+ .add(options.quantity * (config.billing_units || 1))
+ .toNumber();
+ }
- try {
- expect(recEntitlement).to.exist;
- if (entitlement.allowance_type === AllowanceType.Unlimited) {
- expect(recEntitlement.unlimited).to.equal(true);
- expect(recEntitlement.balance).to.equal(null);
- expect(recEntitlement.used).to.equal(null);
- } else if ("balance" in entitlement) {
- expect(recEntitlement.balance).to.equal(expectedBalance);
- }
- } catch (error) {
- console.log(
- `Failed to compare main product (entitlements) ${entitlement.feature_id}`,
- );
- console.log("Looking for entitlement: ", entitlement);
- console.log("Received entitlements: ", entitlements);
- throw error;
- }
- }
+ try {
+ expect(recEntitlement).to.exist;
+ if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ expect(recEntitlement.unlimited).to.equal(true);
+ expect(recEntitlement.balance).to.equal(null);
+ expect(recEntitlement.used).to.equal(null);
+ } else if ("balance" in entitlement) {
+ expect(recEntitlement.balance).to.equal(expectedBalance);
+ }
+ } catch (error) {
+ console.log(
+ `Failed to compare main product (entitlements) ${entitlement.feature_id}`,
+ );
+ console.log("Looking for entitlement: ", entitlement);
+ console.log("Received entitlements: ", entitlements);
+ throw error;
+ }
+ }
};
export const checkFeatureHasCorrectBalance = async ({
- customerId,
- feature,
- entitlement,
- expectedBalance,
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance,
}: {
- customerId: string;
- feature: Feature;
- entitlement: Entitlement;
- expectedBalance: number;
+ customerId: string;
+ feature: Feature;
+ entitlement: Entitlement;
+ expectedBalance: number;
}) => {
- const [entitledRes, cusRes] = await Promise.all([
- AutumnCli.entitled(customerId, feature.id, true),
- AutumnCli.getCustomer(customerId),
- ]);
+ const [entitledRes, cusRes] = await Promise.all([
+ AutumnCli.entitled(customerId, feature.id, true),
+ AutumnCli.getCustomer(customerId),
+ ]);
- if (feature.type === FeatureType.Boolean) {
- console.log(" - Checking boolean feature: ", feature.id);
- const { allowed, balanceObj }: any = entitledRes;
- expect(allowed).to.equal(true);
- return;
- }
+ if (feature.type === FeatureType.Boolean) {
+ console.log(" - Checking boolean feature: ", feature.id);
+ const { allowed, balanceObj }: any = entitledRes;
+ expect(allowed).to.equal(true);
+ return;
+ }
- // console.log(
- // ` - Checking entitlement ${feature.id} has ${
- // entitlement.allowance_type == AllowanceType.Unlimited
- // ? "unlimited balance"
- // : `balance of ${expectedBalance}`
- // }`
- // );
+ // console.log(
+ // ` - Checking entitlement ${feature.id} has ${
+ // entitlement.allowance_type == AllowanceType.Unlimited
+ // ? "unlimited balance"
+ // : `balance of ${expectedBalance}`
+ // }`
+ // );
- // Get ent from cusRes
- const { entitlements: cusEnts }: any = cusRes;
- const { allowed, balanceObj }: any = entitledRes;
- const cusEnt = cusEnts.find(
- (e: any) =>
- e.feature_id === feature.id && e.interval == entitlement.interval,
- );
+ // Get ent from cusRes
+ const { entitlements: cusEnts }: any = cusRes;
+ const { allowed, balanceObj }: any = entitledRes;
+ const cusEnt = cusEnts.find(
+ (e: any) =>
+ e.feature_id === feature.id && e.interval == entitlement.interval,
+ );
- expect(cusEnt).to.exist;
+ expect(cusEnt).to.exist;
- if (entitlement.allowance_type === AllowanceType.Unlimited) {
- // Cus ent
- expect(cusEnt.balance).to.equal(null);
- expect(cusEnt.used).to.equal(null);
- expect(cusEnt.unlimited).to.equal(true);
+ if (entitlement.allowance_type === AllowanceType.Unlimited) {
+ // Cus ent
+ expect(cusEnt.balance).to.equal(null);
+ expect(cusEnt.used).to.equal(null);
+ expect(cusEnt.unlimited).to.equal(true);
- // Entitled res
- expect(allowed).to.equal(true);
- expect(balanceObj?.balance).to.equal(null);
- expect(balanceObj?.unlimited).to.equal(true);
- return;
- }
+ // Entitled res
+ expect(allowed).to.equal(true);
+ expect(balanceObj?.balance).to.equal(null);
+ expect(balanceObj?.unlimited).to.equal(true);
+ return;
+ }
- if (expectedBalance === 0) {
- expect(allowed).to.equal(false);
- expect(balanceObj?.balance).to.equal(0);
- expect(cusEnt.balance).to.equal(0);
- return;
- }
+ if (expectedBalance === 0) {
+ expect(allowed).to.equal(false);
+ expect(balanceObj?.balance).to.equal(0);
+ expect(cusEnt.balance).to.equal(0);
+ return;
+ }
- expect(balanceObj?.balance).to.equal(expectedBalance);
- expect(cusEnt.balance).to.equal(expectedBalance);
+ expect(balanceObj?.balance).to.equal(expectedBalance);
+ expect(cusEnt.balance).to.equal(expectedBalance);
};
export const compareProductEntitlements = ({
- customerId,
- product,
- features,
- quantity = 1,
+ customerId,
+ product,
+ features,
+ quantity = 1,
}: {
- customerId: string;
- product: any;
- features: Record;
- quantity?: number;
+ customerId: string;
+ product: any;
+ features: Record;
+ quantity?: number;
}) => {
- for (const entitlement of Object.values(
- product.entitlements,
- ) as Entitlement[]) {
- let feature =
- features[entitlement.feature_id!] ||
- creditSystems[entitlement.feature_id as keyof typeof creditSystems];
+ for (const entitlement of Object.values(
+ product.entitlements,
+ ) as Entitlement[]) {
+ let feature =
+ features[entitlement.feature_id!] ||
+ creditSystems[entitlement.feature_id as keyof typeof creditSystems];
- checkFeatureHasCorrectBalance({
- customerId,
- feature,
- entitlement,
- expectedBalance: (entitlement.allowance || 0) * quantity,
- });
- }
+ checkFeatureHasCorrectBalance({
+ customerId,
+ feature,
+ entitlement,
+ expectedBalance: (entitlement.allowance || 0) * quantity,
+ });
+ }
};
diff --git a/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts b/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts
index 0d379ef86..c9f6dafad 100644
--- a/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts
+++ b/server/tests/utils/cusProductUtils/cusEntSearchUtils.ts
@@ -9,92 +9,92 @@ import { FullCusProduct } from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
export const getLifetimeFreeCusEnt = ({
- cusProduct,
- featureId,
+ cusProduct,
+ featureId,
}: {
- cusProduct: FullCusProduct;
- featureId: string;
+ cusProduct: FullCusProduct;
+ featureId: string;
}) => {
- // Get lifetime free cus ent
- return cusProduct.customer_entitlements.find(
- (cusEnt: FullCustomerEntitlement) => {
- if (cusEnt.entitlement.feature.id !== featureId) {
- return false;
- }
+ // Get lifetime free cus ent
+ return cusProduct.customer_entitlements.find(
+ (cusEnt: FullCustomerEntitlement) => {
+ if (cusEnt.entitlement.feature.id !== featureId) {
+ return false;
+ }
- let relatedCusPrice = getRelatedCusPrice(
- cusEnt,
- cusProduct.customer_prices,
- );
+ let relatedCusPrice = getRelatedCusPrice(
+ cusEnt,
+ cusProduct.customer_prices,
+ );
- if (notNullish(relatedCusPrice)) {
- return false;
- }
+ if (notNullish(relatedCusPrice)) {
+ return false;
+ }
- return cusEnt.entitlement.interval === EntInterval.Lifetime;
- },
- );
+ return cusEnt.entitlement.interval === EntInterval.Lifetime;
+ },
+ );
};
export const getPrepaidCusEnt = ({
- cusProduct,
- featureId,
+ cusProduct,
+ featureId,
}: {
- cusProduct: FullCusProduct;
- featureId: string;
+ cusProduct: FullCusProduct;
+ featureId: string;
}) => {
- // Get prepaid cus ent
- return cusProduct.customer_entitlements.find(
- (cusEnt: FullCustomerEntitlement) => {
- if (cusEnt.entitlement.feature.id !== featureId) {
- return false;
- }
+ // Get prepaid cus ent
+ return cusProduct.customer_entitlements.find(
+ (cusEnt: FullCustomerEntitlement) => {
+ if (cusEnt.entitlement.feature.id !== featureId) {
+ return false;
+ }
- let relatedCusPrice = getRelatedCusPrice(
- cusEnt,
- cusProduct.customer_prices,
- );
+ let relatedCusPrice = getRelatedCusPrice(
+ cusEnt,
+ cusProduct.customer_prices,
+ );
- if (
- relatedCusPrice &&
- getBillingType(relatedCusPrice?.price.config!) ===
- BillingType.UsageInAdvance
- ) {
- return true;
- }
+ if (
+ relatedCusPrice &&
+ getBillingType(relatedCusPrice?.price.config!) ===
+ BillingType.UsageInAdvance
+ ) {
+ return true;
+ }
- return false;
- },
- );
+ return false;
+ },
+ );
};
export const getUsageCusEnt = ({
- cusProduct,
- featureId,
+ cusProduct,
+ featureId,
}: {
- cusProduct: FullCusProduct;
- featureId: string;
+ cusProduct: FullCusProduct;
+ featureId: string;
}) => {
- // Get usage cus ent
- return cusProduct.customer_entitlements.find(
- (cusEnt: FullCustomerEntitlement) => {
- if (cusEnt.entitlement.feature.id !== featureId) {
- return false;
- }
+ // Get usage cus ent
+ return cusProduct.customer_entitlements.find(
+ (cusEnt: FullCustomerEntitlement) => {
+ if (cusEnt.entitlement.feature.id !== featureId) {
+ return false;
+ }
- let relatedCusPrice = getRelatedCusPrice(
- cusEnt,
- cusProduct.customer_prices,
- );
+ let relatedCusPrice = getRelatedCusPrice(
+ cusEnt,
+ cusProduct.customer_prices,
+ );
- if (
- relatedCusPrice &&
- getBillingType(relatedCusPrice?.price.config!) ===
- BillingType.UsageInArrear
- ) {
- return true;
- }
+ if (
+ relatedCusPrice &&
+ getBillingType(relatedCusPrice?.price.config!) ===
+ BillingType.UsageInArrear
+ ) {
+ return true;
+ }
- return false;
- },
- );
+ return false;
+ },
+ );
};
diff --git a/server/tests/utils/cusProductUtils/cusProductUtils.ts b/server/tests/utils/cusProductUtils/cusProductUtils.ts
index 74df7cdb5..03a78a32b 100644
--- a/server/tests/utils/cusProductUtils/cusProductUtils.ts
+++ b/server/tests/utils/cusProductUtils/cusProductUtils.ts
@@ -3,34 +3,34 @@ import { CusService } from "@/internal/customers/CusService.js";
import { AppEnv, CusProductStatus, FullCusProduct } from "@autumn/shared";
export const getMainCusProduct = async ({
- db,
- customerId,
- orgId,
- env,
- productGroup,
+ db,
+ customerId,
+ orgId,
+ env,
+ productGroup,
}: {
- db: DrizzleCli;
- customerId: string;
- orgId: string;
- env: AppEnv;
- productGroup?: string;
+ db: DrizzleCli;
+ customerId: string;
+ orgId: string;
+ env: AppEnv;
+ productGroup?: string;
}) => {
- let customer = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId,
- env,
- withEntities: true,
- inStatuses: [CusProductStatus.Active],
- });
+ let customer = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId,
+ env,
+ withEntities: true,
+ inStatuses: [CusProductStatus.Active],
+ });
- let cusProducts = customer.customer_products;
+ let cusProducts = customer.customer_products;
- let mainCusProduct = cusProducts.find(
- (cusProduct: FullCusProduct) =>
- !cusProduct.product.is_add_on &&
- (productGroup ? cusProduct.product.group === productGroup : true)
- );
+ let mainCusProduct = cusProducts.find(
+ (cusProduct: FullCusProduct) =>
+ !cusProduct.product.is_add_on &&
+ (productGroup ? cusProduct.product.group === productGroup : true),
+ );
- return mainCusProduct;
+ return mainCusProduct;
};
diff --git a/server/tests/utils/expectUtils/expectAttach.ts b/server/tests/utils/expectUtils/expectAttach.ts
index fc6473f9f..15a9803c5 100644
--- a/server/tests/utils/expectUtils/expectAttach.ts
+++ b/server/tests/utils/expectUtils/expectAttach.ts
@@ -1,18 +1,18 @@
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- AppEnv,
- AttachBranch,
- CreateEntity,
- CusProductStatus,
- FeatureOptions,
- Organization,
- ProductV2,
+ AppEnv,
+ AttachBranch,
+ CreateEntity,
+ CusProductStatus,
+ FeatureOptions,
+ Organization,
+ ProductV2,
} from "@autumn/shared";
import {
- getAttachTotal,
- getCurrentOptions,
+ getAttachTotal,
+ getCurrentOptions,
} from "tests/utils/testAttachUtils/testAttachUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectInvoicesCorrect } from "tests/utils/expectUtils/expectProductAttached.js";
@@ -29,236 +29,236 @@ import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.j
import { Decimal } from "decimal.js";
export const attachAndExpectCorrect = async ({
- autumn,
- customerId,
- entityId,
- product,
- otherProducts,
- options,
- stripeCli,
- db,
- org,
- env,
- usage,
- waitForInvoice = 0,
- isCanceled = false,
- skipFeatureCheck = false,
- skipSubCheck = false,
- numSubs,
- entities,
- shouldBeCanceled = false,
- checkNotTrialing = false,
- attachParams,
+ autumn,
+ customerId,
+ entityId,
+ product,
+ otherProducts,
+ options,
+ stripeCli,
+ db,
+ org,
+ env,
+ usage,
+ waitForInvoice = 0,
+ isCanceled = false,
+ skipFeatureCheck = false,
+ skipSubCheck = false,
+ numSubs,
+ entities,
+ shouldBeCanceled = false,
+ checkNotTrialing = false,
+ attachParams,
}: {
- autumn: AutumnInt;
- customerId: string;
- entityId?: string;
- product: ProductV2;
- otherProducts?: ProductV2[];
- options?: FeatureOptions[];
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- usage?: {
- featureId: string;
- value: number;
- }[];
- waitForInvoice?: number;
- isCanceled?: boolean;
- skipFeatureCheck?: boolean;
- skipSubCheck?: boolean;
- numSubs?: number;
- entities?: CreateEntity[];
- shouldBeCanceled?: boolean;
- checkNotTrialing?: boolean;
- attachParams?: AttachParams;
+ autumn: AutumnInt;
+ customerId: string;
+ entityId?: string;
+ product: ProductV2;
+ otherProducts?: ProductV2[];
+ options?: FeatureOptions[];
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ usage?: {
+ featureId: string;
+ value: number;
+ }[];
+ waitForInvoice?: number;
+ isCanceled?: boolean;
+ skipFeatureCheck?: boolean;
+ skipSubCheck?: boolean;
+ numSubs?: number;
+ entities?: CreateEntity[];
+ shouldBeCanceled?: boolean;
+ checkNotTrialing?: boolean;
+ attachParams?: AttachParams;
}) => {
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: product.id,
- entity_id: entityId,
- ...attachParams,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: product.id,
+ entity_id: entityId,
+ ...attachParams,
+ });
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- product_id: product.id,
- entity_id: entityId,
- options: toSnakeCase(options),
- ...attachParams,
- });
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ product_id: product.id,
+ entity_id: entityId,
+ options: toSnakeCase(options),
+ ...attachParams,
+ });
- const logCheckoutRes = false;
- if (logCheckoutRes) {
- console.log("Checkout res:");
- for (const line of checkoutRes.lines) {
- console.log(line.description, line.amount);
- }
- console.log("Total: ", checkoutRes.total);
- console.log("--------------------------------");
- }
+ const logCheckoutRes = false;
+ if (logCheckoutRes) {
+ console.log("Checkout res:");
+ for (const line of checkoutRes.lines) {
+ console.log(line.description, line.amount);
+ }
+ console.log("Total: ", checkoutRes.total);
+ console.log("--------------------------------");
+ }
- const optionsCopy = getCurrentOptions({
- preview,
- options,
- });
+ const optionsCopy = getCurrentOptions({
+ preview,
+ options,
+ });
- // const total = getAttachTotal({
- // preview,
- // options,
- // });
+ // const total = getAttachTotal({
+ // preview,
+ // options,
+ // });
- const { checkout_url } = await autumn.attach({
- customer_id: customerId,
- product_id: product.id,
- entity_id: entityId,
- options: toSnakeCase(options),
- ...attachParams,
- });
+ const { checkout_url } = await autumn.attach({
+ customer_id: customerId,
+ product_id: product.id,
+ entity_id: entityId,
+ options: toSnakeCase(options),
+ ...attachParams,
+ });
- if (checkout_url) {
- await completeCheckoutForm(checkout_url);
- await timeout(5000);
- }
+ if (checkout_url) {
+ await completeCheckoutForm(checkout_url);
+ await timeout(5000);
+ }
- if (waitForInvoice) {
- await timeout(waitForInvoice);
- }
+ if (waitForInvoice) {
+ await timeout(waitForInvoice);
+ }
- let customer;
- if (entityId) {
- customer = await autumn.entities.get(customerId, entityId);
- } else {
- customer = await autumn.customers.get(customerId);
- }
+ let customer;
+ if (entityId) {
+ customer = await autumn.entities.get(customerId, entityId);
+ } else {
+ customer = await autumn.customers.get(customerId);
+ }
- const productCount = customer.products.reduce((acc: number, p: any) => {
- if (
- product.group == p.group &&
- !p.is_add_on &&
- (entityId ? p.entity_id == entityId : true)
- ) {
- return acc + 1;
- } else return acc;
- }, 0);
+ const productCount = customer.products.reduce((acc: number, p: any) => {
+ if (
+ product.group == p.group &&
+ !p.is_add_on &&
+ (entityId ? p.entity_id == entityId : true)
+ ) {
+ return acc + 1;
+ } else return acc;
+ }, 0);
- const branch = preview.branch;
+ const branch = preview.branch;
- if (branch == AttachBranch.Downgrade) {
- expect(
- productCount,
- `customer should only have 2 products (from this group: ${product.group})`
- ).to.equal(2);
- } else {
- expect(
- productCount,
- `customer should only have 1 product (from this group: ${product.group})`
- ).to.equal(1);
- }
+ if (branch == AttachBranch.Downgrade) {
+ expect(
+ productCount,
+ `customer should only have 2 products (from this group: ${product.group})`,
+ ).to.equal(2);
+ } else {
+ expect(
+ productCount,
+ `customer should only have 1 product (from this group: ${product.group})`,
+ ).to.equal(1);
+ }
- expectProductAttached({
- customer,
- product,
- entityId,
- status:
- preview.branch == AttachBranch.Downgrade
- ? CusProductStatus.Scheduled
- : undefined,
- });
+ expectProductAttached({
+ customer,
+ product,
+ entityId,
+ status:
+ preview.branch == AttachBranch.Downgrade
+ ? CusProductStatus.Scheduled
+ : undefined,
+ });
- const skipInvoiceCheck =
- (preview.branch == AttachBranch.UpdatePrepaidQuantity &&
- checkoutRes.total == 0) ||
- preview.branch == AttachBranch.Downgrade;
+ const skipInvoiceCheck =
+ (preview.branch == AttachBranch.UpdatePrepaidQuantity &&
+ checkoutRes.total == 0) ||
+ preview.branch == AttachBranch.Downgrade;
- const freeProduct = isFreeProductV2({ product });
- if (!skipInvoiceCheck && !freeProduct) {
- expectInvoicesCorrect({
- customer,
- first: {
- productId: product.id,
- total: new Decimal(checkoutRes.total).toDecimalPlaces(2).toNumber(),
- },
- });
- }
+ const freeProduct = isFreeProductV2({ product });
+ if (!skipInvoiceCheck && !freeProduct) {
+ expectInvoicesCorrect({
+ customer,
+ first: {
+ productId: product.id,
+ total: new Decimal(checkoutRes.total).toDecimalPlaces(2).toNumber(),
+ },
+ });
+ }
- if (!skipFeatureCheck && branch !== AttachBranch.Downgrade) {
- expectFeaturesCorrect({
- customer,
- product,
- usage,
- options: optionsCopy,
+ if (!skipFeatureCheck && branch !== AttachBranch.Downgrade) {
+ expectFeaturesCorrect({
+ customer,
+ product,
+ usage,
+ options: optionsCopy,
- otherProducts,
- entities,
- });
- }
+ otherProducts,
+ entities,
+ });
+ }
- if (branch == AttachBranch.OneOff) {
- return;
- }
+ if (branch == AttachBranch.OneOff) {
+ return;
+ }
- if (skipSubCheck) return;
+ if (skipSubCheck) return;
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled,
- flags: {
- checkNotTrialing,
- },
- entityId,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled,
+ flags: {
+ checkNotTrialing,
+ },
+ entityId,
+ });
- // await expectSubItemsCorrect({
- // stripeCli,
- // customerId,
- // product,
- // db,
- // org,
- // env,
- // isCanceled,
- // entityId,
- // });
+ // await expectSubItemsCorrect({
+ // stripeCli,
+ // customerId,
+ // product,
+ // db,
+ // org,
+ // env,
+ // isCanceled,
+ // entityId,
+ // });
- // let cus = await autumn.customers.get(customerId);
- // const stripeSubs = await stripeCli.subscriptions.list({
- // customer: cus.stripe_id!,
- // });
+ // let cus = await autumn.customers.get(customerId);
+ // const stripeSubs = await stripeCli.subscriptions.list({
+ // customer: cus.stripe_id!,
+ // });
- // if (numSubs) {
- // expect(stripeSubs.data.length).to.equal(
- // numSubs,
- // `should have ${numSubs} subscriptions`
- // );
- // } else {
- // expect(stripeSubs.data.length).to.equal(
- // 1,
- // "should only have 1 subscription"
- // );
- // }
+ // if (numSubs) {
+ // expect(stripeSubs.data.length).to.equal(
+ // numSubs,
+ // `should have ${numSubs} subscriptions`
+ // );
+ // } else {
+ // expect(stripeSubs.data.length).to.equal(
+ // 1,
+ // "should only have 1 subscription"
+ // );
+ // }
};
export const expectAttachCorrect = async ({
- customer,
- product,
- entityId,
+ customer,
+ product,
+ entityId,
}: {
- customer: Customer;
- product: ProductV2;
- entityId?: string;
+ customer: Customer;
+ product: ProductV2;
+ entityId?: string;
}) => {
- expectProductAttached({
- customer,
- product,
- entityId,
- });
+ expectProductAttached({
+ customer,
+ product,
+ entityId,
+ });
- expectFeaturesCorrect({
- customer,
- product,
- });
+ expectFeaturesCorrect({
+ customer,
+ product,
+ });
};
diff --git a/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts b/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts
index 10c6b4e6e..a67fd5638 100644
--- a/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts
+++ b/server/tests/utils/expectUtils/expectAttach/expectResetAtCorrect.ts
@@ -2,26 +2,26 @@ import { Customer } from "autumn-js";
import { expect } from "chai";
export const expectResetAtCorrect = ({
- cusBefore,
- cusAfter,
+ cusBefore,
+ cusAfter,
}: {
- cusBefore: Customer;
- cusAfter: Customer;
+ cusBefore: Customer;
+ cusAfter: Customer;
}) => {
- const featuresBefore = cusBefore.features;
- const featuresAfter = cusAfter.features;
+ const featuresBefore = cusBefore.features;
+ const featuresAfter = cusAfter.features;
- for (const featureId in featuresBefore) {
- const featureBefore = featuresBefore[featureId];
- if (!featureBefore.next_reset_at) {
- continue;
- }
+ for (const featureId in featuresBefore) {
+ const featureBefore = featuresBefore[featureId];
+ if (!featureBefore.next_reset_at) {
+ continue;
+ }
- const featureAfter = featuresAfter[featureId];
- expect(featureAfter.next_reset_at).to.be.approximately(
- featureBefore.next_reset_at,
- 10000,
- `reset for ${featureId} should be within 10 seconds`,
- );
- }
+ const featureAfter = featuresAfter[featureId];
+ expect(featureAfter.next_reset_at).to.be.approximately(
+ featureBefore.next_reset_at,
+ 10000,
+ `reset for ${featureId} should be within 10 seconds`,
+ );
+ }
};
diff --git a/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts b/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts
index df57434d7..457e3d31e 100644
--- a/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts
+++ b/server/tests/utils/expectUtils/expectAttach/expectTrialEndsAt.ts
@@ -2,25 +2,25 @@ import { Customer } from "autumn-js";
import { expect } from "chai";
export const expectTrialEndsAtCorrect = ({
- cusBefore,
- cusAfter,
+ cusBefore,
+ cusAfter,
}: {
- cusBefore: Customer;
- cusAfter: Customer;
+ cusBefore: Customer;
+ cusAfter: Customer;
}) => {
- let productsBefore = cusBefore.products;
- let productsAfter = cusAfter.products;
+ let productsBefore = cusBefore.products;
+ let productsAfter = cusAfter.products;
- for (const productBefore of productsBefore) {
- // @ts-ignore
- let trialEndsAtBefore = productBefore.trial_ends_at;
+ for (const productBefore of productsBefore) {
+ // @ts-ignore
+ let trialEndsAtBefore = productBefore.trial_ends_at;
- if (!trialEndsAtBefore) {
- continue;
- }
+ if (!trialEndsAtBefore) {
+ continue;
+ }
- const productAfter = productsAfter.find((p) => p.id === productBefore.id);
- // @ts-ignore
- expect(productAfter?.trial_ends_at).to.equal(trialEndsAtBefore);
- }
+ const productAfter = productsAfter.find((p) => p.id === productBefore.id);
+ // @ts-ignore
+ expect(productAfter?.trial_ends_at).to.equal(trialEndsAtBefore);
+ }
};
diff --git a/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts b/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts
index 11ddd669b..9fee45961 100644
--- a/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts
+++ b/server/tests/utils/expectUtils/expectContUse/expectEntityUtils.ts
@@ -4,46 +4,46 @@ import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { expect } from "chai";
export const useEntityBalanceAndExpect = async ({
- autumn,
- customerId,
- featureId,
- entityId,
+ autumn,
+ customerId,
+ featureId,
+ entityId,
}: {
- autumn: AutumnInt;
- customerId: string;
- featureId: string;
- entityId: string;
+ autumn: AutumnInt;
+ customerId: string;
+ featureId: string;
+ entityId: string;
}) => {
- let deduction = new Decimal(Math.random() * 400)
- .toDecimalPlaces(5)
- .toNumber();
+ let deduction = new Decimal(Math.random() * 400)
+ .toDecimalPlaces(5)
+ .toNumber();
- let balanceBefore = await autumn.check({
- customer_id: customerId,
- feature_id: featureId,
- entity_id: entityId,
- });
+ let balanceBefore = await autumn.check({
+ customer_id: customerId,
+ feature_id: featureId,
+ entity_id: entityId,
+ });
- await autumn.track({
- customer_id: customerId,
- feature_id: featureId,
- value: deduction,
- entity_id: entityId,
- });
- await timeout(3000);
+ await autumn.track({
+ customer_id: customerId,
+ feature_id: featureId,
+ value: deduction,
+ entity_id: entityId,
+ });
+ await timeout(3000);
- let balanceAfter = await autumn.check({
- customer_id: customerId,
- feature_id: featureId,
- entity_id: entityId,
- });
+ let balanceAfter = await autumn.check({
+ customer_id: customerId,
+ feature_id: featureId,
+ entity_id: entityId,
+ });
- let expectedBalance = new Decimal(balanceBefore.balance!)
- .sub(deduction)
- .toNumber();
+ let expectedBalance = new Decimal(balanceBefore.balance!)
+ .sub(deduction)
+ .toNumber();
- expect(balanceAfter.balance).to.equal(
- expectedBalance,
- "Entity balance should be correct",
- );
+ expect(balanceAfter.balance).to.equal(
+ expectedBalance,
+ "Entity balance should be correct",
+ );
};
diff --git a/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts b/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts
index 8a134e516..5c9cd8d56 100644
--- a/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts
+++ b/server/tests/utils/expectUtils/expectContUse/expectUpdateContUse.ts
@@ -6,36 +6,36 @@ import { expect } from "chai";
import Stripe from "stripe";
export const attachNewContUseAndExpectCorrect = async ({
- autumn,
- customerId,
- product,
- customItems,
- numInvoices,
+ autumn,
+ customerId,
+ product,
+ customItems,
+ numInvoices,
}: {
- autumn: AutumnInt;
- customerId: string;
- product: ProductV2;
- customItems: ProductItem[];
+ autumn: AutumnInt;
+ customerId: string;
+ product: ProductV2;
+ customItems: ProductItem[];
- numInvoices: number;
+ numInvoices: number;
}) => {
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: product.id,
- is_custom: true,
- items: customItems,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: product.id,
+ is_custom: true,
+ items: customItems,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: product.id,
- is_custom: true,
- items: customItems,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: product.id,
+ is_custom: true,
+ items: customItems,
+ });
- const customer = await autumn.customers.get(customerId);
- const invoices = customer.invoices;
- expect(invoices.length).to.equal(numInvoices);
- expect(invoices[0].total).to.equal(preview.due_today.total);
- return { customer, invoices };
+ const customer = await autumn.customers.get(customerId);
+ const invoices = customer.invoices;
+ expect(invoices.length).to.equal(numInvoices);
+ expect(invoices[0].total).to.equal(preview.due_today.total);
+ return { customer, invoices };
};
diff --git a/server/tests/utils/expectUtils/expectContUseUtils.ts b/server/tests/utils/expectUtils/expectContUseUtils.ts
index 4fe5067a5..f448beb9a 100644
--- a/server/tests/utils/expectUtils/expectContUseUtils.ts
+++ b/server/tests/utils/expectUtils/expectContUseUtils.ts
@@ -14,167 +14,167 @@ import { notNullish } from "@/utils/genUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
export const expectSubQuantityCorrect = async ({
- stripeCli,
- productId,
- usage,
- db,
- org,
- env,
- customerId,
- itemQuantity,
- numReplaceables = 0,
+ stripeCli,
+ productId,
+ usage,
+ db,
+ org,
+ env,
+ customerId,
+ itemQuantity,
+ numReplaceables = 0,
}: {
- stripeCli: Stripe;
- productId: string;
- usage: number;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- customerId: string;
- itemQuantity?: number;
- numReplaceables?: number;
+ stripeCli: Stripe;
+ productId: string;
+ usage: number;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ customerId: string;
+ itemQuantity?: number;
+ numReplaceables?: number;
}) => {
- const fullCus = await CusService.getFull({
- db,
- orgId: org.id,
- env,
- idOrInternalId: customerId,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ orgId: org.id,
+ env,
+ idOrInternalId: customerId,
+ });
- let cusProduct = fullCus.customer_products.find(
- (cp) => cp.product_id === productId
- );
+ let cusProduct = fullCus.customer_products.find(
+ (cp) => cp.product_id === productId,
+ );
- let stripeSubs = await getStripeSubs({
- stripeCli,
- subIds: cusProduct?.subscription_ids,
- });
+ let stripeSubs = await getStripeSubs({
+ stripeCli,
+ subIds: cusProduct?.subscription_ids,
+ });
- let subItems = stripeSubs.flatMap((sub) => sub.items.data);
- let prices = cusProductToPrices({ cusProduct: cusProduct! });
+ let subItems = stripeSubs.flatMap((sub) => sub.items.data);
+ let prices = cusProductToPrices({ cusProduct: cusProduct! });
- let contPrice = findContUsePrice({ prices });
+ let contPrice = findContUsePrice({ prices });
- let subItem = findStripeItemForPrice({
- price: contPrice!,
- stripeItems: subItems,
- });
+ let subItem = findStripeItemForPrice({
+ price: contPrice!,
+ stripeItems: subItems,
+ });
- expect(subItem).to.exist;
+ expect(subItem).to.exist;
- expect(subItem!.quantity).to.equal(
- notNullish(itemQuantity) ? itemQuantity : usage
- );
+ expect(subItem!.quantity).to.equal(
+ notNullish(itemQuantity) ? itemQuantity : usage,
+ );
- // Check num replaceables correct
- let cusEnts = cusProduct?.customer_entitlements;
- let cusEnt = cusEnts?.find((ent) => ent.feature_id === TestFeature.Users);
+ // Check num replaceables correct
+ let cusEnts = cusProduct?.customer_entitlements;
+ let cusEnt = cusEnts?.find((ent) => ent.feature_id === TestFeature.Users);
- expect(cusEnt).to.exist;
- expect(cusEnt?.replaceables.length).to.equal(numReplaceables);
+ expect(cusEnt).to.exist;
+ expect(cusEnt?.replaceables.length).to.equal(numReplaceables);
- let expectedBalance = cusEnt!.entitlement.allowance! - usage;
- expect(cusEnt!.balance).to.equal(expectedBalance);
+ let expectedBalance = cusEnt!.entitlement.allowance! - usage;
+ expect(cusEnt!.balance).to.equal(expectedBalance);
- return {
- fullCus,
- cusProduct,
- stripeSubs,
- };
+ return {
+ fullCus,
+ cusProduct,
+ stripeSubs,
+ };
};
export const expectUpcomingItemsCorrect = async ({
- stripeCli,
- fullCus,
- stripeSubs,
- curUnix,
- unitPrice,
- expectedNumItems = 1,
- quantity,
+ stripeCli,
+ fullCus,
+ stripeSubs,
+ curUnix,
+ unitPrice,
+ expectedNumItems = 1,
+ quantity,
}: {
- stripeCli: Stripe;
- fullCus: FullCustomer;
- stripeSubs: Stripe.Subscription[];
- curUnix: number;
- unitPrice: number;
- expectedNumItems: number;
- quantity: number;
+ stripeCli: Stripe;
+ fullCus: FullCustomer;
+ stripeSubs: Stripe.Subscription[];
+ curUnix: number;
+ unitPrice: number;
+ expectedNumItems: number;
+ quantity: number;
}) => {
- let sub = stripeSubs[0];
- // let upcomingLines = await stripeCli.invoices.listUpcomingLines({
- // subscription: sub.id,
- // });
- // const pendingItems = await stripeCli.invoiceItems.list({
- // pending: true,
- // });
+ let sub = stripeSubs[0];
+ // let upcomingLines = await stripeCli.invoices.listUpcomingLines({
+ // subscription: sub.id,
+ // });
+ // const pendingItems = await stripeCli.invoiceItems.list({
+ // pending: true,
+ // });
- const lineItems = await stripeCli.invoiceItems.list({
- customer: sub.customer as string,
- });
+ const lineItems = await stripeCli.invoiceItems.list({
+ customer: sub.customer as string,
+ });
- const { start, end } = subToPeriodStartEnd({ sub });
+ const { start, end } = subToPeriodStartEnd({ sub });
- let amount = quantity * unitPrice!;
+ let amount = quantity * unitPrice!;
- let proratedAmount = calculateProrationAmount({
- amount,
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- allowNegative: true,
- });
+ let proratedAmount = calculateProrationAmount({
+ amount,
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ allowNegative: true,
+ });
- // console.group();
- // console.group("Upcoming lines");
- // for (const line of lines) {
- // console.log(line.description, line.amount / 100);
- // }
- // console.groupEnd();
- // console.groupEnd();
+ // console.group();
+ // console.group("Upcoming lines");
+ // for (const line of lines) {
+ // console.log(line.description, line.amount / 100);
+ // }
+ // console.groupEnd();
+ // console.groupEnd();
- const firstItem = lineItems.data[0];
- expect(firstItem.amount).to.equal(Math.round(proratedAmount * 100));
+ const firstItem = lineItems.data[0];
+ expect(firstItem.amount).to.equal(Math.round(proratedAmount * 100));
};
export const calcProrationAndExpectInvoice = async ({
- autumn,
- stripeSubs,
- customerId,
- quantity,
- unitPrice,
- curUnix,
- numInvoices,
+ autumn,
+ stripeSubs,
+ customerId,
+ quantity,
+ unitPrice,
+ curUnix,
+ numInvoices,
}: {
- autumn: AutumnInt;
- stripeSubs: Stripe.Subscription[];
- customerId: string;
- quantity: number;
- unitPrice: number;
- curUnix: number;
- numInvoices: number;
+ autumn: AutumnInt;
+ stripeSubs: Stripe.Subscription[];
+ customerId: string;
+ quantity: number;
+ unitPrice: number;
+ curUnix: number;
+ numInvoices: number;
}) => {
- let customer = await autumn.customers.get(customerId);
- let invoices = customer.invoices;
+ let customer = await autumn.customers.get(customerId);
+ let invoices = customer.invoices;
- let sub = stripeSubs[0];
- let amount = quantity * unitPrice;
- const { start, end } = subToPeriodStartEnd({ sub });
- let proratedAmount = calculateProrationAmount({
- amount,
- periodStart: start * 1000,
- periodEnd: end * 1000,
- now: curUnix,
- allowNegative: true,
- });
+ let sub = stripeSubs[0];
+ let amount = quantity * unitPrice;
+ const { start, end } = subToPeriodStartEnd({ sub });
+ let proratedAmount = calculateProrationAmount({
+ amount,
+ periodStart: start * 1000,
+ periodEnd: end * 1000,
+ now: curUnix,
+ allowNegative: true,
+ });
- proratedAmount = Number(proratedAmount.toFixed(2));
+ proratedAmount = Number(proratedAmount.toFixed(2));
- expect(invoices.length).to.equal(
- numInvoices,
- `Should have ${numInvoices} invoices`
- );
- expect(invoices[0].total).to.equal(
- proratedAmount,
- "Latest invoice should be equals to calculated prorated amount"
- );
+ expect(invoices.length).to.equal(
+ numInvoices,
+ `Should have ${numInvoices} invoices`,
+ );
+ expect(invoices[0].total).to.equal(
+ proratedAmount,
+ "Latest invoice should be equals to calculated prorated amount",
+ );
};
diff --git a/server/tests/utils/expectUtils/expectErrUtils.ts b/server/tests/utils/expectUtils/expectErrUtils.ts
index a8b7d3dcb..636b3397b 100644
--- a/server/tests/utils/expectUtils/expectErrUtils.ts
+++ b/server/tests/utils/expectUtils/expectErrUtils.ts
@@ -3,36 +3,36 @@ import { ErrCode } from "@autumn/shared";
import AutumnError from "@/external/autumn/autumnCli.js";
export const expectAutumnError = async ({
- errCode,
- errMessage,
- func,
+ errCode,
+ errMessage,
+ func,
}: {
- errCode?: string;
- errMessage?: string;
- func: () => Promise;
+ errCode?: string;
+ errMessage?: string;
+ func: () => Promise;
}) => {
- try {
- let result = await func();
+ try {
+ let result = await func();
- assert.fail(
- `Expected to receive autumn error ${errCode}, but received none`,
- );
- } catch (error: any) {
- // 1. Expect error to be instance of AutumnError
+ assert.fail(
+ `Expected to receive autumn error ${errCode}, but received none`,
+ );
+ } catch (error: any) {
+ // 1. Expect error to be instance of AutumnError
- expect(error, "Error should be instance of AutumnError").to.be.instanceOf(
- AutumnError,
- );
+ expect(error, "Error should be instance of AutumnError").to.be.instanceOf(
+ AutumnError,
+ );
- if (errMessage) {
- expect(error.message, `Error message should be ${errMessage}`).to.equal(
- errMessage,
- );
- }
+ if (errMessage) {
+ expect(error.message, `Error message should be ${errMessage}`).to.equal(
+ errMessage,
+ );
+ }
- if (errCode) {
- // 2. Expect error code to be the same as the one passed in
- expect(error.code, `Error code should be ${errCode}`).to.equal(errCode);
- }
- }
+ if (errCode) {
+ // 2. Expect error code to be the same as the one passed in
+ expect(error.code, `Error code should be ${errCode}`).to.equal(errCode);
+ }
+ }
};
diff --git a/server/tests/utils/expectUtils/expectFeaturesCorrect.ts b/server/tests/utils/expectUtils/expectFeaturesCorrect.ts
index 08cf49a71..13b0c0681 100644
--- a/server/tests/utils/expectUtils/expectFeaturesCorrect.ts
+++ b/server/tests/utils/expectUtils/expectFeaturesCorrect.ts
@@ -2,105 +2,105 @@ import { nullish } from "@/utils/genUtils.js";
import { expect } from "chai";
import { notNullish } from "@/utils/genUtils.js";
import {
- CreateEntity,
- FeatureOptions,
- FeatureType,
- Infinite,
- ProductV2,
+ CreateEntity,
+ FeatureOptions,
+ FeatureType,
+ Infinite,
+ ProductV2,
} from "@autumn/shared";
import { Customer, Entity } from "autumn-js";
export const expectFeaturesCorrect = ({
- customer,
- product,
- otherProducts,
- productQuantity,
- options,
- usage,
- entities,
+ customer,
+ product,
+ otherProducts,
+ productQuantity,
+ options,
+ usage,
+ entities,
}: {
- customer: Customer | Entity;
- product: ProductV2;
- otherProducts?: ProductV2[];
- productQuantity?: number;
- options?: FeatureOptions[];
- usage?: {
- featureId: string;
- value: number;
- }[];
- entities?: CreateEntity[];
+ customer: Customer | Entity;
+ product: ProductV2;
+ otherProducts?: ProductV2[];
+ productQuantity?: number;
+ options?: FeatureOptions[];
+ usage?: {
+ featureId: string;
+ value: number;
+ }[];
+ entities?: CreateEntity[];
}) => {
- const items = product.items;
+ const items = product.items;
- const featureIds = Array.from(
- new Set(product.items.map((i) => i.feature_id))
- ).filter(notNullish);
+ const featureIds = Array.from(
+ new Set(product.items.map((i) => i.feature_id)),
+ ).filter(notNullish);
- const otherItems = otherProducts?.flatMap((p) => p.items) || [];
+ const otherItems = otherProducts?.flatMap((p) => p.items) || [];
- for (const featureId of featureIds) {
- let includedUsage: string | number = 0;
+ for (const featureId of featureIds) {
+ let includedUsage: string | number = 0;
- let item = items.find((i) => i.feature_id === featureId)!;
- expect(item, `Item ${featureId} exists`).to.exist;
+ let item = items.find((i) => i.feature_id === featureId)!;
+ expect(item, `Item ${featureId} exists`).to.exist;
- if (item.included_usage === undefined) continue;
+ if (item.included_usage === undefined) continue;
- for (const item of [...items, ...otherItems]) {
- if (item.feature_id !== featureId) continue;
- if (item.included_usage == Infinite) {
- includedUsage = Infinite;
- break;
- }
+ for (const item of [...items, ...otherItems]) {
+ if (item.feature_id !== featureId) continue;
+ if (item.included_usage == Infinite) {
+ includedUsage = Infinite;
+ break;
+ }
- let numEntities =
- entities?.filter((e) => e.feature_id === item.entity_feature_id)
- .length || 1;
+ let numEntities =
+ entities?.filter((e) => e.feature_id === item.entity_feature_id)
+ .length || 1;
- includedUsage +=
- (item.included_usage || 0) * numEntities * (productQuantity || 1);
- }
+ includedUsage +=
+ (item.included_usage || 0) * numEntities * (productQuantity || 1);
+ }
- for (const option of options || []) {
- if (option.feature_id !== featureId) continue;
- if (option.feature_id) {
- (includedUsage as number) += option.quantity;
- }
- }
+ for (const option of options || []) {
+ if (option.feature_id !== featureId) continue;
+ if (option.feature_id) {
+ (includedUsage as number) += option.quantity;
+ }
+ }
- const feature = customer.features[featureId!];
+ const feature = customer.features[featureId!];
- expect(feature, `Feature ${featureId} exists`).to.exist;
+ expect(feature, `Feature ${featureId} exists`).to.exist;
- // @ts-ignore
+ // @ts-ignore
- // 1. Check that included usage matches
- expect(
- feature.included_usage,
- `Feature ${featureId} included usage is correct`
- ).to.equal(includedUsage);
+ // 1. Check that included usage matches
+ expect(
+ feature.included_usage,
+ `Feature ${featureId} included usage is correct`,
+ ).to.equal(includedUsage);
- // 2. Check that unlimited is set correctly
- if (item?.included_usage == Infinite) {
- expect(feature.unlimited, `Feature ${featureId} is unlimited`).to.be.true;
- } else {
- expect(
- feature.unlimited == false || nullish(feature.unlimited),
- `Feature ${featureId} is not unlimited`
- );
- }
+ // 2. Check that unlimited is set correctly
+ if (item?.included_usage == Infinite) {
+ expect(feature.unlimited, `Feature ${featureId} is unlimited`).to.be.true;
+ } else {
+ expect(
+ feature.unlimited == false || nullish(feature.unlimited),
+ `Feature ${featureId} is not unlimited`,
+ );
+ }
- // 3. Check that usage is correct...
- let featureUsage =
- usage?.reduce((acc, curr) => {
- if (curr.featureId === featureId) {
- acc += curr.value;
- }
- return acc;
- }, 0) || 0;
+ // 3. Check that usage is correct...
+ let featureUsage =
+ usage?.reduce((acc, curr) => {
+ if (curr.featureId === featureId) {
+ acc += curr.value;
+ }
+ return acc;
+ }, 0) || 0;
- expect(feature.usage, `Feature ${featureId} usage is correct`).to.equal(
- featureUsage
- );
- }
+ expect(feature.usage, `Feature ${featureId} usage is correct`).to.equal(
+ featureUsage,
+ );
+ }
};
diff --git a/server/tests/utils/expectUtils/expectInvoiceUtils.ts b/server/tests/utils/expectUtils/expectInvoiceUtils.ts
index 07b028937..d5791386b 100644
--- a/server/tests/utils/expectUtils/expectInvoiceUtils.ts
+++ b/server/tests/utils/expectUtils/expectInvoiceUtils.ts
@@ -3,91 +3,91 @@ import { cusProductToPrices, cusProductToEnts } from "@autumn/shared";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import {
- Organization,
- BillingInterval,
- UsagePriceConfig,
+ Organization,
+ BillingInterval,
+ UsagePriceConfig,
} from "@autumn/shared";
import { AppEnv } from "autumn-js";
import { Decimal } from "decimal.js";
import Stripe from "stripe";
import { getSubsFromCusId } from "./expectSubUtils.js";
import {
- isArrearPrice,
- isFixedPrice,
+ isArrearPrice,
+ isFixedPrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
export const getExpectedInvoiceTotal = async ({
- customerId,
- productId,
- usage,
- stripeCli,
- db,
- org,
- env,
- onlyIncludeMonthly = false,
- onlyIncludeUsage = false,
- onlyIncludeArrear = false,
- expectExpired = false,
+ customerId,
+ productId,
+ usage,
+ stripeCli,
+ db,
+ org,
+ env,
+ onlyIncludeMonthly = false,
+ onlyIncludeUsage = false,
+ onlyIncludeArrear = false,
+ expectExpired = false,
}: {
- customerId: string;
- productId: string;
- usage: {
- featureId: string;
- entityFeatureId?: string;
- value: number;
- }[];
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- onlyIncludeMonthly?: boolean;
- onlyIncludeUsage?: boolean;
- onlyIncludeArrear?: boolean;
- expectExpired?: boolean;
+ customerId: string;
+ productId: string;
+ usage: {
+ featureId: string;
+ entityFeatureId?: string;
+ value: number;
+ }[];
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ onlyIncludeMonthly?: boolean;
+ onlyIncludeUsage?: boolean;
+ onlyIncludeArrear?: boolean;
+ expectExpired?: boolean;
}) => {
- const { cusProduct } = await getSubsFromCusId({
- stripeCli,
- customerId,
- productId,
- db,
- org,
- env,
- withExpired: expectExpired,
- });
+ const { cusProduct } = await getSubsFromCusId({
+ stripeCli,
+ customerId,
+ productId,
+ db,
+ org,
+ env,
+ withExpired: expectExpired,
+ });
- const prices = cusProductToPrices({ cusProduct });
- const ents = cusProductToEnts({ cusProduct });
+ const prices = cusProductToPrices({ cusProduct });
+ const ents = cusProductToEnts({ cusProduct });
- let total = new Decimal(0);
- for (const price of prices) {
- if (onlyIncludeMonthly && price.config.interval != BillingInterval.Month) {
- continue;
- }
+ let total = new Decimal(0);
+ for (const price of prices) {
+ if (onlyIncludeMonthly && price.config.interval != BillingInterval.Month) {
+ continue;
+ }
- if (onlyIncludeUsage && isFixedPrice({ price })) continue;
+ if (onlyIncludeUsage && isFixedPrice({ price })) continue;
- if (onlyIncludeArrear && !isArrearPrice({ price })) continue;
+ if (onlyIncludeArrear && !isArrearPrice({ price })) continue;
- const config = price.config as UsagePriceConfig;
- const featureId = config.feature_id;
- const ent = getPriceEntitlement(price, ents);
+ const config = price.config as UsagePriceConfig;
+ const featureId = config.feature_id;
+ const ent = getPriceEntitlement(price, ents);
- const usageAmount = usage.find(
- (u) =>
- u.featureId == featureId &&
- (u.entityFeatureId ? u.entityFeatureId == ent.entity_feature_id : true)
- )?.value;
+ const usageAmount = usage.find(
+ (u) =>
+ u.featureId == featureId &&
+ (u.entityFeatureId ? u.entityFeatureId == ent.entity_feature_id : true),
+ )?.value;
- const overage =
- usageAmount && ent.allowance ? usageAmount - ent.allowance : usageAmount;
+ const overage =
+ usageAmount && ent.allowance ? usageAmount - ent.allowance : usageAmount;
- const invoiceAmt = priceToInvoiceAmount({
- price,
- overage,
- });
+ const invoiceAmt = priceToInvoiceAmount({
+ price,
+ overage,
+ });
- total = total.plus(invoiceAmt);
- }
+ total = total.plus(invoiceAmt);
+ }
- return total.toNumber();
+ return total.toNumber();
};
diff --git a/server/tests/utils/expectUtils/expectMultiAttach.ts b/server/tests/utils/expectUtils/expectMultiAttach.ts
index 2fc9a961a..efa92e0e7 100644
--- a/server/tests/utils/expectUtils/expectMultiAttach.ts
+++ b/server/tests/utils/expectUtils/expectMultiAttach.ts
@@ -1,21 +1,21 @@
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
- APIVersion,
- AppEnv,
- AttachBranch,
- CreateEntity,
- CreateReward,
- CusProductStatus,
- FeatureOptions,
- Organization,
- ProductOptions,
- ProductV2,
+ APIVersion,
+ AppEnv,
+ AttachBranch,
+ CreateEntity,
+ CreateReward,
+ CusProductStatus,
+ FeatureOptions,
+ Organization,
+ ProductOptions,
+ ProductV2,
} from "@autumn/shared";
import {
- getAttachTotal,
- getCurrentOptions,
+ getAttachTotal,
+ getCurrentOptions,
} from "tests/utils/testAttachUtils/testAttachUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectInvoicesCorrect } from "tests/utils/expectUtils/expectProductAttached.js";
@@ -33,126 +33,126 @@ import { Decimal } from "decimal.js";
import { completeInvoiceCheckout } from "../stripeUtils/completeInvoiceCheckout.js";
export const expectMultiAttachCorrect = async ({
- autumn,
- customerId,
- entityId,
- products,
- results,
- rewards,
- expectedRewards,
- attachParams,
- db,
- org,
- env,
+ autumn,
+ customerId,
+ entityId,
+ products,
+ results,
+ rewards,
+ expectedRewards,
+ attachParams,
+ db,
+ org,
+ env,
}: {
- autumn?: AutumnInt;
- customerId: string;
- entityId?: string;
- products: ProductOptions[];
- results: {
- product: ProductV2;
- quantity: number;
- status: CusProductStatus;
- entityId?: string;
- }[];
- rewards?: string[];
- expectedRewards?: string[];
- attachParams?: any;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
+ autumn?: AutumnInt;
+ customerId: string;
+ entityId?: string;
+ products: ProductOptions[];
+ results: {
+ product: ProductV2;
+ quantity: number;
+ status: CusProductStatus;
+ entityId?: string;
+ }[];
+ rewards?: string[];
+ expectedRewards?: string[];
+ attachParams?: any;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
}) => {
- autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 });
- const checkoutRes = await autumn.checkout({
- customer_id: customerId,
- products: products,
- entity_id: entityId,
- // @ts-ignore
- reward: rewards,
- ...attachParams,
- });
+ autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 });
+ const checkoutRes = await autumn.checkout({
+ customer_id: customerId,
+ products: products,
+ entity_id: entityId,
+ // @ts-ignore
+ reward: rewards,
+ ...attachParams,
+ });
- const attachRes = await autumn.attach({
- customer_id: customerId,
- products: products,
- entity_id: entityId,
- // @ts-ignore
- reward: rewards,
- ...attachParams,
- });
+ const attachRes = await autumn.attach({
+ customer_id: customerId,
+ products: products,
+ entity_id: entityId,
+ // @ts-ignore
+ reward: rewards,
+ ...attachParams,
+ });
- if (attachRes.checkout_url) {
- if (attachParams?.invoice) {
- await completeInvoiceCheckout({
- url: attachRes.checkout_url,
- isLocal: true,
- });
- }
- await completeCheckoutForm(attachRes.checkout_url);
- await timeout(5000);
- }
+ if (attachRes.checkout_url) {
+ if (attachParams?.invoice) {
+ await completeInvoiceCheckout({
+ url: attachRes.checkout_url,
+ isLocal: true,
+ });
+ }
+ await completeCheckoutForm(attachRes.checkout_url);
+ await timeout(5000);
+ }
- for (const result of results) {
- let customer;
- if (result.entityId) {
- customer = await autumn.entities.get(customerId, result.entityId);
- } else {
- customer = await autumn.customers.get(customerId);
- }
+ for (const result of results) {
+ let customer;
+ if (result.entityId) {
+ customer = await autumn.entities.get(customerId, result.entityId);
+ } else {
+ customer = await autumn.customers.get(customerId);
+ }
- expectProductAttached({
- customer,
- product: result.product,
- status: result.status,
- entityId: result.entityId,
- });
- }
+ expectProductAttached({
+ customer,
+ product: result.product,
+ status: result.status,
+ entityId: result.entityId,
+ });
+ }
- const customer = await autumn.customers.get(customerId);
- const latestInvoice = customer.invoices[0];
- expect(latestInvoice.total).to.equal(checkoutRes.total);
+ const customer = await autumn.customers.get(customerId);
+ const latestInvoice = customer.invoices[0];
+ expect(latestInvoice.total).to.equal(checkoutRes.total);
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- rewards: expectedRewards,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ rewards: expectedRewards,
+ });
- return {
- checkoutRes,
- };
+ return {
+ checkoutRes,
+ };
};
export const expectResultsCorrect = async ({
- autumn,
- customerId,
- results,
+ autumn,
+ customerId,
+ results,
}: {
- autumn?: AutumnInt;
- customerId: string;
- results: {
- product: ProductV2;
- quantity: number;
- status: CusProductStatus;
- entityId?: string;
- }[];
+ autumn?: AutumnInt;
+ customerId: string;
+ results: {
+ product: ProductV2;
+ quantity: number;
+ status: CusProductStatus;
+ entityId?: string;
+ }[];
}) => {
- autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 });
- for (const result of results) {
- let customer;
- if (result.entityId) {
- customer = await autumn.entities.get(customerId, result.entityId);
- } else {
- customer = await autumn.customers.get(customerId);
- }
+ autumn = autumn || new AutumnInt({ version: APIVersion.v1_2 });
+ for (const result of results) {
+ let customer;
+ if (result.entityId) {
+ customer = await autumn.entities.get(customerId, result.entityId);
+ } else {
+ customer = await autumn.customers.get(customerId);
+ }
- expectProductAttached({
- customer,
- product: result.product,
- status: result.status,
- quantity: result.quantity,
- });
- }
+ expectProductAttached({
+ customer,
+ product: result.product,
+ status: result.status,
+ quantity: result.quantity,
+ });
+ }
};
diff --git a/server/tests/utils/expectUtils/expectProductAttached.ts b/server/tests/utils/expectUtils/expectProductAttached.ts
index d64da0d09..c88fcc2ef 100644
--- a/server/tests/utils/expectUtils/expectProductAttached.ts
+++ b/server/tests/utils/expectUtils/expectProductAttached.ts
@@ -109,10 +109,12 @@ export const expectAddOnAttached = ({
productId,
status,
}: {
- customer: Customer & { add_ons: {
- id: string;
- status: CusProductStatus;
- }[] };
+ customer: Customer & {
+ add_ons: {
+ id: string;
+ status: CusProductStatus;
+ }[];
+ };
productId: string;
status?: CusProductStatus;
}) => {
@@ -120,9 +122,12 @@ export const expectAddOnAttached = ({
expect(addOn, `add on ${productId} is attached`).to.exist;
if (status) {
- expect(addOn?.status).to.equal(status, `add on ${productId} should have status ${status}`);
+ expect(addOn?.status).to.equal(
+ status,
+ `add on ${productId} should have status ${status}`,
+ );
}
-}
+};
export const expectInvoicesCorrect = ({
customer,
diff --git a/server/tests/utils/expectUtils/expectScheduleUtils.ts b/server/tests/utils/expectUtils/expectScheduleUtils.ts
index d16c543f6..282cfd586 100644
--- a/server/tests/utils/expectUtils/expectScheduleUtils.ts
+++ b/server/tests/utils/expectUtils/expectScheduleUtils.ts
@@ -6,14 +6,14 @@ import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import {
- AppEnv,
- AttachBranch,
- AttachPreview,
- CusProductStatus,
- FullCusProduct,
- FullCustomer,
- Organization,
- ProductV2,
+ AppEnv,
+ AttachBranch,
+ AttachPreview,
+ CusProductStatus,
+ FullCusProduct,
+ FullCustomer,
+ Organization,
+ ProductV2,
} from "@autumn/shared";
import { expect } from "chai";
import Stripe from "stripe";
@@ -26,213 +26,213 @@ import { addHours } from "date-fns";
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
export const expectNextCycleCorrect = async ({
- autumn,
- preview,
- stripeCli,
- customerId,
- testClockId,
- product,
- db,
- org,
- env,
- advanceClock = true,
+ autumn,
+ preview,
+ stripeCli,
+ customerId,
+ testClockId,
+ product,
+ db,
+ org,
+ env,
+ advanceClock = true,
}: {
- autumn: AutumnInt;
- preview: AttachPreview;
- stripeCli: Stripe;
- customerId: string;
- testClockId: string;
- product: ProductV2;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- advanceClock?: boolean;
+ autumn: AutumnInt;
+ preview: AttachPreview;
+ stripeCli: Stripe;
+ customerId: string;
+ testClockId: string;
+ product: ProductV2;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ advanceClock?: boolean;
}) => {
- if (advanceClock) {
- await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- preview!.due_next_cycle.due_at,
- hoursToFinalizeInvoice
- ).getTime(),
- });
- }
+ if (advanceClock) {
+ await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ preview!.due_next_cycle.due_at,
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ });
+ }
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- expectProductAttached({
- customer,
- product,
- });
+ expectProductAttached({
+ customer,
+ product,
+ });
- await expectSubItemsCorrect({
- stripeCli,
- customerId,
- product,
- db,
- org,
- env,
- });
+ await expectSubItemsCorrect({
+ stripeCli,
+ customerId,
+ product,
+ db,
+ org,
+ env,
+ });
};
export const expectDowngradeCorrect = async ({
- customerId,
- curProduct,
- newProduct,
- autumn,
- stripeCli,
- db,
- org,
- env,
+ customerId,
+ curProduct,
+ newProduct,
+ autumn,
+ stripeCli,
+ db,
+ org,
+ env,
}: {
- customerId: string;
- curProduct: ProductV2;
- newProduct: ProductV2;
- autumn: AutumnInt;
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
+ customerId: string;
+ curProduct: ProductV2;
+ newProduct: ProductV2;
+ autumn: AutumnInt;
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
}) => {
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: newProduct.id,
- });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: newProduct.id,
+ });
- await autumn.attach({
- customer_id: customerId,
- product_id: newProduct.id,
- });
+ await autumn.attach({
+ customer_id: customerId,
+ product_id: newProduct.id,
+ });
- const customer = await autumn.customers.get(customerId);
+ const customer = await autumn.customers.get(customerId);
- const productCount = customer.products.reduce((acc: number, product: any) => {
- if (product.group == curProduct.group) {
- return acc + 1;
- } else return acc;
- }, 0);
+ const productCount = customer.products.reduce((acc: number, product: any) => {
+ if (product.group == curProduct.group) {
+ return acc + 1;
+ } else return acc;
+ }, 0);
- expect(
- productCount,
- "customer should only have 2 products (from this group)"
- ).to.equal(2);
+ expect(
+ productCount,
+ "customer should only have 2 products (from this group)",
+ ).to.equal(2);
- expectProductAttached({
- customer,
- product: curProduct,
- isCanceled: true,
- });
+ expectProductAttached({
+ customer,
+ product: curProduct,
+ isCanceled: true,
+ });
- // const { fullCus } = await expectSubItemsCorrect({
- // stripeCli,
- // customerId,
- // product: curProduct,
- // db,
- // org,
- // env,
- // subCanceled: isFreeProductV2({ product: newProduct }),
- // isCanceled: true,
- // });
+ // const { fullCus } = await expectSubItemsCorrect({
+ // stripeCli,
+ // customerId,
+ // product: curProduct,
+ // db,
+ // org,
+ // env,
+ // subCanceled: isFreeProductV2({ product: newProduct }),
+ // isCanceled: true,
+ // });
- const newProductIsFree = isFreeProductV2({ product: newProduct });
+ const newProductIsFree = isFreeProductV2({ product: newProduct });
- if (newProductIsFree) {
- // let res = await stripeCli.subscriptionSchedules.list({
- // customer: fullCus.processor?.id,
- // });
- // let data = res.data.filter((s) => s.status != "canceled");
- // expect(data.length, "should have no sub schedules").to.equal(0);
- // await expectSubScheduleCorrect({
- // stripeCli,
- // customerId,
- // productId: newProduct.id,
- // db,
- // org,
- // env,
- // });
- }
- expectProductAttached({
- customer,
- product: newProduct,
- status: CusProductStatus.Scheduled,
- });
+ if (newProductIsFree) {
+ // let res = await stripeCli.subscriptionSchedules.list({
+ // customer: fullCus.processor?.id,
+ // });
+ // let data = res.data.filter((s) => s.status != "canceled");
+ // expect(data.length, "should have no sub schedules").to.equal(0);
+ // await expectSubScheduleCorrect({
+ // stripeCli,
+ // customerId,
+ // productId: newProduct.id,
+ // db,
+ // org,
+ // env,
+ // });
+ }
+ expectProductAttached({
+ customer,
+ product: newProduct,
+ status: CusProductStatus.Scheduled,
+ });
- await expectSubToBeCorrect({
- db,
- customerId,
- org,
- env,
- shouldBeCanceled: newProductIsFree,
- });
+ await expectSubToBeCorrect({
+ db,
+ customerId,
+ org,
+ env,
+ shouldBeCanceled: newProductIsFree,
+ });
- expect(preview.branch).to.equal(AttachBranch.Downgrade);
+ expect(preview.branch).to.equal(AttachBranch.Downgrade);
- return {
- preview,
- };
+ return {
+ preview,
+ };
};
export const expectSubScheduleCorrect = async ({
- stripeCli,
- customerId,
- productId,
- db,
- org,
- env,
- fullCus,
+ stripeCli,
+ customerId,
+ productId,
+ db,
+ org,
+ env,
+ fullCus,
}: {
- stripeCli: Stripe;
- customerId: string;
- productId: string;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- fullCus?: FullCustomer;
+ stripeCli: Stripe;
+ customerId: string;
+ productId: string;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ fullCus?: FullCustomer;
}) => {
- // 1. Check schedule
- if (!fullCus) {
- fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
- }
+ // 1. Check schedule
+ if (!fullCus) {
+ fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
+ }
- const cusProduct = fullCus.customer_products.find(
- (cp: FullCusProduct) => cp.product.id == productId
- )!;
+ const cusProduct = fullCus.customer_products.find(
+ (cp: FullCusProduct) => cp.product.id == productId,
+ )!;
- const scheduleSets = await getStripeSchedules({
- stripeCli,
- scheduleIds: cusProduct?.scheduled_ids || [],
- });
+ const scheduleSets = await getStripeSchedules({
+ stripeCli,
+ scheduleIds: cusProduct?.scheduled_ids || [],
+ });
- const stripePrices = scheduleSets.flatMap(
- (schedule) => schedule?.prices || []
- );
+ const stripePrices = scheduleSets.flatMap(
+ (schedule) => schedule?.prices || [],
+ );
- const autumnPrices = cusProductToPrices({ cusProduct });
+ const autumnPrices = cusProductToPrices({ cusProduct });
- let missingUsageCount = 0;
+ let missingUsageCount = 0;
- for (const autumnPrice of autumnPrices) {
- const stripePrice = findStripePriceFromPrices({
- stripePrices,
- autumnPrice,
- });
+ for (const autumnPrice of autumnPrices) {
+ const stripePrice = findStripePriceFromPrices({
+ stripePrices,
+ autumnPrice,
+ });
- if (isV4Usage({ price: autumnPrice!, cusProduct })) {
- missingUsageCount++;
- continue;
- } else {
- expect(stripePrice).to.exist;
- }
- }
+ if (isV4Usage({ price: autumnPrice!, cusProduct })) {
+ missingUsageCount++;
+ continue;
+ } else {
+ expect(stripePrice).to.exist;
+ }
+ }
- expect(
- stripePrices.length,
- "number of schedule items equivalent to number of autumn prices"
- ).to.equal(autumnPrices.length - missingUsageCount);
+ expect(
+ stripePrices.length,
+ "number of schedule items equivalent to number of autumn prices",
+ ).to.equal(autumnPrices.length - missingUsageCount);
};
diff --git a/server/tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.ts b/server/tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.ts
index 9dcb4156e..fb361614a 100644
--- a/server/tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.ts
+++ b/server/tests/utils/expectUtils/expectSingleUse/expectUsageInvoice.ts
@@ -16,64 +16,64 @@ import { expect } from "chai";
// });
export const expectInvoiceAfterUsage = async ({
- autumn,
- customerId,
- entityId,
- featureId,
- product,
- usage,
- stripeCli,
- db,
- org,
- env,
- numInvoices = 2,
- expectExpired = false,
+ autumn,
+ customerId,
+ entityId,
+ featureId,
+ product,
+ usage,
+ stripeCli,
+ db,
+ org,
+ env,
+ numInvoices = 2,
+ expectExpired = false,
}: {
- autumn: AutumnInt;
- customerId: string;
- entityId: string;
- featureId: string;
- product: ProductV2;
- usage: number;
- stripeCli: Stripe;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- numInvoices?: number;
- expectExpired?: boolean;
+ autumn: AutumnInt;
+ customerId: string;
+ entityId: string;
+ featureId: string;
+ product: ProductV2;
+ usage: number;
+ stripeCli: Stripe;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ numInvoices?: number;
+ expectExpired?: boolean;
}) => {
- let entity = await autumn.entities.get(customerId, entityId);
+ let entity = await autumn.entities.get(customerId, entityId);
- if (expectExpired) {
- let matchingProduct = entity.products.find((p: any) => p.id === product.id);
- expect(matchingProduct).to.not.exist;
- } else {
- expectFeaturesCorrect({
- customer: entity,
- product,
- });
- }
+ if (expectExpired) {
+ let matchingProduct = entity.products.find((p: any) => p.id === product.id);
+ expect(matchingProduct).to.not.exist;
+ } else {
+ expectFeaturesCorrect({
+ customer: entity,
+ product,
+ });
+ }
- const invoiceTotal = await getExpectedInvoiceTotal({
- org,
- env,
- customerId,
- productId: product.id,
- stripeCli,
- db,
- usage: [
- {
- featureId,
- value: usage,
- },
- ],
- onlyIncludeMonthly: true,
- onlyIncludeUsage: true,
- expectExpired,
- });
+ const invoiceTotal = await getExpectedInvoiceTotal({
+ org,
+ env,
+ customerId,
+ productId: product.id,
+ stripeCli,
+ db,
+ usage: [
+ {
+ featureId,
+ value: usage,
+ },
+ ],
+ onlyIncludeMonthly: true,
+ onlyIncludeUsage: true,
+ expectExpired,
+ });
- let invoices = entity.invoices;
+ let invoices = entity.invoices;
- expect(invoices.length).to.equal(numInvoices);
- expect(invoices[0].total).to.equal(invoiceTotal);
+ expect(invoices.length).to.equal(numInvoices);
+ expect(invoices[0].total).to.equal(invoiceTotal);
};
diff --git a/server/tests/utils/expectUtils/expectSubUtils.ts b/server/tests/utils/expectUtils/expectSubUtils.ts
index 9921b9025..b9bfb7851 100644
--- a/server/tests/utils/expectUtils/expectSubUtils.ts
+++ b/server/tests/utils/expectUtils/expectSubUtils.ts
@@ -1,8 +1,8 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import {
- findStripeItemForPrice,
- isLicenseItem,
+ findStripeItemForPrice,
+ isLicenseItem,
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
@@ -11,13 +11,13 @@ import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import {
- AppEnv,
- BillingType,
- CusProductStatus,
- FullCusProduct,
- Organization,
- ProductV2,
- UsagePriceConfig,
+ AppEnv,
+ BillingType,
+ CusProductStatus,
+ FullCusProduct,
+ Organization,
+ ProductV2,
+ UsagePriceConfig,
} from "@autumn/shared";
import { expect } from "chai";
import { getDate } from "date-fns";
@@ -25,51 +25,51 @@ import { getDate } from "date-fns";
import Stripe from "stripe";
export const getSubsFromCusId = async ({
- stripeCli,
- customerId,
- productId,
- db,
- org,
- env,
- withExpired = false,
+ stripeCli,
+ customerId,
+ productId,
+ db,
+ org,
+ env,
+ withExpired = false,
}: {
- stripeCli: Stripe;
- customerId: string;
- productId: string;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- withExpired?: boolean;
+ stripeCli: Stripe;
+ customerId: string;
+ productId: string;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ withExpired?: boolean;
}) => {
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- inStatuses: withExpired
- ? [
- CusProductStatus.Active,
- CusProductStatus.Expired,
- CusProductStatus.PastDue,
- CusProductStatus.Scheduled,
- ]
- : undefined,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ inStatuses: withExpired
+ ? [
+ CusProductStatus.Active,
+ CusProductStatus.Expired,
+ CusProductStatus.PastDue,
+ CusProductStatus.Scheduled,
+ ]
+ : undefined,
+ });
- const cusProduct = fullCus.customer_products.find(
- (cp: FullCusProduct) => cp.product.id == productId
- )!;
+ const cusProduct = fullCus.customer_products.find(
+ (cp: FullCusProduct) => cp.product.id == productId,
+ )!;
- const subs: Stripe.Subscription[] = await getStripeSubs({
- stripeCli,
- subIds: cusProduct?.subscription_ids,
- });
+ const subs: Stripe.Subscription[] = await getStripeSubs({
+ stripeCli,
+ subIds: cusProduct?.subscription_ids,
+ });
- return {
- fullCus,
- cusProduct,
- subs,
- };
+ return {
+ fullCus,
+ cusProduct,
+ subs,
+ };
};
// export const expectSubAnchorsSame = async ({
@@ -126,168 +126,168 @@ export const getSubsFromCusId = async ({
// };
const subIsCanceled = ({ sub }: { sub: Stripe.Subscription }) => {
- return (
- notNullish(sub.canceled_at) ||
- notNullish(sub.cancel_at) ||
- sub.cancel_at_period_end
- );
+ return (
+ notNullish(sub.canceled_at) ||
+ notNullish(sub.cancel_at) ||
+ sub.cancel_at_period_end
+ );
};
export const expectSubItemsCorrect = async ({
- stripeCli,
- customerId,
- product,
- db,
- org,
- env,
- subCanceled = false,
- isCanceled = false,
- entityId,
+ stripeCli,
+ customerId,
+ product,
+ db,
+ org,
+ env,
+ subCanceled = false,
+ isCanceled = false,
+ entityId,
}: {
- stripeCli: Stripe;
- customerId: string;
- product: ProductV2;
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- subCanceled?: boolean;
- isCanceled?: boolean;
- entityId?: string;
+ stripeCli: Stripe;
+ customerId: string;
+ product: ProductV2;
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ subCanceled?: boolean;
+ isCanceled?: boolean;
+ entityId?: string;
}) => {
- const fullCus = await CusService.getFull({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- withEntities: true,
- });
+ const fullCus = await CusService.getFull({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ withEntities: true,
+ });
- let entity = entityId ? fullCus.entities.find((e) => e.id == entityId) : null;
+ let entity = entityId ? fullCus.entities.find((e) => e.id == entityId) : null;
- const productId = product.id;
- const cusProduct = fullCus.customer_products.find(
- (cp: FullCusProduct) =>
- cp.product.id == productId &&
- (entity ? cp.internal_entity_id == entity.internal_id : true)
- )!;
+ const productId = product.id;
+ const cusProduct = fullCus.customer_products.find(
+ (cp: FullCusProduct) =>
+ cp.product.id == productId &&
+ (entity ? cp.internal_entity_id == entity.internal_id : true),
+ )!;
- if (isCanceled) {
- expect(
- cusProduct.canceled_at,
- `cus product ${cusProduct.product.id} have field 'canceled_at' set`
- ).to.exist;
- expect(
- cusProduct.canceled,
- `cus product ${cusProduct.product.id} have field 'canceled' set to true`
- ).to.be.true;
- } else {
- expect(
- cusProduct.canceled_at,
- `cus product ${cusProduct.product.id} should not have field 'canceled_at' set`
- ).to.not.exist;
- expect(
- cusProduct.canceled,
- `cus product ${cusProduct.product.id} should not have field 'canceled' set`
- ).to.be.false;
- }
+ if (isCanceled) {
+ expect(
+ cusProduct.canceled_at,
+ `cus product ${cusProduct.product.id} have field 'canceled_at' set`,
+ ).to.exist;
+ expect(
+ cusProduct.canceled,
+ `cus product ${cusProduct.product.id} have field 'canceled' set to true`,
+ ).to.be.true;
+ } else {
+ expect(
+ cusProduct.canceled_at,
+ `cus product ${cusProduct.product.id} should not have field 'canceled_at' set`,
+ ).to.not.exist;
+ expect(
+ cusProduct.canceled,
+ `cus product ${cusProduct.product.id} should not have field 'canceled' set`,
+ ).to.be.false;
+ }
- if (isFreeProductV2({ product })) {
- expect(
- cusProduct.subscription_ids,
- `cus product should have no subs for free product: ${product.name}`
- ).to.be.empty;
- return {
- fullCus,
- };
- }
+ if (isFreeProductV2({ product })) {
+ expect(
+ cusProduct.subscription_ids,
+ `cus product should have no subs for free product: ${product.name}`,
+ ).to.be.empty;
+ return {
+ fullCus,
+ };
+ }
- const subs: Stripe.Subscription[] = await getStripeSubs({
- stripeCli,
- subIds: cusProduct?.subscription_ids,
- });
+ const subs: Stripe.Subscription[] = await getStripeSubs({
+ stripeCli,
+ subIds: cusProduct?.subscription_ids,
+ });
- for (const sub of subs) {
- if (subCanceled) {
- expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
- } else {
- expect(subIsCanceled({ sub }), "sub should not be canceled").to.be.false;
- }
- }
+ for (const sub of subs) {
+ if (subCanceled) {
+ expect(subIsCanceled({ sub }), "sub should be canceled").to.be.true;
+ } else {
+ expect(subIsCanceled({ sub }), "sub should not be canceled").to.be.false;
+ }
+ }
- const subItems = subs.flatMap((sub) => sub.items.data);
- const prices = cusProductToPrices({ cusProduct });
+ const subItems = subs.flatMap((sub) => sub.items.data);
+ const prices = cusProductToPrices({ cusProduct });
- let missingUsageCount = 0;
+ let missingUsageCount = 0;
- for (const price of prices) {
- const subItem = findStripeItemForPrice({
- stripeItems: subItems,
- price,
- }) as Stripe.SubscriptionItem;
+ for (const price of prices) {
+ const subItem = findStripeItemForPrice({
+ stripeItems: subItems,
+ price,
+ }) as Stripe.SubscriptionItem;
- // 1. If usage + v4 + internalEntityId
- if (isV4Usage({ price, cusProduct })) {
- if (nullish(subItem)) {
- missingUsageCount++;
- }
+ // 1. If usage + v4 + internalEntityId
+ if (isV4Usage({ price, cusProduct })) {
+ if (nullish(subItem)) {
+ missingUsageCount++;
+ }
- const usagePriceConfig = price.config as UsagePriceConfig;
+ const usagePriceConfig = price.config as UsagePriceConfig;
- expect(
- nullish(subItem) ||
- (subItem?.quantity === 0 &&
- isLicenseItem({ stripeItem: subItem! })) ||
- subItem?.price.id == usagePriceConfig.stripe_empty_price_id
- ).to.be.true;
- continue;
- } else {
- expect(
- subItem,
- `sub item for price: ${(price.config as any).internal_feature_id || price.config.interval} should exist`
- ).to.exist;
- }
+ expect(
+ nullish(subItem) ||
+ (subItem?.quantity === 0 &&
+ isLicenseItem({ stripeItem: subItem! })) ||
+ subItem?.price.id == usagePriceConfig.stripe_empty_price_id,
+ ).to.be.true;
+ continue;
+ } else {
+ expect(
+ subItem,
+ `sub item for price: ${(price.config as any).internal_feature_id || price.config.interval} should exist`,
+ ).to.exist;
+ }
- // 2. If prepaid...
- let billingType = getBillingType(price.config);
- if (billingType == BillingType.UsageInAdvance) {
- const featureId = (price.config as any).feature_id;
- const options = cusProduct.options.find((o) => o.feature_id == featureId);
+ // 2. If prepaid...
+ let billingType = getBillingType(price.config);
+ if (billingType == BillingType.UsageInAdvance) {
+ const featureId = (price.config as any).feature_id;
+ const options = cusProduct.options.find((o) => o.feature_id == featureId);
- expect(
- options,
- `options should exist for prepaid price (featureId: ${featureId})`
- ).to.exist;
+ expect(
+ options,
+ `options should exist for prepaid price (featureId: ${featureId})`,
+ ).to.exist;
- const expectedQuantity = options?.upcoming_quantity || options?.quantity;
- expect(
- subItem?.quantity,
- `sub item quantity for prepaid price (featureId: ${featureId}) should be ${expectedQuantity}`
- ).to.equal(expectedQuantity);
- continue;
- }
- }
+ const expectedQuantity = options?.upcoming_quantity || options?.quantity;
+ expect(
+ subItem?.quantity,
+ `sub item quantity for prepaid price (featureId: ${featureId}) should be ${expectedQuantity}`,
+ ).to.equal(expectedQuantity);
+ continue;
+ }
+ }
- expect(
- prices.length - missingUsageCount,
- "number of sub items equivalent to number of prices"
- ).to.equal(subItems.length);
+ expect(
+ prices.length - missingUsageCount,
+ "number of sub items equivalent to number of prices",
+ ).to.equal(subItems.length);
- // Expect sub anchors to be the same
- // let periodEnd = subs[0].current_period_end * 1000;
- // let firstDate = getDate(periodEnd);
+ // Expect sub anchors to be the same
+ // let periodEnd = subs[0].current_period_end * 1000;
+ // let firstDate = getDate(periodEnd);
- // for (const sub of subs.slice(1)) {
- // let dateOfAnchor = getDate(sub.current_period_end * 1000);
- // expect(dateOfAnchor).to.approximately(
- // firstDate,
- // 5000,
- // `subscription anchors are the same, +/- 5s`
- // );
- // }
+ // for (const sub of subs.slice(1)) {
+ // let dateOfAnchor = getDate(sub.current_period_end * 1000);
+ // expect(dateOfAnchor).to.approximately(
+ // firstDate,
+ // 5000,
+ // `subscription anchors are the same, +/- 5s`
+ // );
+ // }
- return {
- fullCus,
+ return {
+ fullCus,
- cusProduct,
- };
+ cusProduct,
+ };
};
diff --git a/server/tests/utils/genUtils.ts b/server/tests/utils/genUtils.ts
index 9457586dc..bee65d3f3 100644
--- a/server/tests/utils/genUtils.ts
+++ b/server/tests/utils/genUtils.ts
@@ -1,108 +1,108 @@
import { CusService } from "@/internal/customers/CusService.js";
import {
- CusProductStatus,
- FullCusProduct,
- UsagePriceConfig,
+ CusProductStatus,
+ FullCusProduct,
+ UsagePriceConfig,
} from "@autumn/shared";
import { AutumnCli } from "tests/cli/AutumnCli.js";
export const timeout = (ms: number) => {
- return new Promise((resolve) => setTimeout(resolve, ms));
+ return new Promise((resolve) => setTimeout(resolve, ms));
};
export const batchSendCountEvents = async ({
- customerId,
- eventCount,
- featureId,
+ customerId,
+ eventCount,
+ featureId,
}: {
- customerId: string;
- eventCount: number;
- featureId: string;
+ customerId: string;
+ eventCount: number;
+ featureId: string;
}) => {
- const batchEvents = [];
- for (let i = 0; i < eventCount; i++) {
- batchEvents.push(
- AutumnCli.sendEvent({
- customerId: customerId,
- eventName: featureId,
- }),
- );
- }
+ const batchEvents = [];
+ for (let i = 0; i < eventCount; i++) {
+ batchEvents.push(
+ AutumnCli.sendEvent({
+ customerId: customerId,
+ eventName: featureId,
+ }),
+ );
+ }
- await Promise.all(batchEvents);
- await timeout(10000);
+ await Promise.all(batchEvents);
+ await timeout(10000);
};
export const searchCusProducts = ({
- productId,
- cusProducts,
- status,
+ productId,
+ cusProducts,
+ status,
}: {
- productId: string;
- cusProducts: FullCusProduct[];
- status?: CusProductStatus;
+ productId: string;
+ cusProducts: FullCusProduct[];
+ status?: CusProductStatus;
}) => {
- if (!cusProducts) {
- return undefined;
- }
- return cusProducts.find(
- (cusProduct: FullCusProduct) =>
- cusProduct.product.id === productId &&
- (status ? cusProduct.status === status : true),
- );
+ if (!cusProducts) {
+ return undefined;
+ }
+ return cusProducts.find(
+ (cusProduct: FullCusProduct) =>
+ cusProduct.product.id === productId &&
+ (status ? cusProduct.status === status : true),
+ );
};
export const getFixedPriceAmount = (product: any) => {
- let amount = 0;
- for (const price of product.prices) {
- if (price.config.type === "fixed") {
- amount += price.config.amount;
- }
- }
- return amount;
+ let amount = 0;
+ for (const price of product.prices) {
+ if (price.config.type === "fixed") {
+ amount += price.config.amount;
+ }
+ }
+ return amount;
};
export const getUsagePriceTiers = ({
- product,
- featureId,
+ product,
+ featureId,
}: {
- product: any;
- featureId: string;
+ product: any;
+ featureId: string;
}) => {
- for (const price of product.prices) {
- if (
- price.config.type === "usage" &&
- price.config.feature_id === featureId
- ) {
- return price.config.usage_tiers;
- }
- }
- return [];
+ for (const price of product.prices) {
+ if (
+ price.config.type === "usage" &&
+ price.config.feature_id === featureId
+ ) {
+ return price.config.usage_tiers;
+ }
+ }
+ return [];
};
export const getFeaturePrice = ({
- product,
- featureId,
- cusProducts,
- subId,
+ product,
+ featureId,
+ cusProducts,
+ subId,
}: {
- product: any;
- featureId: string;
- cusProducts: FullCusProduct[];
- subId?: string;
+ product: any;
+ featureId: string;
+ cusProducts: FullCusProduct[];
+ subId?: string;
}) => {
- if (cusProducts.length == 0) {
- return null;
- }
+ if (cusProducts.length == 0) {
+ return null;
+ }
- let mainProduct = cusProducts[0];
+ let mainProduct = cusProducts[0];
- for (const cusPrice of mainProduct.customer_prices) {
- let price = cusPrice.price;
- if ((price.config! as UsagePriceConfig).feature_id === featureId) {
- return price;
- }
- }
+ for (const cusPrice of mainProduct.customer_prices) {
+ let price = cusPrice.price;
+ if ((price.config! as UsagePriceConfig).feature_id === featureId) {
+ return price;
+ }
+ }
- return null;
+ return null;
};
diff --git a/server/tests/utils/general/numberUtils.ts b/server/tests/utils/general/numberUtils.ts
index bbb6d5c5a..e8fcf6b76 100644
--- a/server/tests/utils/general/numberUtils.ts
+++ b/server/tests/utils/general/numberUtils.ts
@@ -1,8 +1,8 @@
export const isValidNumber = (value: any) => {
- let number = parseFloat(value);
- return !isNaN(number) && isFinite(number);
+ let number = parseFloat(value);
+ return !isNaN(number) && isFinite(number);
};
export const numberWithCommas = (x: number) => {
- return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
};
diff --git a/server/tests/utils/init.ts b/server/tests/utils/init.ts
index 0d15bf37e..99734e3f8 100644
--- a/server/tests/utils/init.ts
+++ b/server/tests/utils/init.ts
@@ -10,13 +10,14 @@ import {
type Entitlement,
type Feature,
FeatureType,
- FeatureUsageType, FreeTrialDuration,
+ FeatureUsageType,
+ FreeTrialDuration,
type Organization,
PriceType,
type ProductItem,
RewardReceivedBy,
RewardTriggerEvent,
- RewardType
+ RewardType,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js";
diff --git a/server/tests/utils/productUtils.ts b/server/tests/utils/productUtils.ts
index eab69f507..287de17c2 100644
--- a/server/tests/utils/productUtils.ts
+++ b/server/tests/utils/productUtils.ts
@@ -5,125 +5,125 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
export const createProduct = async ({
- db,
- orgId,
- env,
- autumn,
- product,
- prefix,
+ db,
+ orgId,
+ env,
+ autumn,
+ product,
+ prefix,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- autumn: AutumnInt;
- product: any;
- prefix?: string;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ autumn: AutumnInt;
+ product: any;
+ prefix?: string;
}) => {
- try {
- const products = await ProductService.listFull({
- db,
- orgId,
- env,
- returnAll: true,
- inIds: [product.id],
- });
+ try {
+ const products = await ProductService.listFull({
+ db,
+ orgId,
+ env,
+ returnAll: true,
+ inIds: [product.id],
+ });
- const batchDelete = [];
- for (const prod of products) {
- batchDelete.push(
- ProductService.deleteByInternalId({
- db,
- internalId: prod.internal_id,
- orgId,
- env,
- })
- );
- }
+ const batchDelete = [];
+ for (const prod of products) {
+ batchDelete.push(
+ ProductService.deleteByInternalId({
+ db,
+ internalId: prod.internal_id,
+ orgId,
+ env,
+ }),
+ );
+ }
- await Promise.all(batchDelete);
- } catch (error) {}
+ await Promise.all(batchDelete);
+ } catch (error) {}
- let clone = structuredClone(product);
- if (typeof clone.items === "object") {
- clone.items = Object.values(clone.items);
- }
+ let clone = structuredClone(product);
+ if (typeof clone.items === "object") {
+ clone.items = Object.values(clone.items);
+ }
- if (prefix) {
- clone.id = `${prefix}_${clone.id}`;
- clone.name = `${prefix} ${clone.name}`;
- }
+ if (prefix) {
+ clone.id = `${prefix}_${clone.id}`;
+ clone.name = `${prefix} ${clone.name}`;
+ }
- await autumn.products.create(clone);
+ await autumn.products.create(clone);
};
export const createProducts = async ({
- db,
- orgId,
- env,
- autumn,
- products,
- prefix,
- customerId,
+ db,
+ orgId,
+ env,
+ autumn,
+ products,
+ prefix,
+ customerId,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- autumn: AutumnInt;
- products: any[];
- prefix?: string;
- customerId?: string;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ autumn: AutumnInt;
+ products: any[];
+ prefix?: string;
+ customerId?: string;
}) => {
- if (customerId) {
- try {
- await autumn.customers.delete(customerId);
- } catch (error) {}
- }
+ if (customerId) {
+ try {
+ await autumn.customers.delete(customerId);
+ } catch (error) {}
+ }
- const batchCreate = [];
- for (const product of products) {
- batchCreate.push(
- createProduct({ db, orgId, env, autumn, product, prefix })
- );
- }
+ const batchCreate = [];
+ for (const product of products) {
+ batchCreate.push(
+ createProduct({ db, orgId, env, autumn, product, prefix }),
+ );
+ }
- await Promise.all(batchCreate);
+ await Promise.all(batchCreate);
};
export const createReward = async ({
- db,
- orgId,
- env,
- autumn,
- reward,
- productId,
- onlyUsage = false,
+ db,
+ orgId,
+ env,
+ autumn,
+ reward,
+ productId,
+ onlyUsage = false,
}: {
- db: DrizzleCli;
- orgId: string;
- env: AppEnv;
- autumn: AutumnInt;
- reward: CreateReward;
- productId: string;
- onlyUsage?: boolean;
+ db: DrizzleCli;
+ orgId: string;
+ env: AppEnv;
+ autumn: AutumnInt;
+ reward: CreateReward;
+ productId: string;
+ onlyUsage?: boolean;
}) => {
- let fullProduct = await ProductService.getFull({
- db,
- orgId,
- env,
- idOrInternalId: productId!,
- });
+ let fullProduct = await ProductService.getFull({
+ db,
+ orgId,
+ env,
+ idOrInternalId: productId!,
+ });
- let usagePrices = fullProduct?.prices.filter((price) =>
- isUsagePrice({ price })
- );
+ let usagePrices = fullProduct?.prices.filter((price) =>
+ isUsagePrice({ price }),
+ );
- if (onlyUsage) {
- reward.discount_config!.price_ids = usagePrices?.map((price) => price.id);
- }
+ if (onlyUsage) {
+ reward.discount_config!.price_ids = usagePrices?.map((price) => price.id);
+ }
- try {
- await autumn.rewards.delete(reward.id);
- } catch (error) {}
+ try {
+ await autumn.rewards.delete(reward.id);
+ } catch (error) {}
- await autumn.rewards.create(reward);
+ await autumn.rewards.create(reward);
};
diff --git a/server/tests/utils/scheduleCheckUtils.ts b/server/tests/utils/scheduleCheckUtils.ts
index e9983479e..76474efcd 100644
--- a/server/tests/utils/scheduleCheckUtils.ts
+++ b/server/tests/utils/scheduleCheckUtils.ts
@@ -6,104 +6,104 @@ import { createStripeCli } from "@/external/stripe/utils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const checkScheduleContainsProducts = async ({
- db,
- org,
- env,
- scheduleId,
- schedule,
- productIds,
+ db,
+ org,
+ env,
+ scheduleId,
+ schedule,
+ productIds,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- scheduleId?: string;
- schedule?: Stripe.SubscriptionSchedule;
- productIds: string[];
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ scheduleId?: string;
+ schedule?: Stripe.SubscriptionSchedule;
+ productIds: string[];
}) => {
- if (!schedule && !scheduleId) {
- throw new Error("schedule or scheduleId must be provided");
- }
+ if (!schedule && !scheduleId) {
+ throw new Error("schedule or scheduleId must be provided");
+ }
- if (scheduleId) {
- const stripeCli = createStripeCli({ org: org, env: env });
- schedule = await stripeCli.subscriptionSchedules.retrieve(scheduleId);
- }
+ if (scheduleId) {
+ const stripeCli = createStripeCli({ org: org, env: env });
+ schedule = await stripeCli.subscriptionSchedules.retrieve(scheduleId);
+ }
- let priceCount = 0;
- for (const productId of productIds) {
- let product = await ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId: org.id,
- env: env,
- });
+ let priceCount = 0;
+ for (const productId of productIds) {
+ let product = await ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId: org.id,
+ env: env,
+ });
- for (const price of product.prices) {
- expect(
- schedule!.phases[0].items.some(
- (item) => item.price === price.config!.stripe_price_id,
- ),
- ).to.be.true;
- priceCount++;
- }
- }
+ for (const price of product.prices) {
+ expect(
+ schedule!.phases[0].items.some(
+ (item) => item.price === price.config!.stripe_price_id,
+ ),
+ ).to.be.true;
+ priceCount++;
+ }
+ }
- expect(schedule!.phases[0].items.length).to.equal(priceCount);
+ expect(schedule!.phases[0].items.length).to.equal(priceCount);
};
export const checkSubscriptionContainsProducts = async ({
- db,
- org,
- env,
- subscriptionId,
- productIds,
+ db,
+ org,
+ env,
+ subscriptionId,
+ productIds,
}: {
- db: DrizzleCli;
- org: Organization;
- env: AppEnv;
- subscriptionId: string;
- productIds: string[];
+ db: DrizzleCli;
+ org: Organization;
+ env: AppEnv;
+ subscriptionId: string;
+ productIds: string[];
}) => {
- const stripeCli = createStripeCli({ org: org, env: env });
- const sub = await stripeCli.subscriptions.retrieve(subscriptionId);
+ const stripeCli = createStripeCli({ org: org, env: env });
+ const sub = await stripeCli.subscriptions.retrieve(subscriptionId);
- let totalPriceCount = 0;
- for (const productId of productIds) {
- let product = await ProductService.getFull({
- db,
- idOrInternalId: productId,
- orgId: org.id,
- env: env,
- });
+ let totalPriceCount = 0;
+ for (const productId of productIds) {
+ let product = await ProductService.getFull({
+ db,
+ idOrInternalId: productId,
+ orgId: org.id,
+ env: env,
+ });
- for (const price of product.prices) {
- totalPriceCount++;
- try {
- expect(
- sub.items.data.some(
- (item) => item.price.id === price.config.stripe_price_id,
- ),
- ).to.be.true;
- } catch (error) {
- console.log("Stripe sub prices not matching product prices");
- console.log(
- "Prices:",
- product.prices.map((p: any) => p.config.stripe_price_id),
- );
- console.log(
- "Sub items:",
- sub.items.data.map((i: any) => i.price.id),
- );
- throw error;
- }
- }
- }
- try {
- expect(sub.items.data.length).to.equal(totalPriceCount);
- } catch (error) {
- console.log("Num of sub prices not matching num of product prices");
- console.log("Sub prices:", sub.items.data.length);
- console.log("Product prices:", totalPriceCount);
- throw error;
- }
+ for (const price of product.prices) {
+ totalPriceCount++;
+ try {
+ expect(
+ sub.items.data.some(
+ (item) => item.price.id === price.config.stripe_price_id,
+ ),
+ ).to.be.true;
+ } catch (error) {
+ console.log("Stripe sub prices not matching product prices");
+ console.log(
+ "Prices:",
+ product.prices.map((p: any) => p.config.stripe_price_id),
+ );
+ console.log(
+ "Sub items:",
+ sub.items.data.map((i: any) => i.price.id),
+ );
+ throw error;
+ }
+ }
+ }
+ try {
+ expect(sub.items.data.length).to.equal(totalPriceCount);
+ } catch (error) {
+ console.log("Num of sub prices not matching num of product prices");
+ console.log("Sub prices:", sub.items.data.length);
+ console.log("Product prices:", totalPriceCount);
+ throw error;
+ }
};
diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts
index c4586d9f3..5169ac77e 100644
--- a/server/tests/utils/setup.ts
+++ b/server/tests/utils/setup.ts
@@ -1,14 +1,14 @@
import {
- AppEnv,
- type CreateReward,
- type Feature,
- FeatureType,
- type FullProduct,
- type Organization,
- type Price,
- PriceType,
- type RewardProgram,
- RewardType,
+ AppEnv,
+ type CreateReward,
+ type Feature,
+ FeatureType,
+ type FullProduct,
+ type Organization,
+ type Price,
+ PriceType,
+ type RewardProgram,
+ RewardType,
} from "@autumn/shared";
import axios from "axios";
import type Stripe from "stripe";
@@ -449,7 +449,11 @@ export const setupOrg = async ({
type: reward.type,
discount_config: rewardData.discount_config,
free_product_id: rewardData.free_product_id,
- free_product_config: rewardData.free_product_config?.duration_type && rewardData.free_product_config?.duration_value ? rewardData.free_product_config : undefined,
+ free_product_config:
+ rewardData.free_product_config?.duration_type &&
+ rewardData.free_product_config?.duration_value
+ ? rewardData.free_product_config
+ : undefined,
};
const rewardRes = await autumn.rewards.create(newReward);
diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts
index 4f15ce4fd..b55dd1f6e 100644
--- a/server/tests/utils/stripeUtils.ts
+++ b/server/tests/utils/stripeUtils.ts
@@ -4,12 +4,12 @@ import { timeout } from "./genUtils.js";
import { Stripe } from "stripe";
import { BillingInterval, Customer, FullProduct } from "@autumn/shared";
import {
- addDays,
- addHours,
- addMinutes,
- addMonths,
- addWeeks,
- format,
+ addDays,
+ addHours,
+ addMinutes,
+ addMonths,
+ addWeeks,
+ format,
} from "date-fns";
import puppeteer from "puppeteer-core";
import Browserbase from "@browserbasehq/sdk";
@@ -18,441 +18,443 @@ const STRIPE_TEST_CLOCK_TIMING = 20000; // 30s
import { Hyperbrowser } from "@hyperbrowser/sdk";
const client = new Hyperbrowser({
- apiKey: process.env.HYPERBROWSER_API_KEY || "123",
+ apiKey: process.env.HYPERBROWSER_API_KEY || "123",
});
export const completeCheckoutForm = async (
- url: string,
- overrideQuantity?: number,
- promoCode?: string,
- isLocal?: boolean
+ url: string,
+ overrideQuantity?: number,
+ promoCode?: string,
+ isLocal?: boolean,
) => {
- let browser;
+ let browser;
- if (process.env.NODE_ENV === "development" && !isLocal) {
- const session = await client.sessions.create();
- browser = await puppeteer.connect({
- browserWSEndpoint: session!.wsEndpoint,
- defaultViewport: null,
- });
- } else {
- browser = await puppeteer.launch({
- headless: false,
- executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
- });
- }
+ if (process.env.NODE_ENV === "development" && !isLocal) {
+ const session = await client.sessions.create();
+ browser = await puppeteer.connect({
+ browserWSEndpoint: session!.wsEndpoint,
+ defaultViewport: null,
+ });
+ } else {
+ browser = await puppeteer.launch({
+ headless: false,
+ executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
+ });
+ }
- try {
- const page = await browser.newPage();
- await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
- await page.goto(url);
+ try {
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
+ await page.goto(url);
- // await page.waitForSelector("#payment-method-accordion-item-title-card");
- // await page.click("#payment-method-accordion-item-title-card");
+ // await page.waitForSelector("#payment-method-accordion-item-title-card");
+ // await page.click("#payment-method-accordion-item-title-card");
- await page.waitForSelector("#cardNumber");
- await page.type("#cardNumber", "4242424242424242");
+ await page.waitForSelector("#cardNumber");
+ await page.type("#cardNumber", "4242424242424242");
- await page.waitForSelector("#cardExpiry");
- await page.type("#cardExpiry", "1234");
+ await page.waitForSelector("#cardExpiry");
+ await page.type("#cardExpiry", "1234");
- await page.waitForSelector("#cardCvc");
- await page.type("#cardCvc", "123");
+ await page.waitForSelector("#cardCvc");
+ await page.type("#cardCvc", "123");
- await page.waitForSelector("#billingName");
- await page.type("#billingName", "Test Customer");
- await page.waitForSelector("#billingPostalCode");
- await page.type("#billingPostalCode", "123456");
+ await page.waitForSelector("#billingName");
+ await page.type("#billingName", "Test Customer");
+ await page.waitForSelector("#billingPostalCode");
+ await page.type("#billingPostalCode", "123456");
- if (overrideQuantity) {
- const quantityBtn = await page.$(".AdjustableQuantitySelector");
- await quantityBtn?.evaluate((b: any) => (b as HTMLElement).click());
+ if (overrideQuantity) {
+ const quantityBtn = await page.$(".AdjustableQuantitySelector");
+ await quantityBtn?.evaluate((b: any) => (b as HTMLElement).click());
- await page.waitForSelector("#adjustQuantity");
- await page.click("#adjustQuantity", { clickCount: 3 }); // Select all text
- await page.keyboard.press("Backspace"); // Delete selected text
- await page.type("#adjustQuantity", overrideQuantity.toString());
+ await page.waitForSelector("#adjustQuantity");
+ await page.click("#adjustQuantity", { clickCount: 3 }); // Select all text
+ await page.keyboard.press("Backspace"); // Delete selected text
+ await page.type("#adjustQuantity", overrideQuantity.toString());
- const updateBtn = await page.$(".AdjustQuantityFooter-btn");
- await updateBtn?.evaluate((b: any) => (b as HTMLElement).click());
+ const updateBtn = await page.$(".AdjustQuantityFooter-btn");
+ await updateBtn?.evaluate((b: any) => (b as HTMLElement).click());
- await timeout(1000);
- }
+ await timeout(1000);
+ }
- if (promoCode) {
- await page.waitForSelector("#promotionCode");
- await page.click("#promotionCode");
- await page.type("#promotionCode", promoCode);
- await page.keyboard.press("Enter");
- await timeout(5000);
- }
+ if (promoCode) {
+ await page.waitForSelector("#promotionCode");
+ await page.click("#promotionCode");
+ await page.type("#promotionCode", promoCode);
+ await page.keyboard.press("Enter");
+ await timeout(5000);
+ }
- const submitButton = await page.$(".SubmitButton-TextContainer");
- await submitButton?.evaluate((b: any) => (b as HTMLElement).click());
- await timeout(7000);
- } finally {
- // always close browser
- await browser.close();
- }
+ const submitButton = await page.$(".SubmitButton-TextContainer");
+ await submitButton?.evaluate((b: any) => (b as HTMLElement).click());
+ await timeout(7000);
+ } finally {
+ // always close browser
+ await browser.close();
+ }
};
export const deleteAllStripeProducts = async ({
- stripeCli,
+ stripeCli,
}: {
- stripeCli: Stripe;
+ stripeCli: Stripe;
}) => {
- const stripeProds = await stripeCli.products.list({
- limit: 100,
- });
+ const stripeProds = await stripeCli.products.list({
+ limit: 100,
+ });
- const batchSize = 10;
- for (let i = 0; i < stripeProds.data.length; i += batchSize) {
- const batch = stripeProds.data.slice(i, i + batchSize);
- await Promise.all(
- batch.map(async (prod) => {
- console.log("Deleting stripe product", prod.id);
- try {
- await stripeCli.products.del(prod.id);
- } catch (error) {
- await stripeCli.products.update(prod.id, {
- active: false,
- });
- }
- })
- );
- console.log("Deleted", i, "of", stripeProds.data.length);
- }
+ const batchSize = 10;
+ for (let i = 0; i < stripeProds.data.length; i += batchSize) {
+ const batch = stripeProds.data.slice(i, i + batchSize);
+ await Promise.all(
+ batch.map(async (prod) => {
+ console.log("Deleting stripe product", prod.id);
+ try {
+ await stripeCli.products.del(prod.id);
+ } catch (error) {
+ await stripeCli.products.update(prod.id, {
+ active: false,
+ });
+ }
+ }),
+ );
+ console.log("Deleted", i, "of", stripeProds.data.length);
+ }
};
export const deleteAllStripeTestClocks = async ({
- stripeCli,
+ stripeCli,
}: {
- stripeCli: Stripe;
+ stripeCli: Stripe;
}) => {
- const stripeTestClocks = await stripeCli.testHelpers.testClocks.list({
- limit: 100,
- });
- const batchSize = 10;
- for (let i = 0; i < stripeTestClocks.data.length; i += batchSize) {
- const batch = stripeTestClocks.data.slice(i, i + batchSize);
- await Promise.all(
- batch.map(async (clock) => stripeCli.testHelpers.testClocks.del(clock.id))
- );
- }
+ const stripeTestClocks = await stripeCli.testHelpers.testClocks.list({
+ limit: 100,
+ });
+ const batchSize = 10;
+ for (let i = 0; i < stripeTestClocks.data.length; i += batchSize) {
+ const batch = stripeTestClocks.data.slice(i, i + batchSize);
+ await Promise.all(
+ batch.map(async (clock) =>
+ stripeCli.testHelpers.testClocks.del(clock.id),
+ ),
+ );
+ }
};
export const deleteStripeProduct = async ({
- stripeCli,
- product,
+ stripeCli,
+ product,
}: {
- stripeCli: Stripe;
- product: FullProduct;
+ stripeCli: Stripe;
+ product: FullProduct;
}) => {
- let stripeProd;
- try {
- stripeProd = await stripeCli.products.retrieve(product.processor!.id);
- } catch (error) {
- return;
- }
+ let stripeProd;
+ try {
+ stripeProd = await stripeCli.products.retrieve(product.processor!.id);
+ } catch (error) {
+ return;
+ }
- for (const price of product.prices!) {
- const config = price.config as any;
- if (config.stripe_price_id) {
- const stripePrice = await stripeCli.prices.retrieve(
- config.stripe_price_id
- );
+ for (const price of product.prices!) {
+ const config = price.config as any;
+ if (config.stripe_price_id) {
+ const stripePrice = await stripeCli.prices.retrieve(
+ config.stripe_price_id,
+ );
- await stripeCli.prices.update(config.stripe_price_id, {
- active: false,
- });
+ await stripeCli.prices.update(config.stripe_price_id, {
+ active: false,
+ });
- // Delete default product
- try {
- await stripeCli.products.del(stripePrice.product as string);
- } catch (error) {
- await stripeCli.products.update(stripePrice.product as string, {
- active: false,
- });
- }
- }
+ // Delete default product
+ try {
+ await stripeCli.products.del(stripePrice.product as string);
+ } catch (error) {
+ await stripeCli.products.update(stripePrice.product as string, {
+ active: false,
+ });
+ }
+ }
- if (config.stripe_meter_id) {
- await stripeCli.billing.meters.deactivate(config.stripe_meter_id);
- }
- }
+ if (config.stripe_meter_id) {
+ await stripeCli.billing.meters.deactivate(config.stripe_meter_id);
+ }
+ }
- if (product.processor) {
- // console.log("Stripe product", stripeProd.active, stripeProd.id);
- let stripeProdId = product.processor.id;
- try {
- await stripeCli.products.del(stripeProdId);
- } catch (error) {
- await stripeCli.products.update(stripeProdId, {
- active: false,
- });
- }
- }
+ if (product.processor) {
+ // console.log("Stripe product", stripeProd.active, stripeProd.id);
+ let stripeProdId = product.processor.id;
+ try {
+ await stripeCli.products.del(stripeProdId);
+ } catch (error) {
+ await stripeCli.products.update(stripeProdId, {
+ active: false,
+ });
+ }
+ }
};
export const checkMeteredEventSummary = async ({
- stripeCli,
- meterId,
- customerId,
+ stripeCli,
+ meterId,
+ customerId,
}: {
- stripeCli: Stripe;
- meterId: string;
- customerId: string;
+ stripeCli: Stripe;
+ meterId: string;
+ customerId: string;
}) => {
- const summaries = await stripeCli.billing.meters.listEventSummaries(meterId, {
- customer: customerId,
- start_time: Math.floor(Date.now() / 1000),
- end_time: Math.floor(addMonths(new Date(), 1).getTime() / 1000),
- });
- return summaries;
+ const summaries = await stripeCli.billing.meters.listEventSummaries(meterId, {
+ customer: customerId,
+ start_time: Math.floor(Date.now() / 1000),
+ end_time: Math.floor(addMonths(new Date(), 1).getTime() / 1000),
+ });
+ return summaries;
};
export const advanceTestClock = async ({
- stripeCli,
- testClockId,
- startingFrom,
- numberOfDays,
- numberOfWeeks,
- numberOfHours,
- numberOfMonths,
- advanceTo,
- waitForSeconds,
+ stripeCli,
+ testClockId,
+ startingFrom,
+ numberOfDays,
+ numberOfWeeks,
+ numberOfHours,
+ numberOfMonths,
+ advanceTo,
+ waitForSeconds,
}: {
- stripeCli: Stripe;
- testClockId: string;
- numberOfDays?: number;
- startingFrom?: Date;
- numberOfWeeks?: number;
- numberOfHours?: number;
- numberOfMonths?: number;
- advanceTo?: number;
- waitForSeconds?: number;
+ stripeCli: Stripe;
+ testClockId: string;
+ numberOfDays?: number;
+ startingFrom?: Date;
+ numberOfWeeks?: number;
+ numberOfHours?: number;
+ numberOfMonths?: number;
+ advanceTo?: number;
+ waitForSeconds?: number;
}) => {
- if (!startingFrom) {
- startingFrom = new Date();
- }
+ if (!startingFrom) {
+ startingFrom = new Date();
+ }
- if (numberOfDays) {
- advanceTo = addDays(startingFrom, numberOfDays).getTime();
- }
+ if (numberOfDays) {
+ advanceTo = addDays(startingFrom, numberOfDays).getTime();
+ }
- if (numberOfWeeks) {
- advanceTo = addWeeks(startingFrom, numberOfWeeks).getTime();
- }
+ if (numberOfWeeks) {
+ advanceTo = addWeeks(startingFrom, numberOfWeeks).getTime();
+ }
- if (numberOfHours) {
- advanceTo = addHours(startingFrom, numberOfHours).getTime();
- }
+ if (numberOfHours) {
+ advanceTo = addHours(startingFrom, numberOfHours).getTime();
+ }
- if (numberOfMonths) {
- advanceTo = addMonths(startingFrom, numberOfMonths).getTime();
- }
+ if (numberOfMonths) {
+ advanceTo = addMonths(startingFrom, numberOfMonths).getTime();
+ }
- if (!advanceTo) {
- advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
- }
+ if (!advanceTo) {
+ advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
+ }
- console.log(" - Advancing to: ", format(advanceTo, "dd MMM yyyy HH:mm:ss"));
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.floor(advanceTo / 1000),
- });
+ console.log(" - Advancing to: ", format(advanceTo, "dd MMM yyyy HH:mm:ss"));
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.floor(advanceTo / 1000),
+ });
- await timeout(
- waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING
- );
+ await timeout(
+ waitForSeconds ? waitForSeconds * 1000 : STRIPE_TEST_CLOCK_TIMING,
+ );
- return advanceTo;
+ return advanceTo;
};
export const waitForMeterUpdate = async () => {
- const timeoutSeconds = 160;
- for (let i = 0; i < timeoutSeconds; i += 10) {
- console.log(` - ${i} / ${timeoutSeconds}`);
- await timeout(10000);
- }
+ const timeoutSeconds = 160;
+ for (let i = 0; i < timeoutSeconds; i += 10) {
+ console.log(` - ${i} / ${timeoutSeconds}`);
+ await timeout(10000);
+ }
};
export const advanceClockForInvoice = async ({
- stripeCli,
- testClockId,
- waitForMeterUpdate = false,
- numberOfDays,
- startingFrom,
+ stripeCli,
+ testClockId,
+ waitForMeterUpdate = false,
+ numberOfDays,
+ startingFrom,
}: {
- stripeCli: Stripe;
- testClockId: string;
- waitForMeterUpdate?: boolean;
- numberOfDays?: number;
- startingFrom?: Date;
+ stripeCli: Stripe;
+ testClockId: string;
+ waitForMeterUpdate?: boolean;
+ numberOfDays?: number;
+ startingFrom?: Date;
}) => {
- let advanceTo;
+ let advanceTo;
- if (!startingFrom) {
- startingFrom = new Date();
- }
+ if (!startingFrom) {
+ startingFrom = new Date();
+ }
- if (numberOfDays) {
- advanceTo = addDays(startingFrom, numberOfDays).getTime();
- } else {
- advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
- }
+ if (numberOfDays) {
+ advanceTo = addDays(startingFrom, numberOfDays).getTime();
+ } else {
+ advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
+ }
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.ceil(advanceTo / 1000),
- });
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.ceil(advanceTo / 1000),
+ });
- console.log(
- " - advanceClockForInvoice (1): ",
- format(advanceTo, "dd MMM yyyy HH:mm:ss")
- );
+ console.log(
+ " - advanceClockForInvoice (1): ",
+ format(advanceTo, "dd MMM yyyy HH:mm:ss"),
+ );
- if (waitForMeterUpdate) {
- const timeoutSeconds = 200;
- for (let i = 0; i < timeoutSeconds; i += 10) {
- console.log(` - ${i} / ${timeoutSeconds}`);
- await timeout(10000);
- }
- } else {
- await timeout(STRIPE_TEST_CLOCK_TIMING);
- }
+ if (waitForMeterUpdate) {
+ const timeoutSeconds = 200;
+ for (let i = 0; i < timeoutSeconds; i += 10) {
+ console.log(` - ${i} / ${timeoutSeconds}`);
+ await timeout(10000);
+ }
+ } else {
+ await timeout(STRIPE_TEST_CLOCK_TIMING);
+ }
- // const advanceTo2 = addHours(new Date(advanceTo), 30).getTime();
- const advanceTo2 = addDays(new Date(advanceTo), 4).getTime();
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.floor(advanceTo2 / 1000),
- });
+ // const advanceTo2 = addHours(new Date(advanceTo), 30).getTime();
+ const advanceTo2 = addDays(new Date(advanceTo), 4).getTime();
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.floor(advanceTo2 / 1000),
+ });
- console.log(
- " - advanceClockForInvoice (2): ",
- format(advanceTo2, "dd MMM yyyy HH:mm:ss")
- );
+ console.log(
+ " - advanceClockForInvoice (2): ",
+ format(advanceTo2, "dd MMM yyyy HH:mm:ss"),
+ );
- await timeout(STRIPE_TEST_CLOCK_TIMING);
- return advanceTo2;
+ await timeout(STRIPE_TEST_CLOCK_TIMING);
+ return advanceTo2;
};
export const advanceMonths = async ({
- stripeCli,
- testClockId,
- numberOfMonths,
+ stripeCli,
+ testClockId,
+ numberOfMonths,
}: {
- stripeCli: Stripe;
- testClockId: string;
- numberOfMonths: number;
+ stripeCli: Stripe;
+ testClockId: string;
+ numberOfMonths: number;
}) => {
- let advanceTo = new Date();
- for (let i = 0; i < numberOfMonths; i += 1) {
- // let numMonths = Math.min(numberOfMonths - i, 2);
- ((advanceTo = addMonths(advanceTo, 1)), 10);
- console.log(
- " - Advancing to: ",
- format(advanceTo, "dd MMM yyyy HH:mm:ss")
- );
+ let advanceTo = new Date();
+ for (let i = 0; i < numberOfMonths; i += 1) {
+ // let numMonths = Math.min(numberOfMonths - i, 2);
+ (advanceTo = addMonths(advanceTo, 1)), 10;
+ console.log(
+ " - Advancing to: ",
+ format(advanceTo, "dd MMM yyyy HH:mm:ss"),
+ );
- try {
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.floor(advanceTo.getTime() / 1000),
- });
- } catch (error: any) {
- console.log(" - Advance clock: ", error.message);
- await timeout(10000);
- await stripeCli.testHelpers.testClocks.advance(testClockId, {
- frozen_time: Math.floor(advanceTo.getTime() / 1000),
- });
- }
+ try {
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.floor(advanceTo.getTime() / 1000),
+ });
+ } catch (error: any) {
+ console.log(" - Advance clock: ", error.message);
+ await timeout(10000);
+ await stripeCli.testHelpers.testClocks.advance(testClockId, {
+ frozen_time: Math.floor(advanceTo.getTime() / 1000),
+ });
+ }
- await timeout(15000);
- }
+ await timeout(15000);
+ }
};
// Check billing meter event summary
export const checkBillingMeterEventSummary = async ({
- stripeCli,
- startTime,
- stripeMeterId,
- stripeCustomerId,
+ stripeCli,
+ startTime,
+ stripeMeterId,
+ stripeCustomerId,
}: {
- stripeCli: Stripe;
- startTime: Date;
- stripeMeterId: string;
- stripeCustomerId: string;
+ stripeCli: Stripe;
+ startTime: Date;
+ stripeMeterId: string;
+ stripeCustomerId: string;
}) => {
- let endTime = addMonths(startTime, 1);
- const event = await stripeCli.billing.meters.listEventSummaries(
- stripeMeterId,
- {
- limit: 100,
- start_time: Math.round(startTime.getTime() / 1000),
- end_time: Math.round(endTime.getTime() / 1000),
- customer: stripeCustomerId,
- }
- );
+ let endTime = addMonths(startTime, 1);
+ const event = await stripeCli.billing.meters.listEventSummaries(
+ stripeMeterId,
+ {
+ limit: 100,
+ start_time: Math.round(startTime.getTime() / 1000),
+ end_time: Math.round(endTime.getTime() / 1000),
+ customer: stripeCustomerId,
+ },
+ );
- if (event.data.length === 0) {
- return null;
- } else {
- return event.data[0];
- }
+ if (event.data.length === 0) {
+ return null;
+ } else {
+ return event.data[0];
+ }
};
export const getDiscount = async ({
- stripeCli,
- customer,
- stripeId,
+ stripeCli,
+ customer,
+ stripeId,
}: {
- stripeCli: Stripe;
- customer?: Customer;
- stripeId?: string;
+ stripeCli: Stripe;
+ customer?: Customer;
+ stripeId?: string;
}) => {
- const stripeCustomer: any = await stripeCli.customers.retrieve(
- stripeId || customer!.processor!.id,
- {
- expand: ["discount.coupon"],
- }
- );
+ const stripeCustomer: any = await stripeCli.customers.retrieve(
+ stripeId || customer!.processor!.id,
+ {
+ expand: ["discount.coupon"],
+ },
+ );
- return stripeCustomer.discount;
+ return stripeCustomer.discount;
};
export const stripeToAutumnInterval = ({
- interval,
- intervalCount,
+ interval,
+ intervalCount,
}: {
- interval: string;
- intervalCount: number;
+ interval: string;
+ intervalCount: number;
}) => {
- if (interval === "month" && intervalCount === 1) {
- return BillingInterval.Month;
- }
+ if (interval === "month" && intervalCount === 1) {
+ return BillingInterval.Month;
+ }
- if (interval === "month" && intervalCount === 3) {
- return BillingInterval.Quarter;
- }
+ if (interval === "month" && intervalCount === 3) {
+ return BillingInterval.Quarter;
+ }
- if (interval === "month" && intervalCount === 6) {
- return BillingInterval.SemiAnnual;
- }
+ if (interval === "month" && intervalCount === 6) {
+ return BillingInterval.SemiAnnual;
+ }
- if (
- (interval === "month" && intervalCount === 12) ||
- (interval === "year" && intervalCount === 1)
- ) {
- return BillingInterval.Year;
- }
+ if (
+ (interval === "month" && intervalCount === 12) ||
+ (interval === "year" && intervalCount === 1)
+ ) {
+ return BillingInterval.Year;
+ }
};
export const subItemToAutumnInterval = (item: Stripe.SubscriptionItem) => {
- return {
- interval: item.price.recurring?.interval as BillingInterval,
- intervalCount: item.price.recurring?.interval_count || 1,
- };
- // return stripeToAutumnInterval({
- // interval: item.price.recurring?.interval!,
- // intervalCount: item.price.recurring?.interval_count!,
- // });
+ return {
+ interval: item.price.recurring?.interval as BillingInterval,
+ intervalCount: item.price.recurring?.interval_count || 1,
+ };
+ // return stripeToAutumnInterval({
+ // interval: item.price.recurring?.interval!,
+ // intervalCount: item.price.recurring?.interval_count!,
+ // });
};
diff --git a/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts b/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts
index b52ba51d3..7da20dcc8 100644
--- a/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts
+++ b/server/tests/utils/stripeUtils/completeInvoiceCheckout.ts
@@ -8,195 +8,195 @@ import { Hyperbrowser } from "@hyperbrowser/sdk";
import { timeout } from "../genUtils.js";
const client = new Hyperbrowser({
- apiKey: process.env.HYPERBROWSER_API_KEY,
+ apiKey: process.env.HYPERBROWSER_API_KEY,
});
export const completeInvoiceCheckout = async ({
- url,
- isLocal = false,
+ url,
+ isLocal = false,
}: {
- url: string;
- isLocal?: boolean;
+ url: string;
+ isLocal?: boolean;
}) => {
- let browser;
+ let browser;
- if (process.env.NODE_ENV === "development" && !isLocal) {
- const session = await client.sessions.create();
- browser = await puppeteer.connect({
- browserWSEndpoint: session!.wsEndpoint,
- defaultViewport: null,
- });
- } else {
- browser = await puppeteer.launch({
- headless: false,
- executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
- args: ["--no-sandbox", "--disable-setuid-sandbox"],
- });
- }
+ if (process.env.NODE_ENV === "development" && !isLocal) {
+ const session = await client.sessions.create();
+ browser = await puppeteer.connect({
+ browserWSEndpoint: session!.wsEndpoint,
+ defaultViewport: null,
+ });
+ } else {
+ browser = await puppeteer.launch({
+ headless: false,
+ executablePath: "/Applications/Chromium.app/Contents/MacOS/Chromium",
+ args: ["--no-sandbox", "--disable-setuid-sandbox"],
+ });
+ }
- try {
- const page = await browser.newPage();
- await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
- await page.goto(url);
+ try {
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1280, height: 800 }); // Set standard desktop viewport size
+ await page.goto(url);
- // Wait for the payment element to load
+ // Wait for the payment element to load
- await page.waitForSelector("#payment-element", { timeout: 10000 });
+ await page.waitForSelector("#payment-element", { timeout: 10000 });
- // Wait a bit more for the iframe to fully load
- await new Promise((resolve) => setTimeout(resolve, 3000));
+ // Wait a bit more for the iframe to fully load
+ await new Promise((resolve) => setTimeout(resolve, 3000));
- // Try clicking on the payment element container to expand the accordion
+ // Try clicking on the payment element container to expand the accordion
- await page.click("#payment-element");
+ await page.click("#payment-element");
- await new Promise((resolve) => setTimeout(resolve, 3000));
+ await new Promise((resolve) => setTimeout(resolve, 3000));
- // Get the iframe containing the Stripe elements
- const stripeFrame = await page.$("#payment-element iframe");
- if (!stripeFrame) {
- throw new Error("Stripe iframe not found");
- }
+ // Get the iframe containing the Stripe elements
+ const stripeFrame = await page.$("#payment-element iframe");
+ if (!stripeFrame) {
+ throw new Error("Stripe iframe not found");
+ }
- const frame = await stripeFrame.contentFrame();
- if (!frame) {
- throw new Error("Could not access iframe content");
- }
+ const frame = await stripeFrame.contentFrame();
+ if (!frame) {
+ throw new Error("Could not access iframe content");
+ }
- // Expand the Card accordion inside the Stripe Payment Element if collapsed
- try {
- await frame.waitForSelector('[role="button"][data-value="card"]', {
- timeout: 5000,
- });
- const cardAccordionButton = await frame.$(
- '[role="button"][data-value="card"]'
- );
- if (cardAccordionButton) {
- const isExpanded = await frame.evaluate(
- (el) => el.getAttribute("aria-expanded"),
- cardAccordionButton
- );
- if (isExpanded === "false") {
- await cardAccordionButton.click();
- // wait for the card form fields to render
- await frame.waitForSelector(
- 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[aria-label*="Card number"]',
- { timeout: 5000 }
- );
- }
- }
- } catch (error) {
- console.log("Could not expand card accordion, proceeding:", error);
- }
+ // Expand the Card accordion inside the Stripe Payment Element if collapsed
+ try {
+ await frame.waitForSelector('[role="button"][data-value="card"]', {
+ timeout: 5000,
+ });
+ const cardAccordionButton = await frame.$(
+ '[role="button"][data-value="card"]',
+ );
+ if (cardAccordionButton) {
+ const isExpanded = await frame.evaluate(
+ (el) => el.getAttribute("aria-expanded"),
+ cardAccordionButton,
+ );
+ if (isExpanded === "false") {
+ await cardAccordionButton.click();
+ // wait for the card form fields to render
+ await frame.waitForSelector(
+ 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[aria-label*="Card number"]',
+ { timeout: 5000 },
+ );
+ }
+ }
+ } catch (error) {
+ console.log("Could not expand card accordion, proceeding:", error);
+ }
- // Enter card number - try different possible selectors
- try {
- await frame.waitForSelector(
- 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]',
- { timeout: 2000 }
- );
- const cardNumberInput = await frame.$(
- 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]'
- );
- if (cardNumberInput) {
- await cardNumberInput.click();
- await cardNumberInput.type("4242424242424242");
- }
- } catch (error) {
- console.log("Could not find card number input:", error);
- }
+ // Enter card number - try different possible selectors
+ try {
+ await frame.waitForSelector(
+ 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]',
+ { timeout: 2000 },
+ );
+ const cardNumberInput = await frame.$(
+ 'input[name="number"], input[data-elements-stable-field-name="cardNumber"], input[placeholder*="1234"], input[aria-label*="Card number"]',
+ );
+ if (cardNumberInput) {
+ await cardNumberInput.click();
+ await cardNumberInput.type("4242424242424242");
+ }
+ } catch (error) {
+ console.log("Could not find card number input:", error);
+ }
- // Enter expiry date
- try {
- await frame.waitForSelector(
- 'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]',
- { timeout: 2000 }
- );
- const expiryInput = await frame.$(
- 'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]'
- );
- if (expiryInput) {
- await expiryInput.click();
- await expiryInput.type("1227");
- }
- } catch (error) {
- console.log("Could not find expiry input:", error);
- }
+ // Enter expiry date
+ try {
+ await frame.waitForSelector(
+ 'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]',
+ { timeout: 2000 },
+ );
+ const expiryInput = await frame.$(
+ 'input[name="expiry"], input[data-elements-stable-field-name="cardExpiry"], input[placeholder*="MM"], input[aria-label*="expir"]',
+ );
+ if (expiryInput) {
+ await expiryInput.click();
+ await expiryInput.type("1227");
+ }
+ } catch (error) {
+ console.log("Could not find expiry input:", error);
+ }
- // Enter CVC
- try {
- await frame.waitForSelector(
- 'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]',
- { timeout: 2000 }
- );
- const cvcInput = await frame.$(
- 'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]'
- );
- if (cvcInput) {
- await cvcInput.click();
- await cvcInput.type("123");
- }
- } catch (error) {
- console.log("Could not find CVC input:", error);
- }
+ // Enter CVC
+ try {
+ await frame.waitForSelector(
+ 'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]',
+ { timeout: 2000 },
+ );
+ const cvcInput = await frame.$(
+ 'input[name="cvc"], input[data-elements-stable-field-name="cardCvc"], input[placeholder*="CVC"], input[aria-label*="CVC"]',
+ );
+ if (cvcInput) {
+ await cvcInput.click();
+ await cvcInput.type("123");
+ }
+ } catch (error) {
+ console.log("Could not find CVC input:", error);
+ }
- // Enter postal code
- try {
- await frame.waitForSelector(
- 'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]',
- { timeout: 2000 }
- );
- const postalInput = await frame.$(
- 'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]'
- );
- if (postalInput) {
- await postalInput.click();
- await postalInput.type("SW79SJ");
- }
- } catch (error) {
- console.log("Could not find postal code input:", error);
- }
+ // Enter postal code
+ try {
+ await frame.waitForSelector(
+ 'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]',
+ { timeout: 2000 },
+ );
+ const postalInput = await frame.$(
+ 'input[name="postalCode"], input[data-elements-stable-field-name="postalCode"], input[placeholder*="12345"], input[aria-label*="ZIP"]',
+ );
+ if (postalInput) {
+ await postalInput.click();
+ await postalInput.type("SW79SJ");
+ }
+ } catch (error) {
+ console.log("Could not find postal code input:", error);
+ }
- // Wait a bit for all inputs to be processed
- await new Promise((resolve) => setTimeout(resolve, 2000));
+ // Wait a bit for all inputs to be processed
+ await new Promise((resolve) => setTimeout(resolve, 2000));
- // Click submit/pay button with robust fallbacks
- let clicked = false;
- const submitSelectors = [
- ".SubmitButton-IconContainer",
- ".SubmitButton",
- "button[type=submit]",
- "[data-testid=hosted-payment-submit-button]",
- ];
- for (const sel of submitSelectors) {
- const btn = await page.$(sel);
- if (btn) {
- await btn.evaluate((b: any) => (b as HTMLElement).click());
- clicked = true;
- break;
- }
- }
- if (!clicked) {
- try {
- const handle: any = await page.evaluateHandle(() => {
- const candidates = Array.from(document.querySelectorAll("button"));
- return (
- candidates.find((b) =>
- /pay|pay now|submit|complete/i.test(b.textContent || "")
- ) || null
- );
- });
- if (handle) {
- await handle.evaluate((b: any) => (b as HTMLElement).click());
- clicked = true;
- }
- } catch (e) {
- console.log("Could not find submit button by text:", e);
- }
- }
- await timeout(20000);
- } finally {
- // always close browser
- await browser.close();
- }
+ // Click submit/pay button with robust fallbacks
+ let clicked = false;
+ const submitSelectors = [
+ ".SubmitButton-IconContainer",
+ ".SubmitButton",
+ "button[type=submit]",
+ "[data-testid=hosted-payment-submit-button]",
+ ];
+ for (const sel of submitSelectors) {
+ const btn = await page.$(sel);
+ if (btn) {
+ await btn.evaluate((b: any) => (b as HTMLElement).click());
+ clicked = true;
+ break;
+ }
+ }
+ if (!clicked) {
+ try {
+ const handle: any = await page.evaluateHandle(() => {
+ const candidates = Array.from(document.querySelectorAll("button"));
+ return (
+ candidates.find((b) =>
+ /pay|pay now|submit|complete/i.test(b.textContent || ""),
+ ) || null
+ );
+ });
+ if (handle) {
+ await handle.evaluate((b: any) => (b as HTMLElement).click());
+ clicked = true;
+ }
+ } catch (e) {
+ console.log("Could not find submit button by text:", e);
+ }
+ }
+ await timeout(20000);
+ } finally {
+ // always close browser
+ await browser.close();
+ }
};
diff --git a/server/tests/utils/testAttachUtils/getAttachPreviewTotal.ts b/server/tests/utils/testAttachUtils/getAttachPreviewTotal.ts
index ce757dbc5..5e0c255e5 100644
--- a/server/tests/utils/testAttachUtils/getAttachPreviewTotal.ts
+++ b/server/tests/utils/testAttachUtils/getAttachPreviewTotal.ts
@@ -3,28 +3,28 @@ import { getAttachTotal } from "./testAttachUtils.js";
import { APIVersion, FeatureOptions } from "@autumn/shared";
export const getAttachPreviewTotal = async ({
- customerId,
- productId,
- entityId,
- options,
+ customerId,
+ productId,
+ entityId,
+ options,
}: {
- customerId: string;
- productId: string;
- entityId: string;
- options?: FeatureOptions[];
+ customerId: string;
+ productId: string;
+ entityId: string;
+ options?: FeatureOptions[];
}) => {
- const autumn = new AutumnInt({ version: APIVersion.v1_2 });
- const preview = await autumn.attachPreview({
- customer_id: customerId,
- product_id: productId,
- entity_id: entityId,
- });
+ const autumn = new AutumnInt({ version: APIVersion.v1_2 });
+ const preview = await autumn.attachPreview({
+ customer_id: customerId,
+ product_id: productId,
+ entity_id: entityId,
+ });
- const optionsCopy = structuredClone(options);
- const total = getAttachTotal({
- preview,
- options: optionsCopy,
- });
+ const optionsCopy = structuredClone(options);
+ const total = getAttachTotal({
+ preview,
+ options: optionsCopy,
+ });
- return total;
+ return total;
};
diff --git a/server/tests/utils/testAttachUtils/testAttachUtils.ts b/server/tests/utils/testAttachUtils/testAttachUtils.ts
index dd8f932be..ecf8c671f 100644
--- a/server/tests/utils/testAttachUtils/testAttachUtils.ts
+++ b/server/tests/utils/testAttachUtils/testAttachUtils.ts
@@ -1,10 +1,10 @@
import { notNullish } from "@/utils/genUtils.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import {
- AttachBranch,
- AttachPreview,
- OnIncrease,
- UsageModel,
+ AttachBranch,
+ AttachPreview,
+ OnIncrease,
+ UsageModel,
} from "@autumn/shared";
import { addHours, addMonths } from "date-fns";
import { Decimal } from "decimal.js";
@@ -12,151 +12,151 @@ import Stripe from "stripe";
import { hoursToFinalizeInvoice } from "../constants.js";
export const getCurrentOptions = ({
- preview,
- options,
+ preview,
+ options,
}: {
- preview: AttachPreview;
- options?: any;
+ preview: AttachPreview;
+ options?: any;
}) => {
- const currentOptions = structuredClone(options);
- if (!options) return currentOptions;
+ const currentOptions = structuredClone(options);
+ if (!options) return currentOptions;
- const isUpdatePrepaidQuantity =
- preview?.branch == AttachBranch.UpdatePrepaidQuantity;
+ const isUpdatePrepaidQuantity =
+ preview?.branch == AttachBranch.UpdatePrepaidQuantity;
- for (const option of currentOptions || []) {
- const previewOption = preview?.options.find(
- (o: any) =>
- o.feature_id === option.feature_id || o.feature_id === option.featureId
- );
+ for (const option of currentOptions || []) {
+ const previewOption = preview?.options.find(
+ (o: any) =>
+ o.feature_id === option.feature_id || o.feature_id === option.featureId,
+ );
- const currentQuantity = previewOption.current_quantity || 0;
- const newQuantity = option.quantity || 0;
- let difference = newQuantity - currentQuantity;
- difference = difference / previewOption.billing_units;
+ const currentQuantity = previewOption.current_quantity || 0;
+ const newQuantity = option.quantity || 0;
+ let difference = newQuantity - currentQuantity;
+ difference = difference / previewOption.billing_units;
- const isDecrease = newQuantity < currentQuantity;
- const isIncrease = newQuantity > currentQuantity;
+ const isDecrease = newQuantity < currentQuantity;
+ const isIncrease = newQuantity > currentQuantity;
- if (isDecrease && previewOption.config.on_decrease == "none") {
- option.quantity = currentQuantity;
- continue;
- }
+ if (isDecrease && previewOption.config.on_decrease == "none") {
+ option.quantity = currentQuantity;
+ continue;
+ }
- if (
- isUpdatePrepaidQuantity &&
- isIncrease &&
- previewOption.config.on_increase == OnIncrease.ProrateNextCycle
- ) {
- continue;
- }
- }
+ if (
+ isUpdatePrepaidQuantity &&
+ isIncrease &&
+ previewOption.config.on_increase == OnIncrease.ProrateNextCycle
+ ) {
+ continue;
+ }
+ }
- return currentOptions;
+ return currentOptions;
};
// 1. Calculate total
export const getAttachTotal = ({
- preview,
- options,
+ preview,
+ options,
}: {
- preview: AttachPreview;
- options?: any;
+ preview: AttachPreview;
+ options?: any;
}) => {
- const dueToday = preview?.due_today;
+ const dueToday = preview?.due_today;
- let dueTodayTotal =
- dueToday?.line_items.reduce((acc: any, item: any) => {
- // Skip prepaid items that are already in the options
- if (
- item.usage_model == UsageModel.Prepaid &&
- options.some((o: any) => o.feature_id == item.feature_id)
- ) {
- return acc;
- }
+ let dueTodayTotal =
+ dueToday?.line_items.reduce((acc: any, item: any) => {
+ // Skip prepaid items that are already in the options
+ if (
+ item.usage_model == UsageModel.Prepaid &&
+ options.some((o: any) => o.feature_id == item.feature_id)
+ ) {
+ return acc;
+ }
- if (item.amount) {
- return acc.plus(item.amount);
- }
- return acc;
- }, new Decimal(0)) || new Decimal(0);
+ if (item.amount) {
+ return acc.plus(item.amount);
+ }
+ return acc;
+ }, new Decimal(0)) || new Decimal(0);
- const isUpdatePrepaidQuantity =
- preview?.branch == AttachBranch.UpdatePrepaidQuantity;
- if (isUpdatePrepaidQuantity) {
- dueTodayTotal = new Decimal(0);
- }
+ const isUpdatePrepaidQuantity =
+ preview?.branch == AttachBranch.UpdatePrepaidQuantity;
+ if (isUpdatePrepaidQuantity) {
+ dueTodayTotal = new Decimal(0);
+ }
- for (const option of options || []) {
- const previewOption = preview?.options.find(
- (o: any) =>
- o.feature_id === option.feature_id || o.feature_id === option.featureId
- );
+ for (const option of options || []) {
+ const previewOption = preview?.options.find(
+ (o: any) =>
+ o.feature_id === option.feature_id || o.feature_id === option.featureId,
+ );
- const currentQuantity = previewOption.current_quantity || 0;
- const newQuantity = option.quantity || 0;
- let difference = newQuantity - currentQuantity;
- difference = difference / previewOption.billing_units;
+ const currentQuantity = previewOption.current_quantity || 0;
+ const newQuantity = option.quantity || 0;
+ let difference = newQuantity - currentQuantity;
+ difference = difference / previewOption.billing_units;
- const isDecrease = newQuantity < currentQuantity;
- const isIncrease = newQuantity > currentQuantity;
+ const isDecrease = newQuantity < currentQuantity;
+ const isIncrease = newQuantity > currentQuantity;
- if (isDecrease && previewOption.config.on_decrease == "none") {
- option.quantity = currentQuantity;
- continue;
- }
+ if (isDecrease && previewOption.config.on_decrease == "none") {
+ option.quantity = currentQuantity;
+ continue;
+ }
- if (
- isUpdatePrepaidQuantity &&
- isIncrease &&
- previewOption.config.on_increase == OnIncrease.ProrateNextCycle
- ) {
- continue;
- }
+ if (
+ isUpdatePrepaidQuantity &&
+ isIncrease &&
+ previewOption.config.on_increase == OnIncrease.ProrateNextCycle
+ ) {
+ continue;
+ }
- const differenceAmount = new Decimal(previewOption.price).times(difference);
- dueTodayTotal = dueTodayTotal.plus(differenceAmount);
+ const differenceAmount = new Decimal(previewOption.price).times(difference);
+ dueTodayTotal = dueTodayTotal.plus(differenceAmount);
- // Prorated difference
- if (previewOption.proration_amount) {
- dueTodayTotal = dueTodayTotal.plus(
- new Decimal(previewOption.proration_amount)
- );
- }
+ // Prorated difference
+ if (previewOption.proration_amount) {
+ dueTodayTotal = dueTodayTotal.plus(
+ new Decimal(previewOption.proration_amount),
+ );
+ }
- // let previewOption = preview?.options.find(
- // (o: any) =>
- // o.feature_id === option.feature_id || o.feature_id === option.featureId,
- // );
+ // let previewOption = preview?.options.find(
+ // (o: any) =>
+ // o.feature_id === option.feature_id || o.feature_id === option.featureId,
+ // );
- // if (!previewOption) {
- // continue;
- // }
+ // if (!previewOption) {
+ // continue;
+ // }
- // const prepaidAmt = new Decimal(previewOption.price)
- // .times(option.quantity)
- // .dividedBy(previewOption.billing_units);
+ // const prepaidAmt = new Decimal(previewOption.price)
+ // .times(option.quantity)
+ // .dividedBy(previewOption.billing_units);
- // dueTodayTotal = dueTodayTotal.plus(prepaidAmt);
- }
+ // dueTodayTotal = dueTodayTotal.plus(prepaidAmt);
+ }
- return dueTodayTotal.toDecimalPlaces(2).toNumber();
+ return dueTodayTotal.toDecimalPlaces(2).toNumber();
};
export const advanceToNextInvoice = async ({
- stripeCli,
- testClockId,
+ stripeCli,
+ testClockId,
}: {
- stripeCli: Stripe;
- testClockId: string;
+ stripeCli: Stripe;
+ testClockId: string;
}) => {
- return await advanceTestClock({
- stripeCli,
- testClockId,
- advanceTo: addHours(
- addMonths(new Date(), 1),
- hoursToFinalizeInvoice
- ).getTime(),
- waitForSeconds: 30,
- });
+ return await advanceTestClock({
+ stripeCli,
+ testClockId,
+ advanceTo: addHours(
+ addMonths(new Date(), 1),
+ hoursToFinalizeInvoice,
+ ).getTime(),
+ waitForSeconds: 30,
+ });
};
diff --git a/server/tests/utils/testAttachUtils/trialAttachUtils.ts b/server/tests/utils/testAttachUtils/trialAttachUtils.ts
index 4ae39423e..8b475297d 100644
--- a/server/tests/utils/testAttachUtils/trialAttachUtils.ts
+++ b/server/tests/utils/testAttachUtils/trialAttachUtils.ts
@@ -11,228 +11,228 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js";
export async function manuallyAttachDefaultTrial({
- customerId,
- stripeCli,
- autumn,
- db,
- org,
- env,
- testClockID,
- autumnJs,
- attachPm = "",
- group,
+ customerId,
+ stripeCli,
+ autumn,
+ db,
+ org,
+ env,
+ testClockID,
+ autumnJs,
+ attachPm = "",
+ group,
}: {
- customerId: string;
- stripeCli: Stripe;
- autumn: AutumnInt;
- db: DrizzleCli;
- org: any;
- env: AppEnv;
- testClockID?: string;
- autumnJs: any;
- attachPm?: "success" | "fail" | "";
- group?: string;
+ customerId: string;
+ stripeCli: Stripe;
+ autumn: AutumnInt;
+ db: DrizzleCli;
+ org: any;
+ env: AppEnv;
+ testClockID?: string;
+ autumnJs: any;
+ attachPm?: "success" | "fail" | "";
+ group?: string;
}) {
- try {
- const existingCustomer = await CusService.get({
- db,
- idOrInternalId: customerId,
- orgId: org.id,
- env,
- });
- if (existingCustomer) {
- // Delete via API to clean up properly
- await autumnJs.customers.delete(customerId);
- }
- } catch (error) {
- // Ignore if customer doesn't exist
- console.log("Customer doesn't exist, skipping delete", error);
- }
+ try {
+ const existingCustomer = await CusService.get({
+ db,
+ idOrInternalId: customerId,
+ orgId: org.id,
+ env,
+ });
+ if (existingCustomer) {
+ // Delete via API to clean up properly
+ await autumnJs.customers.delete(customerId);
+ }
+ } catch (error) {
+ // Ignore if customer doesn't exist
+ console.log("Customer doesn't exist, skipping delete", error);
+ }
- // Step 2: Manually create customer in DB (following createNewCustomer.ts logic)
- const customerData = {
- id: customerId,
- name: customerId,
- email: `${customerId}@example.com`,
- metadata: {},
- internal_id: generateId("cus"),
- org_id: org.id,
- created_at: Date.now(),
- env,
- };
+ // Step 2: Manually create customer in DB (following createNewCustomer.ts logic)
+ const customerData = {
+ id: customerId,
+ name: customerId,
+ email: `${customerId}@example.com`,
+ metadata: {},
+ internal_id: generateId("cus"),
+ org_id: org.id,
+ created_at: Date.now(),
+ env,
+ };
- const newCustomer = await CusService.insert({
- db,
- data: customerData,
- });
+ const newCustomer = await CusService.insert({
+ db,
+ data: customerData,
+ });
- if (!newCustomer) {
- throw new Error("Failed to create customer");
- }
+ if (!newCustomer) {
+ throw new Error("Failed to create customer");
+ }
- // Step 3: Get default products (following createNewCustomer.ts logic)
- const allDefaultProds = await ProductService.listDefault({
- db,
- orgId: org.id,
- env,
- });
+ // Step 3: Get default products (following createNewCustomer.ts logic)
+ const allDefaultProds = await ProductService.listDefault({
+ db,
+ orgId: org.id,
+ env,
+ });
- // Filter by group if specified
- const defaultProds = group
- ? allDefaultProds.filter((p) => p.group === group)
- : allDefaultProds;
+ // Filter by group if specified
+ const defaultProds = group
+ ? allDefaultProds.filter((p) => p.group === group)
+ : allDefaultProds;
- const defaultPaidTrialProd = defaultProds.find((p) =>
- isDefaultTrialFullProduct({ product: p })
- );
+ const defaultPaidTrialProd = defaultProds.find((p) =>
+ isDefaultTrialFullProduct({ product: p }),
+ );
- let customer = newCustomer;
+ let customer = newCustomer;
- if (defaultPaidTrialProd) {
- // Step 4: Create Stripe customer with test clock
- const stripeCustomer = await stripeCli.customers.create({
- email: `${customerId}@example.com`,
- test_clock: testClockID ? testClockID : undefined,
- });
+ if (defaultPaidTrialProd) {
+ // Step 4: Create Stripe customer with test clock
+ const stripeCustomer = await stripeCli.customers.create({
+ email: `${customerId}@example.com`,
+ test_clock: testClockID ? testClockID : undefined,
+ });
- // Step 5: Update customer with Stripe processor info (BEFORE attachPmToCus)
- await CusService.update({
- db,
- internalCusId: newCustomer.internal_id,
- update: {
- processor: {
- type: ProcessorType.Stripe,
- id: stripeCustomer.id,
- },
- },
- });
+ // Step 5: Update customer with Stripe processor info (BEFORE attachPmToCus)
+ await CusService.update({
+ db,
+ internalCusId: newCustomer.internal_id,
+ update: {
+ processor: {
+ type: ProcessorType.Stripe,
+ id: stripeCustomer.id,
+ },
+ },
+ });
- // Update local customer object
- customer = {
- ...newCustomer,
- processor: {
- id: stripeCustomer.id,
- type: "stripe",
- },
- } as any;
+ // Update local customer object
+ customer = {
+ ...newCustomer,
+ processor: {
+ id: stripeCustomer.id,
+ type: "stripe",
+ },
+ } as any;
- if (attachPm && testClockID) {
- await attachPmToCus({
- customer: customer,
- org: org,
- env: env,
- db: db,
- testClockId: testClockID,
- willFail: attachPm === "fail",
- });
- }
+ if (attachPm && testClockID) {
+ await attachPmToCus({
+ customer: customer,
+ org: org,
+ env: env,
+ db: db,
+ testClockId: testClockID,
+ willFail: attachPm === "fail",
+ });
+ }
- // Step 6: Manually attach the default trial product (following createNewCustomer.ts logic)
- const req = {
- db,
- org,
- env,
- orgId: org.id,
- logtail: console,
- logger: console,
- } as any;
+ // Step 6: Manually attach the default trial product (following createNewCustomer.ts logic)
+ const req = {
+ db,
+ org,
+ env,
+ orgId: org.id,
+ logtail: console,
+ logger: console,
+ } as any;
- await handleAddProduct({
- req,
- attachParams: newCusToAttachParams({
- req,
- newCus: customer as any,
- products: [defaultPaidTrialProd],
- stripeCli,
- freeTrial: defaultPaidTrialProd.free_trial || null,
- }),
- });
+ await handleAddProduct({
+ req,
+ attachParams: newCusToAttachParams({
+ req,
+ newCus: customer as any,
+ products: [defaultPaidTrialProd],
+ stripeCli,
+ freeTrial: defaultPaidTrialProd.free_trial || null,
+ }),
+ });
- return customer;
- }
+ return customer;
+ }
}
export async function cleanupQueueAndCache() {
- try {
- const { QueueManager } = await import("@/queue/QueueManager.js");
- const queueInstance = await QueueManager.getInstance();
+ try {
+ const { QueueManager } = await import("@/queue/QueueManager.js");
+ const queueInstance = await QueueManager.getInstance();
- // Access private properties to close connections
- if ((queueInstance as any).queue) {
- await (queueInstance as any).queue.close();
- }
- if ((queueInstance as any).backupQueue) {
- await (queueInstance as any).backupQueue.close();
- }
- if ((queueInstance as any).mainConnection) {
- await (queueInstance as any).mainConnection.quit();
- }
- if ((queueInstance as any).backupConnection) {
- await (queueInstance as any).backupConnection.quit();
- }
- } catch (error) {
- // Ignore cleanup errors
- }
+ // Access private properties to close connections
+ if ((queueInstance as any).queue) {
+ await (queueInstance as any).queue.close();
+ }
+ if ((queueInstance as any).backupQueue) {
+ await (queueInstance as any).backupQueue.close();
+ }
+ if ((queueInstance as any).mainConnection) {
+ await (queueInstance as any).mainConnection.quit();
+ }
+ if ((queueInstance as any).backupConnection) {
+ await (queueInstance as any).backupConnection.quit();
+ }
+ } catch (error) {
+ // Ignore cleanup errors
+ }
- try {
- const { CacheManager } = await import("@/external/caching/CacheManager.js");
- const cacheInstance = await CacheManager.getInstance();
- if ((cacheInstance as any).connection) {
- await (cacheInstance as any).connection.quit();
- }
- } catch (error) {
- // Ignore cleanup errors
- }
+ try {
+ const { CacheManager } = await import("@/external/caching/CacheManager.js");
+ const cacheInstance = await CacheManager.getInstance();
+ if ((cacheInstance as any).connection) {
+ await (cacheInstance as any).connection.quit();
+ }
+ } catch (error) {
+ // Ignore cleanup errors
+ }
}
export async function flipDefaultState({
- id,
- autumn,
- state,
+ id,
+ autumn,
+ state,
}: {
- id: string;
- autumn: AutumnInt;
- state: boolean;
+ id: string;
+ autumn: AutumnInt;
+ state: boolean;
}) {
- try {
- let productExists = await autumn.products.get(id);
- if (productExists) {
- await autumn.products.update(id, {
- is_default: state,
- });
- }
- } catch (error) {
- console.log(`Product ${id} doesn't exist, skipping update`);
- }
+ try {
+ let productExists = await autumn.products.get(id);
+ if (productExists) {
+ await autumn.products.update(id, {
+ is_default: state,
+ });
+ }
+ } catch (error) {
+ console.log(`Product ${id} doesn't exist, skipping update`);
+ }
}
export async function flipDefaultStates({
- currentCase,
- autumn,
+ currentCase,
+ autumn,
}: {
- currentCase: number;
- autumn: AutumnInt;
+ currentCase: number;
+ autumn: AutumnInt;
}) {
- let total = 4;
+ let total = 4;
- // Now flip all products from 0 to total-1, only current case should be true
- for (let i = 0; i < total; i++) {
- const id = `defaultTrial${i}_pro`;
- const state = i === currentCase; // Only the current case is true
- await flipDefaultState({
- id,
- autumn,
- state,
- });
- }
+ // Now flip all products from 0 to total-1, only current case should be true
+ for (let i = 0; i < total; i++) {
+ const id = `defaultTrial${i}_pro`;
+ const state = i === currentCase; // Only the current case is true
+ await flipDefaultState({
+ id,
+ autumn,
+ state,
+ });
+ }
- for (let i = 0; i < total; i++) {
- const id = `defaultTrial${i}_free`;
- const state = i === currentCase; // Only the current case is true
- await flipDefaultState({
- id,
- autumn,
- state,
- });
- }
+ for (let i = 0; i < total; i++) {
+ const id = `defaultTrial${i}_free`;
+ const state = i === currentCase; // Only the current case is true
+ await flipDefaultState({
+ id,
+ autumn,
+ state,
+ });
+ }
}
diff --git a/server/tests/utils/testInitUtils.ts b/server/tests/utils/testInitUtils.ts
index c8bc5b8c3..c35b46b4c 100644
--- a/server/tests/utils/testInitUtils.ts
+++ b/server/tests/utils/testInitUtils.ts
@@ -5,36 +5,36 @@ import { initCustomer } from "./init.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const initCustomerWithTestClock = async ({
- customerId,
- org,
- env,
- db,
- fingerprint,
+ customerId,
+ org,
+ env,
+ db,
+ fingerprint,
}: {
- customerId: string;
- org: Organization;
- env: AppEnv;
- db: DrizzleCli;
- fingerprint?: string;
+ customerId: string;
+ org: Organization;
+ env: AppEnv;
+ db: DrizzleCli;
+ fingerprint?: string;
}) => {
- const stripeCli = createStripeCli({ org: org, env: env });
- const testClock = await stripeCli.testHelpers.testClocks.create({
- frozen_time: Math.floor(Date.now() / 1000),
- });
+ const stripeCli = createStripeCli({ org: org, env: env });
+ const testClock = await stripeCli.testHelpers.testClocks.create({
+ frozen_time: Math.floor(Date.now() / 1000),
+ });
- let customer = await initCustomer({
- customer_data: {
- id: customerId,
- name: customerId,
- email: "test@test.com",
- fingerprint,
- },
- db: db,
- org: org,
- env: env,
- testClockId: testClock.id,
- attachPm: true,
- });
+ let customer = await initCustomer({
+ customer_data: {
+ id: customerId,
+ name: customerId,
+ email: "test@test.com",
+ fingerprint,
+ },
+ db: db,
+ org: org,
+ env: env,
+ testClockId: testClock.id,
+ attachPm: true,
+ });
- return { testClockId: testClock.id, customer };
+ return { testClockId: testClock.id, customer };
};
diff --git a/server/tests/utils/testProductUtils/testProductUtils.ts b/server/tests/utils/testProductUtils/testProductUtils.ts
index 1ac865a53..e30bece44 100644
--- a/server/tests/utils/testProductUtils/testProductUtils.ts
+++ b/server/tests/utils/testProductUtils/testProductUtils.ts
@@ -2,75 +2,75 @@ import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUt
import { isPriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
import { nullish } from "@/utils/genUtils.js";
import {
- BillingInterval,
- FixedPriceConfig,
- FullProduct,
- Price,
- ProductItem,
- ProductV2,
+ BillingInterval,
+ FixedPriceConfig,
+ FullProduct,
+ Price,
+ ProductItem,
+ ProductV2,
} from "@autumn/shared";
export const addPrefixToProducts = ({
- products,
- prefix,
+ products,
+ prefix,
}: {
- products: ProductV2[];
- prefix: string;
+ products: ProductV2[];
+ prefix: string;
}) => {
- for (const product of products) {
- product.id = `${prefix}_${product.id}`;
- product.name = `${prefix} ${product.name}`;
- product.group = prefix;
- }
+ for (const product of products) {
+ product.id = `${prefix}_${product.id}`;
+ product.name = `${prefix} ${product.name}`;
+ product.group = prefix;
+ }
- return products;
+ return products;
};
export const replaceItems = ({
- featureId,
- interval,
- intervalCount,
- newItem,
- items,
+ featureId,
+ interval,
+ intervalCount,
+ newItem,
+ items,
}: {
- featureId?: string;
- interval?: BillingInterval;
- intervalCount?: number;
- newItem: ProductItem;
- items: ProductItem[];
+ featureId?: string;
+ interval?: BillingInterval;
+ intervalCount?: number;
+ newItem: ProductItem;
+ items: ProductItem[];
}) => {
- let newItems = structuredClone(items);
+ let newItems = structuredClone(items);
- let index;
- if (featureId) {
- index = newItems.findIndex((item) => item.feature_id == featureId);
- }
+ let index;
+ if (featureId) {
+ index = newItems.findIndex((item) => item.feature_id == featureId);
+ }
- if (interval) {
- index = newItems.findIndex(
- (item) =>
- item.interval == (interval as any) &&
- (intervalCount ? item.interval_count == intervalCount : true) &&
- nullish(item.feature_id)
- );
- }
+ if (interval) {
+ index = newItems.findIndex(
+ (item) =>
+ item.interval == (interval as any) &&
+ (intervalCount ? item.interval_count == intervalCount : true) &&
+ nullish(item.feature_id),
+ );
+ }
- if (index == -1) {
- throw new Error("Item not found");
- }
+ if (index == -1) {
+ throw new Error("Item not found");
+ }
- newItems[index!] = newItem;
+ newItems[index!] = newItem;
- return newItems;
+ return newItems;
};
export const getBasePrice = ({ product }: { product: ProductV2 }) => {
- return product.items.find((item) => isPriceItem(item))?.price || 0;
+ return product.items.find((item) => isPriceItem(item))?.price || 0;
};
export const v1ProductToBasePrice = ({ prices }: { prices: Price[] }) => {
- let fixedPrice = prices.find((price) => isFixedPrice({ price }));
- if (fixedPrice) {
- return (fixedPrice.config as FixedPriceConfig).amount;
- } else return 0;
+ let fixedPrice = prices.find((price) => isFixedPrice({ price }));
+ if (fixedPrice) {
+ return (fixedPrice.config as FixedPriceConfig).amount;
+ } else return 0;
};
diff --git a/setup.js b/setup.js
index aaf949e91..ead02880d 100644
--- a/setup.js
+++ b/setup.js
@@ -6,314 +6,315 @@ import { spawnSync } from "child_process";
import chalk from "chalk";
const genUrlSafeBase64 = (bytes) => {
- return randomBytes(bytes)
- .toString("base64")
- .replace(/\+/g, "-")
- .replace(/\//g, "_")
- .replace(/=+$/g, "");
+ return randomBytes(bytes)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/g, "");
};
const genRandomSubdomain = (length = 10) => {
- const chars = "abcdefghijklmnopqrstuvwxyz";
- let result = "";
- for (let i = 0; i < length; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
+ const chars = "abcdefghijklmnopqrstuvwxyz";
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
};
const genAlphanumericPassword = (length = 24) => {
- const chars =
- "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- let result = "";
- for (let i = 0; i < length; i++) {
- result += chars.charAt(Math.floor(Math.random() * chars.length));
- }
- return result;
+ const chars =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ let result = "";
+ for (let i = 0; i < length; i++) {
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+ return result;
};
// Helper to get or create a Supabase org
const getOrCreateSupabaseOrg = async () => {
- // List orgs (table output)
- const orgListResult = spawnSync("npx", ["supabase", "orgs", "list"], {
- encoding: "utf-8",
- shell: true,
- });
- if (orgListResult.status !== 0) {
- console.error(chalk.red("❌ Failed to list Supabase orgs."));
- process.exit(1);
- }
- const lines = orgListResult.stdout
- .split("\n")
- .map((l) => l.trim())
- .filter(Boolean);
+ // List orgs (table output)
+ const orgListResult = spawnSync("npx", ["supabase", "orgs", "list"], {
+ encoding: "utf-8",
+ shell: true,
+ });
+ if (orgListResult.status !== 0) {
+ console.error(chalk.red("❌ Failed to list Supabase orgs."));
+ process.exit(1);
+ }
+ const lines = orgListResult.stdout
+ .split("\n")
+ .map((l) => l.trim())
+ .filter(Boolean);
- // Find separator line (e.g., "------|------")
- const sepIdx = lines.findIndex(
- (line) => line.includes("|") && line.includes("-")
- );
+ // Find separator line (e.g., "------|------")
+ const sepIdx = lines.findIndex(
+ (line) => line.includes("|") && line.includes("-"),
+ );
- // If only header and separator, no orgs exist
- if (lines.length <= sepIdx + 1) {
- console.log(chalk.yellowBright("No Supabase organizations found."));
- const { createOrg } = await inquirer.prompt([
- {
- type: "confirm",
- name: "createOrg",
- message: chalk.cyan(
- "Would you like to create a new Supabase organization now?"
- ),
- default: true,
- },
- ]);
- if (!createOrg) {
- console.log(
- chalk.red(
- "❌ Cannot continue without a Supabase organization. Exiting."
- )
- );
- process.exit(1);
- }
- const { orgName } = await inquirer.prompt([
- {
- type: "input",
- name: "orgName",
- message: chalk.cyan("Enter a name for your new Supabase organization:"),
- validate: (input) => input && input.length > 2,
- },
- ]);
- console.log(
- chalk.blueBright(`\nCreating Supabase organization '${orgName}'...`)
- );
- const createOrgResult = spawnSync(
- "npx",
- ["supabase", "orgs", "create", orgName],
- { stdio: "inherit", encoding: "utf-8", shell: true }
- );
- if (createOrgResult.status !== 0) {
- console.error(chalk.red("❌ Failed to create Supabase organization."));
- process.exit(1);
- }
- // Done! Org created, return
- return;
- }
+ // If only header and separator, no orgs exist
+ if (lines.length <= sepIdx + 1) {
+ console.log(chalk.yellowBright("No Supabase organizations found."));
+ const { createOrg } = await inquirer.prompt([
+ {
+ type: "confirm",
+ name: "createOrg",
+ message: chalk.cyan(
+ "Would you like to create a new Supabase organization now?",
+ ),
+ default: true,
+ },
+ ]);
+ if (!createOrg) {
+ console.log(
+ chalk.red(
+ "❌ Cannot continue without a Supabase organization. Exiting.",
+ ),
+ );
+ process.exit(1);
+ }
+ const { orgName } = await inquirer.prompt([
+ {
+ type: "input",
+ name: "orgName",
+ message: chalk.cyan("Enter a name for your new Supabase organization:"),
+ validate: (input) => input && input.length > 2,
+ },
+ ]);
+ console.log(
+ chalk.blueBright(`\nCreating Supabase organization '${orgName}'...`),
+ );
+ const createOrgResult = spawnSync(
+ "npx",
+ ["supabase", "orgs", "create", orgName],
+ { stdio: "inherit", encoding: "utf-8", shell: true },
+ );
+ if (createOrgResult.status !== 0) {
+ console.error(chalk.red("❌ Failed to create Supabase organization."));
+ process.exit(1);
+ }
+ // Done! Org created, return
+ return;
+ }
- // At least one org exists, just proceed
- return;
+ // At least one org exists, just proceed
+ return;
};
const generateSupabaseDatabaseUrl = async () => {
- // Step 3: Check if already logged in, otherwise run supabase login
- console.log(
- chalk.magentaBright("\n================ Supabase Setup ================\n")
- );
+ // Step 3: Check if already logged in, otherwise run supabase login
+ console.log(
+ chalk.magentaBright("\n================ Supabase Setup ================\n"),
+ );
- // Check if user is already logged in by trying to list orgs
- console.log(chalk.blueBright("\nChecking Supabase authentication..."));
- const authCheckResult = spawnSync("npx", ["supabase", "orgs", "list"], {
- encoding: "utf-8",
- shell: true,
- stdio: "pipe", // Capture output instead of showing it
- });
+ // Check if user is already logged in by trying to list orgs
+ console.log(chalk.blueBright("\nChecking Supabase authentication..."));
+ const authCheckResult = spawnSync("npx", ["supabase", "orgs", "list"], {
+ encoding: "utf-8",
+ shell: true,
+ stdio: "pipe", // Capture output instead of showing it
+ });
- if (authCheckResult.status !== 0) {
- // User is not logged in
- console.log(chalk.yellowBright("Not logged in to Supabase."));
- console.log(chalk.blueBright("Launching Supabase login..."));
- spawnSync("npx", ["supabase", "login"], { stdio: "inherit", shell: true });
- } else {
- console.log(chalk.greenBright("✅ Already logged in to Supabase."));
- }
+ if (authCheckResult.status !== 0) {
+ // User is not logged in
+ console.log(chalk.yellowBright("Not logged in to Supabase."));
+ console.log(chalk.blueBright("Launching Supabase login..."));
+ spawnSync("npx", ["supabase", "login"], { stdio: "inherit", shell: true });
+ } else {
+ console.log(chalk.greenBright("✅ Already logged in to Supabase."));
+ }
- // Step 3.5: Ensure org exists and select one
- await getOrCreateSupabaseOrg();
+ // Step 3.5: Ensure org exists and select one
+ await getOrCreateSupabaseOrg();
- // Step 4: Prompt for project name and region
- const projectName = "autumn-oss-db";
+ // Step 4: Prompt for project name and region
+ const projectName = "autumn-oss-db";
- // Step 5: Generate DB password (alphanumeric only)
- const dbPassword = genAlphanumericPassword(24); // 24 chars, alphanumeric
+ // Step 5: Generate DB password (alphanumeric only)
+ const dbPassword = genAlphanumericPassword(24); // 24 chars, alphanumeric
- // Step 6: Create Supabase project
- console.log(
- chalk.blueBright(`\nCreating Supabase project '${projectName}'...`)
- );
- const createResult = spawnSync(
- "npx",
- [
- "supabase",
- "projects",
- "create",
- projectName,
- "--db-password",
- dbPassword,
- ],
- { stdio: "inherit", encoding: "utf-8", shell: true }
- );
+ // Step 6: Create Supabase project
+ console.log(
+ chalk.blueBright(`\nCreating Supabase project '${projectName}'...`),
+ );
+ const createResult = spawnSync(
+ "npx",
+ [
+ "supabase",
+ "projects",
+ "create",
+ projectName,
+ "--db-password",
+ dbPassword,
+ ],
+ { stdio: "inherit", encoding: "utf-8", shell: true },
+ );
- if (createResult.status !== 0) {
- console.error("❌ Failed to create Supabase project.");
- process.exit(1);
- }
+ if (createResult.status !== 0) {
+ console.error("❌ Failed to create Supabase project.");
+ process.exit(1);
+ }
- // List supabase projects
- const listResult = spawnSync(
- "npx",
- ["supabase", "projects", "list", "--output", "json"],
- { encoding: "utf-8", shell: true }
- );
+ // List supabase projects
+ const listResult = spawnSync(
+ "npx",
+ ["supabase", "projects", "list", "--output", "json"],
+ { encoding: "utf-8", shell: true },
+ );
- if (listResult.status !== 0) {
- console.error("❌ Failed to list Supabase projects.");
- process.exit(1);
- }
+ if (listResult.status !== 0) {
+ console.error("❌ Failed to list Supabase projects.");
+ process.exit(1);
+ }
- const projects = JSON.parse(listResult.stdout);
- const found = projects.find((p) => p.name === projectName);
- if (!found) {
- console.error(chalk.red("❌ Failed to find new Supabase project."));
- process.exit(1);
- }
- const projectId = found.id;
- const region = found.region;
+ const projects = JSON.parse(listResult.stdout);
+ const found = projects.find((p) => p.name === projectName);
+ if (!found) {
+ console.error(chalk.red("❌ Failed to find new Supabase project."));
+ process.exit(1);
+ }
+ const projectId = found.id;
+ const region = found.region;
- // Step 8: Construct DATABASE_URL
- const databaseUrl = `postgresql://postgres.${projectId}:${dbPassword}@aws-1-${region}.pooler.supabase.com:5432/postgres`;
- console.log(chalk.greenBright(`\nGenerated DB password: ${dbPassword}\n`));
- console.log(chalk.greenBright(`\nYour DATABASE_URL is:\n${databaseUrl}\n`));
- console.log(chalk.yellow("--------------------------------"));
+ // Step 8: Construct DATABASE_URL
+ const databaseUrl = `postgresql://postgres.${projectId}:${dbPassword}@aws-1-${region}.pooler.supabase.com:5432/postgres`;
+ console.log(chalk.greenBright(`\nGenerated DB password: ${dbPassword}\n`));
+ console.log(chalk.greenBright(`\nYour DATABASE_URL is:\n${databaseUrl}\n`));
+ console.log(chalk.yellow("--------------------------------"));
- // Step 9: Prompt to run docker compose up
- console.log(chalk.magentaBright("\nNext steps:"));
- console.log(
- chalk.yellow(
- 'Run "docker compose -f docker-compose.dev.yml up" to start Autumn'
- )
- );
+ // Step 9: Prompt to run docker compose up
+ console.log(chalk.magentaBright("\nNext steps:"));
+ console.log(
+ chalk.yellow(
+ 'Run "docker compose -f docker-compose.dev.yml up" to start Autumn',
+ ),
+ );
- return databaseUrl;
+ return databaseUrl;
};
const handleDatabaseSetup = async () => {
- // Step 2: Ask user what to do for DB
- console.log(
- chalk.magentaBright(
- "\n================ Autumn Database Setup ================\n"
- )
- );
- const { dbOption } = await inquirer.prompt([
- {
- type: "list",
- name: "dbOption",
- message: chalk.cyan("How do you want to set up your database?"),
- choices: [
- { name: "Set up Supabase (Cloud) for Autumn", value: "supabase" },
- { name: "Paste in your own DATABASE_URL", value: "paste" },
- { name: "Paste in your own DATABASE_URL later", value: "later" },
- ],
- default: "supabase",
- },
- ]);
+ // Step 2: Ask user what to do for DB
+ console.log(
+ chalk.magentaBright(
+ "\n================ Autumn Database Setup ================\n",
+ ),
+ );
+ const { dbOption } = await inquirer.prompt([
+ {
+ type: "list",
+ name: "dbOption",
+ message: chalk.cyan("How do you want to set up your database?"),
+ choices: [
+ { name: "Set up Supabase (Cloud) for Autumn", value: "supabase" },
+ { name: "Paste in your own DATABASE_URL", value: "paste" },
+ { name: "Paste in your own DATABASE_URL later", value: "later" },
+ ],
+ default: "supabase",
+ },
+ ]);
- let databaseUrl = "";
+ let databaseUrl = "";
- if (dbOption === "supabase") {
- databaseUrl = await generateSupabaseDatabaseUrl();
- } else if (dbOption === "paste") {
- const res = await inquirer.prompt([
- {
- type: "input",
- name: "databaseUrl",
- message: "Paste in your DATABASE_URL:",
- validate: (input) => input && input.length > 5,
- },
- ]);
- databaseUrl = res.databaseUrl;
- } else if (dbOption === "later") {
- databaseUrl = "";
- }
+ if (dbOption === "supabase") {
+ databaseUrl = await generateSupabaseDatabaseUrl();
+ } else if (dbOption === "paste") {
+ const res = await inquirer.prompt([
+ {
+ type: "input",
+ name: "databaseUrl",
+ message: "Paste in your DATABASE_URL:",
+ validate: (input) => input && input.length > 5,
+ },
+ ]);
+ databaseUrl = res.databaseUrl;
+ } else if (dbOption === "later") {
+ databaseUrl = "";
+ }
- return databaseUrl;
+ return databaseUrl;
};
-
async function main() {
- // Step 1: Generate secrets
- console.log(
- chalk.magentaBright("\n================ Autumn Setup ================\n")
- );
- const localtunnelReservedKey = genRandomSubdomain(32);
- const secrets = {
- BETTER_AUTH_SECRET: genUrlSafeBase64(64),
- ENCRYPTION_IV: genUrlSafeBase64(16),
- ENCRYPTION_PASSWORD: genUrlSafeBase64(64),
- BETTER_AUTH_URL: "http://localhost:8080",
- CLIENT_URL: "http://localhost:3000",
- LOCALTUNNEL_RESERVED_KEY: localtunnelReservedKey,
- STRIPE_WEBHOOK_URL: `https://${localtunnelReservedKey}.loca.lt`,
- };
+ // Step 1: Generate secrets
+ console.log(
+ chalk.magentaBright("\n================ Autumn Setup ================\n"),
+ );
+ const localtunnelReservedKey = genRandomSubdomain(32);
+ const secrets = {
+ BETTER_AUTH_SECRET: genUrlSafeBase64(64),
+ ENCRYPTION_IV: genUrlSafeBase64(16),
+ ENCRYPTION_PASSWORD: genUrlSafeBase64(64),
+ BETTER_AUTH_URL: "http://localhost:8080",
+ CLIENT_URL: "http://localhost:3000",
+ LOCALTUNNEL_RESERVED_KEY: localtunnelReservedKey,
+ STRIPE_WEBHOOK_URL: `https://${localtunnelReservedKey}.loca.lt`,
+ };
- let databaseUrl = "";
- let stripeWebhookVars = [];
+ let databaseUrl = "";
+ let stripeWebhookVars = [];
- databaseUrl = await handleDatabaseSetup();
- // stripeWebhookVars = await handleLocalRunSetup();
+ databaseUrl = await handleDatabaseSetup();
+ // stripeWebhookVars = await handleLocalRunSetup();
- // Step 11: Write to server/.env
- console.log(
- chalk.magentaBright("\n================ Writing .env ================\n")
- );
- const envSections = [];
+ // Step 11: Write to server/.env
+ console.log(
+ chalk.magentaBright("\n================ Writing .env ================\n"),
+ );
+ const envSections = [];
- // Autumn Auth section
- envSections.push(
- "# Auth",
- `BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
- `BETTER_AUTH_URL=${secrets.BETTER_AUTH_URL}`,
- `CLIENT_URL=${secrets.CLIENT_URL}`,
- ""
- );
+ // Autumn Auth section
+ envSections.push(
+ "# Auth",
+ `BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
+ `BETTER_AUTH_URL=${secrets.BETTER_AUTH_URL}`,
+ `CLIENT_URL=${secrets.CLIENT_URL}`,
+ "",
+ );
- // Stripe required section
- envSections.push(
- "# Stripe",
- `LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
- `ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
- `ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
- `STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
- ""
- );
+ // Stripe required section
+ envSections.push(
+ "# Stripe",
+ `LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
+ `ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
+ `ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
+ `STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
+ "",
+ );
- // Database section
- if (databaseUrl) {
- envSections.push("# Database", `DATABASE_URL=${databaseUrl}`, "");
- }
+ // Database section
+ if (databaseUrl) {
+ envSections.push("# Database", `DATABASE_URL=${databaseUrl}`, "");
+ }
- // Stripe Webhooks section
- if (stripeWebhookVars.length > 0) {
- envSections.push("# Stripe Webhooks");
- envSections.push(...stripeWebhookVars);
- envSections.push("");
- }
+ // Stripe Webhooks section
+ if (stripeWebhookVars.length > 0) {
+ envSections.push("# Stripe Webhooks");
+ envSections.push(...stripeWebhookVars);
+ envSections.push("");
+ }
- const envVars = envSections.join("\n");
+ const envVars = envSections.join("\n");
- writeFileSync("server/.env", envVars);
- try {
- copyFileSync("vite/.env.example", "vite/.env");
- } catch (error) {
- console.log(chalk.red("❌ Failed to copy vite/.env.example to vite/.env"));
- console.log(chalk.red("❌ Please copy the file manually"));
- }
+ writeFileSync("server/.env", envVars);
+ try {
+ copyFileSync("vite/.env.example", "vite/.env");
+ } catch (error) {
+ console.log(chalk.red("❌ Failed to copy vite/.env.example to vite/.env"));
+ console.log(chalk.red("❌ Please copy the file manually"));
+ }
- console.log(chalk.greenBright("🎉 Setup complete! 🎉"));
- console.log(chalk.cyan("You can find your env variables in server/.env"));
+ console.log(chalk.greenBright("🎉 Setup complete! 🎉"));
+ console.log(chalk.cyan("You can find your env variables in server/.env"));
- console.log(chalk.cyan("\nNext steps:\n"));
- console.log(chalk.cyan("1. Set up your database tables:\n"));
- console.log(chalk.whiteBright(" bun db:generate && bun db:migrate\n"));
- console.log(chalk.cyan("2. Start Autumn:\n"));
- console.log(chalk.whiteBright(" docker compose -f docker-compose.dev.yml up\n"));
+ console.log(chalk.cyan("\nNext steps:\n"));
+ console.log(chalk.cyan("1. Set up your database tables:\n"));
+ console.log(chalk.whiteBright(" bun db:generate && bun db:migrate\n"));
+ console.log(chalk.cyan("2. Start Autumn:\n"));
+ console.log(
+ chalk.whiteBright(" docker compose -f docker-compose.dev.yml up\n"),
+ );
}
main();
diff --git a/setupci.js b/setupci.js
index 381b5fdc4..92c2d7fd9 100644
--- a/setupci.js
+++ b/setupci.js
@@ -1,89 +1,88 @@
#!/usr/bin/env node
-import { randomBytes } from 'crypto';
-import { writeFileSync, copyFileSync } from 'fs';
-import chalk from 'chalk';
+import { randomBytes } from "crypto";
+import { writeFileSync, copyFileSync } from "fs";
+import chalk from "chalk";
const genUrlSafeBase64 = (bytes) => {
- return randomBytes(bytes)
- .toString('base64')
- .replace(/\+/g, '-')
- .replace(/\//g, '_')
- .replace(/=+$/g, '');
-}
+ return randomBytes(bytes)
+ .toString("base64")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_")
+ .replace(/=+$/g, "");
+};
async function main() {
- // Step 1: Generate secrets
- console.log(chalk.magentaBright('\n================ Autumn Setup ================\n'));
- const secrets = {
- BETTER_AUTH_SECRET: genUrlSafeBase64(64),
- ENCRYPTION_IV: process.env.ENCRYPTION_IV,
- ENCRYPTION_PASSWORD: process.env.ENCRYPTION_PASSWORD,
- BETTER_AUTH_URL: 'http://localhost:8080',
- CLIENT_URL: 'http://localhost:3000',
- LOCALTUNNEL_RESERVED_KEY: process.env.LOCALTUNNEL_RESERVED_KEY,
- STRIPE_WEBHOOK_URL: process.env.STRIPE_WEBHOOK_URL,
- };
+ // Step 1: Generate secrets
+ console.log(
+ chalk.magentaBright("\n================ Autumn Setup ================\n"),
+ );
+ const secrets = {
+ BETTER_AUTH_SECRET: genUrlSafeBase64(64),
+ ENCRYPTION_IV: process.env.ENCRYPTION_IV,
+ ENCRYPTION_PASSWORD: process.env.ENCRYPTION_PASSWORD,
+ BETTER_AUTH_URL: "http://localhost:8080",
+ CLIENT_URL: "http://localhost:3000",
+ LOCALTUNNEL_RESERVED_KEY: process.env.LOCALTUNNEL_RESERVED_KEY,
+ STRIPE_WEBHOOK_URL: process.env.STRIPE_WEBHOOK_URL,
+ };
- let databaseUrl = process.env.DATABASE_URL;
- let stripeWebhookVars = [];
- // stripeWebhookVars = await handleLocalRunSetup();
+ let databaseUrl = process.env.DATABASE_URL;
+ let stripeWebhookVars = [];
+ // stripeWebhookVars = await handleLocalRunSetup();
- // Step 11: Write to server/.env
- console.log(chalk.magentaBright('\n================ Writing .env ================\n'));
- const envSections = [];
+ // Step 11: Write to server/.env
+ console.log(
+ chalk.magentaBright("\n================ Writing .env ================\n"),
+ );
+ const envSections = [];
- // Autumn Auth section
- envSections.push(
- '# Auth',
- `BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
- `BETTER_AUTH_URL=${secrets.BETTER_AUTH_URL}`,
- `CLIENT_URL=${secrets.CLIENT_URL}`,
- ''
- );
+ // Autumn Auth section
+ envSections.push(
+ "# Auth",
+ `BETTER_AUTH_SECRET=${secrets.BETTER_AUTH_SECRET}`,
+ `BETTER_AUTH_URL=${secrets.BETTER_AUTH_URL}`,
+ `CLIENT_URL=${secrets.CLIENT_URL}`,
+ "",
+ );
- // Stripe required section
- envSections.push(
- '# Stripe',
- `LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
- `ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
- `ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
- `STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
- ''
- );
+ // Stripe required section
+ envSections.push(
+ "# Stripe",
+ `LOCALTUNNEL_RESERVED_KEY=${secrets.LOCALTUNNEL_RESERVED_KEY}`,
+ `ENCRYPTION_IV=${secrets.ENCRYPTION_IV}`,
+ `ENCRYPTION_PASSWORD=${secrets.ENCRYPTION_PASSWORD}`,
+ `STRIPE_WEBHOOK_URL=${secrets.STRIPE_WEBHOOK_URL}`,
+ "",
+ );
- // Database section
- if (databaseUrl) {
- envSections.push(
- '# Database',
- `DATABASE_URL=${databaseUrl}`,
- ''
- );
- }
+ // Database section
+ if (databaseUrl) {
+ envSections.push("# Database", `DATABASE_URL=${databaseUrl}`, "");
+ }
- // Stripe Webhooks section
- if (stripeWebhookVars.length > 0) {
- envSections.push('# Stripe Webhooks');
- envSections.push(...stripeWebhookVars);
- envSections.push('');
- }
+ // Stripe Webhooks section
+ if (stripeWebhookVars.length > 0) {
+ envSections.push("# Stripe Webhooks");
+ envSections.push(...stripeWebhookVars);
+ envSections.push("");
+ }
- const envVars = envSections.join('\n');
+ const envVars = envSections.join("\n");
-
- writeFileSync('server/.env', envVars);
- try {
- copyFileSync('vite/.env.example', 'vite/.env');
- } catch (error) {
- console.log(chalk.red('❌ Failed to copy vite/.env.example to vite/.env'));
- console.log(chalk.red('❌ Please copy the file manually'));
- }
+ writeFileSync("server/.env", envVars);
+ try {
+ copyFileSync("vite/.env.example", "vite/.env");
+ } catch (error) {
+ console.log(chalk.red("❌ Failed to copy vite/.env.example to vite/.env"));
+ console.log(chalk.red("❌ Please copy the file manually"));
+ }
- console.log(chalk.greenBright('🎉 Setup complete! 🎉'));
- console.log(chalk.cyan('You can find your env variables in server/.env'));
+ console.log(chalk.greenBright("🎉 Setup complete! 🎉"));
+ console.log(chalk.cyan("You can find your env variables in server/.env"));
- console.log(chalk.cyan('\nNext steps:'));
- console.log(chalk.cyan('Run the following command to start Autumn:'));
- console.log(chalk.cyan(' docker compose -f docker-compose.dev.yml up'));
+ console.log(chalk.cyan("\nNext steps:"));
+ console.log(chalk.cyan("Run the following command to start Autumn:"));
+ console.log(chalk.cyan(" docker compose -f docker-compose.dev.yml up"));
}
-main();
\ No newline at end of file
+main();
diff --git a/shared/archives/orgModels_old.ts b/shared/archives/orgModels_old.ts
index a82709468..524c3ebf9 100644
--- a/shared/archives/orgModels_old.ts
+++ b/shared/archives/orgModels_old.ts
@@ -4,36 +4,36 @@ import { z } from "zod";
import { OrgConfigSchema } from "../models/orgModels/orgConfig.js";
export const StripeConfigSchema = z.object({
- test_api_key: z.string(),
- live_api_key: z.string(),
- test_webhook_secret: z.string(),
- live_webhook_secret: z.string(),
- success_url: z.string(),
+ test_api_key: z.string(),
+ live_api_key: z.string(),
+ test_webhook_secret: z.string(),
+ live_webhook_secret: z.string(),
+ success_url: z.string(),
});
export const SvixConfigSchema = z.object({
- sandbox_app_id: z.string(),
- live_app_id: z.string(),
+ sandbox_app_id: z.string(),
+ live_app_id: z.string(),
});
export const OrganizationSchema = z.object({
- id: z.string(),
- slug: z.string(),
- default_currency: z.string().default("usd"),
- stripe_connected: z.boolean().default(false),
- stripe_config: StripeConfigSchema.optional().nullable(),
+ id: z.string(),
+ slug: z.string(),
+ default_currency: z.string().default("usd"),
+ stripe_connected: z.boolean().default(false),
+ stripe_config: StripeConfigSchema.optional().nullable(),
- test_pkey: z.string(),
- live_pkey: z.string(),
- created_at: z.number(),
+ test_pkey: z.string(),
+ live_pkey: z.string(),
+ created_at: z.number(),
- svix_config: z.object({
- sandbox_app_id: z.string(),
- live_app_id: z.string(),
- }),
+ svix_config: z.object({
+ sandbox_app_id: z.string(),
+ live_app_id: z.string(),
+ }),
- config: OrgConfigSchema,
- api_version: z.number().nullish(),
+ config: OrgConfigSchema,
+ api_version: z.number().nullish(),
});
export type Organization = z.infer;
diff --git a/shared/biome.json b/shared/biome.json
index 18ea166f3..3cdc50acc 100644
--- a/shared/biome.json
+++ b/shared/biome.json
@@ -33,4 +33,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/shared/db/auth-relations.ts b/shared/db/auth-relations.ts
index 4155b4b6f..04e905d55 100644
--- a/shared/db/auth-relations.ts
+++ b/shared/db/auth-relations.ts
@@ -3,27 +3,27 @@ import { invitation, member, user } from "./auth-schema.js";
import { organizations } from "../models/orgModels/orgTable.js";
export const userRelations = relations(user, ({ many }) => ({
- memberships: many(member),
+ memberships: many(member),
}));
export const memberRelations = relations(member, ({ one }) => ({
- organization: one(organizations, {
- fields: [member.organizationId],
- references: [organizations.id],
- }),
- user: one(user, {
- fields: [member.userId],
- references: [user.id],
- }),
+ organization: one(organizations, {
+ fields: [member.organizationId],
+ references: [organizations.id],
+ }),
+ user: one(user, {
+ fields: [member.userId],
+ references: [user.id],
+ }),
}));
export const inviteRelations = relations(invitation, ({ one }) => ({
- organization: one(organizations, {
- fields: [invitation.organizationId],
- references: [organizations.id],
- }),
- inviter: one(user, {
- fields: [invitation.inviterId],
- references: [user.id],
- }),
+ organization: one(organizations, {
+ fields: [invitation.organizationId],
+ references: [organizations.id],
+ }),
+ inviter: one(user, {
+ fields: [invitation.inviterId],
+ references: [user.id],
+ }),
}));
diff --git a/shared/db/auth-schema.ts b/shared/db/auth-schema.ts
index 4ecc16b8b..591462d63 100644
--- a/shared/db/auth-schema.ts
+++ b/shared/db/auth-schema.ts
@@ -1,90 +1,90 @@
import {
- pgTable,
- text,
- timestamp,
- boolean,
- integer,
- foreignKey,
+ pgTable,
+ text,
+ timestamp,
+ boolean,
+ integer,
+ foreignKey,
} from "drizzle-orm/pg-core";
import { organizations } from "./schema.js";
import { relations } from "drizzle-orm";
import { Organization } from "../models/orgModels/orgTable.js";
export const user = pgTable(
- "user",
- {
- id: text("id").primaryKey(),
- name: text("name").notNull(),
- email: text("email").notNull().unique(),
- emailVerified: boolean("email_verified")
- .$defaultFn(() => false)
- .notNull(),
- image: text("image"),
- createdAt: timestamp("created_at", { withTimezone: true })
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true })
- .$defaultFn(() => /* @__PURE__ */ new Date())
- .notNull(),
- role: text("role"),
- banned: boolean("banned"),
- banReason: text("ban_reason"),
- banExpires: timestamp("ban_expires"),
- createdBy: text("created_by"),
- },
- (table) => [
- foreignKey({
- columns: [table.createdBy],
- foreignColumns: [organizations.id],
- name: "user_created_by_fkey",
- }),
- ]
+ "user",
+ {
+ id: text("id").primaryKey(),
+ name: text("name").notNull(),
+ email: text("email").notNull().unique(),
+ emailVerified: boolean("email_verified")
+ .$defaultFn(() => false)
+ .notNull(),
+ image: text("image"),
+ createdAt: timestamp("created_at", { withTimezone: true })
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true })
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ role: text("role"),
+ banned: boolean("banned"),
+ banReason: text("ban_reason"),
+ banExpires: timestamp("ban_expires"),
+ createdBy: text("created_by"),
+ },
+ (table) => [
+ foreignKey({
+ columns: [table.createdBy],
+ foreignColumns: [organizations.id],
+ name: "user_created_by_fkey",
+ }),
+ ],
);
export const session = pgTable("session", {
- id: text("id").primaryKey(),
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
- token: text("token").notNull().unique(),
- createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
- ipAddress: text("ip_address"),
- userAgent: text("user_agent"),
- userId: text("user_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- impersonatedBy: text("impersonated_by"),
- activeOrganizationId: text("active_organization_id"),
+ id: text("id").primaryKey(),
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
+ token: text("token").notNull().unique(),
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
+ ipAddress: text("ip_address"),
+ userAgent: text("user_agent"),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ impersonatedBy: text("impersonated_by"),
+ activeOrganizationId: text("active_organization_id"),
}).enableRLS();
export const account = pgTable("account", {
- id: text("id").primaryKey(),
- accountId: text("account_id").notNull(),
- providerId: text("provider_id").notNull(),
- userId: text("user_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- accessToken: text("access_token"),
- refreshToken: text("refresh_token"),
- idToken: text("id_token"),
- accessTokenExpiresAt: timestamp("access_token_expires_at"),
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
- scope: text("scope"),
- password: text("password"),
- createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
- updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
+ id: text("id").primaryKey(),
+ accountId: text("account_id").notNull(),
+ providerId: text("provider_id").notNull(),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ accessToken: text("access_token"),
+ refreshToken: text("refresh_token"),
+ idToken: text("id_token"),
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
+ scope: text("scope"),
+ password: text("password"),
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull(),
}).enableRLS();
export const verification = pgTable("verification", {
- id: text("id").primaryKey(),
- identifier: text("identifier").notNull(),
- value: text("value").notNull(),
- expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
- createdAt: timestamp("created_at", { withTimezone: true }).$defaultFn(
- () => /* @__PURE__ */ new Date()
- ),
- updatedAt: timestamp("updated_at", { withTimezone: true }).$defaultFn(
- () => /* @__PURE__ */ new Date()
- ),
+ id: text("id").primaryKey(),
+ identifier: text("identifier").notNull(),
+ value: text("value").notNull(),
+ expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true }).$defaultFn(
+ () => /* @__PURE__ */ new Date(),
+ ),
+ updatedAt: timestamp("updated_at", { withTimezone: true }).$defaultFn(
+ () => /* @__PURE__ */ new Date(),
+ ),
}).enableRLS();
// export const organization = pgTable("organization", {
@@ -97,39 +97,39 @@ export const verification = pgTable("verification", {
// });
export const member = pgTable("member", {
- id: text("id").primaryKey(),
- organizationId: text("organization_id")
- .notNull()
- .references(() => organizations.id, { onDelete: "cascade" }),
- userId: text("user_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
- role: text("role").default("member").notNull(),
- createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
+ id: text("id").primaryKey(),
+ organizationId: text("organization_id")
+ .notNull()
+ .references(() => organizations.id, { onDelete: "cascade" }),
+ userId: text("user_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
+ role: text("role").default("member").notNull(),
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull(),
}).enableRLS();
export const invitation = pgTable("invitation", {
- id: text("id").primaryKey(),
- organizationId: text("organization_id")
- .notNull()
- .references(() => organizations.id, { onDelete: "cascade" }),
- email: text("email").notNull(),
- role: text("role"),
- status: text("status").default("pending").notNull(),
- expiresAt: timestamp("expires_at").notNull(),
- inviterId: text("inviter_id")
- .notNull()
- .references(() => user.id, { onDelete: "cascade" }),
+ id: text("id").primaryKey(),
+ organizationId: text("organization_id")
+ .notNull()
+ .references(() => organizations.id, { onDelete: "cascade" }),
+ email: text("email").notNull(),
+ role: text("role"),
+ status: text("status").default("pending").notNull(),
+ expiresAt: timestamp("expires_at").notNull(),
+ inviterId: text("inviter_id")
+ .notNull()
+ .references(() => user.id, { onDelete: "cascade" }),
}).enableRLS();
export const authSchema = {
- user,
- session,
- account,
- verification,
- member,
- invitation,
- organizations,
+ user,
+ session,
+ account,
+ verification,
+ member,
+ invitation,
+ organizations,
};
export type User = typeof user.$inferSelect;
@@ -137,6 +137,6 @@ export type Member = typeof member.$inferSelect;
export type Invite = typeof invitation.$inferSelect;
export type FullInvite = Invite & {
- inviter: User;
- organization: Organization;
+ inviter: User;
+ organization: Organization;
};
diff --git a/shared/db/schema.ts b/shared/db/schema.ts
index 63507b66c..9fcaeae6a 100644
--- a/shared/db/schema.ts
+++ b/shared/db/schema.ts
@@ -72,78 +72,76 @@ import { events } from "../models/eventModels/eventTable.js";
import { replaceables } from "../models/cusProductModels/cusEntModels/replaceableTable.js";
import {
- user,
- session,
- account,
- verification,
- member,
- invitation,
+ user,
+ session,
+ account,
+ verification,
+ member,
+ invitation,
} from "./auth-schema.js";
import {
- userRelations,
- memberRelations,
- inviteRelations,
+ userRelations,
+ memberRelations,
+ inviteRelations,
} from "./auth-relations.js";
export {
- // Tables
- organizations,
- chatResults,
- freeTrials,
- entitlements,
- prices,
- features,
- products,
- customerProducts,
- customerPrices,
- customerEntitlements,
- invoices,
- customers,
- entities,
- apiKeys,
- metadata,
- subscriptions,
- rewards,
- rewardPrograms,
- referralCodes,
- rewardRedemptions,
- migrationJobs,
- migrationErrors,
- actions,
- events,
- replaceables,
- rollovers,
-
- // Auth
- user,
- session,
- account,
- verification,
- member,
- invitation,
-
- // Relations
- organizationsRelations,
- entitlementsRelations,
- featureRelations,
- priceRelations,
- productRelations,
- freeTrialRelations,
- customerProductsRelations,
- customerPricesRelations,
- customerEntitlementsRelations,
- customersRelations,
- entitiesRelations,
- apiKeyRelations,
- rewardProgramRelations,
- referralCodeRelations,
- rewardRedemptionRelations,
- migrationErrorRelations,
- replaceableRelations,
- invoiceRelations,
- rolloverRelations,
- // Auth Relations
- userRelations,
- memberRelations,
- inviteRelations,
+ // Tables
+ organizations,
+ chatResults,
+ freeTrials,
+ entitlements,
+ prices,
+ features,
+ products,
+ customerProducts,
+ customerPrices,
+ customerEntitlements,
+ invoices,
+ customers,
+ entities,
+ apiKeys,
+ metadata,
+ subscriptions,
+ rewards,
+ rewardPrograms,
+ referralCodes,
+ rewardRedemptions,
+ migrationJobs,
+ migrationErrors,
+ actions,
+ events,
+ replaceables,
+ rollovers,
+ // Auth
+ user,
+ session,
+ account,
+ verification,
+ member,
+ invitation,
+ // Relations
+ organizationsRelations,
+ entitlementsRelations,
+ featureRelations,
+ priceRelations,
+ productRelations,
+ freeTrialRelations,
+ customerProductsRelations,
+ customerPricesRelations,
+ customerEntitlementsRelations,
+ customersRelations,
+ entitiesRelations,
+ apiKeyRelations,
+ rewardProgramRelations,
+ referralCodeRelations,
+ rewardRedemptionRelations,
+ migrationErrorRelations,
+ replaceableRelations,
+ invoiceRelations,
+ rolloverRelations,
+ // Auth Relations
+ userRelations,
+ memberRelations,
+ inviteRelations,
};
diff --git a/shared/db/utils.ts b/shared/db/utils.ts
index 4c727cffe..099ab6798 100644
--- a/shared/db/utils.ts
+++ b/shared/db/utils.ts
@@ -1,11 +1,11 @@
import { PgColumn } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
function collatePgColumn>(col: C, collation: string) {
- const originalGetSQLType = col.getSQLType;
- col.getSQLType = function (this: C) {
- return originalGetSQLType.call(this) + ` COLLATE "${collation}"`;
- };
- return col;
+ const originalGetSQLType = col.getSQLType;
+ col.getSQLType = function (this: C) {
+ return originalGetSQLType.call(this) + ` COLLATE "${collation}"`;
+ };
+ return col;
}
export const sqlNow = sql`ROUND(date_part('epoch', NOW()) * 1000)::BIGINT`;
diff --git a/shared/drizzle.config.ts b/shared/drizzle.config.ts
index d34bc8ca0..581cb7eac 100644
--- a/shared/drizzle.config.ts
+++ b/shared/drizzle.config.ts
@@ -3,10 +3,10 @@ config({ path: "../server/.env" });
import { defineConfig } from "drizzle-kit";
export default defineConfig({
- dialect: "postgresql",
- out: "./drizzle",
- schema: "./db/schema.ts",
- dbCredentials: {
- url: process.env.DATABASE_URL!,
- },
+ dialect: "postgresql",
+ out: "./drizzle",
+ schema: "./db/schema.ts",
+ dbCredentials: {
+ url: process.env.DATABASE_URL!,
+ },
});
diff --git a/shared/enums/APIVersion.ts b/shared/enums/APIVersion.ts
index ae6bf1c7b..a69ce9df9 100644
--- a/shared/enums/APIVersion.ts
+++ b/shared/enums/APIVersion.ts
@@ -1,6 +1,6 @@
export enum APIVersion {
- v1 = 1,
- v1_1 = 1.1,
- v1_2 = 1.2,
- v1_4 = 1.4,
+ v1 = 1,
+ v1_1 = 1.1,
+ v1_2 = 1.2,
+ v1_4 = 1.4,
}
diff --git a/shared/enums/AttachErrCode.ts b/shared/enums/AttachErrCode.ts
index 00dfc059b..462ec03b8 100644
--- a/shared/enums/AttachErrCode.ts
+++ b/shared/enums/AttachErrCode.ts
@@ -1,6 +1,6 @@
export enum AttachErrCode {
- // InvalidOptions = "invalid_options",
- ProductAlreadyAttached = "product_already_attached",
+ // InvalidOptions = "invalid_options",
+ ProductAlreadyAttached = "product_already_attached",
- InvalidOptions = "invalid_options",
+ InvalidOptions = "invalid_options",
}
diff --git a/shared/enums/ErrCode.ts b/shared/enums/ErrCode.ts
index d62e32e67..6056b4f55 100644
--- a/shared/enums/ErrCode.ts
+++ b/shared/enums/ErrCode.ts
@@ -1,169 +1,169 @@
export const ErrCode = {
- // Auth
- InvalidApiVersion: "invalid_api_version",
- NoSecretKey: "no_secret_key",
- InvalidSecretKey: "invalid_secret_key",
+ // Auth
+ InvalidApiVersion: "invalid_api_version",
+ NoSecretKey: "no_secret_key",
+ InvalidSecretKey: "invalid_secret_key",
- NoAuthHeader: "no_auth_header",
- InvalidAuthHeader: "invalid_auth_header",
- FailedToVerifySecretKey: "failed_to_verify_secret_key",
- FailedToFetchKeyFromAutumn: "failed_to_fetch_key_from_autumn",
+ NoAuthHeader: "no_auth_header",
+ InvalidAuthHeader: "invalid_auth_header",
+ FailedToVerifySecretKey: "failed_to_verify_secret_key",
+ FailedToFetchKeyFromAutumn: "failed_to_fetch_key_from_autumn",
- NoPublishableKey: "no_publishable_key",
- InvalidPublishableKey: "invalid_publishable_key",
- GetOrgFromPublishableKeyFailed: "get_org_from_publishable_key_failed",
- EndpointNotPublic: "endpoint_not_public",
- FailedToVerifyPublishableKey: "failed_to_verify_publishable_key",
+ NoPublishableKey: "no_publishable_key",
+ InvalidPublishableKey: "invalid_publishable_key",
+ GetOrgFromPublishableKeyFailed: "get_org_from_publishable_key_failed",
+ EndpointNotPublic: "endpoint_not_public",
+ FailedToVerifyPublishableKey: "failed_to_verify_publishable_key",
- // General
- InvalidInputs: "invalid_inputs",
- InvalidRequest: "invalid_request",
- InvalidExpand: "invalid_expand",
- InvalidOptions: "invalid_options",
+ // General
+ InvalidInputs: "invalid_inputs",
+ InvalidRequest: "invalid_request",
+ InvalidExpand: "invalid_expand",
+ InvalidOptions: "invalid_options",
- // Org
- OrgNotFound: "org_not_found",
- OrgHasCustomers: "org_has_customers",
+ // Org
+ OrgNotFound: "org_not_found",
+ OrgHasCustomers: "org_has_customers",
- // Feature
- FeatureNotFound: "feature_not_found",
- InvalidFeature: "invalid_feature",
- DuplicateFeatureId: "duplicate_feature_id",
- InvalidEventName: "invalid_event_name",
- FeatureLimitReached: "feature_limit_reached",
+ // Feature
+ FeatureNotFound: "feature_not_found",
+ InvalidFeature: "invalid_feature",
+ DuplicateFeatureId: "duplicate_feature_id",
+ InvalidEventName: "invalid_event_name",
+ FeatureLimitReached: "feature_limit_reached",
- // Internal
- InternalError: "internal_error",
- DuplicateCustomerId: "duplicate_customer_id",
- StripeKeyNotFound: "stripe_key_not_found",
- DuplicateCustomerEmail: "duplicate_customer_email",
+ // Internal
+ InternalError: "internal_error",
+ DuplicateCustomerId: "duplicate_customer_id",
+ StripeKeyNotFound: "stripe_key_not_found",
+ DuplicateCustomerEmail: "duplicate_customer_email",
- // Stripe
- StripeError: "stripe_error",
- StripeKeyInvalid: "stripe_key_invalid",
- StripeConfigNotFound: "stripe_config_not_found",
- StripeDeleteCustomerFailed: "stripe_delete_customer_failed",
- StripeCreateCustomerFailed: "stripe_create_customer_failed",
- StripeCreateProductFailed: "stripe_create_product_failed",
- StripeCancelSubscriptionFailed: "stripe_cancel_subscription_failed",
- StripeGetPaymentMethodFailed: "stripe_get_payment_method_failed",
- StripeCardDeclined: "stripe_card_declined",
- StripeUpdateSubscriptionFailed: "stripe_update_subscription_failed",
- StripeCancelSubscriptionScheduleFailed:
- "stripe_cancel_subscription_schedule_failed",
+ // Stripe
+ StripeError: "stripe_error",
+ StripeKeyInvalid: "stripe_key_invalid",
+ StripeConfigNotFound: "stripe_config_not_found",
+ StripeDeleteCustomerFailed: "stripe_delete_customer_failed",
+ StripeCreateCustomerFailed: "stripe_create_customer_failed",
+ StripeCreateProductFailed: "stripe_create_product_failed",
+ StripeCancelSubscriptionFailed: "stripe_cancel_subscription_failed",
+ StripeGetPaymentMethodFailed: "stripe_get_payment_method_failed",
+ StripeCardDeclined: "stripe_card_declined",
+ StripeUpdateSubscriptionFailed: "stripe_update_subscription_failed",
+ StripeCancelSubscriptionScheduleFailed:
+ "stripe_cancel_subscription_schedule_failed",
- StripeCreateSubscriptionFailed: "stripe_create_subscription_failed",
+ StripeCreateSubscriptionFailed: "stripe_create_subscription_failed",
- // Price
- PriceNotFound: "price_not_found",
- CreatePriceFailed: "create_price_failed",
- InvalidPrice: "invalid_price",
- InvalidPriceId: "invalid_price_id",
- InvalidPriceOptions: "invalid_price_options",
- InvalidPriceConfig: "invalid_price_config",
- CusPriceNotFound: "cus_price_not_found",
+ // Price
+ PriceNotFound: "price_not_found",
+ CreatePriceFailed: "create_price_failed",
+ InvalidPrice: "invalid_price",
+ InvalidPriceId: "invalid_price_id",
+ InvalidPriceOptions: "invalid_price_options",
+ InvalidPriceConfig: "invalid_price_config",
+ CusPriceNotFound: "cus_price_not_found",
- // Customer
- InvalidCustomer: "invalid_customer",
- CreateCustomerFailed: "create_customer_failed",
- CustomerNotFound: "customer_not_found",
- CustomersNotFound: "customers_not_found",
- CustomerAlreadyHasProduct: "customer_already_has_product",
- CustomerHasNoPaymentMethod: "customer_has_no_payment_method",
- CustomerHasNoBaseProduct: "customer_has_no_base_product",
- AttachProductToCustomerFailed: "attach_product_to_customer_failed",
- CustomerEntitlementNotFound: "customer_entitlement_not_found",
- MultipleCustomersFound: "multiple_customers_found",
- InvalidUpdateCustomerParams: "invalid_update_customer_params",
+ // Customer
+ InvalidCustomer: "invalid_customer",
+ CreateCustomerFailed: "create_customer_failed",
+ CustomerNotFound: "customer_not_found",
+ CustomersNotFound: "customers_not_found",
+ CustomerAlreadyHasProduct: "customer_already_has_product",
+ CustomerHasNoPaymentMethod: "customer_has_no_payment_method",
+ CustomerHasNoBaseProduct: "customer_has_no_base_product",
+ AttachProductToCustomerFailed: "attach_product_to_customer_failed",
+ CustomerEntitlementNotFound: "customer_entitlement_not_found",
+ MultipleCustomersFound: "multiple_customers_found",
+ InvalidUpdateCustomerParams: "invalid_update_customer_params",
- // Product
- InvalidProduct: "invalid_product",
- ProductNotFound: "product_not_found",
- ProductHasCustomers: "product_has_customers",
- ProductHasNoPrices: "product_has_no_prices",
- ProductHasDifferentRecurringIntervals:
- "product_has_different_recurring_intervals",
- CreateStripeProductFailed: "create_stripe_product_failed",
- DeleteStripeProductFailed: "delete_stripe_product_failed",
- CreateStripeSubscriptionFailed: "create_stripe_subscription_failed",
- UpdateCusProductFailed: "update_customer_product_failed",
- DefaultProductNotAllowedPrice: "default_product_not_allowed_price",
- UpgradeFailed: "upgrade_failed",
- ProductAlreadyExists: "product_already_exists",
- ProductHasRewardPrograms: "product_has_reward_programs",
+ // Product
+ InvalidProduct: "invalid_product",
+ ProductNotFound: "product_not_found",
+ ProductHasCustomers: "product_has_customers",
+ ProductHasNoPrices: "product_has_no_prices",
+ ProductHasDifferentRecurringIntervals:
+ "product_has_different_recurring_intervals",
+ CreateStripeProductFailed: "create_stripe_product_failed",
+ DeleteStripeProductFailed: "delete_stripe_product_failed",
+ CreateStripeSubscriptionFailed: "create_stripe_subscription_failed",
+ UpdateCusProductFailed: "update_customer_product_failed",
+ DefaultProductNotAllowedPrice: "default_product_not_allowed_price",
+ UpgradeFailed: "upgrade_failed",
+ ProductAlreadyExists: "product_already_exists",
+ ProductHasRewardPrograms: "product_has_reward_programs",
- // Cus Product
- CusProductNotFound: "cus_product_not_found",
+ // Cus Product
+ CusProductNotFound: "cus_product_not_found",
- // Entitlements
- InvalidEntitlement: "invalid_entitlement",
- CreateEntitlementFailed: "create_entitlement_failed",
- DeleteEntitlementFailed: "delete_entitlement_failed",
- InsufficientBalance: "insufficient_balance",
+ // Entitlements
+ InvalidEntitlement: "invalid_entitlement",
+ CreateEntitlementFailed: "create_entitlement_failed",
+ DeleteEntitlementFailed: "delete_entitlement_failed",
+ InsufficientBalance: "insufficient_balance",
- // Invoice
- CreateInvoiceFailed: "create_invoice_failed",
+ // Invoice
+ CreateInvoiceFailed: "create_invoice_failed",
- // Event
- InvalidEvent: "invalid_event",
- CreateEventFailed: "create_event_failed",
- DuplicateEvent: "duplicate_event",
+ // Event
+ InvalidEvent: "invalid_event",
+ CreateEventFailed: "create_event_failed",
+ DuplicateEvent: "duplicate_event",
- // Cus Product
- NoActiveCusProducts: "no_active_cus_products",
- GetCusProductsFailed: "get_cus_products_failed",
+ // Cus Product
+ NoActiveCusProducts: "no_active_cus_products",
+ GetCusProductsFailed: "get_cus_products_failed",
- // Cus Price
- GetCusPriceFailed: "get_cus_price_failed",
+ // Cus Price
+ GetCusPriceFailed: "get_cus_price_failed",
- // Pay for invoice
- PayInvoiceFailed: "invoice_payment_failed",
+ // Pay for invoice
+ PayInvoiceFailed: "invoice_payment_failed",
- // Rewards
- InvalidReward: "invalid_reward",
- PromoCodeAlreadyExistsInStripe: "promo_code_already_exists_in_stripe",
+ // Rewards
+ InvalidReward: "invalid_reward",
+ PromoCodeAlreadyExistsInStripe: "promo_code_already_exists_in_stripe",
- // Entity
- EntityNotFound: "entity_not_found",
- EntityAlreadyDeleted: "entity_already_deleted",
+ // Entity
+ EntityNotFound: "entity_not_found",
+ EntityAlreadyDeleted: "entity_already_deleted",
- // Referral codes
- ReferralCodeMaxRedemptionsReached: "referral_code_max_redemptions_reached",
- ReferralNotFound: "referral_not_found",
- CustomerAlreadyRedeemedReferralCode:
- "customer_already_redeemed_referral_code",
- CustomerCannotRedeemOwnCode: "customer_cannot_redeem_own_code",
+ // Referral codes
+ ReferralCodeMaxRedemptionsReached: "referral_code_max_redemptions_reached",
+ ReferralNotFound: "referral_not_found",
+ CustomerAlreadyRedeemedReferralCode:
+ "customer_already_redeemed_referral_code",
+ CustomerCannotRedeemOwnCode: "customer_cannot_redeem_own_code",
- // Product items
- InvalidProductItem: "invalid_product_item",
+ // Product items
+ InvalidProductItem: "invalid_product_item",
- // Entity
- EntityIdRequired: "entity_id_required",
+ // Entity
+ EntityIdRequired: "entity_id_required",
- // Subscription
- InsertSubscriptionFailed: "insert_subscription_failed",
- UpdateSubscriptionFailed: "update_subscription_failed",
+ // Subscription
+ InsertSubscriptionFailed: "insert_subscription_failed",
+ UpdateSubscriptionFailed: "update_subscription_failed",
- // Rewards
- RewardNotFound: "reward_not_found",
- RewardProgramNotFound: "reward_program_not_found",
- InsertRewardProgramFailed: "insert_reward_program_failed",
- InsertReferralCodeFailed: "insert_referral_code_failed",
- ReferralCodeNotFound: "referral_code_not_found",
- UpdateRewardRedemptionFailed: "update_reward_redemption_failed",
- RewardRedemptionNotFound: "reward_redemption_not_found",
- InsertRewardRedemptionFailed: "insert_reward_redemption_failed",
+ // Rewards
+ RewardNotFound: "reward_not_found",
+ RewardProgramNotFound: "reward_program_not_found",
+ InsertRewardProgramFailed: "insert_reward_program_failed",
+ InsertReferralCodeFailed: "insert_referral_code_failed",
+ ReferralCodeNotFound: "referral_code_not_found",
+ UpdateRewardRedemptionFailed: "update_reward_redemption_failed",
+ RewardRedemptionNotFound: "reward_redemption_not_found",
+ InsertRewardRedemptionFailed: "insert_reward_redemption_failed",
- // Migration
- InsertMigrationJobFailed: "insert_migration_job_failed",
- InsertMigrationErrorFailed: "insert_migration_error_failed",
- MigrationJobNotFound: "migration_job_not_found",
+ // Migration
+ InsertMigrationJobFailed: "insert_migration_job_failed",
+ InsertMigrationErrorFailed: "insert_migration_error_failed",
+ MigrationJobNotFound: "migration_job_not_found",
- // Supabase
- SupabaseNotFound: "supabase_not_found",
- // Entities
- EntityBalanceNotFound: "entity_balance_not_found",
+ // Supabase
+ SupabaseNotFound: "supabase_not_found",
+ // Entities
+ EntityBalanceNotFound: "entity_balance_not_found",
- // ClickHouse
- ClickHouseDisabled: "clickhouse_disabled",
+ // ClickHouse
+ ClickHouseDisabled: "clickhouse_disabled",
};
diff --git a/shared/enums/LoggerAction.ts b/shared/enums/LoggerAction.ts
index 53d025092..427fa135e 100644
--- a/shared/enums/LoggerAction.ts
+++ b/shared/enums/LoggerAction.ts
@@ -1,11 +1,11 @@
export enum LoggerAction {
- AdjustAllowance = "adjust_allowance",
+ AdjustAllowance = "adjust_allowance",
- // Stripe Webhook
- StripeWebhook = "stripe_webhook",
- StripeWebhookInvoiceCreated = "stripe_webhook_invoice_created",
+ // Stripe Webhook
+ StripeWebhook = "stripe_webhook",
+ StripeWebhookInvoiceCreated = "stripe_webhook_invoice_created",
- // Invoice
- InsertStripeInvoice = "insert_stripe_invoice",
- CreateFullCusProduct = "create_full_cus_product",
+ // Invoice
+ InsertStripeInvoice = "insert_stripe_invoice",
+ CreateFullCusProduct = "create_full_cus_product",
}
diff --git a/shared/enums/SuccessCode.ts b/shared/enums/SuccessCode.ts
index cdd7ca009..75d4d1a46 100644
--- a/shared/enums/SuccessCode.ts
+++ b/shared/enums/SuccessCode.ts
@@ -1,25 +1,25 @@
export enum SuccessCode {
- // Events
- EventReceived = "event_received",
- EventReceivedCustomerCreated = "event_received_customer_created",
+ // Events
+ EventReceived = "event_received",
+ EventReceivedCustomerCreated = "event_received_customer_created",
- // Entitlements
- FeatureFound = "feature_found",
- FeatureFoundCustomerCreated = "feature_found_customer_created",
+ // Entitlements
+ FeatureFound = "feature_found",
+ FeatureFoundCustomerCreated = "feature_found_customer_created",
- // Attach
- FeaturesUpdated = "features_updated",
- UpgradedToNewProduct = "upgraded_to_new_product",
- UpgradedToNewVersion = "upgraded_to_new_version",
- UpdatedSameProduct = "updated_same_product",
- PrepaidQuantityUpdated = "prepaid_quantity_updated",
- DowngradeScheduled = "downgrade_scheduled",
+ // Attach
+ FeaturesUpdated = "features_updated",
+ UpgradedToNewProduct = "upgraded_to_new_product",
+ UpgradedToNewVersion = "upgraded_to_new_version",
+ UpdatedSameProduct = "updated_same_product",
+ PrepaidQuantityUpdated = "prepaid_quantity_updated",
+ DowngradeScheduled = "downgrade_scheduled",
- FreeProductAttached = "free_product_attached",
- CheckoutCreated = "checkout_created",
- NewProductAttached = "new_product_attached",
- OneOffProductAttached = "one_off_product_attached",
- ProductFound = "product_found",
+ FreeProductAttached = "free_product_attached",
+ CheckoutCreated = "checkout_created",
+ NewProductAttached = "new_product_attached",
+ OneOffProductAttached = "one_off_product_attached",
+ ProductFound = "product_found",
- RenewedProduct = "renewed_product",
+ RenewedProduct = "renewed_product",
}
diff --git a/shared/enums/WebhookEventType.ts b/shared/enums/WebhookEventType.ts
index 1312f8dd9..395860927 100644
--- a/shared/enums/WebhookEventType.ts
+++ b/shared/enums/WebhookEventType.ts
@@ -1,4 +1,4 @@
export enum WebhookEventType {
- CustomerProductsUpdated = "customer.products.updated",
- CustomerThresholdReached = "customer.threshold_reached",
+ CustomerProductsUpdated = "customer.products.updated",
+ CustomerThresholdReached = "customer.threshold_reached",
}
diff --git a/shared/index.ts b/shared/index.ts
index fade638f1..4ee0eaaeb 100644
--- a/shared/index.ts
+++ b/shared/index.ts
@@ -129,4 +129,4 @@ export * from "./utils/productDisplayUtils/getProductItemRes.js";
export * from "./utils/productDisplayUtils/sortProductItems.js";
export * from "./utils/productUtils/priceToInvoiceAmount.js";
export * from "./utils/productUtils.js";
-export * from "./utils/rewardUtils/rewardMigrationUtils.js";
\ No newline at end of file
+export * from "./utils/rewardUtils/rewardMigrationUtils.js";
diff --git a/shared/models/analyticsModels/actionEnums.ts b/shared/models/analyticsModels/actionEnums.ts
index d9ec7bbf8..61eb862a9 100644
--- a/shared/models/analyticsModels/actionEnums.ts
+++ b/shared/models/analyticsModels/actionEnums.ts
@@ -1,13 +1,13 @@
export enum AuthType {
- SecretKey = "secret_key",
- PublicKey = "public_key",
- Dashboard = "dashboard",
- Stripe = "stripe",
- Unknown = "unknown",
+ SecretKey = "secret_key",
+ PublicKey = "public_key",
+ Dashboard = "dashboard",
+ Stripe = "stripe",
+ Unknown = "unknown",
}
export enum ActionType {
- CustomerCreated = "customer.created",
- CustomerProductsUpdated = "customer.products.updated",
- CustomerFeaturesUpdated = "customer.features.updated",
+ CustomerCreated = "customer.created",
+ CustomerProductsUpdated = "customer.products.updated",
+ CustomerFeaturesUpdated = "customer.features.updated",
}
diff --git a/shared/models/analyticsModels/actionTable.ts b/shared/models/analyticsModels/actionTable.ts
index d7afc0ee0..5d87661b7 100644
--- a/shared/models/analyticsModels/actionTable.ts
+++ b/shared/models/analyticsModels/actionTable.ts
@@ -1,9 +1,9 @@
import {
- foreignKey,
- jsonb,
- pgTable,
- text,
- timestamp,
+ foreignKey,
+ jsonb,
+ pgTable,
+ text,
+ timestamp,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { ActionType, AuthType } from "./actionEnums.js";
@@ -13,51 +13,49 @@ import { entities } from "../cusModels/entityModels/entityTable.js";
import { collatePgColumn } from "../../db/utils.js";
export const actions = pgTable(
- "actions",
- {
- id: text().primaryKey().notNull(),
+ "actions",
+ {
+ id: text().primaryKey().notNull(),
- request_id: text("request_id").notNull(),
+ request_id: text("request_id").notNull(),
- org_id: text("org_id").notNull(),
- org_slug: text("org_slug").notNull(),
- env: text().notNull(),
+ org_id: text("org_id").notNull(),
+ org_slug: text("org_slug").notNull(),
+ env: text().notNull(),
- customer_id: text("customer_id"),
- internal_customer_id: text("internal_customer_id"),
+ customer_id: text("customer_id"),
+ internal_customer_id: text("internal_customer_id"),
- entity_id: text("entity_id"),
- internal_entity_id: text("internal_entity_id"),
+ entity_id: text("entity_id"),
+ internal_entity_id: text("internal_entity_id"),
- type: text("type").$type().notNull(),
- auth_type: text("auth_type").$type().notNull(),
+ type: text("type").$type().notNull(),
+ auth_type: text("auth_type").$type().notNull(),
- method: text("method").notNull(),
- path: text("path").notNull(),
+ method: text("method").notNull(),
+ path: text("path").notNull(),
- timestamp: timestamp({ withTimezone: true })
- .notNull()
- .default(sql`now()`),
+ timestamp: timestamp({ withTimezone: true }).notNull().default(sql`now()`),
- properties: jsonb("properties"),
- },
- (table) => [
- foreignKey({
- columns: [table.org_id],
- foreignColumns: [organizations.id],
- name: "actions_org_id_fkey",
- }).onDelete("set null"),
- foreignKey({
- columns: [table.internal_customer_id],
- foreignColumns: [customers.internal_id],
- name: "actions_customer_id_fkey",
- }).onDelete("set null"),
- foreignKey({
- columns: [table.internal_entity_id],
- foreignColumns: [entities.internal_id],
- name: "actions_entity_id_fkey",
- }).onDelete("set null"),
- ],
+ properties: jsonb("properties"),
+ },
+ (table) => [
+ foreignKey({
+ columns: [table.org_id],
+ foreignColumns: [organizations.id],
+ name: "actions_org_id_fkey",
+ }).onDelete("set null"),
+ foreignKey({
+ columns: [table.internal_customer_id],
+ foreignColumns: [customers.internal_id],
+ name: "actions_customer_id_fkey",
+ }).onDelete("set null"),
+ foreignKey({
+ columns: [table.internal_entity_id],
+ foreignColumns: [entities.internal_id],
+ name: "actions_entity_id_fkey",
+ }).onDelete("set null"),
+ ],
).enableRLS();
collatePgColumn(actions.id, "C");
diff --git a/shared/models/attachModels/attachBody.ts b/shared/models/attachModels/attachBody.ts
index c50249dbb..18ff2bc25 100644
--- a/shared/models/attachModels/attachBody.ts
+++ b/shared/models/attachModels/attachBody.ts
@@ -5,122 +5,122 @@ import { CreateFreeTrialSchema } from "../productModels/freeTrialModels/freeTria
import { notNullish } from "../../utils/utils.js";
export const ProductOptions = z.object({
- product_id: z.string(),
- quantity: z.number().nullish(),
- entity_id: z.string().nullish(),
- options: z.array(FeatureOptionsSchema).nullish(),
+ product_id: z.string(),
+ quantity: z.number().nullish(),
+ entity_id: z.string().nullish(),
+ options: z.array(FeatureOptionsSchema).nullish(),
});
export const AttachBodySchema = z
- .object({
- // Customer Info
- customer_id: z
- .string()
- .describe("ID of the customer to attach the product to"),
+ .object({
+ // Customer Info
+ customer_id: z
+ .string()
+ .describe("ID of the customer to attach the product to"),
- customer_data: z
- .any()
- .nullish()
- .describe("Customer data if using attach to auto create customer"),
+ customer_data: z
+ .any()
+ .nullish()
+ .describe("Customer data if using attach to auto create customer"),
- // Entity Info
- entity_id: z.string().nullish(),
- entity_data: z.any().nullish(),
+ // Entity Info
+ entity_id: z.string().nullish(),
+ entity_data: z.any().nullish(),
- // Product Info
- product_id: z.string().nullish(),
- product_ids: z.array(z.string()).min(1).nullish(),
- options: z.array(FeatureOptionsSchema).nullish(),
+ // Product Info
+ product_id: z.string().nullish(),
+ product_ids: z.array(z.string()).min(1).nullish(),
+ options: z.array(FeatureOptionsSchema).nullish(),
- products: z.array(ProductOptions).nullish(),
+ products: z.array(ProductOptions).nullish(),
- // Custom Product
- is_custom: z.boolean().optional(),
- items: z.array(ProductItemSchema).optional(),
- free_trial: CreateFreeTrialSchema.or(z.boolean()).optional(),
+ // Custom Product
+ is_custom: z.boolean().optional(),
+ items: z.array(ProductItemSchema).optional(),
+ free_trial: CreateFreeTrialSchema.or(z.boolean()).optional(),
- // New Version
- version: z.number().optional(),
+ // New Version
+ version: z.number().optional(),
- // Others
- success_url: z.string().optional(),
- force_checkout: z.boolean().optional(),
- invoice_only: z.boolean().optional(),
- metadata: z.any().optional(),
- billing_cycle_anchor: z.number().optional(),
- checkout_session_params: z.any().optional(),
- reward: z.string().or(z.array(z.string())).optional(),
- invoice: z.boolean().optional(),
- enable_product_immediately: z.boolean().optional(),
- finalize_invoice: z.boolean().optional(),
+ // Others
+ success_url: z.string().optional(),
+ force_checkout: z.boolean().optional(),
+ invoice_only: z.boolean().optional(),
+ metadata: z.any().optional(),
+ billing_cycle_anchor: z.number().optional(),
+ checkout_session_params: z.any().optional(),
+ reward: z.string().or(z.array(z.string())).optional(),
+ invoice: z.boolean().optional(),
+ enable_product_immediately: z.boolean().optional(),
+ finalize_invoice: z.boolean().optional(),
- // Checkout params
- skip_checkout: z.boolean().optional(),
- setup_payment: z.boolean().optional(),
- })
- .refine(
- (data) => {
- if (!data.product_id && !data.product_ids && !data.products) {
- return false;
- }
+ // Checkout params
+ skip_checkout: z.boolean().optional(),
+ setup_payment: z.boolean().optional(),
+ })
+ .refine(
+ (data) => {
+ if (!data.product_id && !data.product_ids && !data.products) {
+ return false;
+ }
- return true;
- },
- {
- message: "`product_id` is not provided",
- }
- )
- .refine(
- (data) => {
- if (data.product_id && data.product_ids) {
- return false;
- }
+ return true;
+ },
+ {
+ message: "`product_id` is not provided",
+ },
+ )
+ .refine(
+ (data) => {
+ if (data.product_id && data.product_ids) {
+ return false;
+ }
- return true;
- },
- {
- message: "provide either one `product_id` or `product_ids`",
- }
- )
- .refine(
- (data) => {
- if (
- notNullish(data.product_ids) &&
- new Set(data.product_ids).size !== data.product_ids!.length
- ) {
- return false;
- }
+ return true;
+ },
+ {
+ message: "provide either one `product_id` or `product_ids`",
+ },
+ )
+ .refine(
+ (data) => {
+ if (
+ notNullish(data.product_ids) &&
+ new Set(data.product_ids).size !== data.product_ids!.length
+ ) {
+ return false;
+ }
- return true;
- },
- {
- message: "Can't pass in duplicate product_ids",
- }
- )
- .refine(
- (data) => {
- if (data.product_ids && data.is_custom) {
- return false;
- }
+ return true;
+ },
+ {
+ message: "Can't pass in duplicate product_ids",
+ },
+ )
+ .refine(
+ (data) => {
+ if (data.product_ids && data.is_custom) {
+ return false;
+ }
- return true;
- },
- {
- message: "Can't pass in product_ids if is_custom is true",
- }
- )
- .refine(
- (data) => {
- if (data.items && !data.is_custom) {
- return false;
- }
+ return true;
+ },
+ {
+ message: "Can't pass in product_ids if is_custom is true",
+ },
+ )
+ .refine(
+ (data) => {
+ if (data.items && !data.is_custom) {
+ return false;
+ }
- return true;
- },
- {
- message: "Can't pass in items if is_custom is false",
- }
- );
+ return true;
+ },
+ {
+ message: "Can't pass in items if is_custom is false",
+ },
+ );
export type AttachBody = z.infer;
export type ProductOptions = z.infer;
diff --git a/shared/models/attachModels/attachEnums/AttachBranch.ts b/shared/models/attachModels/attachEnums/AttachBranch.ts
index 21e259060..788c9feb5 100644
--- a/shared/models/attachModels/attachEnums/AttachBranch.ts
+++ b/shared/models/attachModels/attachEnums/AttachBranch.ts
@@ -1,28 +1,28 @@
export enum AttachBranch {
- MultiAttach = "multi_attach",
- MultiAttachUpdate = "multi_attach_update",
+ MultiAttach = "multi_attach",
+ MultiAttachUpdate = "multi_attach_update",
- // Done
- MultiProduct = "multi_product",
- OneOff = "one_off",
- New = "new",
- AddOn = "add_on",
+ // Done
+ MultiProduct = "multi_product",
+ OneOff = "one_off",
+ New = "new",
+ AddOn = "add_on",
- // Same product
- NewVersion = "new_version",
- SameCustomEnts = "same_custom_ents",
- SameCustom = "same_custom",
+ // Same product
+ NewVersion = "new_version",
+ SameCustomEnts = "same_custom_ents",
+ SameCustom = "same_custom",
- // Done
- UpdatePrepaidQuantity = "update_prepaid_quantity",
- Renew = "renew",
+ // Done
+ UpdatePrepaidQuantity = "update_prepaid_quantity",
+ Renew = "renew",
- // Handle upgrades / downgrades
- MainIsTrial = "main_is_trial",
- MainIsFree = "main_is_free",
- Upgrade = "upgrade",
- Downgrade = "downgrade",
+ // Handle upgrades / downgrades
+ MainIsTrial = "main_is_trial",
+ MainIsFree = "main_is_free",
+ Upgrade = "upgrade",
+ Downgrade = "downgrade",
- // Branch
- Cancel = "cancel",
+ // Branch
+ Cancel = "cancel",
}
diff --git a/shared/models/attachModels/attachEnums/AttachConfig.ts b/shared/models/attachModels/attachEnums/AttachConfig.ts
index d45e837f4..505a4747c 100644
--- a/shared/models/attachModels/attachEnums/AttachConfig.ts
+++ b/shared/models/attachModels/attachEnums/AttachConfig.ts
@@ -1,22 +1,22 @@
import { AttachBranch } from "./AttachBranch.js";
export enum ProrationBehavior {
- Immediately = "immediately",
- NextBilling = "next_billing",
- None = "none",
+ Immediately = "immediately",
+ NextBilling = "next_billing",
+ None = "none",
}
export interface AttachConfig {
- onlyCheckout: boolean;
- invoiceCheckout: boolean;
- carryUsage: boolean; // Whether to carry over existing usages
- branch: AttachBranch;
- proration: ProrationBehavior;
- disableTrial: boolean;
- invoiceOnly: boolean;
- disableMerge: boolean;
- sameIntervals: boolean;
- carryTrial: boolean;
- finalizeInvoice: boolean;
- requirePaymentMethod: boolean;
+ onlyCheckout: boolean;
+ invoiceCheckout: boolean;
+ carryUsage: boolean; // Whether to carry over existing usages
+ branch: AttachBranch;
+ proration: ProrationBehavior;
+ disableTrial: boolean;
+ invoiceOnly: boolean;
+ disableMerge: boolean;
+ sameIntervals: boolean;
+ carryTrial: boolean;
+ finalizeInvoice: boolean;
+ requirePaymentMethod: boolean;
}
diff --git a/shared/models/attachModels/attachEnums/AttachFunction.ts b/shared/models/attachModels/attachEnums/AttachFunction.ts
index 01d8cb805..6b554ff7f 100644
--- a/shared/models/attachModels/attachEnums/AttachFunction.ts
+++ b/shared/models/attachModels/attachEnums/AttachFunction.ts
@@ -1,16 +1,16 @@
export enum AttachFunction {
- CreateCheckout = "create_checkout",
- AddProduct = "add_product",
- OneOff = "one_off",
- UpdateEnts = "update_ents", // only update entitlements
- ScheduleProduct = "schedule_product",
- UpdatePrepaidQuantity = "update_prepaid_quantity",
- Renew = "renew",
- // UpdateProduct = "update_product", // update product
+ CreateCheckout = "create_checkout",
+ AddProduct = "add_product",
+ OneOff = "one_off",
+ UpdateEnts = "update_ents", // only update entitlements
+ ScheduleProduct = "schedule_product",
+ UpdatePrepaidQuantity = "update_prepaid_quantity",
+ Renew = "renew",
+ // UpdateProduct = "update_product", // update product
- UpgradeSameInterval = "upgrade_same_interval",
- UpgradeDiffInterval = "upgrade_diff_interval",
- MultiAttach = "multi_attach",
+ UpgradeSameInterval = "upgrade_same_interval",
+ UpgradeDiffInterval = "upgrade_diff_interval",
+ MultiAttach = "multi_attach",
}
/* Handle checkout / public error:
diff --git a/shared/models/attachModels/attachPreviewModels.ts b/shared/models/attachModels/attachPreviewModels.ts
index b04c318e4..e7e46e129 100644
--- a/shared/models/attachModels/attachPreviewModels.ts
+++ b/shared/models/attachModels/attachPreviewModels.ts
@@ -6,27 +6,27 @@ import { AttachBranch } from "./attachEnums/AttachBranch.js";
import { AttachFunction } from "./attachEnums/AttachFunction.js";
export interface PreviewLineItem {
- amount?: number | undefined;
- description: string;
- price: string;
- price_id: string;
- usage_model?: UsageModel;
- feature_id?: string;
+ amount?: number | undefined;
+ description: string;
+ price: string;
+ price_id: string;
+ usage_model?: UsageModel;
+ feature_id?: string;
}
export interface AttachPreview {
- func: AttachFunction;
- branch: AttachBranch;
- options: any;
- new_items: any;
- due_today: {
- line_items: PreviewLineItem[];
- total: number;
- };
- due_next_cycle: {
- line_items: PreviewLineItem[];
- due_at: number;
- };
- free_trial?: FreeTrial | null;
- current_product?: FullProduct;
+ func: AttachFunction;
+ branch: AttachBranch;
+ options: any;
+ new_items: any;
+ due_today: {
+ line_items: PreviewLineItem[];
+ total: number;
+ };
+ due_next_cycle: {
+ line_items: PreviewLineItem[];
+ due_at: number;
+ };
+ free_trial?: FreeTrial | null;
+ current_product?: FullProduct;
}
diff --git a/shared/models/attachModels/checkoutModels.ts b/shared/models/attachModels/checkoutModels.ts
index 79ed7cd1c..9976904e7 100644
--- a/shared/models/attachModels/checkoutModels.ts
+++ b/shared/models/attachModels/checkoutModels.ts
@@ -5,28 +5,28 @@ import { ProductResponseSchema } from "../productV2Models/productResponseModels.
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
export const CheckoutLineSchema = z.object({
- description: z.string(),
- amount: z.number(),
- item: ProductItemResponseSchema.nullish(),
+ description: z.string(),
+ amount: z.number(),
+ item: ProductItemResponseSchema.nullish(),
});
export const CheckoutResponseSchema = z.object({
- url: z.string().nullish(),
- customer_id: z.string(),
- lines: z.array(CheckoutLineSchema),
- product: ProductResponseSchema.nullish(),
- current_product: ProductResponseSchema.nullish(),
- options: z.array(FeatureOptionsSchema).nullish(),
- total: z.number().nullish(),
- currency: z.string().nullish(),
- has_prorations: z.boolean().nullish(),
- // next_cycle_at: z.number().nullish(),
- next_cycle: z
- .object({
- starts_at: z.number().nullish(),
- total: z.number().nullish(),
- })
- .nullish(),
+ url: z.string().nullish(),
+ customer_id: z.string(),
+ lines: z.array(CheckoutLineSchema),
+ product: ProductResponseSchema.nullish(),
+ current_product: ProductResponseSchema.nullish(),
+ options: z.array(FeatureOptionsSchema).nullish(),
+ total: z.number().nullish(),
+ currency: z.string().nullish(),
+ has_prorations: z.boolean().nullish(),
+ // next_cycle_at: z.number().nullish(),
+ next_cycle: z
+ .object({
+ starts_at: z.number().nullish(),
+ total: z.number().nullish(),
+ })
+ .nullish(),
});
export type CheckoutLine = z.infer;
diff --git a/shared/models/authModels/membership.ts b/shared/models/authModels/membership.ts
index 82098312f..3b62eb667 100644
--- a/shared/models/authModels/membership.ts
+++ b/shared/models/authModels/membership.ts
@@ -1,6 +1,6 @@
import { Member, User } from "../../db/auth-schema.js";
export interface Membership {
- user: User;
- member: Member;
+ user: User;
+ member: Member;
}
diff --git a/shared/models/chatResultModels/chatResultFeature.ts b/shared/models/chatResultModels/chatResultFeature.ts
index f290677cc..a7d46f3bf 100644
--- a/shared/models/chatResultModels/chatResultFeature.ts
+++ b/shared/models/chatResultModels/chatResultFeature.ts
@@ -1,28 +1,28 @@
import { z } from "zod";
export const ChatFeatureCreditSchema = z.object({
- metered_feature_id: z.string().nonempty(),
- credit_cost: z.number().gt(0),
+ metered_feature_id: z.string().nonempty(),
+ credit_cost: z.number().gt(0),
});
export const ChatResultFeatureSchema = z.object({
- id: z.string(),
- name: z.string(),
- type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"]),
+ id: z.string(),
+ name: z.string(),
+ type: z.enum(["boolean", "single_use", "continuous_use", "credit_system"]),
- display: z.object({
- singular: z.string(),
- plural: z.string(),
- }),
+ display: z.object({
+ singular: z.string(),
+ plural: z.string(),
+ }),
- credit_schema: z
- .array(
- z.object({
- metered_feature_id: z.string(),
- credit_cost: z.number(),
- }),
- )
- .nullish(),
+ credit_schema: z
+ .array(
+ z.object({
+ metered_feature_id: z.string(),
+ credit_cost: z.number(),
+ }),
+ )
+ .nullish(),
});
export type ChatResultFeature = z.infer;
diff --git a/shared/models/chatResultModels/chatResultTable.ts b/shared/models/chatResultModels/chatResultTable.ts
index 726d2bcdb..5d93878b5 100644
--- a/shared/models/chatResultModels/chatResultTable.ts
+++ b/shared/models/chatResultModels/chatResultTable.ts
@@ -5,14 +5,14 @@ import { ProductV2 } from "../productV2Models/productV2Models.js";
import { collatePgColumn } from "../../db/utils.js";
export const chatResults = pgTable("chat_results", {
- id: text("id").primaryKey(),
- created_at: numeric("created_at"),
- data: jsonb("data")
- .$type<{
- features: ChatResultFeature[];
- products: ProductV2[];
- }>()
- .notNull(),
+ id: text("id").primaryKey(),
+ created_at: numeric("created_at"),
+ data: jsonb("data")
+ .$type<{
+ features: ChatResultFeature[];
+ products: ProductV2[];
+ }>()
+ .notNull(),
}).enableRLS();
collatePgColumn(chatResults.id, "C");
diff --git a/shared/models/checkModels/checkPreviewModels.ts b/shared/models/checkModels/checkPreviewModels.ts
index 18da38ba1..f89ef2ab4 100644
--- a/shared/models/checkModels/checkPreviewModels.ts
+++ b/shared/models/checkModels/checkPreviewModels.ts
@@ -3,78 +3,78 @@ import { Infinite } from "../productModels/productEnums.js";
import { ProductResponse } from "../productV2Models/productResponseModels.js";
export enum AttachScenario {
- Scheduled = "scheduled",
- Active = "active",
- New = "new",
- Renew = "renew",
- Upgrade = "upgrade",
- Downgrade = "downgrade",
- Cancel = "cancel",
- Expired = "expired",
+ Scheduled = "scheduled",
+ Active = "active",
+ New = "new",
+ Renew = "renew",
+ Upgrade = "upgrade",
+ Downgrade = "downgrade",
+ Cancel = "cancel",
+ Expired = "expired",
}
export interface PreviewItem {
- price: string;
- description: string;
- usage_model?: UsageModel;
- amount?: number;
+ price: string;
+ description: string;
+ usage_model?: UsageModel;
+ amount?: number;
}
export interface CheckProductPreview {
- // title: string;
- // message: string;
- scenario: AttachScenario;
- product_id: string;
- product_name: string;
- recurring: boolean;
- error_on_attach?: boolean;
- next_cycle_at?: number;
- current_product_name?: string;
+ // title: string;
+ // message: string;
+ scenario: AttachScenario;
+ product_id: string;
+ product_name: string;
+ recurring: boolean;
+ error_on_attach?: boolean;
+ next_cycle_at?: number;
+ current_product_name?: string;
- items?: {
- price: string;
- description: string;
- usage_model?: UsageModel;
- }[];
+ items?: {
+ price: string;
+ description: string;
+ usage_model?: UsageModel;
+ }[];
- options?: {
- feature_id: string;
- feature_name: string;
- billing_units: number;
- included_usage: number;
- price?: number;
- tiers?: {
- to: number | typeof Infinite;
- amount: number;
- }[];
- }[];
+ options?: {
+ feature_id: string;
+ feature_name: string;
+ billing_units: number;
+ included_usage: number;
+ price?: number;
+ tiers?: {
+ to: number | typeof Infinite;
+ amount: number;
+ }[];
+ }[];
- due_today?: {
- price: number;
- currency: string;
- };
+ due_today?: {
+ price: number;
+ currency: string;
+ };
- due_next_cycle?: {
- price: number;
- currency: string;
- };
+ due_next_cycle?: {
+ price: number;
+ currency: string;
+ };
- product?: ProductResponse;
- payment_method?: any;
+ product?: ProductResponse;
+ payment_method?: any;
}
export enum FeaturePreviewScenario {
- UsageLimit = "usage_limit",
- FeatureFlag = "feature_flag",
+ UsageLimit = "usage_limit",
+ FeatureFlag = "feature_flag",
}
export interface CheckFeaturePreview {
- title: string;
- message: string;
+ title: string;
+ message: string;
- scenario: FeaturePreviewScenario;
- feature_id: string;
- feature_name: string;
+ scenario: FeaturePreviewScenario;
+ feature_id: string;
+ feature_name: string;
- products: ProductResponse[];
+ products: ProductResponse[];
}
diff --git a/shared/models/cusModels/cusExpand.ts b/shared/models/cusModels/cusExpand.ts
index a53c144de..eb883f61a 100644
--- a/shared/models/cusModels/cusExpand.ts
+++ b/shared/models/cusModels/cusExpand.ts
@@ -1,8 +1,8 @@
export enum CusExpand {
- Invoices = "invoices",
- TrialsUsed = "trials_used",
- Rewards = "rewards",
- Entities = "entities",
- Referrals = "referrals",
- PaymentMethod = "payment_method",
+ Invoices = "invoices",
+ TrialsUsed = "trials_used",
+ Rewards = "rewards",
+ Entities = "entities",
+ Referrals = "referrals",
+ PaymentMethod = "payment_method",
}
diff --git a/shared/models/cusModels/cusModels.ts b/shared/models/cusModels/cusModels.ts
index 4dfe956f4..dddc7fabc 100644
--- a/shared/models/cusModels/cusModels.ts
+++ b/shared/models/cusModels/cusModels.ts
@@ -2,89 +2,89 @@ import { z } from "zod";
import { AppEnv } from "../genModels/genEnums.js";
export const CustomerSchema = z.object({
- id: z.string().nullish(), // given by user
- name: z.string().nullish(),
- email: z.string().nullish(),
- fingerprint: z.string().nullish(),
+ id: z.string().nullish(), // given by user
+ name: z.string().nullish(),
+ email: z.string().nullish(),
+ fingerprint: z.string().nullish(),
- // Internal
- internal_id: z.string(),
- org_id: z.string(),
- created_at: z.number(),
- env: z.nativeEnum(AppEnv),
- processor: z.any(),
- metadata: z.record(z.any(), z.any()).nullish().default({}),
+ // Internal
+ internal_id: z.string(),
+ org_id: z.string(),
+ created_at: z.number(),
+ env: z.nativeEnum(AppEnv),
+ processor: z.any(),
+ metadata: z.record(z.any(), z.any()).nullish().default({}),
});
export const CreateCustomerSchema = z.object({
- id: z
- .string()
- .refine(
- (val) => {
- if (val === null) return true; // Allow null/undefined
+ id: z
+ .string()
+ .refine(
+ (val) => {
+ if (val === null) return true; // Allow null/undefined
- return /^[a-zA-Z0-9_-]+$/.test(val);
- },
- (val) => {
- if (val === "") return { message: "can't be an empty string" };
+ return /^[a-zA-Z0-9_-]+$/.test(val);
+ },
+ (val) => {
+ if (val === "") return { message: "can't be an empty string" };
- // Check for specific invalid characters and provide targeted messages
- if (val.includes("@")) {
- return {
- message:
- "ID cannot contain @ symbol. Use only letters, numbers, underscores, and hyphens.",
- };
- }
- if (val.includes(" ")) {
- return {
- message:
- "ID cannot contain spaces. Use only letters, numbers, underscores, and hyphens.",
- };
- }
- if (val.includes(".")) {
- return {
- message:
- "ID cannot contain periods. Use only letters, numbers, underscores, and hyphens.",
- };
- }
- if (/[^a-zA-Z0-9_-]/.test(val)) {
- const invalidChar = val.match(/[^a-zA-Z0-9_-]/)?.[0];
- return {
- message: `ID cannot contain '${invalidChar}'. Use only letters, numbers, underscores, and hyphens.`,
- };
- }
+ // Check for specific invalid characters and provide targeted messages
+ if (val.includes("@")) {
+ return {
+ message:
+ "ID cannot contain @ symbol. Use only letters, numbers, underscores, and hyphens.",
+ };
+ }
+ if (val.includes(" ")) {
+ return {
+ message:
+ "ID cannot contain spaces. Use only letters, numbers, underscores, and hyphens.",
+ };
+ }
+ if (val.includes(".")) {
+ return {
+ message:
+ "ID cannot contain periods. Use only letters, numbers, underscores, and hyphens.",
+ };
+ }
+ if (/[^a-zA-Z0-9_-]/.test(val)) {
+ const invalidChar = val.match(/[^a-zA-Z0-9_-]/)?.[0];
+ return {
+ message: `ID cannot contain '${invalidChar}'. Use only letters, numbers, underscores, and hyphens.`,
+ };
+ }
- return {
- message:
- "ID must contain only letters, numbers, underscores, and hyphens.",
- };
- }
- )
- .nullish(),
- name: z.string().nullish(),
- email: z
- .string()
- .email({ message: "not a valid email address" })
- .or(z.literal(""))
- .nullish(),
- fingerprint: z.string().nullish(),
- metadata: z.record(z.any(), z.any()).default({}).nullish(),
- stripe_id: z.string().nullish(),
+ return {
+ message:
+ "ID must contain only letters, numbers, underscores, and hyphens.",
+ };
+ },
+ )
+ .nullish(),
+ name: z.string().nullish(),
+ email: z
+ .string()
+ .email({ message: "not a valid email address" })
+ .or(z.literal(""))
+ .nullish(),
+ fingerprint: z.string().nullish(),
+ metadata: z.record(z.any(), z.any()).default({}).nullish(),
+ stripe_id: z.string().nullish(),
});
export const CustomerDataSchema = z.object({
- name: z.string().nullish(),
- email: z.string().nullish(),
- fingerprint: z.string().nullish(),
- metadata: z.record(z.any(), z.any()).nullish().default({}),
- stripe_id: z.string().nullish(),
+ name: z.string().nullish(),
+ email: z.string().nullish(),
+ fingerprint: z.string().nullish(),
+ metadata: z.record(z.any(), z.any()).nullish().default({}),
+ stripe_id: z.string().nullish(),
});
export const CustomerResponseSchema = CustomerSchema.omit({
- // created_at: true,
- // env: true,
- // processor: true,
- org_id: true,
+ // created_at: true,
+ // env: true,
+ // processor: true,
+ org_id: true,
});
export type Customer = z.infer;
diff --git a/shared/models/cusModels/cusRelations.ts b/shared/models/cusModels/cusRelations.ts
index 596c50d67..e5a757335 100644
--- a/shared/models/cusModels/cusRelations.ts
+++ b/shared/models/cusModels/cusRelations.ts
@@ -5,10 +5,10 @@ import { entities } from "./entityModels/entityTable.js";
import { organizations } from "../orgModels/orgTable.js";
export const customersRelations = relations(customers, ({ one, many }) => ({
- customer_products: many(customerProducts),
- entities: many(entities),
- org: one(organizations, {
- fields: [customers.org_id],
- references: [organizations.id],
- }),
+ customer_products: many(customerProducts),
+ entities: many(entities),
+ org: one(organizations, {
+ fields: [customers.org_id],
+ references: [organizations.id],
+ }),
}));
diff --git a/shared/models/cusModels/cusResModels/cusFeatureResponse.ts b/shared/models/cusModels/cusResModels/cusFeatureResponse.ts
index d571e540e..fcaee7bd1 100644
--- a/shared/models/cusModels/cusResModels/cusFeatureResponse.ts
+++ b/shared/models/cusModels/cusResModels/cusFeatureResponse.ts
@@ -3,77 +3,77 @@ import { EntInterval } from "../../productModels/entModels/entEnums.js";
import { ProductItemFeatureType } from "../../productV2Models/productItemModels/productItemModels.js";
export const CusRolloverSchema = z.object({
- balance: z.number(),
- expires_at: z.number(),
+ balance: z.number(),
+ expires_at: z.number(),
});
export const CusEntResponseSchema = z.object({
- feature_id: z.string(),
- interval: z.nativeEnum(EntInterval).nullish(),
- interval_count: z.number().nullish(),
- unlimited: z.boolean().nullish(),
- balance: z.number().nullish(), //
- usage: z.number().nullish(),
- included_usage: z.number().nullish(),
- next_reset_at: z.number().nullish(),
- overage_allowed: z.boolean().nullish(),
- usage_limit: z.number().nullish(),
- rollovers: z.array(CusRolloverSchema).nullish(),
+ feature_id: z.string(),
+ interval: z.nativeEnum(EntInterval).nullish(),
+ interval_count: z.number().nullish(),
+ unlimited: z.boolean().nullish(),
+ balance: z.number().nullish(), //
+ usage: z.number().nullish(),
+ included_usage: z.number().nullish(),
+ next_reset_at: z.number().nullish(),
+ overage_allowed: z.boolean().nullish(),
+ usage_limit: z.number().nullish(),
+ rollovers: z.array(CusRolloverSchema).nullish(),
});
export const CoreCusFeatureResponseSchema = z.object({
- interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).nullish(),
- interval_count: z.number().nullish(),
- unlimited: z.boolean().nullish(),
- balance: z.number().nullish(),
- usage: z.number().nullish(),
- included_usage: z.number().nullish(),
- next_reset_at: z.number().nullish(),
- overage_allowed: z.boolean().nullish(),
+ interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).nullish(),
+ interval_count: z.number().nullish(),
+ unlimited: z.boolean().nullish(),
+ balance: z.number().nullish(),
+ usage: z.number().nullish(),
+ included_usage: z.number().nullish(),
+ next_reset_at: z.number().nullish(),
+ overage_allowed: z.boolean().nullish(),
- breakdown: z
- .array(
- z.object({
- interval: z.nativeEnum(EntInterval),
- interval_count: z.number().nullish(),
- balance: z.number().nullish(),
- usage: z.number().nullish(),
- included_usage: z.number().nullish(),
- next_reset_at: z.number().nullish(),
- })
- )
- .nullish(),
- credit_schema: z
- .array(
- z.object({
- feature_id: z.string(),
- credit_amount: z.number(),
- })
- )
- .nullish(),
+ breakdown: z
+ .array(
+ z.object({
+ interval: z.nativeEnum(EntInterval),
+ interval_count: z.number().nullish(),
+ balance: z.number().nullish(),
+ usage: z.number().nullish(),
+ included_usage: z.number().nullish(),
+ next_reset_at: z.number().nullish(),
+ }),
+ )
+ .nullish(),
+ credit_schema: z
+ .array(
+ z.object({
+ feature_id: z.string(),
+ credit_amount: z.number(),
+ }),
+ )
+ .nullish(),
- usage_limit: z.number().nullish(),
- rollovers: z.array(CusRolloverSchema).nullish(),
+ usage_limit: z.number().nullish(),
+ rollovers: z.array(CusRolloverSchema).nullish(),
});
export const CusEntResponseV2Schema = z
- .object({
- id: z.string(),
- type: z.nativeEnum(ProductItemFeatureType),
- name: z.string().nullish(),
- })
- .extend(CoreCusFeatureResponseSchema.shape);
+ .object({
+ id: z.string(),
+ type: z.nativeEnum(ProductItemFeatureType),
+ name: z.string().nullish(),
+ })
+ .extend(CoreCusFeatureResponseSchema.shape);
export const CheckResponseSchema = z
- .object({
- allowed: z.boolean(),
- customer_id: z.string(),
- feature_id: z.string(),
- entity_id: z.string().nullish(),
- required_balance: z.number(),
- code: z.string(),
- })
- .extend(CoreCusFeatureResponseSchema.shape);
+ .object({
+ allowed: z.boolean(),
+ customer_id: z.string(),
+ feature_id: z.string(),
+ entity_id: z.string().nullish(),
+ required_balance: z.number(),
+ code: z.string(),
+ })
+ .extend(CoreCusFeatureResponseSchema.shape);
export type CusEntResponse = z.infer;
export type CusEntResponseV2 = z.infer;
diff --git a/shared/models/cusModels/cusResModels/cusProductResponse.ts b/shared/models/cusModels/cusResModels/cusProductResponse.ts
index 6441dceb1..45b25eebe 100644
--- a/shared/models/cusModels/cusResModels/cusProductResponse.ts
+++ b/shared/models/cusModels/cusResModels/cusProductResponse.ts
@@ -3,33 +3,33 @@ import { CusProductStatus } from "../../cusProductModels/cusProductEnums.js";
import { ProductItemResponseSchema } from "../../productV2Models/productItemModels/prodItemResponseModels.js";
export const CusProductResponseSchema = z.object({
- id: z.string(),
- name: z.string().nullable(),
- group: z.string().nullable(),
- status: z.nativeEnum(CusProductStatus),
- // created_at: z.number(),
- canceled: z.boolean().nullish(),
- trialing: z.boolean().nullish(),
+ id: z.string(),
+ name: z.string().nullable(),
+ group: z.string().nullable(),
+ status: z.nativeEnum(CusProductStatus),
+ // created_at: z.number(),
+ canceled: z.boolean().nullish(),
+ trialing: z.boolean().nullish(),
- canceled_at: z.number().nullish(),
- started_at: z.number(),
- is_default: z.boolean(),
- is_add_on: z.boolean(),
- version: z.number().nullish(),
+ canceled_at: z.number().nullish(),
+ started_at: z.number(),
+ is_default: z.boolean(),
+ is_add_on: z.boolean(),
+ version: z.number().nullish(),
- stripe_subscription_ids: z.array(z.string()).nullish(),
+ stripe_subscription_ids: z.array(z.string()).nullish(),
- current_period_start: z.number().nullish(),
- current_period_end: z.number().nullish(),
- entity_id: z.string().nullish(),
- items: z.array(ProductItemResponseSchema).nullish(),
- quantity: z.number().optional(),
- prepaid_quantities: z
- .array(
- z.object({
- quantity: z.number(),
- feature_id: z.string(),
- })
- )
- .nullish(),
+ current_period_start: z.number().nullish(),
+ current_period_end: z.number().nullish(),
+ entity_id: z.string().nullish(),
+ items: z.array(ProductItemResponseSchema).nullish(),
+ quantity: z.number().optional(),
+ prepaid_quantities: z
+ .array(
+ z.object({
+ quantity: z.number(),
+ feature_id: z.string(),
+ }),
+ )
+ .nullish(),
});
diff --git a/shared/models/cusModels/cusResModels/cusReferralsResponse.ts b/shared/models/cusModels/cusResModels/cusReferralsResponse.ts
index ad497e141..af313c8d4 100644
--- a/shared/models/cusModels/cusResModels/cusReferralsResponse.ts
+++ b/shared/models/cusModels/cusResModels/cusReferralsResponse.ts
@@ -1,14 +1,14 @@
import z from "zod";
export const CusReferralResponseSchema = z.object({
- program_id: z.string(),
- customer: z.object({
- id: z.string(),
- name: z.string().nullish(),
- email: z.string().nullish(),
- }),
- reward_applied: z.boolean(),
- created_at: z.number(),
+ program_id: z.string(),
+ customer: z.object({
+ id: z.string(),
+ name: z.string().nullish(),
+ email: z.string().nullish(),
+ }),
+ reward_applied: z.boolean(),
+ created_at: z.number(),
});
export type CusReferralResponse = z.infer;
diff --git a/shared/models/cusModels/cusResponseModels.ts b/shared/models/cusModels/cusResponseModels.ts
index 3b428802c..9a5b1b39c 100644
--- a/shared/models/cusModels/cusResponseModels.ts
+++ b/shared/models/cusModels/cusResponseModels.ts
@@ -7,33 +7,33 @@ import { EntityResponseSchema } from "./entityModels/entityResModels.js";
import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js";
export const TrialUsedResponseSchema = z.object({
- product_id: z.string(),
- customer_id: z.string(),
- fingerprint: z.string().nullish(),
+ product_id: z.string(),
+ customer_id: z.string(),
+ fingerprint: z.string().nullish(),
});
export const CusResponseSchema = z.object({
- // Internal fields
- autumn_id: z.string().nullish(),
+ // Internal fields
+ autumn_id: z.string().nullish(),
- id: z.string().nullable(),
- created_at: z.number(),
- name: z.string().nullable(),
- email: z.string().nullable(),
- fingerprint: z.string().nullable(),
- stripe_id: z.string().nullable().default(null),
- env: z.nativeEnum(AppEnv),
+ id: z.string().nullable(),
+ created_at: z.number(),
+ name: z.string().nullable(),
+ email: z.string().nullable(),
+ fingerprint: z.string().nullable(),
+ stripe_id: z.string().nullable().default(null),
+ env: z.nativeEnum(AppEnv),
- products: z.array(CusProductResponseSchema),
- features: z.any(),
+ products: z.array(CusProductResponseSchema),
+ features: z.any(),
- invoices: z.array(InvoiceResponseSchema).optional(),
- trials_used: z.array(TrialUsedResponseSchema).optional(),
- rewards: RewardResponseSchema.nullish(),
- metadata: z.record(z.any(), z.any()).default({}),
- entities: z.array(EntityResponseSchema).optional(),
- referrals: z.array(CusReferralResponseSchema).optional(),
- payment_method: z.any().nullish(),
+ invoices: z.array(InvoiceResponseSchema).optional(),
+ trials_used: z.array(TrialUsedResponseSchema).optional(),
+ rewards: RewardResponseSchema.nullish(),
+ metadata: z.record(z.any(), z.any()).default({}),
+ entities: z.array(EntityResponseSchema).optional(),
+ referrals: z.array(CusReferralResponseSchema).optional(),
+ payment_method: z.any().nullish(),
});
export type CusResponse = z.infer;
diff --git a/shared/models/cusModels/cusTable.ts b/shared/models/cusModels/cusTable.ts
index 1333d0467..5341f3650 100644
--- a/shared/models/cusModels/cusTable.ts
+++ b/shared/models/cusModels/cusTable.ts
@@ -1,43 +1,43 @@
import {
- pgTable,
- text,
- numeric,
- jsonb,
- unique,
- foreignKey,
+ pgTable,
+ text,
+ numeric,
+ jsonb,
+ unique,
+ foreignKey,
} from "drizzle-orm/pg-core";
import { collatePgColumn } from "../../db/utils.js";
import { organizations } from "../orgModels/orgTable.js";
import { sql } from "drizzle-orm";
export type CustomerProcessor = {
- type: "stripe";
- id: string;
+ type: "stripe";
+ id: string;
};
export const customers = pgTable(
- "customers",
- {
- internal_id: text("internal_id").primaryKey().notNull(),
- org_id: text("org_id").notNull(),
- created_at: numeric({ mode: "number" }).notNull(),
- name: text(),
- id: text(),
- email: text(),
- fingerprint: text().default(sql`null`),
- metadata: jsonb().$type>(),
+ "customers",
+ {
+ internal_id: text("internal_id").primaryKey().notNull(),
+ org_id: text("org_id").notNull(),
+ created_at: numeric({ mode: "number" }).notNull(),
+ name: text(),
+ id: text(),
+ email: text(),
+ fingerprint: text().default(sql`null`),
+ metadata: jsonb().$type>(),
- env: text().notNull(),
- processor: jsonb().$type(),
- },
- (table) => [
- unique("cus_id_constraint").on(table.org_id, table.id, table.env),
- foreignKey({
- columns: [table.org_id],
- foreignColumns: [organizations.id],
- name: "customers_org_id_fkey",
- }).onDelete("cascade"),
- ],
+ env: text().notNull(),
+ processor: jsonb().$type(),
+ },
+ (table) => [
+ unique("cus_id_constraint").on(table.org_id, table.id, table.env),
+ foreignKey({
+ columns: [table.org_id],
+ foreignColumns: [organizations.id],
+ name: "customers_org_id_fkey",
+ }).onDelete("cascade"),
+ ],
).enableRLS();
collatePgColumn(customers.internal_id, "C");
diff --git a/shared/models/cusModels/entityModels/entityExpand.ts b/shared/models/cusModels/entityModels/entityExpand.ts
index d442cf73b..c3f357a47 100644
--- a/shared/models/cusModels/entityModels/entityExpand.ts
+++ b/shared/models/cusModels/entityModels/entityExpand.ts
@@ -1,3 +1,3 @@
export enum EntityExpand {
- Invoices = "invoices",
+ Invoices = "invoices",
}
diff --git a/shared/models/cusModels/entityModels/entityModels.ts b/shared/models/cusModels/entityModels/entityModels.ts
index 5c1156de1..9113f8c86 100644
--- a/shared/models/cusModels/entityModels/entityModels.ts
+++ b/shared/models/cusModels/entityModels/entityModels.ts
@@ -2,32 +2,32 @@ import { z } from "zod";
import { Feature } from "../../featureModels/featureModels.js";
export const EntitySchema = z.object({
- id: z.string(),
- org_id: z.string(),
- created_at: z.number(),
- internal_id: z.string(),
- internal_customer_id: z.string(),
- env: z.string(),
- name: z.string(),
- deleted: z.boolean(),
- feature_id: z.string(),
- internal_feature_id: z.string(),
+ id: z.string(),
+ org_id: z.string(),
+ created_at: z.number(),
+ internal_id: z.string(),
+ internal_customer_id: z.string(),
+ env: z.string(),
+ name: z.string(),
+ deleted: z.boolean(),
+ feature_id: z.string(),
+ internal_feature_id: z.string(),
});
export const CreateEntitySchema = z.object({
- id: z.string(),
- name: z.string().nullish(),
- feature_id: z.string(),
+ id: z.string(),
+ name: z.string().nullish(),
+ feature_id: z.string(),
});
export const EntityDataSchema = z.object({
- name: z.string(), // Name of entity
- feature_id: z.string(), // Feature ID of entity
+ name: z.string(), // Name of entity
+ feature_id: z.string(), // Feature ID of entity
});
export type Entity = z.infer;
export type EntityWithFeature = Entity & {
- feature: Feature;
+ feature: Feature;
};
export type CreateEntity = z.infer;
export type EntityData = z.infer;
diff --git a/shared/models/cusModels/entityModels/entityRelations.ts b/shared/models/cusModels/entityModels/entityRelations.ts
index 8734ccb30..6117f32f5 100644
--- a/shared/models/cusModels/entityModels/entityRelations.ts
+++ b/shared/models/cusModels/entityModels/entityRelations.ts
@@ -5,16 +5,16 @@ import { features } from "../../featureModels/featureTable.js";
import { organizations } from "../../orgModels/orgTable.js";
export const entitiesRelations = relations(entities, ({ one }) => ({
- customer: one(customers, {
- fields: [entities.internal_customer_id],
- references: [customers.internal_id],
- }),
- feature: one(features, {
- fields: [entities.internal_feature_id],
- references: [features.internal_id],
- }),
- organization: one(organizations, {
- fields: [entities.org_id],
- references: [organizations.id],
- }),
+ customer: one(customers, {
+ fields: [entities.internal_customer_id],
+ references: [customers.internal_id],
+ }),
+ feature: one(features, {
+ fields: [entities.internal_feature_id],
+ references: [features.internal_id],
+ }),
+ organization: one(organizations, {
+ fields: [entities.org_id],
+ references: [organizations.id],
+ }),
}));
diff --git a/shared/models/cusModels/entityModels/entityResModels.ts b/shared/models/cusModels/entityModels/entityResModels.ts
index 0d54b3b78..8bdc17311 100644
--- a/shared/models/cusModels/entityModels/entityResModels.ts
+++ b/shared/models/cusModels/entityModels/entityResModels.ts
@@ -6,16 +6,16 @@ import { CusProductResponseSchema } from "../cusResModels/cusProductResponse.js"
import { CusEntResponseV2Schema } from "../cusResModels/cusFeatureResponse.js";
export const EntityResponseSchema = z.object({
- id: z.string().nullable(),
- name: z.string().nullable(),
- customer_id: z.string(),
- feature_id: z.string().nullish(),
+ id: z.string().nullable(),
+ name: z.string().nullable(),
+ customer_id: z.string(),
+ feature_id: z.string().nullish(),
- created_at: z.number(),
- env: z.nativeEnum(AppEnv),
- products: z.array(CusProductResponseSchema).optional(),
- features: z.record(z.string(), CusEntResponseV2Schema).optional(),
- invoices: z.array(InvoiceResponseSchema).optional(),
+ created_at: z.number(),
+ env: z.nativeEnum(AppEnv),
+ products: z.array(CusProductResponseSchema).optional(),
+ features: z.record(z.string(), CusEntResponseV2Schema).optional(),
+ invoices: z.array(InvoiceResponseSchema).optional(),
});
export type EntityResponse = z.infer;
diff --git a/shared/models/cusModels/entityModels/entityTable.ts b/shared/models/cusModels/entityModels/entityTable.ts
index 396c75ee8..9b0d1745d 100644
--- a/shared/models/cusModels/entityModels/entityTable.ts
+++ b/shared/models/cusModels/entityModels/entityTable.ts
@@ -1,9 +1,9 @@
import {
- boolean,
- foreignKey,
- numeric,
- pgTable,
- unique,
+ boolean,
+ foreignKey,
+ numeric,
+ pgTable,
+ unique,
} from "drizzle-orm/pg-core";
import { text } from "drizzle-orm/pg-core";
import { customers } from "../cusTable.js";
@@ -11,43 +11,43 @@ import { features } from "../../featureModels/featureTable.js";
import { organizations } from "../../orgModels/orgTable.js";
export const entities = pgTable(
- "entities",
- {
- id: text(),
- org_id: text("org_id"),
- created_at: numeric({ mode: "number" }).notNull(),
- internal_id: text("internal_id").primaryKey().notNull(),
- internal_customer_id: text("internal_customer_id").notNull(),
- env: text(),
- name: text(),
- deleted: boolean().default(false).notNull(),
- internal_feature_id: text("internal_feature_id"),
+ "entities",
+ {
+ id: text(),
+ org_id: text("org_id"),
+ created_at: numeric({ mode: "number" }).notNull(),
+ internal_id: text("internal_id").primaryKey().notNull(),
+ internal_customer_id: text("internal_customer_id").notNull(),
+ env: text(),
+ name: text(),
+ deleted: boolean().default(false).notNull(),
+ internal_feature_id: text("internal_feature_id"),
- // Optional...
- feature_id: text("feature_id"),
- },
- (table) => [
- foreignKey({
- columns: [table.internal_customer_id],
- foreignColumns: [customers.internal_id],
- name: "entities_internal_customer_id_fkey",
- }).onDelete("cascade"),
- foreignKey({
- columns: [table.internal_feature_id],
- foreignColumns: [features.internal_id],
- name: "entities_internal_feature_id_fkey",
- }).onDelete("cascade"),
- foreignKey({
- columns: [table.org_id],
- foreignColumns: [organizations.id],
- name: "entities_org_id_fkey",
- }).onDelete("cascade"),
+ // Optional...
+ feature_id: text("feature_id"),
+ },
+ (table) => [
+ foreignKey({
+ columns: [table.internal_customer_id],
+ foreignColumns: [customers.internal_id],
+ name: "entities_internal_customer_id_fkey",
+ }).onDelete("cascade"),
+ foreignKey({
+ columns: [table.internal_feature_id],
+ foreignColumns: [features.internal_id],
+ name: "entities_internal_feature_id_fkey",
+ }).onDelete("cascade"),
+ foreignKey({
+ columns: [table.org_id],
+ foreignColumns: [organizations.id],
+ name: "entities_org_id_fkey",
+ }).onDelete("cascade"),
- unique("entity_id_constraint").on(
- table.org_id,
- table.env,
- table.internal_customer_id,
- table.id,
- ),
- ],
+ unique("entity_id_constraint").on(
+ table.org_id,
+ table.env,
+ table.internal_customer_id,
+ table.id,
+ ),
+ ],
);
diff --git a/shared/models/cusModels/fullCusModel.ts b/shared/models/cusModels/fullCusModel.ts
index 794a80256..93aad59bc 100644
--- a/shared/models/cusModels/fullCusModel.ts
+++ b/shared/models/cusModels/fullCusModel.ts
@@ -6,15 +6,15 @@ import { Entity } from "./entityModels/entityModels.js";
import { Invoice } from "./invoiceModels/invoiceModels.js";
export type FullCustomer = Customer & {
- customer_products: FullCusProduct[];
- entities: Entity[];
- entity?: Entity;
- trials_used?: {
- product_id: string;
- customer_id: string;
- fingerprint: string;
- }[];
- invoices?: Invoice[];
- subscriptions?: Subscription[];
- events?: Event[];
+ customer_products: FullCusProduct[];
+ entities: Entity[];
+ entity?: Entity;
+ trials_used?: {
+ product_id: string;
+ customer_id: string;
+ fingerprint: string;
+ }[];
+ invoices?: Invoice[];
+ subscriptions?: Subscription[];
+ events?: Event[];
};
diff --git a/shared/models/cusModels/invoiceModels/invoiceItemModels.ts b/shared/models/cusModels/invoiceModels/invoiceItemModels.ts
index e336a10c6..7552b6f69 100644
--- a/shared/models/cusModels/invoiceModels/invoiceItemModels.ts
+++ b/shared/models/cusModels/invoiceModels/invoiceItemModels.ts
@@ -1,22 +1,22 @@
import { z } from "zod";
const InvoiceItemSchema = z.object({
- id: z.string(),
- created_at: z.number(),
- updated_at: z.number(),
+ id: z.string(),
+ created_at: z.number(),
+ updated_at: z.number(),
- // customer_id: z.string(), // just to show
- customer_id: z.string(),
- customer_price_id: z.string(),
- period_start: z.number(),
- period_end: z.number(),
- proration_start: z.number(),
- proration_end: z.number(),
+ // customer_id: z.string(), // just to show
+ customer_id: z.string(),
+ customer_price_id: z.string(),
+ period_start: z.number(),
+ period_end: z.number(),
+ proration_start: z.number(),
+ proration_end: z.number(),
- quantity: z.number(), // of feature
- amount: z.number(),
- currency: z.string(),
- added_to_stripe: z.boolean(),
+ quantity: z.number(), // of feature
+ amount: z.number(),
+ currency: z.string(),
+ added_to_stripe: z.boolean(),
});
export type InvoiceItem = z.infer;
diff --git a/shared/models/cusModels/invoiceModels/invoiceModels.ts b/shared/models/cusModels/invoiceModels/invoiceModels.ts
index 7162c62a4..88e9ec7a3 100644
--- a/shared/models/cusModels/invoiceModels/invoiceModels.ts
+++ b/shared/models/cusModels/invoiceModels/invoiceModels.ts
@@ -1,48 +1,48 @@
import { z } from "zod";
export enum InvoiceStatus {
- Draft = "draft",
- Open = "open",
- Void = "void",
- Paid = "paid",
- Uncollectible = "uncollectible",
+ Draft = "draft",
+ Open = "open",
+ Void = "void",
+ Paid = "paid",
+ Uncollectible = "uncollectible",
}
export const InvoiceDiscountSchema = z.object({
- stripe_coupon_id: z.string(), // Stripe ID
- coupon_name: z.string(),
- amount_off: z.number(),
- amount_used: z.number(),
+ stripe_coupon_id: z.string(), // Stripe ID
+ coupon_name: z.string(),
+ amount_off: z.number(),
+ amount_used: z.number(),
});
export const InvoiceItemSchema = z.object({
- price_id: z.string().nullish(),
- stripe_id: z.string(),
- internal_feature_id: z.string().nullable(),
- description: z.string(),
- period_start: z.number(),
- period_end: z.number(),
+ price_id: z.string().nullish(),
+ stripe_id: z.string(),
+ internal_feature_id: z.string().nullable(),
+ description: z.string(),
+ period_start: z.number(),
+ period_end: z.number(),
});
export const InvoiceSchema = z.object({
- id: z.string(),
- created_at: z.number(),
- internal_customer_id: z.string(),
- internal_entity_id: z.string().nullable(),
- product_ids: z.array(z.string()),
- internal_product_ids: z.array(z.string()),
+ id: z.string(),
+ created_at: z.number(),
+ internal_customer_id: z.string(),
+ internal_entity_id: z.string().nullable(),
+ product_ids: z.array(z.string()),
+ internal_product_ids: z.array(z.string()),
- // Stripe fields
- stripe_id: z.string(),
- status: z.nativeEnum(InvoiceStatus).nullable().optional(),
- hosted_invoice_url: z.string().nullable(),
+ // Stripe fields
+ stripe_id: z.string(),
+ status: z.nativeEnum(InvoiceStatus).nullable().optional(),
+ hosted_invoice_url: z.string().nullable(),
- // Total amount of the invoice
- total: z.number(),
- currency: z.string(),
- receipt_url: z.string().nullish(),
- discounts: z.array(InvoiceDiscountSchema),
- items: z.array(InvoiceItemSchema),
+ // Total amount of the invoice
+ total: z.number(),
+ currency: z.string(),
+ receipt_url: z.string().nullish(),
+ discounts: z.array(InvoiceDiscountSchema),
+ items: z.array(InvoiceItemSchema),
});
export type Invoice = z.infer;
diff --git a/shared/models/cusModels/invoiceModels/invoiceRelations.ts b/shared/models/cusModels/invoiceModels/invoiceRelations.ts
index 1e0ff309a..944d50fa5 100644
--- a/shared/models/cusModels/invoiceModels/invoiceRelations.ts
+++ b/shared/models/cusModels/invoiceModels/invoiceRelations.ts
@@ -3,8 +3,8 @@ import { invoices } from "./invoiceTable.js";
import { relations } from "drizzle-orm";
export const invoiceRelations = relations(invoices, ({ one }) => ({
- customer: one(customers, {
- fields: [invoices.internal_customer_id],
- references: [customers.internal_id],
- }),
+ customer: one(customers, {
+ fields: [invoices.internal_customer_id],
+ references: [customers.internal_id],
+ }),
}));
diff --git a/shared/models/cusModels/invoiceModels/invoiceResponseModels.ts b/shared/models/cusModels/invoiceModels/invoiceResponseModels.ts
index dc6cdaf12..cff72fa86 100644
--- a/shared/models/cusModels/invoiceModels/invoiceResponseModels.ts
+++ b/shared/models/cusModels/invoiceModels/invoiceResponseModels.ts
@@ -1,24 +1,24 @@
import { z } from "zod";
export const InvoiceItemResponseSchema = z.object({
- description: z.string(),
- period_start: z.number(),
- period_end: z.number(),
+ description: z.string(),
+ period_start: z.number(),
+ period_end: z.number(),
- feature_id: z.string().optional(),
- feature_name: z.string().optional(),
+ feature_id: z.string().optional(),
+ feature_name: z.string().optional(),
});
export const InvoiceResponseSchema = z.object({
- product_ids: z.array(z.string()),
- stripe_id: z.string(),
- status: z.string(),
- total: z.number(),
- currency: z.string(),
- created_at: z.number(),
- hosted_invoice_url: z.string().nullish(),
- // period_start: z.number().nullish(),
- // period_end: z.number().nullish(),
+ product_ids: z.array(z.string()),
+ stripe_id: z.string(),
+ status: z.string(),
+ total: z.number(),
+ currency: z.string(),
+ created_at: z.number(),
+ hosted_invoice_url: z.string().nullish(),
+ // period_start: z.number().nullish(),
+ // period_end: z.number().nullish(),
});
export const InvoiceResponseListSchema = z.array(InvoiceResponseSchema);
diff --git a/shared/models/cusModels/invoiceModels/invoiceTable.ts b/shared/models/cusModels/invoiceModels/invoiceTable.ts
index b0d2fede9..5b2eea154 100644
--- a/shared/models/cusModels/invoiceModels/invoiceTable.ts
+++ b/shared/models/cusModels/invoiceModels/invoiceTable.ts
@@ -6,39 +6,39 @@ import { entities } from "../entityModels/entityTable.js";
import { InferSelectModel, InferInsertModel } from "drizzle-orm";
export const invoices = pgTable(
- "invoices",
- {
- id: text("id").primaryKey(),
- created_at: numeric({ mode: "number" }).notNull().default(sqlNow),
- product_ids: text("product_ids").array().default([]),
- internal_product_ids: text("internal_product_ids").array().default([]),
+ "invoices",
+ {
+ id: text("id").primaryKey(),
+ created_at: numeric({ mode: "number" }).notNull().default(sqlNow),
+ product_ids: text("product_ids").array().default([]),
+ internal_product_ids: text("internal_product_ids").array().default([]),
- internal_customer_id: text("internal_customer_id").notNull(),
- internal_entity_id: text("internal_entity_id"),
+ internal_customer_id: text("internal_customer_id").notNull(),
+ internal_entity_id: text("internal_entity_id"),
- stripe_id: text("stripe_id").notNull(),
- status: text("status").notNull().default("draft"),
- hosted_invoice_url: text("hosted_invoice_url"),
- total: numeric({ mode: "number" }).notNull().default(0),
- currency: text("currency").notNull().default("usd"),
- discounts: jsonb("discounts").$type