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 ( -
- + 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:
-
    -
  1. First calls /entitled to check message allowance
  2. -
  3. If allowed, calls /events to record the message
  4. -
  5. Updates remaining message count
  6. -
-
-
+
+
+
How it works:
+
    +
  1. First calls /entitled to check message allowance
  2. +
  3. If allowed, calls /events to record the message
  4. +
  5. Updates remaining message count
  6. +
+
+
-
- -
-
- ); +
+ +
+
+ ); } 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 && ( - - )} -
- -
-
-
- ); +
+ Current Plan + + {hasPro ? ( + + Pro + + ) : ( + + Free + + )} + +
+
+
+
+ {!hasPro && ( + + )} +
+ +
+
+
+ ); } 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().array().default([]), - items: jsonb("items").$type().array().default([]), - }, - (table) => [ - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "invoices_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internal_entity_id], - foreignColumns: [entities.internal_id], - name: "invoices_internal_entity_id_fkey", - }).onDelete("cascade"), - ], + 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().array().default([]), + items: jsonb("items").$type().array().default([]), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "invoices_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_entity_id], + foreignColumns: [entities.internal_id], + name: "invoices_internal_entity_id_fkey", + }).onDelete("cascade"), + ], ); collatePgColumn(invoices.id, "C"); export type InvoiceRow = InferSelectModel; -export type InsertInvoiceRow = InferInsertModel; \ No newline at end of file +export type InsertInvoiceRow = InferInsertModel; diff --git a/shared/models/cusProductModels/cusEntModels/cusEntModels.ts b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts index 3b8665d30..0f41ec99c 100644 --- a/shared/models/cusProductModels/cusEntModels/cusEntModels.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts @@ -5,44 +5,44 @@ import { ReplaceableSchema } from "./replaceableSchema.js"; import { RolloverSchema } from "./rolloverModels/rolloverTable.js"; export const EntityBalanceSchema = z.object({ - id: z.string(), - balance: z.number(), - adjustment: z.number(), + id: z.string(), + balance: z.number(), + adjustment: z.number(), }); export const CustomerEntitlementSchema = z.object({ - // Foreign keys - id: z.string(), - internal_customer_id: z.string(), - internal_feature_id: z.string(), - customer_id: z.string().nullish(), // for debugging purposes - feature_id: z.string(), // for debugging purposes + // Foreign keys + id: z.string(), + internal_customer_id: z.string(), + internal_feature_id: z.string(), + customer_id: z.string().nullish(), // for debugging purposes + feature_id: z.string(), // for debugging purposes - customer_product_id: z.string(), - entitlement_id: z.string().nullable(), - created_at: z.number(), + customer_product_id: z.string(), + entitlement_id: z.string().nullable(), + created_at: z.number(), - // Balance fields - unlimited: z.boolean().nullish(), - balance: z.number().nullish().default(0), + // Balance fields + unlimited: z.boolean().nullish(), + balance: z.number().nullish().default(0), - usage_allowed: z.boolean().nullable(), - next_reset_at: z.number().nullable(), - adjustment: z.number().nullish().default(0), + usage_allowed: z.boolean().nullable(), + next_reset_at: z.number().nullable(), + adjustment: z.number().nullish().default(0), - // Group by fields - entities: z.record(z.string(), EntityBalanceSchema).nullish(), + // Group by fields + entities: z.record(z.string(), EntityBalanceSchema).nullish(), }); export const FullCustomerEntitlementSchema = CustomerEntitlementSchema.extend({ - entitlement: EntitlementWithFeatureSchema, - replaceables: z.array(ReplaceableSchema), - rollovers: z.array(RolloverSchema), + entitlement: EntitlementWithFeatureSchema, + replaceables: z.array(ReplaceableSchema), + rollovers: z.array(RolloverSchema), }); export type EntityBalance = z.infer; export type CustomerEntitlement = z.infer; export type FullCustomerEntitlement = z.infer< - typeof FullCustomerEntitlementSchema + typeof FullCustomerEntitlementSchema >; diff --git a/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts b/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts index 8453f23c9..369003470 100644 --- a/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts @@ -9,25 +9,25 @@ import { replaceables } from "./replaceableTable.js"; import { rollovers } from "./rolloverModels/rolloverTable.js"; export const customerEntitlementsRelations = relations( - customerEntitlements, - ({ one, many }) => ({ - customer_product: one(customerProducts, { - fields: [customerEntitlements.customer_product_id], - references: [customerProducts.id], - }), - customer: one(customers, { - fields: [customerEntitlements.internal_customer_id], - references: [customers.internal_id], - }), - entitlement: one(entitlements, { - fields: [customerEntitlements.entitlement_id], - references: [entitlements.id], - }), - feature: one(features, { - fields: [customerEntitlements.internal_feature_id], - references: [features.internal_id], - }), - replaceables: many(replaceables), - rollovers: many(rollovers), - }) + customerEntitlements, + ({ one, many }) => ({ + customer_product: one(customerProducts, { + fields: [customerEntitlements.customer_product_id], + references: [customerProducts.id], + }), + customer: one(customers, { + fields: [customerEntitlements.internal_customer_id], + references: [customers.internal_id], + }), + entitlement: one(entitlements, { + fields: [customerEntitlements.entitlement_id], + references: [entitlements.id], + }), + feature: one(features, { + fields: [customerEntitlements.internal_feature_id], + references: [features.internal_id], + }), + replaceables: many(replaceables), + rollovers: many(rollovers), + }), ); diff --git a/shared/models/cusProductModels/cusEntModels/cusEntTable.ts b/shared/models/cusProductModels/cusEntModels/cusEntTable.ts index df0a07ea9..5f483c535 100644 --- a/shared/models/cusProductModels/cusEntModels/cusEntTable.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntTable.ts @@ -1,12 +1,12 @@ import { - pgTable, - numeric, - boolean, - foreignKey, - unique, - text, - jsonb, - index, + pgTable, + numeric, + boolean, + foreignKey, + unique, + text, + jsonb, + index, } from "drizzle-orm/pg-core"; import { features } from "../../featureModels/featureTable.js"; @@ -17,48 +17,48 @@ import { customerProducts } from "../cusProductTable.js"; import { entitlements } from "../../../db/schema.js"; export const customerEntitlements = pgTable( - "customer_entitlements", - { - id: text().primaryKey().notNull(), - customer_product_id: text().notNull(), - entitlement_id: text().notNull(), - internal_customer_id: text().notNull(), - internal_feature_id: text().notNull(), + "customer_entitlements", + { + id: text().primaryKey().notNull(), + customer_product_id: text().notNull(), + entitlement_id: text().notNull(), + internal_customer_id: text().notNull(), + internal_feature_id: text().notNull(), - unlimited: boolean("unlimited").default(false), - balance: numeric({ mode: "number" }).notNull().default(0), - created_at: numeric({ mode: "number" }).notNull(), - next_reset_at: numeric({ mode: "number" }), - usage_allowed: boolean("usage_allowed").default(false), - adjustment: numeric({ mode: "number" }), - entities: jsonb("entities").$type>(), + unlimited: boolean("unlimited").default(false), + balance: numeric({ mode: "number" }).notNull().default(0), + created_at: numeric({ mode: "number" }).notNull(), + next_reset_at: numeric({ mode: "number" }), + usage_allowed: boolean("usage_allowed").default(false), + adjustment: numeric({ mode: "number" }), + entities: jsonb("entities").$type>(), - // Optional... - customer_id: text("customer_id"), - feature_id: text("feature_id"), - }, - (table) => [ - foreignKey({ - columns: [table.internal_feature_id], - foreignColumns: [features.internal_id], - name: "entitlements_internal_feature_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.customer_product_id], - foreignColumns: [customerProducts.id], - name: "customer_entitlements_customer_product_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - foreignKey({ - columns: [table.entitlement_id], - foreignColumns: [entitlements.id], - name: "customer_entitlements_entitlement_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - index("idx_customer_entitlements_product_id").on(table.customer_product_id), - ], + // Optional... + customer_id: text("customer_id"), + feature_id: text("feature_id"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_feature_id], + foreignColumns: [features.internal_id], + name: "entitlements_internal_feature_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.customer_product_id], + foreignColumns: [customerProducts.id], + name: "customer_entitlements_customer_product_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + foreignKey({ + columns: [table.entitlement_id], + foreignColumns: [entitlements.id], + name: "customer_entitlements_entitlement_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + index("idx_customer_entitlements_product_id").on(table.customer_product_id), + ], ); collatePgColumn(customerEntitlements.id, "C"); diff --git a/shared/models/cusProductModels/cusEntModels/cusEntWithProduct.ts b/shared/models/cusProductModels/cusEntModels/cusEntWithProduct.ts index 53b4620b4..6d4c548a8 100644 --- a/shared/models/cusProductModels/cusEntModels/cusEntWithProduct.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntWithProduct.ts @@ -3,17 +3,17 @@ import { FullCustomerEntitlementSchema } from "./cusEntModels.js"; import { z } from "zod"; export const FullCusEntWithProductSchema = FullCustomerEntitlementSchema.extend( - { - customer_product: CusProductSchema, - }, + { + customer_product: CusProductSchema, + }, ); export const FullCusEntWithFullCusProductSchema = - FullCustomerEntitlementSchema.extend({ - customer_product: FullCusProductSchema, - }); + FullCustomerEntitlementSchema.extend({ + customer_product: FullCusProductSchema, + }); export type FullCusEntWithProduct = z.infer; export type FullCusEntWithFullCusProduct = z.infer< - typeof FullCusEntWithFullCusProductSchema + typeof FullCusEntWithFullCusProductSchema >; diff --git a/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts b/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts index 6dc27e14a..f4052536d 100644 --- a/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts +++ b/shared/models/cusProductModels/cusEntModels/replaceableRelations.ts @@ -3,8 +3,8 @@ import { replaceables } from "./replaceableTable.js"; import { customerEntitlements } from "./cusEntTable.js"; export const replaceableRelations = relations(replaceables, ({ one }) => ({ - customer_entitlement: one(customerEntitlements, { - fields: [replaceables.cus_ent_id], - references: [customerEntitlements.id], - }), + customer_entitlement: one(customerEntitlements, { + fields: [replaceables.cus_ent_id], + references: [customerEntitlements.id], + }), })); diff --git a/shared/models/cusProductModels/cusEntModels/replaceableSchema.ts b/shared/models/cusProductModels/cusEntModels/replaceableSchema.ts index 9b220b65a..3f6a9c7be 100644 --- a/shared/models/cusProductModels/cusEntModels/replaceableSchema.ts +++ b/shared/models/cusProductModels/cusEntModels/replaceableSchema.ts @@ -2,19 +2,19 @@ import { z } from "zod"; import { EntitlementSchema } from "../../productModels/entModels/entModels.js"; export const ReplaceableSchema = z.object({ - id: z.string(), - cus_ent_id: z.string(), - created_at: z.number(), - from_entity_id: z.string().nullish(), - delete_next_cycle: z.boolean(), + id: z.string(), + cus_ent_id: z.string(), + created_at: z.number(), + from_entity_id: z.string().nullish(), + delete_next_cycle: z.boolean(), }); export const AttachReplaceableSchema = z.object({ - ent: EntitlementSchema, - id: z.string(), - created_at: z.number(), - from_entity_id: z.string().nullish(), - delete_next_cycle: z.boolean(), + ent: EntitlementSchema, + id: z.string(), + created_at: z.number(), + from_entity_id: z.string().nullish(), + delete_next_cycle: z.boolean(), }); // export type Replaceable = z.infer; diff --git a/shared/models/cusProductModels/cusEntModels/replaceableTable.ts b/shared/models/cusProductModels/cusEntModels/replaceableTable.ts index 08960105f..bbb52811c 100644 --- a/shared/models/cusProductModels/cusEntModels/replaceableTable.ts +++ b/shared/models/cusProductModels/cusEntModels/replaceableTable.ts @@ -1,10 +1,10 @@ import { - pgTable, - boolean, - foreignKey, - text, - bigint, - index, + pgTable, + boolean, + foreignKey, + text, + bigint, + index, } from "drizzle-orm/pg-core"; import { collatePgColumn } from "../../../db/utils.js"; @@ -12,22 +12,22 @@ import { customerEntitlements } from "./cusEntTable.js"; import { createInsertSchema } from "drizzle-zod"; export const replaceables = pgTable( - "replaceables", - { - id: text().primaryKey().notNull(), - cus_ent_id: text().notNull(), - created_at: bigint({ mode: "number" }).notNull(), - from_entity_id: text(), - delete_next_cycle: boolean("delete_next_cycle").notNull().default(false), - }, - (table) => [ - foreignKey({ - columns: [table.cus_ent_id], - foreignColumns: [customerEntitlements.id], - name: "replaceables_cus_ent_id_fkey", - }).onDelete("cascade"), - index("idx_replaceables_cus_ent_id").on(table.cus_ent_id), - ], + "replaceables", + { + id: text().primaryKey().notNull(), + cus_ent_id: text().notNull(), + created_at: bigint({ mode: "number" }).notNull(), + from_entity_id: text(), + delete_next_cycle: boolean("delete_next_cycle").notNull().default(false), + }, + (table) => [ + foreignKey({ + columns: [table.cus_ent_id], + foreignColumns: [customerEntitlements.id], + name: "replaceables_cus_ent_id_fkey", + }).onDelete("cascade"), + index("idx_replaceables_cus_ent_id").on(table.cus_ent_id), + ], ).enableRLS(); collatePgColumn(replaceables.id, "C"); diff --git a/shared/models/cusProductModels/cusEntModels/resetCusEnt.ts b/shared/models/cusProductModels/cusEntModels/resetCusEnt.ts index b717551ca..61f52e9cb 100644 --- a/shared/models/cusProductModels/cusEntModels/resetCusEnt.ts +++ b/shared/models/cusProductModels/cusEntModels/resetCusEnt.ts @@ -1,11 +1,11 @@ import { Customer } from "../../cusModels/cusModels.js"; import { CusProduct } from "../cusProductModels.js"; import { - CustomerEntitlement, - FullCustomerEntitlement, + CustomerEntitlement, + FullCustomerEntitlement, } from "./cusEntModels.js"; export type ResetCusEnt = FullCustomerEntitlement & { - customer: Customer; - customer_product: CusProduct; + customer: Customer; + customer_product: CusProduct; }; diff --git a/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverRelations.ts b/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverRelations.ts index 35cf54c76..861c1b949 100644 --- a/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverRelations.ts +++ b/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverRelations.ts @@ -3,8 +3,8 @@ import { rollovers } from "./rolloverTable.js"; import { customerEntitlements } from "../cusEntTable.js"; export const rolloverRelations = relations(rollovers, ({ one }) => ({ - customer_entitlement: one(customerEntitlements, { - fields: [rollovers.cus_ent_id], - references: [customerEntitlements.id], - }), + customer_entitlement: one(customerEntitlements, { + fields: [rollovers.cus_ent_id], + references: [customerEntitlements.id], + }), })); diff --git a/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.ts b/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.ts index 3da143aba..2893fa9b1 100644 --- a/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.ts +++ b/shared/models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.ts @@ -1,53 +1,53 @@ import { z } from "zod"; import { customerEntitlements } from "../cusEntTable.js"; import { - foreignKey, - pgTable, - numeric, - jsonb, - text, - index, + foreignKey, + pgTable, + numeric, + jsonb, + text, + index, } from "drizzle-orm/pg-core"; export const EntityRolloverBalanceSchema = z.object({ - id: z.string(), - balance: z.number(), - usage: z.number(), + id: z.string(), + balance: z.number(), + usage: z.number(), }); export const RolloverSchema = z.object({ - id: z.string(), - cus_ent_id: z.string(), - balance: z.number(), - usage: z.number().default(0), - expires_at: z.number().nullable(), - entities: z.record(z.string(), EntityRolloverBalanceSchema), + id: z.string(), + cus_ent_id: z.string(), + balance: z.number(), + usage: z.number().default(0), + expires_at: z.number().nullable(), + entities: z.record(z.string(), EntityRolloverBalanceSchema), }); export const rollovers = pgTable( - "rollovers", - { - id: text("id").primaryKey().notNull(), - cus_ent_id: text("cus_ent_id").notNull(), - balance: numeric({ mode: "number" }).notNull(), - expires_at: numeric({ mode: "number" }), - usage: numeric({ mode: "number" }).default(0).notNull(), - entities: jsonb("entities") - .$type>() - .notNull() - .default({}), - }, - (table) => [ - foreignKey({ - columns: [table.cus_ent_id], - foreignColumns: [customerEntitlements.id], - name: "rollover_cus_ent_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), + "rollovers", + { + id: text("id").primaryKey().notNull(), + cus_ent_id: text("cus_ent_id").notNull(), + balance: numeric({ mode: "number" }).notNull(), + expires_at: numeric({ mode: "number" }), + usage: numeric({ mode: "number" }).default(0).notNull(), + entities: jsonb("entities") + .$type>() + .notNull() + .default({}), + }, + (table) => [ + foreignKey({ + columns: [table.cus_ent_id], + foreignColumns: [customerEntitlements.id], + name: "rollover_cus_ent_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), - index("idx_rollovers_cus_ent_id").on(table.cus_ent_id), - ] + index("idx_rollovers_cus_ent_id").on(table.cus_ent_id), + ], ).enableRLS(); export type Rollover = z.infer; diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts index d71b66de5..700b140b5 100644 --- a/shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts @@ -2,16 +2,16 @@ import { z } from "zod"; import { PriceSchema } from "../../productModels/priceModels/priceModels.js"; export const CustomerPriceSchema = z.object({ - id: z.string(), - internal_customer_id: z.string(), - customer_product_id: z.string(), - created_at: z.number(), + id: z.string(), + internal_customer_id: z.string(), + customer_product_id: z.string(), + created_at: z.number(), - price_id: z.string().nullable(), + price_id: z.string().nullable(), }); export const FullCustomerPriceSchema = CustomerPriceSchema.extend({ - price: PriceSchema, + price: PriceSchema, }); export type CustomerPrice = z.infer; diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts index b77463a6b..51257521b 100644 --- a/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts @@ -6,19 +6,19 @@ import { customerPrices } from "./cusPriceTable.js"; import { customerProducts } from "../cusProductTable.js"; export const customerPricesRelations = relations( - customerPrices, - ({ one, many }) => ({ - customerProduct: one(customerProducts, { - fields: [customerPrices.customer_product_id], - references: [customerProducts.id], - }), - customer: one(customers, { - fields: [customerPrices.internal_customer_id], - references: [customers.internal_id], - }), - price: one(prices, { - fields: [customerPrices.price_id], - references: [prices.id], - }), - }), + customerPrices, + ({ one, many }) => ({ + customerProduct: one(customerProducts, { + fields: [customerPrices.customer_product_id], + references: [customerProducts.id], + }), + customer: one(customers, { + fields: [customerPrices.internal_customer_id], + references: [customers.internal_id], + }), + price: one(prices, { + fields: [customerPrices.price_id], + references: [prices.id], + }), + }), ); diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts index 55c7fb056..47f49dafb 100644 --- a/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts @@ -6,31 +6,31 @@ import { prices } from "../../productModels/priceModels/priceTable.js"; import { customerProducts } from "../cusProductTable.js"; export const customerPrices = pgTable( - "customer_prices", - { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }).notNull(), - price_id: text("price_id"), - options: jsonb(), - internal_customer_id: text("internal_customer_id"), - customer_product_id: text("customer_product_id"), - }, - (table) => [ - foreignKey({ - columns: [table.customer_product_id], - foreignColumns: [customerProducts.id], - name: "customer_prices_customer_product_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "customer_prices_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.price_id], - foreignColumns: [prices.id], - name: "customer_prices_price_id_fkey", - }), - index("idx_customer_prices_product_id").on(table.customer_product_id), - ], + "customer_prices", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull(), + price_id: text("price_id"), + options: jsonb(), + internal_customer_id: text("internal_customer_id"), + customer_product_id: text("customer_product_id"), + }, + (table) => [ + foreignKey({ + columns: [table.customer_product_id], + foreignColumns: [customerProducts.id], + name: "customer_prices_customer_product_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "customer_prices_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.price_id], + foreignColumns: [prices.id], + name: "customer_prices_price_id_fkey", + }), + index("idx_customer_prices_product_id").on(table.customer_product_id), + ], ); diff --git a/shared/models/cusProductModels/cusProductEnums.ts b/shared/models/cusProductModels/cusProductEnums.ts index e83f7aabd..b021365fd 100644 --- a/shared/models/cusProductModels/cusProductEnums.ts +++ b/shared/models/cusProductModels/cusProductEnums.ts @@ -1,13 +1,13 @@ export enum CollectionMethod { - ChargeAutomatically = "charge_automatically", - SendInvoice = "send_invoice", + ChargeAutomatically = "charge_automatically", + SendInvoice = "send_invoice", } export enum CusProductStatus { - Scheduled = "scheduled", - Active = "active", - PastDue = "past_due", - Expired = "expired", - Unknown = "unknown", - Trialing = "trialing", + Scheduled = "scheduled", + Active = "active", + PastDue = "past_due", + Expired = "expired", + Unknown = "unknown", + Trialing = "trialing", } diff --git a/shared/models/cusProductModels/cusProductModels.ts b/shared/models/cusProductModels/cusProductModels.ts index 3267d7794..d5264f713 100644 --- a/shared/models/cusProductModels/cusProductModels.ts +++ b/shared/models/cusProductModels/cusProductModels.ts @@ -3,84 +3,84 @@ import { ProcessorType } from "../genModels/genEnums.js"; import { ProductSchema } from "../productModels/productModels.js"; import { PriceSchema } from "../productModels/priceModels/priceModels.js"; import { - CustomerEntitlementSchema, - FullCustomerEntitlementSchema, + CustomerEntitlementSchema, + FullCustomerEntitlementSchema, } from "./cusEntModels/cusEntModels.js"; import { EntitlementSchema } from "../productModels/entModels/entModels.js"; import { FeatureSchema } from "../featureModels/featureModels.js"; import { CustomerSchema } from "../cusModels/cusModels.js"; import { FreeTrialSchema } from "../productModels/freeTrialModels/freeTrialModels.js"; import { - CustomerPriceSchema, - FullCustomerPriceSchema, + CustomerPriceSchema, + FullCustomerPriceSchema, } from "./cusPriceModels/cusPriceModels.js"; import { CollectionMethod } from "./cusProductEnums.js"; import { CusProductStatus } from "./cusProductEnums.js"; export const FeatureOptionsSchema = z.object({ - feature_id: z.string(), - quantity: z.number(), // same as prepaid - upcoming_quantity: z.number().nullish(), + feature_id: z.string(), + quantity: z.number(), // same as prepaid + upcoming_quantity: z.number().nullish(), - adjustable_quantity: z.boolean().nullish(), - internal_feature_id: z.string().nullish(), + adjustable_quantity: z.boolean().nullish(), + internal_feature_id: z.string().nullish(), }); export const BillingCycleAnchorConfig = z.object({ - month: z.number(), - day: z.number(), - hour: z.number(), - minute: z.number(), - second: z.number(), + month: z.number(), + day: z.number(), + hour: z.number(), + minute: z.number(), + second: z.number(), }); export const CusProductSchema = z.object({ - id: z.string(), - internal_product_id: z.string(), - product_id: z.string(), - internal_customer_id: z.string(), - customer_id: z.string().nullish(), - internal_entity_id: z.string().nullish(), - entity_id: z.string().nullish(), - created_at: z.number(), + id: z.string(), + internal_product_id: z.string(), + product_id: z.string(), + internal_customer_id: z.string(), + customer_id: z.string().nullish(), + internal_entity_id: z.string().nullish(), + entity_id: z.string().nullish(), + created_at: z.number(), - // Useful for event-driven subscriptions (and usage-based to check limits) - status: z.nativeEnum(CusProductStatus), - canceled: z.boolean().default(false), + // Useful for event-driven subscriptions (and usage-based to check limits) + status: z.nativeEnum(CusProductStatus), + canceled: z.boolean().default(false), - starts_at: z.number().default(Date.now()), - trial_ends_at: z.number().optional().nullable(), - canceled_at: z.number().optional().nullable(), - ended_at: z.number().optional().nullable(), + starts_at: z.number().default(Date.now()), + trial_ends_at: z.number().optional().nullable(), + canceled_at: z.number().optional().nullable(), + ended_at: z.number().optional().nullable(), - options: z.array(FeatureOptionsSchema), - free_trial_id: z.string().optional().nullable(), - collection_method: z.nativeEnum(CollectionMethod), + options: z.array(FeatureOptionsSchema), + free_trial_id: z.string().optional().nullable(), + collection_method: z.nativeEnum(CollectionMethod), - // Fixed-cycle configuration - subscription_ids: z.array(z.string()).nullish(), - scheduled_ids: z.array(z.string()).nullish(), - processor: z - .object({ - type: z.nativeEnum(ProcessorType), - subscription_id: z.string().optional().nullable(), - subscription_schedule_id: z.string().optional().nullable(), - last_invoice_id: z.string().optional().nullable(), - }) - .optional(), + // Fixed-cycle configuration + subscription_ids: z.array(z.string()).nullish(), + scheduled_ids: z.array(z.string()).nullish(), + processor: z + .object({ + type: z.nativeEnum(ProcessorType), + subscription_id: z.string().optional().nullable(), + subscription_schedule_id: z.string().optional().nullable(), + last_invoice_id: z.string().optional().nullable(), + }) + .optional(), - quantity: z.number().default(1), - api_version: z.number().nullish(), + quantity: z.number().default(1), + api_version: z.number().nullish(), }); export const FullCusProductSchema = CusProductSchema.extend({ - customer_prices: z.array(FullCustomerPriceSchema), - customer_entitlements: z.array(FullCustomerEntitlementSchema), + customer_prices: z.array(FullCustomerPriceSchema), + customer_entitlements: z.array(FullCustomerEntitlementSchema), - customer: CustomerSchema.optional(), - product: ProductSchema, - free_trial: FreeTrialSchema.nullish(), - is_custom: z.boolean().default(false), + customer: CustomerSchema.optional(), + product: ProductSchema, + free_trial: FreeTrialSchema.nullish(), + is_custom: z.boolean().default(false), }); export type CusProduct = z.infer; diff --git a/shared/models/cusProductModels/cusProductRelations.ts b/shared/models/cusProductModels/cusProductRelations.ts index f6a83ca04..a924679d0 100644 --- a/shared/models/cusProductModels/cusProductRelations.ts +++ b/shared/models/cusProductModels/cusProductRelations.ts @@ -7,21 +7,21 @@ import { customerEntitlements } from "./cusEntModels/cusEntTable.js"; import { customerPrices } from "./cusPriceModels/cusPriceTable.js"; export const customerProductsRelations = relations( - customerProducts, - ({ one, many }) => ({ - customer: one(customers, { - fields: [customerProducts.internal_customer_id], - references: [customers.internal_id], - }), - product: one(products, { - fields: [customerProducts.internal_product_id], - references: [products.internal_id], - }), - free_trial: one(freeTrials, { - fields: [customerProducts.free_trial_id], - references: [freeTrials.id], - }), - customer_entitlements: many(customerEntitlements), - customer_prices: many(customerPrices), - }), + customerProducts, + ({ one, many }) => ({ + customer: one(customers, { + fields: [customerProducts.internal_customer_id], + references: [customers.internal_id], + }), + product: one(products, { + fields: [customerProducts.internal_product_id], + references: [products.internal_id], + }), + free_trial: one(freeTrials, { + fields: [customerProducts.free_trial_id], + references: [freeTrials.id], + }), + customer_entitlements: many(customerEntitlements), + customer_prices: many(customerPrices), + }), ); diff --git a/shared/models/cusProductModels/cusProductTable.ts b/shared/models/cusProductModels/cusProductTable.ts index 682e696fd..6e8357bfb 100644 --- a/shared/models/cusProductModels/cusProductTable.ts +++ b/shared/models/cusProductModels/cusProductTable.ts @@ -1,10 +1,10 @@ import { - pgTable, - text, - numeric, - jsonb, - boolean, - foreignKey, + pgTable, + text, + numeric, + jsonb, + boolean, + foreignKey, } from "drizzle-orm/pg-core"; import { freeTrials } from "../productModels/freeTrialModels/freeTrialTable.js"; import { customers } from "../cusModels/cusTable.js"; @@ -14,67 +14,67 @@ import { entities } from "../cusModels/entityModels/entityTable.js"; import { InferSelectModel, InferInsertModel } from "drizzle-orm"; export type CustomerProductProcessor = { - type: "stripe"; - id: string; + type: "stripe"; + id: string; }; export const customerProducts = pgTable( - "customer_products", - { - id: text().primaryKey().notNull(), - internal_customer_id: text("internal_customer_id").notNull(), - internal_product_id: text("internal_product_id").notNull(), - internal_entity_id: text("internal_entity_id"), + "customer_products", + { + id: text().primaryKey().notNull(), + internal_customer_id: text("internal_customer_id").notNull(), + internal_product_id: text("internal_product_id").notNull(), + internal_entity_id: text("internal_entity_id"), - created_at: numeric({ mode: "number" }), - status: text(), - processor: jsonb().$type(), - canceled: boolean("canceled").default(false), - canceled_at: numeric({ mode: "number" }), - ended_at: numeric({ mode: "number" }), - starts_at: numeric({ mode: "number" }), - options: jsonb().array(), - product_id: text("product_id"), - free_trial_id: text("free_trial_id"), - trial_ends_at: numeric({ mode: "number" }), - collection_method: text("collection_method").default( - "charge_automatically" - ), - subscription_ids: text("subscription_ids").array(), - scheduled_ids: text("scheduled_ids").array(), - quantity: numeric({ mode: "number" }).default(1), + created_at: numeric({ mode: "number" }), + status: text(), + processor: jsonb().$type(), + canceled: boolean("canceled").default(false), + canceled_at: numeric({ mode: "number" }), + ended_at: numeric({ mode: "number" }), + starts_at: numeric({ mode: "number" }), + options: jsonb().array(), + product_id: text("product_id"), + free_trial_id: text("free_trial_id"), + trial_ends_at: numeric({ mode: "number" }), + collection_method: text("collection_method").default( + "charge_automatically", + ), + subscription_ids: text("subscription_ids").array(), + scheduled_ids: text("scheduled_ids").array(), + quantity: numeric({ mode: "number" }).default(1), - is_custom: boolean("is_custom").default(false).notNull(), + is_custom: boolean("is_custom").default(false).notNull(), - // Optional... - customer_id: text("customer_id"), - entity_id: text("entity_id"), - api_version: numeric({ mode: "number" }), - }, - (table) => [ - foreignKey({ - columns: [table.free_trial_id], - foreignColumns: [freeTrials.id], - name: "customer_products_free_trial_id_fkey", - }), - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "customer_products_internal_customer_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - foreignKey({ - columns: [table.internal_product_id], - foreignColumns: [products.internal_id], - name: "customer_products_internal_product_id_fkey", - }), - foreignKey({ - columns: [table.internal_entity_id], - foreignColumns: [entities.internal_id], - name: "customer_products_internal_entity_id_fkey", - }).onDelete("set null"), - ] + // Optional... + customer_id: text("customer_id"), + entity_id: text("entity_id"), + api_version: numeric({ mode: "number" }), + }, + (table) => [ + foreignKey({ + columns: [table.free_trial_id], + foreignColumns: [freeTrials.id], + name: "customer_products_free_trial_id_fkey", + }), + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "customer_products_internal_customer_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + foreignKey({ + columns: [table.internal_product_id], + foreignColumns: [products.internal_id], + name: "customer_products_internal_product_id_fkey", + }), + foreignKey({ + columns: [table.internal_entity_id], + foreignColumns: [entities.internal_id], + name: "customer_products_internal_entity_id_fkey", + }).onDelete("set null"), + ], ); collatePgColumn(customerProducts.id, "C"); diff --git a/shared/models/devModels/apiKeyModels.ts b/shared/models/devModels/apiKeyModels.ts index 32fc8fbe7..103ceb5d1 100644 --- a/shared/models/devModels/apiKeyModels.ts +++ b/shared/models/devModels/apiKeyModels.ts @@ -1,13 +1,13 @@ import { AppEnv } from "../genModels/genEnums.js"; export type ApiKey = { - id: string; - org_id: string; - user_id: string | null; - name: string; - prefix: string; - created_at: number; - env: AppEnv; - hashed_key: string; - meta: any; + id: string; + org_id: string; + user_id: string | null; + name: string; + prefix: string; + created_at: number; + env: AppEnv; + hashed_key: string; + meta: any; }; diff --git a/shared/models/devModels/apiKeyRelations.ts b/shared/models/devModels/apiKeyRelations.ts index 61e13ac4d..e94994162 100644 --- a/shared/models/devModels/apiKeyRelations.ts +++ b/shared/models/devModels/apiKeyRelations.ts @@ -3,8 +3,8 @@ import { organizations } from "../orgModels/orgTable.js"; import { apiKeys } from "./apiKeyTable.js"; export const apiKeyRelations = relations(apiKeys, ({ one }) => ({ - org: one(organizations, { - fields: [apiKeys.org_id], - references: [organizations.id], - }), + org: one(organizations, { + fields: [apiKeys.org_id], + references: [organizations.id], + }), })); diff --git a/shared/models/devModels/apiKeyTable.ts b/shared/models/devModels/apiKeyTable.ts index 890eafd41..8466afeeb 100644 --- a/shared/models/devModels/apiKeyTable.ts +++ b/shared/models/devModels/apiKeyTable.ts @@ -1,37 +1,37 @@ import { - pgTable, - text, - numeric, - jsonb, - foreignKey, - unique, - index, + pgTable, + text, + numeric, + jsonb, + foreignKey, + unique, + index, } from "drizzle-orm/pg-core"; import { organizations } from "../orgModels/orgTable.js"; import { collatePgColumn, sqlNow } from "../../db/utils.js"; export const apiKeys = pgTable( - "api_keys", - { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }).notNull().default(sqlNow), - name: text(), - prefix: text(), - org_id: text("org_id"), - user_id: text("user_id"), - env: text(), - hashed_key: text("hashed_key"), - meta: jsonb(), - }, - (table) => [ - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "api_keys_org_id_fkey", - }).onDelete("cascade"), - unique("api_keys_hashed_key_key").on(table.hashed_key), - // index("api_keys_hashed_key_key").on(table.hashed_key), - ], + "api_keys", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull().default(sqlNow), + name: text(), + prefix: text(), + org_id: text("org_id"), + user_id: text("user_id"), + env: text(), + hashed_key: text("hashed_key"), + meta: jsonb(), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "api_keys_org_id_fkey", + }).onDelete("cascade"), + unique("api_keys_hashed_key_key").on(table.hashed_key), + // index("api_keys_hashed_key_key").on(table.hashed_key), + ], ); collatePgColumn(apiKeys.id, "C"); diff --git a/shared/models/eventModels/eventModels.ts b/shared/models/eventModels/eventModels.ts index ea0302859..360f8dd22 100644 --- a/shared/models/eventModels/eventModels.ts +++ b/shared/models/eventModels/eventModels.ts @@ -19,14 +19,14 @@ import { z } from "zod"; // }); export const CreateEventSchema = z.object({ - customer_id: z.string().nonempty(), - event_name: z.string().nonempty(), - properties: z.record(z.string(), z.any()).nullish(), - timestamp: z.number().nullish(), - idempotency_key: z.string().nullish(), - value: z.number().nullish(), - set_usage: z.boolean().nullish(), - entity_id: z.string().nullish(), + customer_id: z.string().nonempty(), + event_name: z.string().nonempty(), + properties: z.record(z.string(), z.any()).nullish(), + timestamp: z.number().nullish(), + idempotency_key: z.string().nullish(), + value: z.number().nullish(), + set_usage: z.boolean().nullish(), + entity_id: z.string().nullish(), }); // export type Event = z.infer; diff --git a/shared/models/eventModels/eventTable.ts b/shared/models/eventModels/eventTable.ts index 0626e2cec..4c25a6d2b 100644 --- a/shared/models/eventModels/eventTable.ts +++ b/shared/models/eventModels/eventTable.ts @@ -1,54 +1,54 @@ import { sql } from "drizzle-orm"; import { - pgTable, - text, - numeric, - boolean, - jsonb, - foreignKey, - unique, - bigint, - timestamp, + pgTable, + text, + numeric, + boolean, + jsonb, + foreignKey, + unique, + bigint, + timestamp, } from "drizzle-orm/pg-core"; import { sqlNow } from "../../db/utils.js"; import { customers } from "../cusModels/cusTable.js"; export const events = pgTable( - "events", - { - id: text().primaryKey().notNull(), - org_id: text("org_id").notNull(), - org_slug: text("org_slug").notNull(), - internal_customer_id: text("internal_customer_id"), - env: text().notNull(), - created_at: bigint({ mode: "number" }), - timestamp: timestamp({ mode: "date", withTimezone: true }), + "events", + { + id: text().primaryKey().notNull(), + org_id: text("org_id").notNull(), + org_slug: text("org_slug").notNull(), + internal_customer_id: text("internal_customer_id"), + env: text().notNull(), + created_at: bigint({ mode: "number" }), + timestamp: timestamp({ mode: "date", withTimezone: true }), - event_name: text("event_name").notNull(), - idempotency_key: text("idempotency_key").default(sql`null`), - value: numeric({ mode: "number" }), - set_usage: boolean("set_usage").default(false), - entity_id: text("entity_id"), - internal_entity_id: text("internal_entity_id"), + event_name: text("event_name").notNull(), + idempotency_key: text("idempotency_key").default(sql`null`), + value: numeric({ mode: "number" }), + set_usage: boolean("set_usage").default(false), + entity_id: text("entity_id"), + internal_entity_id: text("internal_entity_id"), - // Optional stuff... - customer_id: text("customer_id").notNull(), - properties: jsonb().$type>(), - }, - (table) => [ - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "events_internal_customer_id_fkey", - }).onDelete("cascade"), - unique("unique_event_constraint").on( - table.org_id, - table.env, - table.customer_id, - table.event_name, - table.idempotency_key - ), - ] + // Optional stuff... + customer_id: text("customer_id").notNull(), + properties: jsonb().$type>(), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "events_internal_customer_id_fkey", + }).onDelete("cascade"), + unique("unique_event_constraint").on( + table.org_id, + table.env, + table.customer_id, + table.event_name, + table.idempotency_key, + ), + ], ); export type Event = typeof events.$inferSelect; diff --git a/shared/models/featureModels/featureConfig/creditConfig.ts b/shared/models/featureModels/featureConfig/creditConfig.ts index 3d5be4aaa..5dfc6a195 100644 --- a/shared/models/featureModels/featureConfig/creditConfig.ts +++ b/shared/models/featureModels/featureConfig/creditConfig.ts @@ -2,20 +2,20 @@ import { FeatureUsageType } from "../featureEnums.js"; import { z } from "zod"; export const CreditSchemaItemSchema = z.object({ - metered_feature_id: z.string(), - feature_amount: z.number(), - credit_amount: z.number(), + metered_feature_id: z.string(), + feature_amount: z.number(), + credit_amount: z.number(), }); export const CreditSystemConfigSchema = z.object({ - schema: z.array( - z.object({ - metered_feature_id: z.string(), - feature_amount: z.number(), - credit_amount: z.number(), - }), - ), - usage_type: z.nativeEnum(FeatureUsageType), + schema: z.array( + z.object({ + metered_feature_id: z.string(), + feature_amount: z.number(), + credit_amount: z.number(), + }), + ), + usage_type: z.nativeEnum(FeatureUsageType), }); export type CreditSystemConfig = z.infer; diff --git a/shared/models/featureModels/featureConfig/meteredConfig.ts b/shared/models/featureModels/featureConfig/meteredConfig.ts index a45cca7a5..3921f3af9 100644 --- a/shared/models/featureModels/featureConfig/meteredConfig.ts +++ b/shared/models/featureModels/featureConfig/meteredConfig.ts @@ -2,31 +2,31 @@ import { z } from "zod"; import { FeatureUsageType } from "../featureEnums.js"; export interface Expression { - property: string; - operator: string; - value: string[]; + property: string; + operator: string; + value: string[]; } export interface Aggregate { - type: string; - property: string | null; + type: string; + property: string | null; } export const ExpressionSchema = z.object({ - property: z.string(), - operator: z.string(), - value: z.array(z.string()), + property: z.string(), + operator: z.string(), + value: z.array(z.string()), }); export const AggregateSchema = z.object({ - type: z.string(), - property: z.string().nullable(), + type: z.string(), + property: z.string().nullable(), }); export const MeteredConfigSchema = z.object({ - filters: z.array(ExpressionSchema), - aggregate: AggregateSchema, - usage_type: z.enum([FeatureUsageType.Single, FeatureUsageType.Continuous]), + filters: z.array(ExpressionSchema), + aggregate: AggregateSchema, + usage_type: z.enum([FeatureUsageType.Single, FeatureUsageType.Continuous]), }); export type MeteredConfig = z.infer; diff --git a/shared/models/featureModels/featureEnums.ts b/shared/models/featureModels/featureEnums.ts index d9e5f8250..3e6cba479 100644 --- a/shared/models/featureModels/featureEnums.ts +++ b/shared/models/featureModels/featureEnums.ts @@ -1,15 +1,15 @@ export enum FeatureType { - Boolean = "boolean", - Metered = "metered", - CreditSystem = "credit_system", + Boolean = "boolean", + Metered = "metered", + CreditSystem = "credit_system", } export enum AggregateType { - Count = "count", - Sum = "sum", + Count = "count", + Sum = "sum", } export enum FeatureUsageType { - Single = "single_use", - Continuous = "continuous_use", + Single = "single_use", + Continuous = "continuous_use", } diff --git a/shared/models/featureModels/featureModels.ts b/shared/models/featureModels/featureModels.ts index 5d3a14c68..8c615090e 100644 --- a/shared/models/featureModels/featureModels.ts +++ b/shared/models/featureModels/featureModels.ts @@ -3,38 +3,38 @@ import { AppEnv } from "../genModels/genEnums.js"; import { FeatureType } from "./featureEnums.js"; export const FeatureSchema = z.object({ - internal_id: z.string(), - org_id: z.string(), - created_at: z.number(), - env: z.nativeEnum(AppEnv), + internal_id: z.string(), + org_id: z.string(), + created_at: z.number(), + env: z.nativeEnum(AppEnv), - id: z.string().nonempty(), - name: z.string().nonempty(), - type: z.nativeEnum(FeatureType), - config: z.any(), - display: z - .object({ - singular: z.string().optional(), - plural: z.string().optional(), - }) - .nullish(), - archived: z.boolean(), + id: z.string().nonempty(), + name: z.string().nonempty(), + type: z.nativeEnum(FeatureType), + config: z.any(), + display: z + .object({ + singular: z.string().optional(), + plural: z.string().optional(), + }) + .nullish(), + archived: z.boolean(), }); export const CreateFeatureSchema = FeatureSchema.pick({ - id: true, - name: true, - type: true, - config: true, - display: true, + id: true, + name: true, + type: true, + config: true, + display: true, }); export const MinFeatureSchema = z.object({ - internal_id: z.string(), - id: z.string(), - name: z.string(), - type: z.nativeEnum(FeatureType), - config: z.any(), + internal_id: z.string(), + id: z.string(), + name: z.string(), + type: z.nativeEnum(FeatureType), + config: z.any(), }); export type Feature = z.infer; diff --git a/shared/models/featureModels/featureRelations.ts b/shared/models/featureModels/featureRelations.ts index 883219ed6..16f741530 100644 --- a/shared/models/featureModels/featureRelations.ts +++ b/shared/models/featureModels/featureRelations.ts @@ -3,8 +3,8 @@ import { organizations } from "../orgModels/orgTable.js"; import { features } from "./featureTable.js"; export const featureRelations = relations(features, ({ one }) => ({ - org: one(organizations, { - fields: [features.org_id], - references: [organizations.id], - }), + org: one(organizations, { + fields: [features.org_id], + references: [organizations.id], + }), })); diff --git a/shared/models/featureModels/featureResModels.ts b/shared/models/featureModels/featureResModels.ts index f4a745314..86d6ec9bf 100644 --- a/shared/models/featureModels/featureResModels.ts +++ b/shared/models/featureModels/featureResModels.ts @@ -1,35 +1,35 @@ import { z } from "zod"; export enum APIFeatureType { - Boolean = "boolean", - SingleUsage = "single_use", - ContinuousUse = "continuous_use", - CreditSystem = "credit_system", + Boolean = "boolean", + SingleUsage = "single_use", + ContinuousUse = "continuous_use", + CreditSystem = "credit_system", } export const APIFeatureSchema = z.object({ - id: z.string(), - name: z.string().nullish(), - type: z.nativeEnum(APIFeatureType), - display: z - .object({ - singular: z.string(), - plural: z.string(), - }) - .nullish(), + id: z.string(), + name: z.string().nullish(), + type: z.nativeEnum(APIFeatureType), + display: z + .object({ + singular: z.string(), + plural: z.string(), + }) + .nullish(), - credit_schema: z - .array( - z.object({ - metered_feature_id: z.string(), - credit_cost: z.number(), - }) - ) - .nullish(), - archived: z.boolean().nullish(), + credit_schema: z + .array( + z.object({ + metered_feature_id: z.string(), + credit_cost: z.number(), + }), + ) + .nullish(), + archived: z.boolean().nullish(), }); export const UpdateAPIFeatureSchema = APIFeatureSchema.extend({ - archived: z.boolean().optional(), + archived: z.boolean().optional(), }).partial(); export type APIFeature = z.infer; diff --git a/shared/models/featureModels/featureTable.ts b/shared/models/featureModels/featureTable.ts index 56bbee35e..06d0677fb 100644 --- a/shared/models/featureModels/featureTable.ts +++ b/shared/models/featureModels/featureTable.ts @@ -1,11 +1,11 @@ import { - foreignKey, - jsonb, - numeric, - pgTable, - text, - unique, - boolean, + foreignKey, + jsonb, + numeric, + pgTable, + text, + unique, + boolean, } from "drizzle-orm/pg-core"; import { relations, sql } from "drizzle-orm"; @@ -15,35 +15,33 @@ import { CreditSystemConfig } from "./featureConfig/creditConfig.js"; import { collatePgColumn } from "../../db/utils.js"; type FeatureDisplay = { - singular: string; - plural: string; + singular: string; + plural: string; }; export const features = pgTable( - "features", - { - internal_id: text("internal_id").primaryKey().notNull(), - org_id: text("org_id").notNull(), - created_at: numeric({ mode: "number" }), - env: text(), + "features", + { + internal_id: text("internal_id").primaryKey().notNull(), + org_id: text("org_id").notNull(), + created_at: numeric({ mode: "number" }), + env: text(), - id: text().notNull(), - name: text(), - type: text().notNull(), - config: jsonb().$type(), - display: jsonb() - .default(sql`null`) - .$type(), - archived: boolean("archived").notNull().default(false), - }, - (table) => [ - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "features_org_id_fkey", - }).onDelete("cascade"), - unique("feature_id_constraint").on(table.org_id, table.id, table.env), - ], + id: text().notNull(), + name: text(), + type: text().notNull(), + config: jsonb().$type(), + display: jsonb().default(sql`null`).$type(), + archived: boolean("archived").notNull().default(false), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "features_org_id_fkey", + }).onDelete("cascade"), + unique("feature_id_constraint").on(table.org_id, table.id, table.env), + ], ); collatePgColumn(features.internal_id, "C"); diff --git a/shared/models/genModels/genEnums.ts b/shared/models/genModels/genEnums.ts index c8b25ee00..e8138d205 100644 --- a/shared/models/genModels/genEnums.ts +++ b/shared/models/genModels/genEnums.ts @@ -1,18 +1,18 @@ export enum AppEnv { - Sandbox = "sandbox", - Live = "live", + Sandbox = "sandbox", + Live = "live", } export enum Duration { - Minute = "minute", - Hour = "hour", - Day = "day", - Week = "week", - Month = "month", - Year = "year", - Lifetime = "lifetime", + Minute = "minute", + Hour = "hour", + Day = "day", + Week = "week", + Month = "month", + Year = "year", + Lifetime = "lifetime", } export enum ProcessorType { - Stripe = "stripe", + Stripe = "stripe", } diff --git a/shared/models/migrationModels/migrationErrorRelations.ts b/shared/models/migrationModels/migrationErrorRelations.ts index 8820e9683..3bf12e978 100644 --- a/shared/models/migrationModels/migrationErrorRelations.ts +++ b/shared/models/migrationModels/migrationErrorRelations.ts @@ -3,11 +3,11 @@ import { migrationErrors } from "./migrationErrorTable.js"; import { customers } from "../cusModels/cusTable.js"; export const migrationErrorRelations = relations( - migrationErrors, - ({ one }) => ({ - customer: one(customers, { - fields: [migrationErrors.internal_customer_id], - references: [customers.internal_id], - }), - }), + migrationErrors, + ({ one }) => ({ + customer: one(customers, { + fields: [migrationErrors.internal_customer_id], + references: [customers.internal_id], + }), + }), ); diff --git a/shared/models/migrationModels/migrationErrorTable.ts b/shared/models/migrationModels/migrationErrorTable.ts index 699a10b61..dc659f0bd 100644 --- a/shared/models/migrationModels/migrationErrorTable.ts +++ b/shared/models/migrationModels/migrationErrorTable.ts @@ -1,41 +1,41 @@ import { - pgTable, - text, - numeric, - jsonb, - foreignKey, - primaryKey, + pgTable, + text, + numeric, + jsonb, + foreignKey, + primaryKey, } from "drizzle-orm/pg-core"; import { migrationJobs } from "./migrationJobTable.js"; import { customers } from "../cusModels/cusTable.js"; export const migrationErrors = pgTable( - "migration_errors", - { - internal_customer_id: text().notNull(), - migration_job_id: text().notNull(), - created_at: numeric({ mode: "number" }), - updated_at: numeric({ mode: "number" }), - data: jsonb(), - message: text(), - code: text(), - }, - (table) => [ - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "migration_customers_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.migration_job_id], - foreignColumns: [migrationJobs.id], - name: "migration_customers_migration_job_id_fkey", - }).onDelete("cascade"), + "migration_errors", + { + internal_customer_id: text().notNull(), + migration_job_id: text().notNull(), + created_at: numeric({ mode: "number" }), + updated_at: numeric({ mode: "number" }), + data: jsonb(), + message: text(), + code: text(), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "migration_customers_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.migration_job_id], + foreignColumns: [migrationJobs.id], + name: "migration_customers_migration_job_id_fkey", + }).onDelete("cascade"), - primaryKey({ - columns: [table.internal_customer_id, table.migration_job_id], - name: "migration_errors_pkey", - }), - ], + primaryKey({ + columns: [table.internal_customer_id, table.migration_job_id], + name: "migration_errors_pkey", + }), + ], ); diff --git a/shared/models/migrationModels/migrationJobTable.ts b/shared/models/migrationModels/migrationJobTable.ts index ac2a8b936..118b64270 100644 --- a/shared/models/migrationModels/migrationJobTable.ts +++ b/shared/models/migrationModels/migrationJobTable.ts @@ -3,34 +3,34 @@ import { products } from "../productModels/productTable.js"; import { organizations } from "../orgModels/orgTable.js"; export const migrationJobs = pgTable( - "migration_jobs", - { - id: text().primaryKey().notNull(), - org_id: text().notNull(), - env: text().notNull(), + "migration_jobs", + { + id: text().primaryKey().notNull(), + org_id: text().notNull(), + env: text().notNull(), - created_at: numeric({ mode: "number" }).notNull(), - updated_at: numeric({ mode: "number" }), - current_step: text(), - from_internal_product_id: text(), - to_internal_product_id: text(), - step_details: jsonb(), - }, - (table) => [ - foreignKey({ - columns: [table.from_internal_product_id], - foreignColumns: [products.internal_id], - name: "migration_jobs_from_internal_product_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "migration_jobs_org_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.to_internal_product_id], - foreignColumns: [products.internal_id], - name: "migration_jobs_to_internal_product_id_fkey", - }).onDelete("cascade"), - ], + created_at: numeric({ mode: "number" }).notNull(), + updated_at: numeric({ mode: "number" }), + current_step: text(), + from_internal_product_id: text(), + to_internal_product_id: text(), + step_details: jsonb(), + }, + (table) => [ + foreignKey({ + columns: [table.from_internal_product_id], + foreignColumns: [products.internal_id], + name: "migration_jobs_from_internal_product_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "migration_jobs_org_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.to_internal_product_id], + foreignColumns: [products.internal_id], + name: "migration_jobs_to_internal_product_id_fkey", + }).onDelete("cascade"), + ], ); diff --git a/shared/models/migrationModels/migrationModels.ts b/shared/models/migrationModels/migrationModels.ts index 46b93c4f9..41c349bec 100644 --- a/shared/models/migrationModels/migrationModels.ts +++ b/shared/models/migrationModels/migrationModels.ts @@ -2,51 +2,51 @@ import { z } from "zod"; import { AppEnv } from "../genModels/genEnums.js"; export enum MigrationJobStep { - // Pending = "pending", - Queued = "queued", - // InsertCustomers = "insert_customers", - GetCustomers = "get_customers", - MigrateCustomers = "migrate_customers", - Finished = "finished", - Failed = "failed", + // Pending = "pending", + Queued = "queued", + // InsertCustomers = "insert_customers", + GetCustomers = "get_customers", + MigrateCustomers = "migrate_customers", + Finished = "finished", + Failed = "failed", } export const MigrationJobSchema = z.object({ - id: z.string(), - created_at: z.number(), - updated_at: z.number(), - current_step: z.nativeEnum(MigrationJobStep), + id: z.string(), + created_at: z.number(), + updated_at: z.number(), + current_step: z.nativeEnum(MigrationJobStep), - from_internal_product_id: z.string(), - to_internal_product_id: z.string(), + from_internal_product_id: z.string(), + to_internal_product_id: z.string(), - // from_csv_id: z.string().nullish(), - step_details: z.record(z.string(), z.any()), + // from_csv_id: z.string().nullish(), + step_details: z.record(z.string(), z.any()), - // Other details - org_id: z.string(), - env: z.nativeEnum(AppEnv), + // Other details + org_id: z.string(), + env: z.nativeEnum(AppEnv), }); export type MigrationJob = z.infer; // MIGRATION CUSTOMER export enum MigrationCustomerStatus { - Pending = "pending", - Finished = "finished", - Failed = "failed", + Pending = "pending", + Finished = "finished", + Failed = "failed", } export const MigrationErrorSchema = z.object({ - migration_job_id: z.string(), - internal_customer_id: z.string(), + migration_job_id: z.string(), + internal_customer_id: z.string(), - created_at: z.number(), - updated_at: z.number(), + created_at: z.number(), + updated_at: z.number(), - data: z.any(), - code: z.string(), - message: z.string(), + data: z.any(), + code: z.string(), + message: z.string(), }); export type MigrationError = z.infer; diff --git a/shared/models/orgModels/frontendOrg.ts b/shared/models/orgModels/frontendOrg.ts index becc55b9b..327b0d20c 100644 --- a/shared/models/orgModels/frontendOrg.ts +++ b/shared/models/orgModels/frontendOrg.ts @@ -1,17 +1,17 @@ import { z } from "zod"; export const FrontendOrgSchema = z.object({ - id: z.string(), - name: z.string(), - logo: z.string().nullable(), - slug: z.string(), + id: z.string(), + name: z.string(), + logo: z.string().nullable(), + slug: z.string(), - success_url: z.string(), - default_currency: z.string(), - stripe_connected: z.boolean(), - created_at: z.number(), - test_pkey: z.string().nullable(), - live_pkey: z.string().nullable(), + success_url: z.string(), + default_currency: z.string(), + stripe_connected: z.boolean(), + created_at: z.number(), + test_pkey: z.string().nullable(), + live_pkey: z.string().nullable(), }); export type FrontendOrg = z.infer; diff --git a/shared/models/orgModels/orgConfig.ts b/shared/models/orgModels/orgConfig.ts index f786a1bf7..dace8a7c6 100644 --- a/shared/models/orgModels/orgConfig.ts +++ b/shared/models/orgModels/orgConfig.ts @@ -1,27 +1,27 @@ import { z } from "zod"; export const OrgConfigSchema = z.object({ - bill_upgrade_immediately: z.boolean().default(true), - convert_to_charge_automatically: z.boolean().default(true), - anchor_start_of_month: z.boolean().default(false), // If true, the billing cycle will start on the first day of the month - cancel_on_past_due: z.boolean().default(false), + bill_upgrade_immediately: z.boolean().default(true), + convert_to_charge_automatically: z.boolean().default(true), + anchor_start_of_month: z.boolean().default(false), // If true, the billing cycle will start on the first day of the month + cancel_on_past_due: z.boolean().default(false), - prorate_unused: z.boolean().default(true), + prorate_unused: z.boolean().default(true), - api_version: z.number().default(0.2), - checkout_on_failed_payment: z.boolean().default(true), // false for pipeline? - reverse_deduction_order: z.boolean().default(false), + api_version: z.number().default(0.2), + checkout_on_failed_payment: z.boolean().default(true), // false for pipeline? + reverse_deduction_order: z.boolean().default(false), - include_past_due: z.boolean().default(true), - // include_check_past_due: z.boolean().default(false), + include_past_due: z.boolean().default(true), + // include_check_past_due: z.boolean().default(false), - sync_status: z.boolean().default(true), - merge_billing_cycles: z.boolean().default(true), - multiple_trials: z.boolean().default(false), - allow_paid_default: z.boolean().default(false), - cache_customer: z.boolean().default(false), - invoice_memos: z.boolean().default(false), - entity_product: z.boolean().default(false), + sync_status: z.boolean().default(true), + merge_billing_cycles: z.boolean().default(true), + multiple_trials: z.boolean().default(false), + allow_paid_default: z.boolean().default(false), + cache_customer: z.boolean().default(false), + invoice_memos: z.boolean().default(false), + entity_product: z.boolean().default(false), }); export type OrgConfig = z.infer; diff --git a/shared/models/orgModels/orgRelations.ts b/shared/models/orgModels/orgRelations.ts index b215f3172..7dba3e844 100644 --- a/shared/models/orgModels/orgRelations.ts +++ b/shared/models/orgModels/orgRelations.ts @@ -6,7 +6,7 @@ import { user } from "../../db/auth-schema.js"; import { member } from "../../db/auth-schema.js"; export const organizationsRelations = relations(organizations, ({ many }) => ({ - api_keys: many(apiKeys), - features: many(features), - members: many(member), + api_keys: many(apiKeys), + features: many(features), + members: many(member), })); diff --git a/shared/models/orgModels/orgTable.ts b/shared/models/orgModels/orgTable.ts index 271e259c7..ff94da07d 100644 --- a/shared/models/orgModels/orgTable.ts +++ b/shared/models/orgModels/orgTable.ts @@ -1,33 +1,33 @@ import { - pgTable, - text, - numeric, - jsonb, - boolean, - unique, - timestamp, + pgTable, + text, + numeric, + jsonb, + boolean, + unique, + timestamp, } from "drizzle-orm/pg-core"; import { OrgConfig } from "./orgConfig.js"; import { sql } from "drizzle-orm"; export type SvixConfig = { - sandbox_app_id: string; - live_app_id: string; + sandbox_app_id: string; + live_app_id: string; }; export type StripeConfig = { - test_api_key?: string; - live_api_key?: string; - test_webhook_secret?: string; - live_webhook_secret?: string; + test_api_key?: string; + live_api_key?: string; + test_webhook_secret?: string; + live_webhook_secret?: string; - sandbox_success_url?: string; - success_url?: string; + sandbox_success_url?: string; + success_url?: string; }; export type OrgProcessorConfig = { - success_url: string; + success_url: string; }; // logo: text("logo"), @@ -35,37 +35,37 @@ export type OrgProcessorConfig = { // metadata: text("metadata"), export const organizations = pgTable( - "organizations", - { - id: text().primaryKey(), - slug: text().notNull().unique(), + "organizations", + { + id: text().primaryKey(), + slug: text().notNull().unique(), - // Better Auth - name: text("name").notNull(), - logo: text("logo"), - createdAt: timestamp("createdAt", { withTimezone: true }).notNull(), - metadata: text("metadata"), + // Better Auth + name: text("name").notNull(), + logo: text("logo"), + createdAt: timestamp("createdAt", { withTimezone: true }).notNull(), + metadata: text("metadata"), - // Stripe - default_currency: text("default_currency").default("usd"), - stripe_connected: boolean("stripe_connected").default(false), + // Stripe + default_currency: text("default_currency").default("usd"), + stripe_connected: boolean("stripe_connected").default(false), - stripe_config: jsonb("stripe_config").$type(), - test_pkey: text("test_pkey"), - live_pkey: text("live_pkey"), - svix_config: jsonb("svix_config") - .$type() - .default(sql`'{}'::jsonb`), - created_at: numeric({ mode: "number" }), - config: jsonb().default({}).notNull().$type(), - created_by: text("created_by"), - }, - (table) => [ - unique("organizations_test_pkey_key").on(table.test_pkey), - unique("organizations_live_pkey_key").on(table.live_pkey), - ] + stripe_config: jsonb("stripe_config").$type(), + test_pkey: text("test_pkey"), + live_pkey: text("live_pkey"), + svix_config: jsonb("svix_config") + .$type() + .default(sql`'{}'::jsonb`), + created_at: numeric({ mode: "number" }), + config: jsonb().default({}).notNull().$type(), + created_by: text("created_by"), + }, + (table) => [ + unique("organizations_test_pkey_key").on(table.test_pkey), + unique("organizations_live_pkey_key").on(table.live_pkey), + ], ); export type Organization = typeof organizations.$inferSelect & { - api_version: number; + api_version: number; }; diff --git a/shared/models/otherModels/metadataModels.ts b/shared/models/otherModels/metadataModels.ts index b19db87ab..c71578eea 100644 --- a/shared/models/otherModels/metadataModels.ts +++ b/shared/models/otherModels/metadataModels.ts @@ -1,10 +1,10 @@ import { z } from "zod"; export const AutumnMetadataSchema = z.object({ - id: z.string(), - created_at: z.number(), - expires_at: z.number(), - data: z.any(), + id: z.string(), + created_at: z.number(), + expires_at: z.number(), + data: z.any(), }); export type AutumnMetadata = z.infer; diff --git a/shared/models/otherModels/metadataTable.ts b/shared/models/otherModels/metadataTable.ts index c951d1469..c71b3edfd 100644 --- a/shared/models/otherModels/metadataTable.ts +++ b/shared/models/otherModels/metadataTable.ts @@ -2,8 +2,8 @@ import { pgTable, text, numeric, jsonb } from "drizzle-orm/pg-core"; import { sqlNow } from "../../db/utils.js"; export const metadata = pgTable("metadata", { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }).notNull().default(sqlNow), - expires_at: numeric({ mode: "number" }), - data: jsonb(), + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull().default(sqlNow), + expires_at: numeric({ mode: "number" }), + data: jsonb(), }); diff --git a/shared/models/productModels/entModels/entEnums.ts b/shared/models/productModels/entModels/entEnums.ts index 4eaa9fc9c..bf8613abe 100644 --- a/shared/models/productModels/entModels/entEnums.ts +++ b/shared/models/productModels/entModels/entEnums.ts @@ -1,11 +1,11 @@ export enum EntInterval { - Lifetime = "lifetime", - Minute = "minute", - Hour = "hour", - Day = "day", - Week = "week", - Month = "month", - Quarter = "quarter", - SemiAnnual = "semi_annual", - Year = "year", + Lifetime = "lifetime", + Minute = "minute", + Hour = "hour", + Day = "day", + Week = "week", + Month = "month", + Quarter = "quarter", + SemiAnnual = "semi_annual", + Year = "year", } diff --git a/shared/models/productModels/entModels/entModels.ts b/shared/models/productModels/entModels/entModels.ts index 91e2cd5e3..522ee3494 100644 --- a/shared/models/productModels/entModels/entModels.ts +++ b/shared/models/productModels/entModels/entModels.ts @@ -4,47 +4,47 @@ import { EntInterval } from "./entEnums.js"; import { RolloverConfigSchema } from "../../productV2Models/productItemModels/productItemModels.js"; export enum AllowanceType { - Fixed = "fixed", - Unlimited = "unlimited", - None = "none", + Fixed = "fixed", + Unlimited = "unlimited", + None = "none", } export const EntitlementSchema = z.object({ - // Required fields - no .optional() - id: z.string(), - created_at: z.number(), - internal_feature_id: z.string(), - internal_product_id: z.string(), - is_custom: z.boolean().default(false), + // Required fields - no .optional() + id: z.string(), + created_at: z.number(), + internal_feature_id: z.string(), + internal_product_id: z.string(), + is_custom: z.boolean().default(false), - allowance_type: z.nativeEnum(AllowanceType).optional().nullable(), - allowance: z.number().nullish(), - interval: z.nativeEnum(EntInterval).optional().nullable(), - interval_count: z.number().default(1), + allowance_type: z.nativeEnum(AllowanceType).optional().nullable(), + allowance: z.number().nullish(), + interval: z.nativeEnum(EntInterval).optional().nullable(), + interval_count: z.number().default(1), - carry_from_previous: z.boolean().default(false).optional(), - entity_feature_id: z.string().nullish(), + carry_from_previous: z.boolean().default(false).optional(), + entity_feature_id: z.string().nullish(), - // Part of create entitlement - org_id: z.string().optional(), - feature_id: z.string().optional(), - usage_limit: z.number().nullable().optional().default(null), + // Part of create entitlement + org_id: z.string().optional(), + feature_id: z.string().optional(), + usage_limit: z.number().nullable().optional().default(null), - rollover: RolloverConfigSchema.nullish(), + rollover: RolloverConfigSchema.nullish(), }); export const CreateEntitlementSchema = z.object({ - id: z.string().nullish(), - internal_feature_id: z.string(), - feature_id: z.string(), - allowance_type: z.nativeEnum(AllowanceType).nullish(), - allowance: z.number().nullish(), - interval: z.nativeEnum(EntInterval).nullish(), - interval_count: z.number().nullish(), - carry_from_previous: z.boolean().default(false), - entity_feature_id: z.string().nullish(), - usage_limit: z.number().nullish().default(null), - rollover: RolloverConfigSchema.nullish(), + id: z.string().nullish(), + internal_feature_id: z.string(), + feature_id: z.string(), + allowance_type: z.nativeEnum(AllowanceType).nullish(), + allowance: z.number().nullish(), + interval: z.nativeEnum(EntInterval).nullish(), + interval_count: z.number().nullish(), + carry_from_previous: z.boolean().default(false), + entity_feature_id: z.string().nullish(), + usage_limit: z.number().nullish().default(null), + rollover: RolloverConfigSchema.nullish(), }); export type CreateEntitlement = z.infer; @@ -52,11 +52,11 @@ export type CreateEntitlement = z.infer; export type Entitlement = z.infer; export const EntitlementWithFeatureSchema = EntitlementSchema.extend({ - feature: FeatureSchema, + feature: FeatureSchema, }); export type EntitlementWithFeature = z.infer< - typeof EntitlementWithFeatureSchema + typeof EntitlementWithFeatureSchema >; export type FullEntitlement = z.infer; diff --git a/shared/models/productModels/entModels/entRelations.ts b/shared/models/productModels/entModels/entRelations.ts index 41076f4a3..ac160d5cc 100644 --- a/shared/models/productModels/entModels/entRelations.ts +++ b/shared/models/productModels/entModels/entRelations.ts @@ -5,12 +5,12 @@ import { entitlements } from "./entTable.js"; import { products } from "../productTable.js"; export const entitlementsRelations = relations(entitlements, ({ one }) => ({ - feature: one(features, { - fields: [entitlements.internal_feature_id], - references: [features.internal_id], - }), - product: one(products, { - fields: [entitlements.internal_product_id], - references: [products.internal_id], - }), + feature: one(features, { + fields: [entitlements.internal_feature_id], + references: [features.internal_id], + }), + product: one(products, { + fields: [entitlements.internal_product_id], + references: [products.internal_id], + }), })); diff --git a/shared/models/productModels/entModels/entTable.ts b/shared/models/productModels/entModels/entTable.ts index 1f1919e7b..19d892e92 100644 --- a/shared/models/productModels/entModels/entTable.ts +++ b/shared/models/productModels/entModels/entTable.ts @@ -1,12 +1,12 @@ import { - pgTable, - numeric, - boolean, - foreignKey, - unique, - text, - index, - jsonb, + pgTable, + numeric, + boolean, + foreignKey, + unique, + text, + index, + jsonb, } from "drizzle-orm/pg-core"; import { features } from "../../featureModels/featureTable.js"; @@ -17,45 +17,45 @@ import { collatePgColumn } from "../../../db/utils.js"; import { RolloverConfig } from "../../../index.js"; export const entitlements = pgTable( - "entitlements", - { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }).notNull(), - internal_feature_id: text().notNull(), - internal_product_id: text().notNull(), - is_custom: boolean().default(false), + "entitlements", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull(), + internal_feature_id: text().notNull(), + internal_product_id: text().notNull(), + is_custom: boolean().default(false), - allowance_type: text(), - allowance: numeric({ mode: "number" }), - interval: text(), - interval_count: numeric({ mode: "number" }).default(1), + allowance_type: text(), + allowance: numeric({ mode: "number" }), + interval: text(), + interval_count: numeric({ mode: "number" }).default(1), - carry_from_previous: boolean("carry_from_previous").default(false), - entity_feature_id: text("entity_feature_id").default(sql`null`), + carry_from_previous: boolean("carry_from_previous").default(false), + entity_feature_id: text("entity_feature_id").default(sql`null`), - // Optional fields - org_id: text("org_id"), - feature_id: text("feature_id"), - usage_limit: numeric({ mode: "number" }), + // Optional fields + org_id: text("org_id"), + feature_id: text("feature_id"), + usage_limit: numeric({ mode: "number" }), - rollover: jsonb().$type(), - }, - (table) => [ - foreignKey({ - columns: [table.internal_feature_id], - foreignColumns: [features.internal_id], - name: "entitlements_internal_feature_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internal_product_id], - foreignColumns: [products.internal_id], - name: "entitlements_internal_product_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - unique("entitlements_id_key").on(table.id), - index("idx_entitlements_internal_product_id").on(table.internal_product_id), - ] + rollover: jsonb().$type(), + }, + (table) => [ + foreignKey({ + columns: [table.internal_feature_id], + foreignColumns: [features.internal_id], + name: "entitlements_internal_feature_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_product_id], + foreignColumns: [products.internal_id], + name: "entitlements_internal_product_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + unique("entitlements_id_key").on(table.id), + index("idx_entitlements_internal_product_id").on(table.internal_product_id), + ], ); export const EntInsertSchema = createInsertSchema(entitlements); diff --git a/shared/models/productModels/freeTrialModels/freeTrialEnums.ts b/shared/models/productModels/freeTrialModels/freeTrialEnums.ts index b6b2be995..81f1b51fd 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialEnums.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialEnums.ts @@ -1,5 +1,5 @@ export enum FreeTrialDuration { - Day = "day", - Month = "month", - Year = "year", + Day = "day", + Month = "month", + Year = "year", } diff --git a/shared/models/productModels/freeTrialModels/freeTrialModels.ts b/shared/models/productModels/freeTrialModels/freeTrialModels.ts index 1264f66a5..8d4b1d593 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialModels.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialModels.ts @@ -2,34 +2,34 @@ import { z } from "zod"; import { FreeTrialDuration } from "./freeTrialEnums.js"; export const FreeTrialSchema = z.object({ - id: z.string(), - duration: z.nativeEnum(FreeTrialDuration), - length: z.number(), - unique_fingerprint: z.boolean(), + id: z.string(), + duration: z.nativeEnum(FreeTrialDuration), + length: z.number(), + unique_fingerprint: z.boolean(), - created_at: z.number(), - internal_product_id: z.string(), - is_custom: z.boolean(), - card_required: z.boolean(), + created_at: z.number(), + internal_product_id: z.string(), + is_custom: z.boolean(), + card_required: z.boolean(), }); export const CreateFreeTrialSchema = z.object({ - length: z - .string() - .or(z.number()) - .transform((val) => Number(val)), - unique_fingerprint: z.boolean().default(false), - duration: z.nativeEnum(FreeTrialDuration).default(FreeTrialDuration.Day), - card_required: z.boolean().default(true), + length: z + .string() + .or(z.number()) + .transform((val) => Number(val)), + unique_fingerprint: z.boolean().default(false), + duration: z.nativeEnum(FreeTrialDuration).default(FreeTrialDuration.Day), + card_required: z.boolean().default(true), }); export const FreeTrialResponseSchema = z.object({ - // id: z.string(), - duration: z.nativeEnum(FreeTrialDuration), - length: z.number(), - unique_fingerprint: z.boolean(), - trial_available: z.boolean().nullish().default(true), - card_required: z.boolean().nullish(), + // id: z.string(), + duration: z.nativeEnum(FreeTrialDuration), + length: z.number(), + unique_fingerprint: z.boolean(), + trial_available: z.boolean().nullish().default(true), + card_required: z.boolean().nullish(), }); export type FreeTrial = z.infer; diff --git a/shared/models/productModels/freeTrialModels/freeTrialRelations.ts b/shared/models/productModels/freeTrialModels/freeTrialRelations.ts index cc8e720d8..4e681f24a 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialRelations.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialRelations.ts @@ -3,8 +3,8 @@ import { freeTrials } from "./freeTrialTable.js"; import { products } from "../productTable.js"; export const freeTrialRelations = relations(freeTrials, ({ one }) => ({ - product: one(products, { - fields: [freeTrials.internal_product_id], - references: [products.internal_id], - }), + product: one(products, { + fields: [freeTrials.internal_product_id], + references: [products.internal_id], + }), })); diff --git a/shared/models/productModels/freeTrialModels/freeTrialTable.ts b/shared/models/productModels/freeTrialModels/freeTrialTable.ts index 92f8a22a9..2fbb5eab0 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialTable.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialTable.ts @@ -1,29 +1,29 @@ import { - boolean, - foreignKey, - numeric, - pgTable, - text, + boolean, + foreignKey, + numeric, + pgTable, + text, } from "drizzle-orm/pg-core"; import { products } from "../productTable.js"; export const freeTrials = pgTable( - "free_trials", - { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }).notNull(), - internal_product_id: text("internal_product_id"), - duration: text().default("day"), - length: numeric({ mode: "number" }), - unique_fingerprint: boolean("unique_fingerprint"), - is_custom: boolean("is_custom").default(false), - card_required: boolean("card_required").default(false), - }, - (table) => [ - foreignKey({ - columns: [table.internal_product_id], - foreignColumns: [products.internal_id], - name: "free_trials_internal_product_id_fkey", - }).onDelete("cascade"), - ], + "free_trials", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull(), + internal_product_id: text("internal_product_id"), + duration: text().default("day"), + length: numeric({ mode: "number" }), + unique_fingerprint: boolean("unique_fingerprint"), + is_custom: boolean("is_custom").default(false), + card_required: boolean("card_required").default(false), + }, + (table) => [ + foreignKey({ + columns: [table.internal_product_id], + foreignColumns: [products.internal_id], + name: "free_trials_internal_product_id_fkey", + }).onDelete("cascade"), + ], ); diff --git a/shared/models/productModels/priceModels/priceConfig/fixedPriceConfig.ts b/shared/models/productModels/priceModels/priceConfig/fixedPriceConfig.ts index 7f88d631d..1b17f1299 100644 --- a/shared/models/productModels/priceModels/priceConfig/fixedPriceConfig.ts +++ b/shared/models/productModels/priceModels/priceConfig/fixedPriceConfig.ts @@ -2,12 +2,12 @@ import { z } from "zod"; import { BillingInterval } from "../priceEnums.js"; export const FixedPriceConfigSchema = z.object({ - type: z.string(), - amount: z.number().min(0), - interval: z.nativeEnum(BillingInterval), - interval_count: z.number().nullish(), - stripe_price_id: z.string().nullish(), - stripe_product_id: z.null().or(z.undefined()), + type: z.string(), + amount: z.number().min(0), + interval: z.nativeEnum(BillingInterval), + interval_count: z.number().nullish(), + stripe_price_id: z.string().nullish(), + stripe_product_id: z.null().or(z.undefined()), }); export type FixedPriceConfig = z.infer; diff --git a/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts b/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts index 319749d00..1bdeb3a8c 100644 --- a/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts +++ b/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts @@ -3,38 +3,38 @@ import { Infinite } from "../../productEnums.js"; import { BillingInterval } from "../priceEnums.js"; export enum BillWhen { - InAdvance = "in_advance", - StartOfPeriod = "start_of_period", - EndOfPeriod = "end_of_period", + InAdvance = "in_advance", + StartOfPeriod = "start_of_period", + EndOfPeriod = "end_of_period", } export const UsageTierSchema = z.object({ - to: z.number().or(z.literal(Infinite)), - amount: z.number(), + to: z.number().or(z.literal(Infinite)), + amount: z.number(), }); export type UsageTier = z.infer; export const UsagePriceConfigSchema = z.object({ - type: z.string(), - bill_when: z.nativeEnum(BillWhen), - billing_units: z.number().nullish(), + type: z.string(), + bill_when: z.nativeEnum(BillWhen), + billing_units: z.number().nullish(), - internal_feature_id: z.string(), - feature_id: z.string(), - usage_tiers: z.array(UsageTierSchema), - interval: z.nativeEnum(BillingInterval), - interval_count: z.number().nullish(), + internal_feature_id: z.string(), + feature_id: z.string(), + usage_tiers: z.array(UsageTierSchema), + interval: z.nativeEnum(BillingInterval), + interval_count: z.number().nullish(), - // For usage in arrear - stripe_meter_id: z.string().nullish(), - stripe_price_id: z.string().nullish(), - stripe_empty_price_id: z.string().nullish(), - stripe_product_id: z.string().nullish(), - stripe_placeholder_price_id: z.string().nullish(), - stripe_event_name: z.string().nullish(), + // For usage in arrear + stripe_meter_id: z.string().nullish(), + stripe_price_id: z.string().nullish(), + stripe_empty_price_id: z.string().nullish(), + stripe_product_id: z.string().nullish(), + stripe_placeholder_price_id: z.string().nullish(), + stripe_event_name: z.string().nullish(), - should_prorate: z.boolean().optional(), + should_prorate: z.boolean().optional(), }); export type UsagePriceConfig = z.infer; diff --git a/shared/models/productModels/priceModels/priceEnums.ts b/shared/models/productModels/priceModels/priceEnums.ts index e9b7b1a2d..5532bb830 100644 --- a/shared/models/productModels/priceModels/priceEnums.ts +++ b/shared/models/productModels/priceModels/priceEnums.ts @@ -1,23 +1,23 @@ export enum BillingInterval { - OneOff = "one_off", - Week = "week", - Month = "month", - Quarter = "quarter", - SemiAnnual = "semi_annual", - Year = "year", + OneOff = "one_off", + Week = "week", + Month = "month", + Quarter = "quarter", + SemiAnnual = "semi_annual", + Year = "year", } export enum PriceType { - Fixed = "fixed", - Usage = "usage", + Fixed = "fixed", + Usage = "usage", } export enum BillingType { - OneOff = "one_off", - FixedCycle = "fixed_cycle", + OneOff = "one_off", + FixedCycle = "fixed_cycle", - UsageBelowThreshold = "usage_below_threshold", - UsageInAdvance = "usage_in_advance", - UsageInArrear = "usage_in_arrear", - InArrearProrated = "in_arrear_prorated", + UsageBelowThreshold = "usage_below_threshold", + UsageInAdvance = "usage_in_advance", + UsageInArrear = "usage_in_arrear", + InArrearProrated = "in_arrear_prorated", } diff --git a/shared/models/productModels/priceModels/priceModels.ts b/shared/models/productModels/priceModels/priceModels.ts index 46702ba9f..d0e905aa6 100644 --- a/shared/models/productModels/priceModels/priceModels.ts +++ b/shared/models/productModels/priceModels/priceModels.ts @@ -6,26 +6,26 @@ import { OnDecrease } from "../../productV2Models/productItemModels/productItemE import { OnIncrease } from "../../productV2Models/productItemModels/productItemEnums.js"; const ProrationConfigSchema = z.object({ - on_increase: z.nativeEnum(OnIncrease).default(OnIncrease.ProrateImmediately), - on_decrease: z.nativeEnum(OnDecrease).default(OnDecrease.ProrateImmediately), + on_increase: z.nativeEnum(OnIncrease).default(OnIncrease.ProrateImmediately), + on_decrease: z.nativeEnum(OnDecrease).default(OnDecrease.ProrateImmediately), }); export const PriceSchema = z.object({ - id: z.string(), - internal_product_id: z.string(), + id: z.string(), + internal_product_id: z.string(), - org_id: z.string().optional(), - created_at: z.number().optional(), - billing_type: z.nativeEnum(BillingType).nullish(), - is_custom: z.boolean().optional(), - config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), - entitlement_id: z.string().nullish(), + org_id: z.string().optional(), + created_at: z.number().optional(), + billing_type: z.nativeEnum(BillingType).nullish(), + is_custom: z.boolean().optional(), + config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), + entitlement_id: z.string().nullish(), - proration_config: ProrationConfigSchema.nullable(), + proration_config: ProrationConfigSchema.nullable(), }); export const CreatePriceSchema = z.object({ - config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), + config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), }); export type Price = z.infer; diff --git a/shared/models/productModels/priceModels/priceRelations.ts b/shared/models/productModels/priceModels/priceRelations.ts index a4e04b89c..4adbb2f9e 100644 --- a/shared/models/productModels/priceModels/priceRelations.ts +++ b/shared/models/productModels/priceModels/priceRelations.ts @@ -3,8 +3,8 @@ import { prices } from "./priceTable.js"; import { products } from "../productTable.js"; export const priceRelations = relations(prices, ({ one }) => ({ - product: one(products, { - fields: [prices.internal_product_id], - references: [products.internal_id], - }), + product: one(products, { + fields: [prices.internal_product_id], + references: [products.internal_id], + }), })); diff --git a/shared/models/productModels/priceModels/priceTable.ts b/shared/models/productModels/priceModels/priceTable.ts index c1cd48965..12afe21b0 100644 --- a/shared/models/productModels/priceModels/priceTable.ts +++ b/shared/models/productModels/priceModels/priceTable.ts @@ -1,12 +1,12 @@ import { - boolean, - foreignKey, - unique, - pgTable, - numeric, - jsonb, - text, - index, + boolean, + foreignKey, + unique, + pgTable, + numeric, + jsonb, + text, + index, } from "drizzle-orm/pg-core"; import { entitlements } from "../entModels/entTable.js"; @@ -18,36 +18,36 @@ import { collatePgColumn } from "../../../db/utils.js"; import { ProrationConfig } from "./priceModels.js"; export const prices = pgTable( - "prices", - { - id: text().primaryKey().notNull(), - org_id: text("org_id").notNull(), - internal_product_id: text("internal_product_id").notNull(), - config: jsonb().$type(), - created_at: numeric({ mode: "number" }).notNull(), - billing_type: text("billing_type"), - is_custom: boolean("is_custom").default(false), - entitlement_id: text("entitlement_id").default(sql`null`), - proration_config: jsonb("proration_config") - .$type() - .default(sql`null`), - }, - (table) => [ - foreignKey({ - columns: [table.entitlement_id], - foreignColumns: [entitlements.id], - name: "prices_entitlement_id_fkey", - }), - foreignKey({ - columns: [table.internal_product_id], - foreignColumns: [products.internal_id], - name: "prices_internal_product_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - unique("prices_id_key").on(table.id), - index("idx_prices_internal_product_id").on(table.internal_product_id), - ], + "prices", + { + id: text().primaryKey().notNull(), + org_id: text("org_id").notNull(), + internal_product_id: text("internal_product_id").notNull(), + config: jsonb().$type(), + created_at: numeric({ mode: "number" }).notNull(), + billing_type: text("billing_type"), + is_custom: boolean("is_custom").default(false), + entitlement_id: text("entitlement_id").default(sql`null`), + proration_config: jsonb("proration_config") + .$type() + .default(sql`null`), + }, + (table) => [ + foreignKey({ + columns: [table.entitlement_id], + foreignColumns: [entitlements.id], + name: "prices_entitlement_id_fkey", + }), + foreignKey({ + columns: [table.internal_product_id], + foreignColumns: [products.internal_id], + name: "prices_internal_product_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + unique("prices_id_key").on(table.id), + index("idx_prices_internal_product_id").on(table.internal_product_id), + ], ); collatePgColumn(prices.id, "C"); diff --git a/shared/models/productModels/pricecnProductModel.ts b/shared/models/productModels/pricecnProductModel.ts index 199f30a98..9cff4a9a6 100644 --- a/shared/models/productModels/pricecnProductModel.ts +++ b/shared/models/productModels/pricecnProductModel.ts @@ -1,18 +1,18 @@ export interface PricecnProduct { - name: string; - description?: string; + name: string; + description?: string; - price: { - primaryText: string; - secondaryText?: string; - }; + price: { + primaryText: string; + secondaryText?: string; + }; - everythingFrom?: string; - buttonText?: string; - recommendText?: string; + everythingFrom?: string; + buttonText?: string; + recommendText?: string; - items: { - primaryText: string; - secondaryText?: string; - }[]; + items: { + primaryText: string; + secondaryText?: string; + }[]; } diff --git a/shared/models/productModels/productModels.ts b/shared/models/productModels/productModels.ts index 0a62a63fd..2715a24c0 100644 --- a/shared/models/productModels/productModels.ts +++ b/shared/models/productModels/productModels.ts @@ -6,81 +6,81 @@ import { FreeTrialSchema } from "./freeTrialModels/freeTrialModels.js"; import { AppEnv } from "../genModels/genEnums.js"; export const ProductSchema = z.object({ - id: z.string(), - name: z.string(), - is_add_on: z.boolean(), - is_default: z.boolean(), - version: z.number(), - group: z.string(), + id: z.string(), + name: z.string(), + is_add_on: z.boolean(), + is_default: z.boolean(), + version: z.number(), + group: z.string(), - env: z.nativeEnum(AppEnv), - internal_id: z.string(), - org_id: z.string(), - created_at: z.number(), + env: z.nativeEnum(AppEnv), + internal_id: z.string(), + org_id: z.string(), + created_at: z.number(), - processor: z - .object({ - type: z.string(), - id: z.string(), - }) - .nullish(), - base_variant_id: z.string().nullable(), - archived: z.boolean().default(false), + processor: z + .object({ + type: z.string(), + id: z.string(), + }) + .nullish(), + base_variant_id: z.string().nullable(), + archived: z.boolean().default(false), }); export const CreateProductSchema = z.object({ - id: z.string(), - name: z.string().default(""), - is_add_on: z.boolean().default(false), - is_default: z.boolean().default(false), - version: z.number().optional().default(1), - group: z.string().optional().default(""), + id: z.string(), + name: z.string().default(""), + is_add_on: z.boolean().default(false), + is_default: z.boolean().default(false), + version: z.number().optional().default(1), + group: z.string().optional().default(""), }); export const UpdateProductSchema = z.object({ - id: z.string().nullish(), - name: z.string().optional(), - is_add_on: z.boolean().optional(), - is_default: z.boolean().optional(), - group: z.string().optional(), - archived: z.boolean().optional(), + id: z.string().nullish(), + name: z.string().optional(), + is_add_on: z.boolean().optional(), + is_default: z.boolean().optional(), + group: z.string().optional(), + archived: z.boolean().optional(), }); export const FrontendProductSchema = ProductSchema.omit({ - org_id: true, - created_at: true, - env: true, - processor: true, + org_id: true, + created_at: true, + env: true, + processor: true, }).extend({ - isActive: z.boolean(), - prices: z.array(PriceSchema), - entitlements: z.array( - EntitlementSchema.extend({ - feature: FeatureSchema, - }) - ), - free_trial: FreeTrialSchema, - options: z.any(), + isActive: z.boolean(), + prices: z.array(PriceSchema), + entitlements: z.array( + EntitlementSchema.extend({ + feature: FeatureSchema, + }), + ), + free_trial: FreeTrialSchema, + options: z.any(), }); export const FullProductSchema = ProductSchema.extend({ - prices: z.array(PriceSchema), - entitlements: z.array( - EntitlementSchema.extend({ - feature: FeatureSchema, - }), - ), - free_trial: FreeTrialSchema.nullish(), - free_trials: z.array(FreeTrialSchema).nullish(), - free_trial_ids: z.array(z.string()).nullish(), + prices: z.array(PriceSchema), + entitlements: z.array( + EntitlementSchema.extend({ + feature: FeatureSchema, + }), + ), + free_trial: FreeTrialSchema.nullish(), + free_trials: z.array(FreeTrialSchema).nullish(), + free_trial_ids: z.array(z.string()).nullish(), }); export type ProductCounts = { - active: number; - canceled: number; - custom: number; - trialing: number; - all: number; + active: number; + canceled: number; + custom: number; + trialing: number; + all: number; }; export type Product = z.infer; diff --git a/shared/models/productModels/productRelations.ts b/shared/models/productModels/productRelations.ts index 990903740..95035abf6 100644 --- a/shared/models/productModels/productRelations.ts +++ b/shared/models/productModels/productRelations.ts @@ -6,12 +6,12 @@ import { freeTrials } from "./freeTrialModels/freeTrialTable.js"; import { organizations } from "../orgModels/orgTable.js"; export const productRelations = relations(products, ({ many, one }) => ({ - entitlements: many(entitlements), - prices: many(prices), + entitlements: many(entitlements), + prices: many(prices), - free_trials: many(freeTrials), - org: one(organizations, { - fields: [products.org_id], - references: [organizations.id], - }), + free_trials: many(freeTrials), + org: one(organizations, { + fields: [products.org_id], + references: [organizations.id], + }), })); diff --git a/shared/models/productModels/productTable.ts b/shared/models/productModels/productTable.ts index 4cc429179..e5a9767bf 100644 --- a/shared/models/productModels/productTable.ts +++ b/shared/models/productModels/productTable.ts @@ -1,53 +1,51 @@ import { - boolean, - foreignKey, - jsonb, - numeric, - pgTable, - text, - unique, + boolean, + foreignKey, + jsonb, + numeric, + pgTable, + text, + unique, } from "drizzle-orm/pg-core"; import { sql } from "drizzle-orm"; import { organizations } from "../orgModels/orgTable.js"; import { collatePgColumn, sqlNow } from "../../db/utils.js"; type ProductProcessor = { - type: string; - id: string; + type: string; + id: string; }; export const products = pgTable( - "products", - { - internal_id: text("internal_id").primaryKey().notNull(), - id: text().notNull(), - name: text(), - org_id: text("org_id").notNull(), - created_at: numeric({ mode: "number" }).notNull().default(sqlNow), - env: text().notNull(), - is_add_on: boolean("is_add_on").notNull().default(false), - is_default: boolean("is_default").notNull().default(false), - group: text().default(""), - version: numeric({ mode: "number" }).notNull().default(1), - processor: jsonb() - .$type() - .default(sql`null`), - base_variant_id: text("base_variant_id"), - archived: boolean("archived").notNull().default(false), - }, - (table) => [ - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "products_org_id_fkey", - }).onDelete("cascade"), - unique("unique_product").on( - table.org_id, - table.id, - table.env, - table.version, - ), - ], + "products", + { + internal_id: text("internal_id").primaryKey().notNull(), + id: text().notNull(), + name: text(), + org_id: text("org_id").notNull(), + created_at: numeric({ mode: "number" }).notNull().default(sqlNow), + env: text().notNull(), + is_add_on: boolean("is_add_on").notNull().default(false), + is_default: boolean("is_default").notNull().default(false), + group: text().default(""), + version: numeric({ mode: "number" }).notNull().default(1), + processor: jsonb().$type().default(sql`null`), + base_variant_id: text("base_variant_id"), + archived: boolean("archived").notNull().default(false), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "products_org_id_fkey", + }).onDelete("cascade"), + unique("unique_product").on( + table.org_id, + table.id, + table.env, + table.version, + ), + ], ); -collatePgColumn(products.internal_id, "C"); \ No newline at end of file +collatePgColumn(products.internal_id, "C"); diff --git a/shared/models/productV2Models/productItemModels/featureItem.ts b/shared/models/productV2Models/productItemModels/featureItem.ts index 1ca38afdd..9575bcf31 100644 --- a/shared/models/productV2Models/productItemModels/featureItem.ts +++ b/shared/models/productV2Models/productItemModels/featureItem.ts @@ -3,32 +3,32 @@ import { Infinite } from "../../productModels/productEnums.js"; import { z } from "zod"; export const FeatureItemSchema = ProductItemSchema.pick({ - feature_id: true, - feature_type: true, - included_usage: true, - interval: true, - interval_count: true, - entity_feature_id: true, - reset_usage_when_enabled: true, - config: true, + feature_id: true, + feature_type: true, + included_usage: true, + interval: true, + interval_count: true, + entity_feature_id: true, + reset_usage_when_enabled: true, + config: true, }).extend({ - feature_id: z.string().nonempty(), - included_usage: z - .number() - .or(z.string()) - .transform((val) => { - if (val === "Unlimited") { - return Infinite; - } - let num = Number(val); + feature_id: z.string().nonempty(), + included_usage: z + .number() + .or(z.string()) + .transform((val) => { + if (val === "Unlimited") { + return Infinite; + } + let num = Number(val); - if (isNaN(num) || num <= 0) { - num = 0; - } + if (isNaN(num) || num <= 0) { + num = 0; + } - return num; - }) - .nullish(), + return num; + }) + .nullish(), }); export type FeatureItem = z.infer; diff --git a/shared/models/productV2Models/productItemModels/featurePriceItem.ts b/shared/models/productV2Models/productItemModels/featurePriceItem.ts index 93391f973..4ed0d617c 100644 --- a/shared/models/productV2Models/productItemModels/featurePriceItem.ts +++ b/shared/models/productV2Models/productItemModels/featurePriceItem.ts @@ -2,23 +2,23 @@ import { ProductItemSchema } from "./productItemModels.js"; import { z } from "zod"; export const FeaturePriceItemSchema = ProductItemSchema.pick({ - feature_id: true, - feature_type: true, - included_usage: true, - interval: true, - interval_count: true, - usage_model: true, + feature_id: true, + feature_type: true, + included_usage: true, + interval: true, + interval_count: true, + usage_model: true, - price: true, - tiers: true, - billing_units: true, + price: true, + tiers: true, + billing_units: true, - reset_usage_when_enabled: true, - usage_limit: true, - config: true, + reset_usage_when_enabled: true, + usage_limit: true, + config: true, }).extend({ - feature_id: z.string().nonempty(), - included_usage: z.number().nonnegative().nullish(), + feature_id: z.string().nonempty(), + included_usage: z.number().nonnegative().nullish(), }); export type FeaturePriceItem = z.infer; diff --git a/shared/models/productV2Models/productItemModels/priceItem.ts b/shared/models/productV2Models/productItemModels/priceItem.ts index db075b3b2..4ba1214b9 100644 --- a/shared/models/productV2Models/productItemModels/priceItem.ts +++ b/shared/models/productV2Models/productItemModels/priceItem.ts @@ -2,11 +2,11 @@ import { ProductItemSchema } from "./productItemModels.js"; import { z } from "zod"; export const PriceItemSchema = ProductItemSchema.pick({ - price: true, - interval: true, - interval_count: true, + price: true, + interval: true, + interval_count: true, }).extend({ - price: z.number().nonnegative(), + price: z.number().nonnegative(), }); export type PriceItem = z.infer; diff --git a/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts b/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts index 59919e511..8d44f1197 100644 --- a/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts +++ b/shared/models/productV2Models/productItemModels/prodItemResponseModels.ts @@ -1,43 +1,43 @@ import { z } from "zod"; import { - PriceTierSchema, - ProductItemFeatureType, - ProductItemInterval, - ProductItemType, - UsageModel, + PriceTierSchema, + ProductItemFeatureType, + ProductItemInterval, + ProductItemType, + UsageModel, } from "./productItemModels.js"; import { Infinite } from "../../productModels/productEnums.js"; import { APIFeatureSchema } from "../../featureModels/featureResModels.js"; export const ProductItemResponseSchema = z.object({ - // Feature stuff - type: z.nativeEnum(ProductItemType).nullish(), - feature_id: z.string().nullish(), - feature_type: z.nativeEnum(ProductItemFeatureType).nullish(), + // Feature stuff + type: z.nativeEnum(ProductItemType).nullish(), + feature_id: z.string().nullish(), + feature_type: z.nativeEnum(ProductItemFeatureType).nullish(), - // Feature response - feature: APIFeatureSchema.nullish(), + // Feature response + feature: APIFeatureSchema.nullish(), - included_usage: z.number().or(z.literal(Infinite)).nullish(), - interval: z.nativeEnum(ProductItemInterval).nullish(), - interval_count: z.number().nullish(), + included_usage: z.number().or(z.literal(Infinite)).nullish(), + interval: z.nativeEnum(ProductItemInterval).nullish(), + interval_count: z.number().nullish(), - // Price config - price: z.number().nullish(), - tiers: z.array(PriceTierSchema).nullish(), - usage_model: z.nativeEnum(UsageModel).nullish(), - billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) - reset_usage_when_enabled: z.boolean().nullish(), - quantity: z.number().nullish(), - next_cycle_quantity: z.number().nullish(), - entity_feature_id: z.string().nullish(), + // Price config + price: z.number().nullish(), + tiers: z.array(PriceTierSchema).nullish(), + usage_model: z.nativeEnum(UsageModel).nullish(), + billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) + reset_usage_when_enabled: z.boolean().nullish(), + quantity: z.number().nullish(), + next_cycle_quantity: z.number().nullish(), + entity_feature_id: z.string().nullish(), - display: z - .object({ - primary_text: z.string(), - secondary_text: z.string().nullish(), - }) - .nullish(), + display: z + .object({ + primary_text: z.string(), + secondary_text: z.string().nullish(), + }) + .nullish(), }); export type ProductItemResponse = z.infer; diff --git a/shared/models/productV2Models/productItemModels/productItemEnums.ts b/shared/models/productV2Models/productItemModels/productItemEnums.ts index 49b48e0bc..8a0de62b3 100644 --- a/shared/models/productV2Models/productItemModels/productItemEnums.ts +++ b/shared/models/productV2Models/productItemModels/productItemEnums.ts @@ -1,15 +1,15 @@ export enum OnIncrease { - BillImmediately = "bill_immediately", - ProrateImmediately = "prorate_immediately", - ProrateNextCycle = "prorate_next_cycle", - BillNextCycle = "bill_next_cycle", + BillImmediately = "bill_immediately", + ProrateImmediately = "prorate_immediately", + ProrateNextCycle = "prorate_next_cycle", + BillNextCycle = "bill_next_cycle", } export enum OnDecrease { - Prorate = "prorate", - ProrateImmediately = "prorate_immediately", - ProrateNextCycle = "prorate_next_cycle", - None = "none", // replaceable strategy - // KeepUsage = "keep_usage", - NoProrations = "no_prorations", + Prorate = "prorate", + ProrateImmediately = "prorate_immediately", + ProrateNextCycle = "prorate_next_cycle", + None = "none", // replaceable strategy + // KeepUsage = "keep_usage", + NoProrations = "no_prorations", } diff --git a/shared/models/productV2Models/productItemModels/productItemModels.ts b/shared/models/productV2Models/productItemModels/productItemModels.ts index 72dd7128d..0746f6610 100644 --- a/shared/models/productV2Models/productItemModels/productItemModels.ts +++ b/shared/models/productV2Models/productItemModels/productItemModels.ts @@ -6,97 +6,97 @@ import { OnDecrease } from "./productItemEnums.js"; export const TierInfinite = "inf"; export enum ProductItemInterval { - // None = "none", + // None = "none", - // Reset interval - Minute = "minute", - Hour = "hour", - Day = "day", - Week = "week", + // Reset interval + Minute = "minute", + Hour = "hour", + Day = "day", + Week = "week", - // Billing interval - Month = "month", - Quarter = "quarter", - SemiAnnual = "semi_annual", - Year = "year", + // Billing interval + Month = "month", + Quarter = "quarter", + SemiAnnual = "semi_annual", + Year = "year", } export enum ProductItemType { - Feature = "feature", - FeaturePrice = "priced_feature", - Price = "price", + Feature = "feature", + FeaturePrice = "priced_feature", + Price = "price", } export const PriceTierSchema = z.object({ - to: z.number().or(z.literal(TierInfinite)), - amount: z.number(), + to: z.number().or(z.literal(TierInfinite)), + amount: z.number(), }); export enum UsageModel { - Prepaid = "prepaid", - PayPerUse = "pay_per_use", + Prepaid = "prepaid", + PayPerUse = "pay_per_use", } export enum ProductItemFeatureType { - SingleUse = "single_use", - ContinuousUse = "continuous_use", - Boolean = "boolean", - Static = "static", + SingleUse = "single_use", + ContinuousUse = "continuous_use", + Boolean = "boolean", + Static = "static", } export enum RolloverDuration { - Month = "month", - Forever = "forever", + Month = "month", + Forever = "forever", } export const RolloverConfigSchema = z.object({ - max: z.number().nullable(), - duration: z.nativeEnum(RolloverDuration).default(RolloverDuration.Month), - length: z.number(), + max: z.number().nullable(), + duration: z.nativeEnum(RolloverDuration).default(RolloverDuration.Month), + length: z.number(), }); const ProductItemConfigSchema = z.object({ - on_increase: z.nativeEnum(OnIncrease).nullish(), - on_decrease: z.nativeEnum(OnDecrease).nullish(), - rollover: RolloverConfigSchema.nullish(), + on_increase: z.nativeEnum(OnIncrease).nullish(), + on_decrease: z.nativeEnum(OnDecrease).nullish(), + rollover: RolloverConfigSchema.nullish(), }); export const ProductItemSchema = z.object({ - // Feature stuff - feature_id: z.string().nullish(), - feature_type: z.nativeEnum(ProductItemFeatureType).nullish(), - included_usage: z.union([z.number(), z.literal(Infinite)]).nullish(), - interval: z.nativeEnum(ProductItemInterval).nullish(), - interval_count: z.number().nullish(), - entity_feature_id: z.string().nullish(), + // Feature stuff + feature_id: z.string().nullish(), + feature_type: z.nativeEnum(ProductItemFeatureType).nullish(), + included_usage: z.union([z.number(), z.literal(Infinite)]).nullish(), + interval: z.nativeEnum(ProductItemInterval).nullish(), + interval_count: z.number().nullish(), + entity_feature_id: z.string().nullish(), - // Price config - usage_model: z.nativeEnum(UsageModel).nullish(), - price: z.number().nullish(), - tiers: z.array(PriceTierSchema).nullish(), - billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) - usage_limit: z.number().nullish(), + // Price config + usage_model: z.nativeEnum(UsageModel).nullish(), + price: z.number().nullish(), + tiers: z.array(PriceTierSchema).nullish(), + billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units) + usage_limit: z.number().nullish(), - // Others - // carry_over_usage: z.boolean().nullish(), - reset_usage_when_enabled: z.boolean().nullish(), + // Others + // carry_over_usage: z.boolean().nullish(), + reset_usage_when_enabled: z.boolean().nullish(), - config: ProductItemConfigSchema.nullish(), + config: ProductItemConfigSchema.nullish(), - // Stored in backend - created_at: z.number().nullish(), - entitlement_id: z.string().nullish(), - price_id: z.string().nullish(), - price_config: z.any().nullish(), + // Stored in backend + created_at: z.number().nullish(), + entitlement_id: z.string().nullish(), + price_id: z.string().nullish(), + price_config: z.any().nullish(), }); export const LimitedItemSchema = ProductItemSchema.extend({ - included_usage: z.number(), + included_usage: z.number(), }); export const FrontendProductItem = ProductItemSchema.extend({ - isPrice: z.boolean(), - isVariable: z.boolean().nullish(), + isPrice: z.boolean(), + isVariable: z.boolean().nullish(), }); export type ProductItem = z.infer; diff --git a/shared/models/productV2Models/productV2Models.ts b/shared/models/productV2Models/productV2Models.ts index c93e41eb6..4aeee0585 100644 --- a/shared/models/productV2Models/productV2Models.ts +++ b/shared/models/productV2Models/productV2Models.ts @@ -3,20 +3,20 @@ import { FreeTrialSchema } from "../productModels/freeTrialModels/freeTrialModel import { ProductItemSchema } from "./productItemModels/productItemModels.js"; export const ProductV2Schema = z.object({ - internal_id: z.string().nullish(), + internal_id: z.string().nullish(), - id: z.string(), - name: z.string(), - is_add_on: z.boolean(), - is_default: z.boolean(), - version: z.number().default(1), - group: z.string(), + id: z.string(), + name: z.string(), + is_add_on: z.boolean(), + is_default: z.boolean(), + version: z.number().default(1), + group: z.string(), - free_trial: FreeTrialSchema.nullish(), - items: z.array(ProductItemSchema), - created_at: z.number(), - stripe_id: z.string().nullish(), - archived: z.boolean().default(false).nullish(), + free_trial: FreeTrialSchema.nullish(), + items: z.array(ProductItemSchema), + created_at: z.number(), + stripe_id: z.string().nullish(), + archived: z.boolean().default(false).nullish(), }); export type ProductV2 = z.infer; diff --git a/shared/models/rewardModels/referralModels/referralCodeRelations.ts b/shared/models/rewardModels/referralModels/referralCodeRelations.ts index 34a90af37..f1ddc352f 100644 --- a/shared/models/rewardModels/referralModels/referralCodeRelations.ts +++ b/shared/models/rewardModels/referralModels/referralCodeRelations.ts @@ -5,17 +5,17 @@ import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; import { rewardRedemptions } from "./rewardRedemptionTable.js"; export const referralCodeRelations = relations( - referralCodes, - ({ many, one }) => ({ - reward_program: one(rewardPrograms, { - fields: [referralCodes.internal_reward_program_id], - references: [rewardPrograms.internal_id], - }), - customer: one(customers, { - fields: [referralCodes.internal_customer_id], - references: [customers.internal_id], - }), + referralCodes, + ({ many, one }) => ({ + reward_program: one(rewardPrograms, { + fields: [referralCodes.internal_reward_program_id], + references: [rewardPrograms.internal_id], + }), + customer: one(customers, { + fields: [referralCodes.internal_customer_id], + references: [customers.internal_id], + }), - reward_redemptions: many(rewardRedemptions), - }), + reward_redemptions: many(rewardRedemptions), + }), ); diff --git a/shared/models/rewardModels/referralModels/referralCodeTable.ts b/shared/models/rewardModels/referralModels/referralCodeTable.ts index 71ab0b030..8b93ded6e 100644 --- a/shared/models/rewardModels/referralModels/referralCodeTable.ts +++ b/shared/models/rewardModels/referralModels/referralCodeTable.ts @@ -1,47 +1,47 @@ import { - text, - foreignKey, - pgTable, - numeric, - primaryKey, - unique, + text, + foreignKey, + pgTable, + numeric, + primaryKey, + unique, } from "drizzle-orm/pg-core"; import { customers } from "../../cusModels/cusTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; import { organizations } from "../../orgModels/orgTable.js"; export const referralCodes = pgTable( - "referral_codes", - { - code: text().notNull(), - org_id: text("org_id").notNull(), - env: text().notNull(), - internal_customer_id: text("internal_customer_id"), - internal_reward_program_id: text("internal_reward_program_id"), - id: text().notNull(), - created_at: numeric({ mode: "number" }), - }, + "referral_codes", + { + code: text().notNull(), + org_id: text("org_id").notNull(), + env: text().notNull(), + internal_customer_id: text("internal_customer_id"), + internal_reward_program_id: text("internal_reward_program_id"), + id: text().notNull(), + created_at: numeric({ mode: "number" }), + }, - (table) => [ - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "referral_codes_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internal_reward_program_id], - foreignColumns: [rewardPrograms.internal_id], - name: "referral_codes_internal_reward_program_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "referral_codes_org_id_fkey", - }).onDelete("cascade"), - primaryKey({ - columns: [table.code, table.org_id, table.env], - name: "referral_codes_pkey", - }), - unique("referral_codes_id_key").on(table.id), - ], + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "referral_codes_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_reward_program_id], + foreignColumns: [rewardPrograms.internal_id], + name: "referral_codes_internal_reward_program_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "referral_codes_org_id_fkey", + }).onDelete("cascade"), + primaryKey({ + columns: [table.code, table.org_id, table.env], + name: "referral_codes_pkey", + }), + unique("referral_codes_id_key").on(table.id), + ], ); diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts index f3b9dcfc9..4ed8c489e 100644 --- a/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts +++ b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts @@ -5,21 +5,21 @@ import { referralCodes } from "./referralCodeTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; export const rewardRedemptionRelations = relations( - rewardRedemptions, - ({ one }) => ({ - customer: one(customers, { - fields: [rewardRedemptions.internal_customer_id], - references: [customers.internal_id], - }), + rewardRedemptions, + ({ one }) => ({ + customer: one(customers, { + fields: [rewardRedemptions.internal_customer_id], + references: [customers.internal_id], + }), - referral_code: one(referralCodes, { - fields: [rewardRedemptions.referral_code_id], - references: [referralCodes.id], - }), + referral_code: one(referralCodes, { + fields: [rewardRedemptions.referral_code_id], + references: [referralCodes.id], + }), - reward_program: one(rewardPrograms, { - fields: [rewardRedemptions.internal_reward_program_id], - references: [rewardPrograms.internal_id], - }), - }), + reward_program: one(rewardPrograms, { + fields: [rewardRedemptions.internal_reward_program_id], + references: [rewardPrograms.internal_id], + }), + }), ); diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts index 7f833783e..fef25e076 100644 --- a/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts +++ b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts @@ -1,42 +1,42 @@ import { - text, - foreignKey, - boolean, - pgTable, - numeric, + text, + foreignKey, + boolean, + pgTable, + numeric, } from "drizzle-orm/pg-core"; import { customers } from "../../cusModels/cusTable.js"; import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; import { referralCodes } from "./referralCodeTable.js"; export const rewardRedemptions = pgTable( - "reward_redemptions", - { - id: text().primaryKey().notNull(), - created_at: numeric({ mode: "number" }), - updated_at: numeric({ mode: "number" }), - internal_customer_id: text("internal_customer_id"), - triggered: boolean(), - internal_reward_program_id: text("internal_reward_program_id"), - applied: boolean().default(false), - redeemer_applied: boolean().default(false), - referral_code_id: text("referral_code_id"), - }, - (table) => [ - foreignKey({ - columns: [table.internal_customer_id], - foreignColumns: [customers.internal_id], - name: "reward_redemptions_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internal_reward_program_id], - foreignColumns: [rewardPrograms.internal_id], - name: "reward_redemptions_internal_reward_program_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.referral_code_id], - foreignColumns: [referralCodes.id], - name: "reward_redemptions_referral_code_id_fkey", - }).onDelete("cascade"), - ] + "reward_redemptions", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }), + updated_at: numeric({ mode: "number" }), + internal_customer_id: text("internal_customer_id"), + triggered: boolean(), + internal_reward_program_id: text("internal_reward_program_id"), + applied: boolean().default(false), + redeemer_applied: boolean().default(false), + referral_code_id: text("referral_code_id"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "reward_redemptions_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_reward_program_id], + foreignColumns: [rewardPrograms.internal_id], + name: "reward_redemptions_internal_reward_program_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.referral_code_id], + foreignColumns: [referralCodes.id], + name: "reward_redemptions_referral_code_id_fkey", + }).onDelete("cascade"), + ], ); diff --git a/shared/models/rewardModels/rewardModels/rewardEnums.ts b/shared/models/rewardModels/rewardModels/rewardEnums.ts index 5099e1c98..fd538f201 100644 --- a/shared/models/rewardModels/rewardModels/rewardEnums.ts +++ b/shared/models/rewardModels/rewardModels/rewardEnums.ts @@ -1,22 +1,22 @@ export enum RewardCategory { - FreeProduct = "free_product", - Discount = "discount", + FreeProduct = "free_product", + Discount = "discount", } export enum CouponDurationType { - OneOff = "one_off", - Months = "months", - Forever = "forever", + OneOff = "one_off", + Months = "months", + Forever = "forever", } export enum DiscountType { - Percentage = "percentage", - Fixed = "fixed", + Percentage = "percentage", + Fixed = "fixed", } export enum RewardType { - PercentageDiscount = "percentage_discount", - FixedDiscount = "fixed_discount", - FreeProduct = "free_product", - InvoiceCredits = "invoice_credits", + PercentageDiscount = "percentage_discount", + FixedDiscount = "fixed_discount", + FreeProduct = "free_product", + InvoiceCredits = "invoice_credits", } diff --git a/shared/models/rewardModels/rewardModels/rewardModels.ts b/shared/models/rewardModels/rewardModels/rewardModels.ts index 12adfb5d4..116afdf5b 100644 --- a/shared/models/rewardModels/rewardModels/rewardModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardModels.ts @@ -2,48 +2,48 @@ import { z } from "zod"; import { CouponDurationType, RewardType } from "./rewardEnums.js"; const PromoCodeSchema = z.object({ - code: z.string(), + code: z.string(), }); export const DiscountConfigSchema = z.object({ - discount_value: z.number(), - duration_type: z.nativeEnum(CouponDurationType), - duration_value: z.number(), - should_rollover: z.boolean().optional(), - apply_to_all: z.boolean().optional(), - price_ids: z.array(z.string()).optional(), + discount_value: z.number(), + duration_type: z.nativeEnum(CouponDurationType), + duration_value: z.number(), + should_rollover: z.boolean().optional(), + apply_to_all: z.boolean().optional(), + price_ids: z.array(z.string()).optional(), }); export const FreeProductConfigSchema = z.object({ - duration_type: z.nativeEnum(CouponDurationType), - duration_value: z.number(), + duration_type: z.nativeEnum(CouponDurationType), + duration_value: z.number(), }); const RewardSchema = z.object({ - name: z.string().nullish(), + name: z.string().nullish(), - promo_codes: z.array(PromoCodeSchema), - id: z.string(), - type: z.nativeEnum(RewardType), + promo_codes: z.array(PromoCodeSchema), + id: z.string(), + type: z.nativeEnum(RewardType), - free_product_id: z.string().nullish(), - discount_config: DiscountConfigSchema.nullish(), - free_product_config: FreeProductConfigSchema.nullish(), + free_product_id: z.string().nullish(), + discount_config: DiscountConfigSchema.nullish(), + free_product_config: FreeProductConfigSchema.nullish(), - internal_id: z.string(), - org_id: z.string(), - env: z.string(), - created_at: z.number(), + internal_id: z.string(), + org_id: z.string(), + env: z.string(), + created_at: z.number(), }); export const CreateRewardSchema = z.object({ - name: z.string(), - promo_codes: z.array(PromoCodeSchema), - id: z.string(), - type: z.nativeEnum(RewardType).nullish(), - discount_config: DiscountConfigSchema.nullish(), - free_product_config: FreeProductConfigSchema.nullish(), - free_product_id: z.string().nullish(), + name: z.string(), + promo_codes: z.array(PromoCodeSchema), + id: z.string(), + type: z.nativeEnum(RewardType).nullish(), + discount_config: DiscountConfigSchema.nullish(), + free_product_config: FreeProductConfigSchema.nullish(), + free_product_id: z.string().nullish(), }); export type PromoCode = z.infer; diff --git a/shared/models/rewardModels/rewardModels/rewardResponseModels.ts b/shared/models/rewardModels/rewardModels/rewardResponseModels.ts index 4233be3db..b47a62511 100644 --- a/shared/models/rewardModels/rewardModels/rewardResponseModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardResponseModels.ts @@ -3,25 +3,25 @@ import { RewardType } from "./rewardEnums.js"; import { CouponDurationType } from "./rewardEnums.js"; export const DiscountResponseSchema = z.object({ - id: z.string(), // either from Autumn or Stripe - name: z.string(), + id: z.string(), // either from Autumn or Stripe + name: z.string(), - type: z.nativeEnum(RewardType), - discount_value: z.number(), - // stripe_coupon_id: z.string(), // coupon ID - // stripe_discount_id: z.string(), // stripe discount ID - duration_type: z.nativeEnum(CouponDurationType), - duration_value: z.number().nullish(), - currency: z.string().nullish(), + type: z.nativeEnum(RewardType), + discount_value: z.number(), + // stripe_coupon_id: z.string(), // coupon ID + // stripe_discount_id: z.string(), // stripe discount ID + duration_type: z.nativeEnum(CouponDurationType), + duration_value: z.number().nullish(), + currency: z.string().nullish(), - start: z.number().nullish(), - end: z.number().nullish(), + start: z.number().nullish(), + end: z.number().nullish(), - subscription_id: z.string().nullable(), + subscription_id: z.string().nullable(), }); export const RewardResponseSchema = z.object({ - discounts: z.array(DiscountResponseSchema), + discounts: z.array(DiscountResponseSchema), }); export type RewardResponse = z.infer; diff --git a/shared/models/rewardModels/rewardModels/rewardTable.ts b/shared/models/rewardModels/rewardModels/rewardTable.ts index b8248ef6e..d9e3e6d79 100644 --- a/shared/models/rewardModels/rewardModels/rewardTable.ts +++ b/shared/models/rewardModels/rewardModels/rewardTable.ts @@ -2,9 +2,9 @@ import type { InferInsertModel, InferSelectModel } from "drizzle-orm"; import { foreignKey, jsonb, numeric, pgTable, text } from "drizzle-orm/pg-core"; import { organizations } from "../../orgModels/orgTable.js"; import type { - DiscountConfig, - FreeProductConfig, - PromoCode, + DiscountConfig, + FreeProductConfig, + PromoCode, } from "./rewardModels.js"; export const rewards = pgTable( diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts index 709d0aa6e..bd01d5f4b 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts @@ -1,9 +1,9 @@ export enum RewardTriggerEvent { - CustomerCreation = "customer_creation", - Checkout = "checkout", + CustomerCreation = "customer_creation", + Checkout = "checkout", } export enum RewardReceivedBy { - Referrer = "referrer", - All = "all", + Referrer = "referrer", + All = "all", } diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts index 8a10fd9c3..f6f867155 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts @@ -4,38 +4,38 @@ import { RewardReceivedBy } from "./rewardProgramEnums.js"; import { RewardTriggerEvent } from "./rewardProgramEnums.js"; export const RewardProgram = z.object({ - internal_id: z.string(), - id: z.string(), + internal_id: z.string(), + id: z.string(), - when: z.nativeEnum(RewardTriggerEvent), - product_ids: z.array(z.string()).optional(), - exclude_trial: z.boolean().optional(), + when: z.nativeEnum(RewardTriggerEvent), + product_ids: z.array(z.string()).optional(), + exclude_trial: z.boolean().optional(), - internal_reward_id: z.string(), + internal_reward_id: z.string(), - unlimited_redemptions: z.boolean().optional(), - max_redemptions: z.number().optional(), + unlimited_redemptions: z.boolean().optional(), + max_redemptions: z.number().optional(), - org_id: z.string(), - env: z.string(), - created_at: z.number(), + org_id: z.string(), + env: z.string(), + created_at: z.number(), - received_by: z.nativeEnum(RewardReceivedBy), + received_by: z.nativeEnum(RewardReceivedBy), }); export const CreateRewardProgram = z.object({ - id: z.string(), - when: z.nativeEnum(RewardTriggerEvent), - product_ids: z.array(z.string()).optional(), - exclude_trial: z.boolean().optional(), - internal_reward_id: z.string(), - max_redemptions: z.number().optional(), - received_by: z.nativeEnum(RewardReceivedBy), + id: z.string(), + when: z.nativeEnum(RewardTriggerEvent), + product_ids: z.array(z.string()).optional(), + exclude_trial: z.boolean().optional(), + internal_reward_id: z.string(), + max_redemptions: z.number().optional(), + received_by: z.nativeEnum(RewardReceivedBy), }); export type RewardProgram = z.infer; export type CreateRewardProgram = z.infer; export type FullRewardProgram = RewardProgram & { - reward: Reward; + reward: Reward; }; diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts index d3da2afae..7e4633759 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts @@ -3,8 +3,8 @@ import { rewardPrograms } from "./rewardProgramTable.js"; import { rewards } from "../rewardModels/rewardTable.js"; export const rewardProgramRelations = relations(rewardPrograms, ({ one }) => ({ - reward: one(rewards, { - fields: [rewardPrograms.internal_reward_id], - references: [rewards.internal_id], - }), + reward: one(rewards, { + fields: [rewardPrograms.internal_reward_id], + references: [rewards.internal_id], + }), })); diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts index 5f83640bc..4e9d3eebe 100644 --- a/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts @@ -5,34 +5,34 @@ import { rewards } from "../rewardModels/rewardTable.js"; import { InferSelectModel, InferInsertModel } from "drizzle-orm"; export const rewardPrograms = pgTable( - "reward_programs", - { - internal_id: text("internal_id").primaryKey().notNull(), - id: text(), - created_at: numeric({ mode: "number" }), - internal_reward_id: text("internal_reward_id"), - max_redemptions: numeric({ mode: "number" }), - unlimited_redemptions: boolean("unlimited_redemptions").default(false), - org_id: text("org_id"), - env: text(), - when: text().default("immediately"), - product_ids: text("product_ids").array().default([""]), - exclude_trial: boolean("exclude_trial").default(false), - received_by: text("received_by"), - }, - (table) => [ - foreignKey({ - columns: [table.internal_reward_id], - foreignColumns: [rewards.internal_id], - name: "reward_triggers_internal_reward_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "reward_triggers_org_id_fkey", - }).onDelete("cascade"), - ], + "reward_programs", + { + internal_id: text("internal_id").primaryKey().notNull(), + id: text(), + created_at: numeric({ mode: "number" }), + internal_reward_id: text("internal_reward_id"), + max_redemptions: numeric({ mode: "number" }), + unlimited_redemptions: boolean("unlimited_redemptions").default(false), + org_id: text("org_id"), + env: text(), + when: text().default("immediately"), + product_ids: text("product_ids").array().default([""]), + exclude_trial: boolean("exclude_trial").default(false), + received_by: text("received_by"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_reward_id], + foreignColumns: [rewards.internal_id], + name: "reward_triggers_internal_reward_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "reward_triggers_org_id_fkey", + }).onDelete("cascade"), + ], ); export type RewardProgramRow = InferSelectModel; -export type InsertRewardProgramRow = InferInsertModel; \ No newline at end of file +export type InsertRewardProgramRow = InferInsertModel; diff --git a/shared/models/subModels/subModels.ts b/shared/models/subModels/subModels.ts index 5558363a2..fe75d98cd 100644 --- a/shared/models/subModels/subModels.ts +++ b/shared/models/subModels/subModels.ts @@ -2,19 +2,19 @@ import { z } from "zod"; import { AppEnv } from "../genModels/genEnums.js"; export const SubscriptionSchema = z.object({ - id: z.string(), - stripe_id: z.string().nullable(), - stripe_schedule_id: z.string().nullable(), + id: z.string(), + stripe_id: z.string().nullable(), + stripe_schedule_id: z.string().nullable(), - created_at: z.number(), - // metadata: z.record(z.string(), z.any()), - usage_features: z.array(z.string()), - org_id: z.string(), + created_at: z.number(), + // metadata: z.record(z.string(), z.any()), + usage_features: z.array(z.string()), + org_id: z.string(), - current_period_start: z.number().nullable(), - current_period_end: z.number().nullable(), + current_period_start: z.number().nullable(), + current_period_end: z.number().nullable(), - env: z.nativeEnum(AppEnv), + env: z.nativeEnum(AppEnv), }); export type Subscription = z.infer; diff --git a/shared/models/subModels/subTable.ts b/shared/models/subModels/subTable.ts index 551bdbbf2..456c4344c 100644 --- a/shared/models/subModels/subTable.ts +++ b/shared/models/subModels/subTable.ts @@ -1,40 +1,40 @@ import { - pgTable, - text, - numeric, - jsonb, - foreignKey, - unique, + pgTable, + text, + numeric, + jsonb, + foreignKey, + unique, } from "drizzle-orm/pg-core"; import { organizations } from "../orgModels/orgTable.js"; import { sqlNow } from "../../db/utils.js"; import { InferSelectModel, InferInsertModel } from "drizzle-orm"; export const subscriptions = pgTable( - "subscriptions", - { - id: text().primaryKey().notNull(), - org_id: text("org_id").notNull(), - stripe_id: text("stripe_id"), - stripe_schedule_id: text("stripe_schedule_id"), - created_at: numeric({ mode: "number" }).notNull().default(sqlNow), - metadata: jsonb().default({}), - usage_features: text("usage_features").array().default([]), - env: text(), + "subscriptions", + { + id: text().primaryKey().notNull(), + org_id: text("org_id").notNull(), + stripe_id: text("stripe_id"), + stripe_schedule_id: text("stripe_schedule_id"), + created_at: numeric({ mode: "number" }).notNull().default(sqlNow), + metadata: jsonb().default({}), + usage_features: text("usage_features").array().default([]), + env: text(), - current_period_start: numeric({ mode: "number" }), - current_period_end: numeric({ mode: "number" }), - }, - (table) => [ - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "subscriptions_org_id_fkey", - }).onDelete("cascade"), + current_period_start: numeric({ mode: "number" }), + current_period_end: numeric({ mode: "number" }), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "subscriptions_org_id_fkey", + }).onDelete("cascade"), - unique("subscriptions_stripe_id_key").on(table.stripe_id), - ], + unique("subscriptions_stripe_id_key").on(table.stripe_id), + ], ); export type SubscriptionRow = InferSelectModel; -export type InsertSubscriptionRow = InferInsertModel; \ No newline at end of file +export type InsertSubscriptionRow = InferInsertModel; diff --git a/shared/package.json b/shared/package.json index 624b58bae..6d3cc8b45 100644 --- a/shared/package.json +++ b/shared/package.json @@ -1,47 +1,47 @@ { - "name": "@autumn/shared", - "type": "module", - "version": "1.0.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "author": "Recase Inc.", - "license": "Apache-2.0", - "scripts": { - "build:tsc": "tsc", - "build": "bun build ./index.ts --outdir dist --format esm --target bun --external zod", - "dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build && bun run dev:dts\"", - "dev:dts": "tsc --emitDeclarationOnly --outDir dist --project tsconfig.json", - "dev:bun": "bun ./index.ts --outdir dist --target bun --external zod --watch", - "db:push": "bun db:generate && bun db:migrate", - "db:generate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit generate --config drizzle.config.ts", - "db:migrate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit migrate --config drizzle.config.ts", - "db:studio": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit studio --config drizzle.config.ts" - }, - "dependencies": { - "date-fns": "^4.1.0", - "decimal.js": "^10.5.0", - "dotenv": "^16.5.0", - "drizzle-kit": "^0.31.1", - "drizzle-orm": "^0.43.1", - "drizzle-zod": "^0.8.2" - }, - "peerDependencies": { - "zod": "^3.25.23" - }, - "devDependencies": { - "@types/bun": "latest", - "@types/node": "^24.0.3", - "@typescript/native-preview": "^7.0.0-dev.20250731.1", - "cross-env": "^7.0.3", - "nodemon": "^3.1.7", - "tsx": "^4.19.4", - "typescript": "^5.7.2" - }, - "private": true + "name": "@autumn/shared", + "type": "module", + "version": "1.0.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "author": "Recase Inc.", + "license": "Apache-2.0", + "scripts": { + "build:tsc": "tsc", + "build": "bun build ./index.ts --outdir dist --format esm --target bun --external zod", + "dev": "bunx nodemon --ext ts --ignore dist --exec \"bun run build && bun run dev:dts\"", + "dev:dts": "tsc --emitDeclarationOnly --outDir dist --project tsconfig.json", + "dev:bun": "bun ./index.ts --outdir dist --target bun --external zod --watch", + "db:push": "bun db:generate && bun db:migrate", + "db:generate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit generate --config drizzle.config.ts", + "db:migrate": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit migrate --config drizzle.config.ts", + "db:studio": "cross-env NODE_OPTIONS=\"--import tsx\" bunx drizzle-kit studio --config drizzle.config.ts" + }, + "dependencies": { + "date-fns": "^4.1.0", + "decimal.js": "^10.5.0", + "dotenv": "^16.5.0", + "drizzle-kit": "^0.31.1", + "drizzle-orm": "^0.43.1", + "drizzle-zod": "^0.8.2" + }, + "peerDependencies": { + "zod": "^3.25.23" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^24.0.3", + "@typescript/native-preview": "^7.0.0-dev.20250731.1", + "cross-env": "^7.0.3", + "nodemon": "^3.1.7", + "tsx": "^4.19.4", + "typescript": "^5.7.2" + }, + "private": true } diff --git a/shared/tsconfig.json b/shared/tsconfig.json index 480c11925..53948bd9c 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -1,19 +1,18 @@ { - "compilerOptions": { - "strict": true, - "composite": true, - "declaration": true, - "declarationMap": true, - "esModuleInterop": true, - "skipLibCheck": true, - "module": "NodeNext", - "moduleResolution": "NodeNext", - "target": "ES2020", - "outDir": "./dist", - "rootDir": ".", - - }, - "include": ["./**/*"], - "types": ["node"], - "exclude": ["node_modules", "dist"] + "compilerOptions": { + "strict": true, + "composite": true, + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2020", + "outDir": "./dist", + "rootDir": "." + }, + "include": ["./**/*"], + "types": ["node"], + "exclude": ["node_modules", "dist"] } diff --git a/shared/utils/cusEntUtils/balanceUtils.ts b/shared/utils/cusEntUtils/balanceUtils.ts index 6b600c801..0411f80db 100644 --- a/shared/utils/cusEntUtils/balanceUtils.ts +++ b/shared/utils/cusEntUtils/balanceUtils.ts @@ -2,70 +2,70 @@ import { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntMod import { notNullish, nullish } from "../utils.js"; 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, + }; }; 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, - }); - } else { - let entityBalance = cusEnt.entities?.[entityId!]?.balance; - let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0; + if (notNullish(entitlement.entity_feature_id)) { + if (nullish(entityId)) { + return getSummedEntityBalances({ + cusEnt, + }); + } else { + let entityBalance = cusEnt.entities?.[entityId!]?.balance; + let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0; - if (nullish(entityBalance)) { - return { balance: 0, adjustment: 0, unused: 0, count: 1 }; - } + if (nullish(entityBalance)) { + return { balance: 0, adjustment: 0, unused: 0, count: 1 }; + } - return { - balance: entityBalance || 0, - adjustment, - unused: 0, - count: 1, - }; - } - } + return { + balance: entityBalance || 0, + adjustment, + 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, + }; }; diff --git a/shared/utils/cusEntUtils/sortCusEntsForDeduction.ts b/shared/utils/cusEntUtils/sortCusEntsForDeduction.ts index 43b24ad97..1f04975f6 100644 --- a/shared/utils/cusEntUtils/sortCusEntsForDeduction.ts +++ b/shared/utils/cusEntUtils/sortCusEntsForDeduction.ts @@ -5,109 +5,109 @@ import { AllowanceType } from "../../models/productModels/entModels/entModels.js import { entIntervalToValue } from "../intervalUtils.js"; export const sortCusEntsForDeduction = ( - cusEnts: (FullCustomerEntitlement & { - customer_product?: FullCusProduct; - })[], - reverseOrder: boolean = false + cusEnts: (FullCustomerEntitlement & { + customer_product?: FullCusProduct; + })[], + reverseOrder: boolean = false, ) => { - cusEnts.sort((a, b) => { - const aEnt = a.entitlement; - const bEnt = b.entitlement; + cusEnts.sort((a, b) => { + const aEnt = a.entitlement; + const bEnt = b.entitlement; - // 1. If boolean, go first - if (aEnt.feature.type == FeatureType.Boolean) { - return -1; - } + // 1. If boolean, go first + if (aEnt.feature.type == FeatureType.Boolean) { + return -1; + } - if (bEnt.feature.type == FeatureType.Boolean) { - return 1; - } + if (bEnt.feature.type == FeatureType.Boolean) { + return 1; + } - // 1. If a is credit system and b is not, a should go last - if ( - aEnt.feature.type == FeatureType.CreditSystem && - bEnt.feature.type != FeatureType.CreditSystem - ) { - return 1; - } + // 1. If a is credit system and b is not, a should go last + if ( + aEnt.feature.type == FeatureType.CreditSystem && + bEnt.feature.type != FeatureType.CreditSystem + ) { + return 1; + } - // 2. If a is not credit system and b is, a should go first - if ( - aEnt.feature.type != FeatureType.CreditSystem && - bEnt.feature.type == FeatureType.CreditSystem - ) { - return -1; - } + // 2. If a is not credit system and b is, a should go first + if ( + aEnt.feature.type != FeatureType.CreditSystem && + bEnt.feature.type == FeatureType.CreditSystem + ) { + return -1; + } - // 2. Sort by unlimited (unlimited goes first) - if ( - aEnt.allowance_type == AllowanceType.Unlimited && - bEnt.allowance_type != AllowanceType.Unlimited - ) { - return -1; - } + // 2. Sort by unlimited (unlimited goes first) + if ( + aEnt.allowance_type == AllowanceType.Unlimited && + bEnt.allowance_type != AllowanceType.Unlimited + ) { + return -1; + } - if ( - aEnt.allowance_type != AllowanceType.Unlimited && - bEnt.allowance_type == AllowanceType.Unlimited - ) { - return 1; - } + if ( + aEnt.allowance_type != AllowanceType.Unlimited && + bEnt.allowance_type == AllowanceType.Unlimited + ) { + return 1; + } - // If one has usage_allowed, it should go last - if (!a.usage_allowed && b.usage_allowed) { - return -1; - } + // If one has usage_allowed, it should go last + if (!a.usage_allowed && b.usage_allowed) { + return -1; + } - if (!b.usage_allowed && a.usage_allowed) { - return 1; - } + if (!b.usage_allowed && a.usage_allowed) { + return 1; + } - // If one has a next_reset_at, it should go first - let nextResetFirst = reverseOrder ? 1 : -1; + // If one has a next_reset_at, it should go first + let nextResetFirst = reverseOrder ? 1 : -1; - if (a.next_reset_at && !b.next_reset_at) { - return nextResetFirst; - } + if (a.next_reset_at && !b.next_reset_at) { + return nextResetFirst; + } - // If b has a next_reset_at, it should go first - if (!a.next_reset_at && b.next_reset_at) { - return -nextResetFirst; - } + // If b has a next_reset_at, it should go first + if (!a.next_reset_at && b.next_reset_at) { + return -nextResetFirst; + } - // 3. Sort by interval - let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count); - let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count); - if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) { - if (reverseOrder) { - return bVal.sub(aVal).toNumber(); - // return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval]; - } else { - return aVal.sub(bVal).toNumber(); - // return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval]; - } - } + // 3. Sort by interval + let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count); + let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count); + if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) { + if (reverseOrder) { + return bVal.sub(aVal).toNumber(); + // return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval]; + } else { + return aVal.sub(bVal).toNumber(); + // return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval]; + } + } - // Check if a is main product - let aIsAddOn = a.customer_product?.product?.is_add_on; - let bIsAddOn = b.customer_product?.product?.is_add_on; + // Check if a is main product + let aIsAddOn = a.customer_product?.product?.is_add_on; + let bIsAddOn = b.customer_product?.product?.is_add_on; - if (aIsAddOn && !bIsAddOn) { - return 1; - } + if (aIsAddOn && !bIsAddOn) { + return 1; + } - if (!aIsAddOn && bIsAddOn) { - return -1; - } + if (!aIsAddOn && bIsAddOn) { + return -1; + } - // 4. Sort by created_at - return a.created_at - b.created_at; - }); + // 4. Sort by created_at + return a.created_at - b.created_at; + }); - // console.log( - // `Cus ents after (${reverseOrder ? "reversed" : "normal"})`, - // cusEnts.map( - // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}` - // ) - // ); + // console.log( + // `Cus ents after (${reverseOrder ? "reversed" : "normal"})`, + // cusEnts.map( + // (ce) => `${ce.entitlement.feature_id} - ${ce.entitlement.interval}` + // ) + // ); }; diff --git a/shared/utils/cusProductUtils/classifyCusProduct.ts b/shared/utils/cusProductUtils/classifyCusProduct.ts index c0e786de9..77101dc4d 100644 --- a/shared/utils/cusProductUtils/classifyCusProduct.ts +++ b/shared/utils/cusProductUtils/classifyCusProduct.ts @@ -1,17 +1,17 @@ import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js"; export const isCanceled = ({ cusProduct }: { cusProduct: FullCusProduct }) => { - return cusProduct.canceled; + return cusProduct.canceled; }; export const isTrialing = ({ - cusProduct, - now, + cusProduct, + now, }: { - cusProduct: FullCusProduct; - now?: number; + cusProduct: FullCusProduct; + now?: number; }) => { - return ( - cusProduct.trial_ends_at && cusProduct.trial_ends_at > (now || Date.now()) - ); + return ( + cusProduct.trial_ends_at && cusProduct.trial_ends_at > (now || Date.now()) + ); }; diff --git a/shared/utils/cusProductUtils/convertCusProduct.ts b/shared/utils/cusProductUtils/convertCusProduct.ts index 989f44ba7..e88d5e100 100644 --- a/shared/utils/cusProductUtils/convertCusProduct.ts +++ b/shared/utils/cusProductUtils/convertCusProduct.ts @@ -9,112 +9,112 @@ import { FullCusEntWithFullCusProduct } from "../../models/cusProductModels/cusE import { FullProduct } from "../../models/productModels/productModels.js"; export const cusProductsToPrices = ({ - cusProducts, + cusProducts, }: { - cusProducts: FullCusProduct[]; + cusProducts: FullCusProduct[]; }) => { - return cusProducts.flatMap((cp) => cusProductToPrices({ cusProduct: cp })); + return cusProducts.flatMap((cp) => cusProductToPrices({ cusProduct: cp })); }; export const cusProductsToCusPrices = ({ - cusProducts, - inStatuses, - billingType, + cusProducts, + inStatuses, + billingType, }: { - cusProducts: FullCusProduct[]; - inStatuses?: CusProductStatus[]; - billingType?: BillingType; + cusProducts: FullCusProduct[]; + inStatuses?: CusProductStatus[]; + billingType?: BillingType; }) => { - const cusPrices: FullCustomerPrice[] = []; + const cusPrices: FullCustomerPrice[] = []; - for (const cusProduct of cusProducts) { - if (inStatuses && !inStatuses.includes(cusProduct.status)) { - continue; - } + for (const cusProduct of cusProducts) { + if (inStatuses && !inStatuses.includes(cusProduct.status)) { + continue; + } - let prices = cusProduct.customer_prices; - if (billingType) { - prices = prices.filter( - (cp) => getBillingType(cp.price.config) === billingType - ); - } + let prices = cusProduct.customer_prices; + if (billingType) { + prices = prices.filter( + (cp) => getBillingType(cp.price.config) === billingType, + ); + } - cusPrices.push(...prices); - } + cusPrices.push(...prices); + } - return cusPrices; + return cusPrices; }; export const cusProductsToCusEnts = ({ - cusProducts, - inStatuses = [CusProductStatus.Active], - reverseOrder = false, - featureId, + cusProducts, + inStatuses = [CusProductStatus.Active], + reverseOrder = false, + featureId, }: { - cusProducts: FullCusProduct[]; - inStatuses?: CusProductStatus[]; - reverseOrder?: boolean; - featureId?: string; + cusProducts: FullCusProduct[]; + inStatuses?: CusProductStatus[]; + reverseOrder?: boolean; + featureId?: string; }) => { - let cusEnts: FullCustomerEntitlement[] = []; + let cusEnts: FullCustomerEntitlement[] = []; - for (const cusProduct of cusProducts) { - if (!inStatuses.includes(cusProduct.status)) { - continue; - } + for (const cusProduct of cusProducts) { + if (!inStatuses.includes(cusProduct.status)) { + continue; + } - cusEnts.push( - ...cusProduct.customer_entitlements.map((cusEnt) => ({ - ...cusEnt, - customer_product: cusProduct, - })) - ); - } + cusEnts.push( + ...cusProduct.customer_entitlements.map((cusEnt) => ({ + ...cusEnt, + customer_product: cusProduct, + })), + ); + } - if (featureId) { - cusEnts = cusEnts.filter( - (cusEnt) => cusEnt.entitlement.feature_id === featureId - ); - } + if (featureId) { + cusEnts = cusEnts.filter( + (cusEnt) => cusEnt.entitlement.feature_id === featureId, + ); + } - sortCusEntsForDeduction(cusEnts, reverseOrder); + sortCusEntsForDeduction(cusEnts, reverseOrder); - return cusEnts as FullCusEntWithFullCusProduct[]; + return cusEnts as FullCusEntWithFullCusProduct[]; }; export const cusProductToPrices = ({ - cusProduct, - billingType, + cusProduct, + billingType, }: { - cusProduct: FullCusProduct; - billingType?: BillingType; + cusProduct: FullCusProduct; + billingType?: BillingType; }) => { - let prices = cusProduct.customer_prices.map((cp) => cp.price); + let prices = cusProduct.customer_prices.map((cp) => cp.price); - if (billingType) { - prices = prices.filter((p) => getBillingType(p.config) === billingType); - } + if (billingType) { + prices = prices.filter((p) => getBillingType(p.config) === billingType); + } - return prices; + return prices; }; export const cusProductToEnts = ({ - cusProduct, + cusProduct, }: { - cusProduct: FullCusProduct; + cusProduct: FullCusProduct; }) => { - return cusProduct.customer_entitlements.map((ce) => ce.entitlement); + return cusProduct.customer_entitlements.map((ce) => ce.entitlement); }; export const cusProductToProduct = ({ - cusProduct, + cusProduct, }: { - cusProduct: FullCusProduct; + cusProduct: FullCusProduct; }) => { - return { - ...cusProduct.product, - prices: cusProductToPrices({ cusProduct }), - entitlements: cusProductToEnts({ cusProduct }), - free_trial: cusProduct.free_trial, - } as FullProduct; + return { + ...cusProduct.product, + prices: cusProductToPrices({ cusProduct }), + entitlements: cusProductToEnts({ cusProduct }), + free_trial: cusProduct.free_trial, + } as FullProduct; }; diff --git a/shared/utils/cusProductUtils/cusProductConstants.ts b/shared/utils/cusProductUtils/cusProductConstants.ts index b3d8403f7..9d34a34c3 100644 --- a/shared/utils/cusProductUtils/cusProductConstants.ts +++ b/shared/utils/cusProductUtils/cusProductConstants.ts @@ -1,12 +1,12 @@ import { CusProductStatus } from "../../models/cusProductModels/cusProductEnums.js"; 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, ]; diff --git a/shared/utils/cusProductUtils/cusProductUtils.ts b/shared/utils/cusProductUtils/cusProductUtils.ts index 5fa5f1edc..ef7b2ca70 100644 --- a/shared/utils/cusProductUtils/cusProductUtils.ts +++ b/shared/utils/cusProductUtils/cusProductUtils.ts @@ -2,33 +2,33 @@ import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.j import { notNullish } from "../utils.js"; export const getTotalCusProdQuantity = ({ - cusProducts, - productId, + cusProducts, + productId, }: { - cusProducts: FullCusProduct[]; - productId: string; + cusProducts: FullCusProduct[]; + productId: string; }) => { - return cusProducts - .filter((cp) => cp.product_id === productId) - .reduce((acc, curr) => { - if (notNullish(curr.internal_entity_id)) { - return acc + 1; - } else { - return acc + (curr.quantity || 1); - } - }, 0); + return cusProducts + .filter((cp) => cp.product_id === productId) + .reduce((acc, curr) => { + if (notNullish(curr.internal_entity_id)) { + return acc + 1; + } else { + return acc + (curr.quantity || 1); + } + }, 0); }; export const getCusProductMinQuantity = ({ - cusProducts, - productId, + cusProducts, + productId, }: { - cusProducts: FullCusProduct[]; - productId: string; + cusProducts: FullCusProduct[]; + productId: string; }) => { - return ( - cusProducts.filter( - (cp) => cp.product_id === productId && notNullish(cp.internal_entity_id) - ).length || 0 - ); + return ( + cusProducts.filter( + (cp) => cp.product_id === productId && notNullish(cp.internal_entity_id), + ).length || 0 + ); }; diff --git a/shared/utils/cusProductUtils/formatCusProductUtils.ts b/shared/utils/cusProductUtils/formatCusProductUtils.ts index 0d0f68975..2cc76a0ff 100644 --- a/shared/utils/cusProductUtils/formatCusProductUtils.ts +++ b/shared/utils/cusProductUtils/formatCusProductUtils.ts @@ -1,12 +1,12 @@ import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js"; export const logCusProducts = ({ - cusProducts, + cusProducts, }: { - cusProducts: FullCusProduct[]; + cusProducts: FullCusProduct[]; }) => { - console.log(`CUS PRODUCTS:`); - for (const cusProduct of cusProducts) { - console.log(`${cusProduct.product.id} - ${cusProduct.status}`); - } + console.log(`CUS PRODUCTS:`); + for (const cusProduct of cusProducts) { + console.log(`${cusProduct.product.id} - ${cusProduct.status}`); + } }; diff --git a/shared/utils/cusProductUtils/productIdToCusProduct.ts b/shared/utils/cusProductUtils/productIdToCusProduct.ts index 9c0e1f521..9dd72cf13 100644 --- a/shared/utils/cusProductUtils/productIdToCusProduct.ts +++ b/shared/utils/cusProductUtils/productIdToCusProduct.ts @@ -3,37 +3,37 @@ import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.j import { nullish } from "../utils.js"; export const productToCusProduct = ({ - productId, - cusProducts, - internalEntityId, - cusProductId, - version, - inStatuses, + productId, + cusProducts, + internalEntityId, + cusProductId, + version, + inStatuses, }: { - productId: string; - cusProducts: FullCusProduct[]; - internalEntityId?: string; - cusProductId?: string; - version?: number; - inStatuses?: CusProductStatus[]; + productId: string; + cusProducts: FullCusProduct[]; + internalEntityId?: string; + cusProductId?: string; + version?: number; + inStatuses?: CusProductStatus[]; }) => { - if (cusProductId) { - return cusProducts.find((cusProduct) => cusProduct.id === cusProductId); - } + if (cusProductId) { + return cusProducts.find((cusProduct) => cusProduct.id === cusProductId); + } - return cusProducts.find((cusProduct) => { - let prodIdMatch = cusProduct.product.id === productId; + return cusProducts.find((cusProduct) => { + let prodIdMatch = cusProduct.product.id === productId; - let entityMatch = internalEntityId - ? cusProduct.internal_entity_id === internalEntityId - : nullish(cusProduct.internal_entity_id); + let entityMatch = internalEntityId + ? cusProduct.internal_entity_id === internalEntityId + : nullish(cusProduct.internal_entity_id); - let versionMatch = version ? cusProduct.product.version === version : true; + let versionMatch = version ? cusProduct.product.version === version : true; - let statusMatch = inStatuses - ? inStatuses.includes(cusProduct.status) - : true; + let statusMatch = inStatuses + ? inStatuses.includes(cusProduct.status) + : true; - return prodIdMatch && entityMatch && versionMatch && statusMatch; - }); + return prodIdMatch && entityMatch && versionMatch && statusMatch; + }); }; diff --git a/shared/utils/displayUtils.ts b/shared/utils/displayUtils.ts index 8919b7cf3..bf27a4dbf 100644 --- a/shared/utils/displayUtils.ts +++ b/shared/utils/displayUtils.ts @@ -3,162 +3,162 @@ import { Organization } from "../models/orgModels/orgTable.js"; import { format } from "date-fns"; import { notNullish, nullish } from "./utils.js"; export const getFeatureName = ({ - feature, - plural, - units, - capitalize = false, + feature, + plural, + units, + capitalize = false, }: { - feature?: Feature; - plural?: boolean; - units?: any; - capitalize?: boolean; + feature?: Feature; + plural?: boolean; + units?: any; + capitalize?: boolean; }) => { - if (!feature) { - return ""; - } + if (!feature) { + return ""; + } - let featureName = feature.name || ""; + let featureName = feature.name || ""; - if (feature.display) { - let finalPlural; - // Case 1: If units and nullish plural - if (notNullish(units) && nullish(plural)) { - finalPlural = units !== 1; - } else if (nullish(units) && notNullish(plural)) { - finalPlural = plural; - } else { - finalPlural = plural || false; - } + if (feature.display) { + let finalPlural; + // Case 1: If units and nullish plural + if (notNullish(units) && nullish(plural)) { + finalPlural = units !== 1; + } else if (nullish(units) && notNullish(plural)) { + finalPlural = plural; + } else { + finalPlural = plural || false; + } - // if (units && units === 1) { - // plural = false; - // } - // // if (!plural) { - // // if (units && units === 1) { - // // plural = false; - // // } else { - // // plural = true; - // // } - // // } + // if (units && units === 1) { + // plural = false; + // } + // // if (!plural) { + // // if (units && units === 1) { + // // plural = false; + // // } else { + // // plural = true; + // // } + // // } - if (finalPlural) { - featureName = feature.display.plural || featureName; - } else { - featureName = feature.display.singular || featureName; - } - } + if (finalPlural) { + 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 && feature.name.length > 0) { - return `${feature.name.charAt(0).toUpperCase()}${feature.name.slice(1)}`; - } + if (feature.name && feature.name.length > 0) { + return `${feature.name.charAt(0).toUpperCase()}${feature.name.slice(1)}`; + } - return feature.name; + return feature.name; }; export const getSingularAndPlural = ({ - feature, - capitalize = false, + feature, + capitalize = false, }: { - feature: Feature; - capitalize?: boolean; + feature: Feature; + capitalize?: boolean; }) => { - return { - singular: getFeatureName({ feature, plural: false, capitalize }), - plural: getFeatureName({ feature, plural: true, capitalize }), - }; + return { + singular: getFeatureName({ feature, plural: false, capitalize }), + plural: getFeatureName({ feature, plural: true, capitalize }), + }; }; export const numberWithCommas = (x: number) => { - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; export const usageToFeatureName = ({ - usage, - feature, + usage, + feature, }: { - usage: number; - feature: Feature; + usage: number; + feature: Feature; }) => { - const { singular, plural } = getSingularAndPlural({ feature }); + const { singular, plural } = getSingularAndPlural({ feature }); - if (usage == 1) { - return singular; - } + if (usage == 1) { + return singular; + } - return plural; + return plural; }; export const getFeatureInvoiceDescription = ({ - feature, - usage, - billingUnits = 1, - prodName, - isPrepaid = false, - fromUnix, + feature, + usage, + billingUnits = 1, + prodName, + isPrepaid = false, + fromUnix, }: { - feature: Feature; - usage: number; - billingUnits?: number | null; - prodName?: string; - isPrepaid?: boolean; - fromUnix?: number; + feature: Feature; + usage: number; + billingUnits?: number | null; + prodName?: string; + isPrepaid?: boolean; + fromUnix?: number; }) => { - const { singular, plural } = getSingularAndPlural({ feature }); + const { singular, plural } = getSingularAndPlural({ feature }); - const usageStr = numberWithCommas(Math.ceil(usage)); + const usageStr = numberWithCommas(Math.ceil(usage)); - let result = ""; + let result = ""; - if (isPrepaid && billingUnits && billingUnits > 1) { - result = `${usageStr} x ${billingUnits} ${plural}`; // eg. 4 x 100 credits - } else { - if (usage == 1) { - result = `${usageStr} ${singular}`; // eg. 1 credit - } else { - result = `${usageStr} ${plural}`; // eg. 4 credits - } - } + if (isPrepaid && billingUnits && billingUnits > 1) { + result = `${usageStr} x ${billingUnits} ${plural}`; // eg. 4 x 100 credits + } else { + if (usage == 1) { + result = `${usageStr} ${singular}`; // eg. 1 credit + } else { + result = `${usageStr} ${plural}`; // eg. 4 credits + } + } - if (prodName) { - result = `${prodName} - ${result}`; - } + if (prodName) { + result = `${prodName} - ${result}`; + } - if (fromUnix) { - result = `${result} (from ${format(fromUnix, "d MMM yyyy")})`; - } + if (fromUnix) { + result = `${result} (from ${format(fromUnix, "d MMM yyyy")})`; + } - return result; + return result; }; 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/shared/utils/featureUtils.ts b/shared/utils/featureUtils.ts index 0be2058fc..3d80677a3 100644 --- a/shared/utils/featureUtils.ts +++ b/shared/utils/featureUtils.ts @@ -4,29 +4,29 @@ import { Feature } from "../models/featureModels/featureModels.js"; import { APIFeatureSchema } from "../models/featureModels/featureResModels.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, - }); + 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, + }); }; diff --git a/shared/utils/intervalUtils.ts b/shared/utils/intervalUtils.ts index c029082e5..9a3450b23 100644 --- a/shared/utils/intervalUtils.ts +++ b/shared/utils/intervalUtils.ts @@ -3,95 +3,95 @@ import { EntInterval } from "../models/productModels/entModels/entEnums.js"; import { BillingInterval } from "../models/productModels/priceModels/priceEnums.js"; export 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); }; export type IntervalConfig = { - interval: BillingInterval; - intervalCount?: number | null; + interval: BillingInterval; + intervalCount?: number | null; }; export const intervalsDifferent = ({ - intervalA, - intervalB, + intervalA, + intervalB, }: { - intervalA: IntervalConfig; - intervalB: IntervalConfig; + intervalA: IntervalConfig; + intervalB: IntervalConfig; }) => { - let valA = intervalToValue(intervalA.interval, intervalA.intervalCount); - let valB = intervalToValue(intervalB.interval, intervalB.intervalCount); - return valA != valB; + let valA = intervalToValue(intervalA.interval, intervalA.intervalCount); + let valB = intervalToValue(intervalB.interval, intervalB.intervalCount); + return valA != valB; }; export const intervalsSame = ({ - intervalA, - intervalB, + intervalA, + intervalB, }: { - intervalA: IntervalConfig; - intervalB: IntervalConfig; + intervalA: IntervalConfig; + intervalB: IntervalConfig; }) => { - return !intervalsDifferent({ intervalA, intervalB }); + return !intervalsDifferent({ intervalA, intervalB }); }; type EntIntervalConfig = { - interval: EntInterval; - intervalCount?: number | null; + interval: EntInterval; + intervalCount?: number | null; }; export const entIntervalToValue = ( - interval?: EntInterval | null, - intervalCount?: number | null + interval?: EntInterval | null, + intervalCount?: number | null, ) => { - if (!interval) { - return new Decimal(10000000); - } + if (!interval) { + return new Decimal(10000000); + } - const intervalToBaseVal: Record = { - [EntInterval.Minute]: 1, - [EntInterval.Hour]: 60, - [EntInterval.Day]: 1 * 60 * 24, - [EntInterval.Week]: 1 * 60 * 24 * 7, - [EntInterval.Month]: 1 * 60 * 24 * 30, - [EntInterval.Quarter]: 1 * 60 * 24 * 90, - [EntInterval.SemiAnnual]: 1 * 60 * 24 * 180, - [EntInterval.Year]: 1 * 60 * 24 * 365, - [EntInterval.Lifetime]: 1000000000, - }; + const intervalToBaseVal: Record = { + [EntInterval.Minute]: 1, + [EntInterval.Hour]: 60, + [EntInterval.Day]: 1 * 60 * 24, + [EntInterval.Week]: 1 * 60 * 24 * 7, + [EntInterval.Month]: 1 * 60 * 24 * 30, + [EntInterval.Quarter]: 1 * 60 * 24 * 90, + [EntInterval.SemiAnnual]: 1 * 60 * 24 * 180, + [EntInterval.Year]: 1 * 60 * 24 * 365, + [EntInterval.Lifetime]: 1000000000, + }; - const baseValue = intervalToBaseVal[interval]; - return new Decimal(baseValue).mul(intervalCount ?? 1); + const baseValue = intervalToBaseVal[interval]; + return new Decimal(baseValue).mul(intervalCount ?? 1); }; export const entIntervalsSame = ({ - intervalA, - intervalB, + intervalA, + intervalB, }: { - intervalA: EntIntervalConfig; - intervalB: EntIntervalConfig; + intervalA: EntIntervalConfig; + intervalB: EntIntervalConfig; }) => { - const valA = entIntervalToValue(intervalA.interval, intervalA.intervalCount); - const valB = entIntervalToValue(intervalB.interval, intervalB.intervalCount); - return valA.eq(valB); + const valA = entIntervalToValue(intervalA.interval, intervalA.intervalCount); + const valB = entIntervalToValue(intervalB.interval, intervalB.intervalCount); + return valA.eq(valB); }; export const entIntervalsDifferent = ({ - intervalA, - intervalB, + intervalA, + intervalB, }: { - intervalA: EntIntervalConfig; - intervalB: EntIntervalConfig; + intervalA: EntIntervalConfig; + intervalB: EntIntervalConfig; }) => { - return !entIntervalsSame({ intervalA, intervalB }); + return !entIntervalsSame({ intervalA, intervalB }); }; diff --git a/shared/utils/productDisplayUtils.ts b/shared/utils/productDisplayUtils.ts index ef1d1f2f1..475f7fb23 100644 --- a/shared/utils/productDisplayUtils.ts +++ b/shared/utils/productDisplayUtils.ts @@ -1,235 +1,235 @@ import { Feature } from "../models/featureModels/featureModels.js"; import { Infinite } from "../models/productModels/productEnums.js"; import { - ProductItem, - ProductItemFeatureType, - ProductItemInterval, + ProductItem, + ProductItemFeatureType, + ProductItemInterval, } from "../models/productV2Models/productItemModels/productItemModels.js"; import { - getFeatureName, - numberWithCommas, - formatAmount, + getFeatureName, + numberWithCommas, + formatAmount, } from "./displayUtils.js"; import { - isFeatureItem, - isFeaturePriceItem, - isPriceItem, + isFeatureItem, + isFeaturePriceItem, + isPriceItem, } from "./productDisplayUtils/getItemType.js"; import { notNullish, nullish } from "./utils.js"; 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, - }); - } + let tiers = item.tiers; + if (tiers) { + if (tiers.length == 1) { + return formatAmount({ + currency, + amount: 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({ - currency, - amount: firstPrice, - })} - ${formatAmount({ - currency, - amount: lastPrice, - })}`; - } + return `${formatAmount({ + currency, + amount: firstPrice, + })} - ${formatAmount({ + currency, + amount: lastPrice, + })}`; + } }; export const getIntervalString = ({ - interval, - intervalCount, + interval, + intervalCount, }: { - interval: ProductItemInterval; - intervalCount?: number | null; + interval: ProductItemInterval; + intervalCount?: number | null; }) => { - if (!interval) return ""; - if (intervalCount == 1) { - return `per ${interval}`; - } - return `per ${intervalCount} ${interval}s`; + if (!interval) return ""; + if (intervalCount == 1) { + return `per ${interval}`; + } + return `per ${intervalCount} ${interval}s`; }; export const getFeatureItemDisplay = ({ - item, - feature, + item, + feature, }: { - item: ProductItem; - feature?: Feature; + item: ProductItem; + feature?: Feature; }) => { - if (!feature) { - throw new Error(`Feature ${item.feature_id} not found`); - } - // 1. If feature - if (item.feature_type == ProductItemFeatureType.Static) { - return { - primary_text: getFeatureName({ - feature, - plural: false, - capitalize: true, - }), - }; - } + if (!feature) { + throw new Error(`Feature ${item.feature_id} not found`); + } + // 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}`, - }; + return { + primary_text: `${includedUsageTxt}${featureName}`, + }; }; 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 primaryText = formatAmount({ + currency, + amount: item.price as number, + }); - const intervalStr = getIntervalString({ - interval: item.interval!, - intervalCount: item.interval_count, - }); + const 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 Error(`Feature ${item.feature_id} not found`); - } + if (!feature) { + throw new Error(`Feature ${item.feature_id} not found`); + } - // 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, + }) + : ""; - 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, - secondary_text: `per ${priceStr2} ${intervalStr}`, - }; - } + if (isMainPrice) { + return { + primary_text: priceStr, + secondary_text: `per ${priceStr2} ${intervalStr}`, + }; + } - return { - primary_text: priceStr + ` per ${priceStr2} ${intervalStr}`, - secondary_text: "", - }; + return { + primary_text: priceStr + ` per ${priceStr2} ${intervalStr}`, + secondary_text: "", + }; }; export const getProductItemDisplay = ({ - item, - features, - currency = "usd", + item, + features, + currency = "usd", }: { - item: ProductItem; - features: Feature[]; - currency?: string | null; + item: ProductItem; + features: Feature[]; + currency?: string | null; }) => { - 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, - }); - } + if (isFeaturePriceItem(item)) { + return getFeaturePriceItemDisplay({ + item, + feature: features.find((f) => f.id === item.feature_id), + currency, + }); + } - return { - primary_text: "couldn't detect item type", - secondary_text: "", - }; + return { + primary_text: "couldn't detect item type", + secondary_text: "", + }; }; diff --git a/shared/utils/productDisplayUtils/getItemType.ts b/shared/utils/productDisplayUtils/getItemType.ts index f106c631e..1e04d3b66 100644 --- a/shared/utils/productDisplayUtils/getItemType.ts +++ b/shared/utils/productDisplayUtils/getItemType.ts @@ -1,44 +1,44 @@ import { - ProductItem, - ProductItemType, + ProductItem, + ProductItemType, } from "../../models/productV2Models/productItemModels/productItemModels.js"; import { notNullish, nullish } from "../utils.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/shared/utils/productDisplayUtils/getProductItemRes.ts b/shared/utils/productDisplayUtils/getProductItemRes.ts index 283c604ba..77edb6f5e 100644 --- a/shared/utils/productDisplayUtils/getProductItemRes.ts +++ b/shared/utils/productDisplayUtils/getProductItemRes.ts @@ -2,8 +2,8 @@ import { FeatureOptions } from "../../models/cusProductModels/cusProductModels.j import { Feature } from "../../models/featureModels/featureModels.js"; import { ProductItemResponseSchema } from "../../models/productV2Models/productItemModels/prodItemResponseModels.js"; import { - ProductItem, - UsageModel, + ProductItem, + UsageModel, } from "../../models/productV2Models/productItemModels/productItemModels.js"; import { toAPIFeature } from "../featureUtils.js"; import { getProductItemDisplay } from "../productDisplayUtils.js"; @@ -12,136 +12,136 @@ import { getItemType } from "./getItemType.js"; import { Decimal } from "decimal.js"; 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(); }; export type Proration = { - start: number; - end: number; + start: number; + end: number; }; 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, + }; + } + } }; export const getProductItemResponse = ({ - item, - features, - currency, - withDisplay = true, - options, + item, + features, + currency, + withDisplay = true, + options, }: { - item: ProductItem; - features: Feature[]; - currency?: string | null; - withDisplay?: boolean; - options?: FeatureOptions[]; + item: ProductItem; + features: Feature[]; + currency?: string | null; + withDisplay?: boolean; + options?: FeatureOptions[]; }) => { - // 1. Get item type - let type = getItemType(item); + // 1. Get item type + let type = getItemType(item); - // 2. Get display - let display = getProductItemDisplay({ - item, - features, - currency, - }); + // 2. Get display + let display = getProductItemDisplay({ + item, + features, + currency, + }); - let priceData = itemToPriceOrTiers({ item }); + let priceData = itemToPriceOrTiers({ item }); - let quantity = undefined; - let upcomingQuantity = undefined; + let quantity = undefined; + let upcomingQuantity = undefined; - if (item.usage_model == UsageModel.Prepaid && notNullish(options)) { - let 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)) { + let 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; + } - let 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, - }); + let 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, + }); }; diff --git a/shared/utils/productDisplayUtils/sortProductItems.ts b/shared/utils/productDisplayUtils/sortProductItems.ts index 36b0b7578..ece0cd198 100644 --- a/shared/utils/productDisplayUtils/sortProductItems.ts +++ b/shared/utils/productDisplayUtils/sortProductItems.ts @@ -3,45 +3,45 @@ import { ProductItem } from "../../models/productV2Models/productItemModels/prod import { isFeaturePriceItem, isPriceItem } from "./getItemType.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; }; diff --git a/shared/utils/productUtils.ts b/shared/utils/productUtils.ts index bad686ce8..aa0aeb1a2 100644 --- a/shared/utils/productUtils.ts +++ b/shared/utils/productUtils.ts @@ -1,85 +1,85 @@ import { - BillingType, - Price, - ProductItem, - ProductResponse, - ProductV2, + BillingType, + Price, + ProductItem, + ProductResponse, + ProductV2, } from "../index.js"; import { nullish } from "./utils.js"; import { - isFeatureItem, - isFeaturePriceItem, - isPriceItem, + isFeatureItem, + isFeaturePriceItem, + isPriceItem, } from "./productDisplayUtils/getItemType.js"; import { Decimal } from "decimal.js"; export const isFreeProductV2 = ({ items }: { items: ProductItem[] }) => { - return items.every((item) => nullish(item.price) && nullish(item.tiers)); + return items.every((item) => nullish(item.price) && nullish(item.tiers)); }; export const isProductUpgradeV2 = ({ - items1, - items2, + items1, + items2, }: { - items1: ProductItem[]; - items2: ProductItem[]; + items1: ProductItem[]; + items2: ProductItem[]; }) => { - if ( - !isFreeProductV2({ items: items1 }) && - isFreeProductV2({ items: items2 }) - ) { - return false; - } + if ( + !isFreeProductV2({ items: items1 }) && + isFreeProductV2({ items: items2 }) + ) { + return false; + } - const prices1 = items1.filter( - (item) => isFeaturePriceItem(item) || isPriceItem(item) - ); - const prices2 = items2.filter( - (item) => isFeaturePriceItem(item) || isPriceItem(item) - ); + const prices1 = items1.filter( + (item) => isFeaturePriceItem(item) || isPriceItem(item), + ); + const prices2 = items2.filter( + (item) => isFeaturePriceItem(item) || isPriceItem(item), + ); - // 2. Get total price for each product - const getTotalPrice = (items: ProductItem[]) => { - let totalPrice = new Decimal(0); - for (const item of items) { - if (item.price) totalPrice = totalPrice.plus(item.price); - if (item.tiers) { - let tierTotal = item.tiers.reduce( - (acc, tier) => acc.plus(tier.amount), - new Decimal(0) - ); - totalPrice = totalPrice.plus(tierTotal); - } - } - return totalPrice.toNumber(); - }; + // 2. Get total price for each product + const getTotalPrice = (items: ProductItem[]) => { + let totalPrice = new Decimal(0); + for (const item of items) { + if (item.price) totalPrice = totalPrice.plus(item.price); + if (item.tiers) { + let tierTotal = item.tiers.reduce( + (acc, tier) => acc.plus(tier.amount), + new Decimal(0), + ); + totalPrice = totalPrice.plus(tierTotal); + } + } + return totalPrice.toNumber(); + }; - // Return interval...? + // Return interval...? - return getTotalPrice(prices1) < getTotalPrice(prices2); + return getTotalPrice(prices1) < getTotalPrice(prices2); }; export const sortProductsV2 = ({ products }: { products: ProductV2[] }) => { - products.sort((a, b) => { - if (a.is_add_on !== b.is_add_on) { - return a.is_add_on ? 1 : -1; - } + products.sort((a, b) => { + if (a.is_add_on !== b.is_add_on) { + return a.is_add_on ? 1 : -1; + } - return 0; - }); + return 0; + }); - return products.sort((a, b) => { - // Secondary sort: by add-on status (non-add-ons first) - if (a.is_add_on !== b.is_add_on) { - return a.is_add_on ? 1 : -1; - } + return products.sort((a, b) => { + // Secondary sort: by add-on status (non-add-ons first) + if (a.is_add_on !== b.is_add_on) { + return a.is_add_on ? 1 : -1; + } - // Primary sort: by price (using upgrade logic) - let isUpgrade = isProductUpgradeV2({ - items1: a.items, - items2: b.items, - }); + // Primary sort: by price (using upgrade logic) + let isUpgrade = isProductUpgradeV2({ + items1: a.items, + items2: b.items, + }); - return isUpgrade ? -1 : 1; - }); + return isUpgrade ? -1 : 1; + }); }; diff --git a/shared/utils/productUtils/convertUtils.ts b/shared/utils/productUtils/convertUtils.ts index 68004a511..5cc9b04aa 100644 --- a/shared/utils/productUtils/convertUtils.ts +++ b/shared/utils/productUtils/convertUtils.ts @@ -25,29 +25,29 @@ import { Price } from "../../models/productModels/priceModels/priceModels.js"; // }; export const entToPrice = ({ - ent, - prices, + ent, + prices, }: { - ent: Entitlement; - prices: Price[]; + ent: Entitlement; + prices: Price[]; }) => { - return prices.find( - (price) => - price.entitlement_id === ent.id && - price.internal_product_id === ent.internal_product_id - ); + return prices.find( + (price) => + price.entitlement_id === ent.id && + price.internal_product_id === ent.internal_product_id, + ); }; export const priceToEnt = ({ - price, - entitlements, + price, + entitlements, }: { - price: Price; - entitlements: Entitlement[]; + price: Price; + entitlements: Entitlement[]; }) => { - return entitlements.find( - (ent) => - ent.id === price.entitlement_id && - ent.internal_product_id === price.internal_product_id - ); + return entitlements.find( + (ent) => + ent.id === price.entitlement_id && + ent.internal_product_id === price.internal_product_id, + ); }; diff --git a/shared/utils/productUtils/priceToInvoiceAmount.ts b/shared/utils/productUtils/priceToInvoiceAmount.ts index e209b1772..a10213beb 100644 --- a/shared/utils/productUtils/priceToInvoiceAmount.ts +++ b/shared/utils/productUtils/priceToInvoiceAmount.ts @@ -4,160 +4,160 @@ import { BillingType } from "../../models/productModels/priceModels/priceEnums.j import { Price } from "../../models/productModels/priceModels/priceModels.js"; import { Infinite } from "../../models/productModels/productEnums.js"; import { - ProductItem, - UsageModel, + ProductItem, + UsageModel, } from "../../models/productV2Models/productItemModels/productItemModels.js"; import { isPriceItem } from "../productDisplayUtils/getItemType.js"; import { - calculateProrationAmount, - Proration, + calculateProrationAmount, + Proration, } from "../productDisplayUtils/getProductItemRes.js"; import { nullish } from "../utils.js"; import { getBillingType, isFixedPrice } from "./priceUtils.js"; import { Decimal } from "decimal.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, - overage, - proration, - now, + price, + item, + quantity, + overage, + proration, + now, }: { - price?: Price; - item?: ProductItem; - quantity?: number; // quantity should be multiplied by billing units - overage?: number; - proration?: Proration; - now?: number; + price?: Price; + item?: ProductItem; + quantity?: number; // quantity should be multiplied by billing units + 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; - } else { - const config = price.config as UsagePriceConfig; - let billingType = getBillingType(config); + if (price) { + if (isFixedPrice({ price })) { + amount = (price.config as FixedPriceConfig).amount; + } 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/shared/utils/productUtils/priceUtils.ts b/shared/utils/productUtils/priceUtils.ts index de8942859..8cfd4d375 100644 --- a/shared/utils/productUtils/priceUtils.ts +++ b/shared/utils/productUtils/priceUtils.ts @@ -3,181 +3,181 @@ import { Decimal } from "decimal.js"; import { notNullish } from "../utils.js"; import { FixedPriceConfig } from "../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js"; import { - BillWhen, - UsagePriceConfig, + BillWhen, + UsagePriceConfig, } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; import { - BillingInterval, - BillingType, - PriceType, + BillingInterval, + BillingType, + PriceType, } from "../../models/productModels/priceModels/priceEnums.js"; import { Price } from "../../models/productModels/priceModels/priceModels.js"; import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js"; import { - OnDecrease, - OnIncrease, + OnDecrease, + OnIncrease, } from "../../models/productV2Models/productItemModels/productItemEnums.js"; import { APIVersion } from "../../enums/APIVersion.js"; 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 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 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 isUsage && (excludeOneOff ? !isOneOff : true); - }); + return prices.some((price) => { + let isUsage = getBillingType(price.config) == BillingType.UsageInAdvance; + let isOneOff = price.config.interval == BillingInterval.OneOff; + 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/shared/utils/productV2Utils/mapToProductV2.ts b/shared/utils/productV2Utils/mapToProductV2.ts index eac2adfb6..97a28cd9c 100644 --- a/shared/utils/productV2Utils/mapToProductV2.ts +++ b/shared/utils/productV2Utils/mapToProductV2.ts @@ -7,51 +7,51 @@ import { toProductItem } from "./productItemUtils/mapToItem.js"; import { getItemFeatureType } from "./productItemUtils/productItemUtils.js"; export const mapToProductV2 = ({ - product, - features, + product, + features, }: { - product: FullProduct; - features?: Feature[]; + product: FullProduct; + features?: Feature[]; }): ProductV2 => { - let items: ProductItem[] = []; + let items: ProductItem[] = []; - for (const ent of product.entitlements) { - let relatedPrice = entToPrice({ ent, prices: product.prices }); - items.push(toProductItem({ ent, price: relatedPrice })); - } + for (const ent of product.entitlements) { + let relatedPrice = entToPrice({ ent, prices: product.prices }); + items.push(toProductItem({ ent, price: relatedPrice })); + } - for (const price of product.prices) { - let relatedEnt = priceToEnt({ price, entitlements: product.entitlements }); + for (const price of product.prices) { + let relatedEnt = priceToEnt({ price, entitlements: product.entitlements }); - if (!relatedEnt) { - items.push(toProductItem({ price })); - } - } + if (!relatedEnt) { + items.push(toProductItem({ price })); + } + } - if (!features) { - features = product.entitlements.map((ent) => ent.feature); - } + if (!features) { + features = product.entitlements.map((ent) => ent.feature); + } - 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/shared/utils/productV2Utils/productItemUtils/mapToItem.ts b/shared/utils/productV2Utils/productItemUtils/mapToItem.ts index 3da559b15..650017e98 100644 --- a/shared/utils/productV2Utils/productItemUtils/mapToItem.ts +++ b/shared/utils/productV2Utils/productItemUtils/mapToItem.ts @@ -1,143 +1,143 @@ import { FeatureType } from "../../../models/featureModels/featureEnums.js"; import { - AllowanceType, - EntitlementWithFeature, + AllowanceType, + EntitlementWithFeature, } from "../../../models/productModels/entModels/entModels.js"; import { FixedPriceConfig } from "../../../models/productModels/priceModels/priceConfig/fixedPriceConfig.js"; import { - BillWhen, - UsagePriceConfig, + BillWhen, + UsagePriceConfig, } from "../../../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; import { Price } from "../../../models/productModels/priceModels/priceModels.js"; import { Infinite } from "../../../models/productModels/productEnums.js"; import { - ProductItem, - ProductItemFeatureType, - TierInfinite, - UsageModel, + ProductItem, + ProductItemFeatureType, + TierInfinite, + UsageModel, } from "../../../models/productV2Models/productItemModels/productItemModels.js"; import { nullish } from "../../utils.js"; import { - billingToItemInterval, - entToItemInterval, + billingToItemInterval, + entToItemInterval, } from "./productItemUtils.js"; export const toProductItem = ({ - ent, - price, + ent, + price, }: { - ent?: EntitlementWithFeature; - price?: Price; + ent?: EntitlementWithFeature; + price?: Price; }) => { - if (nullish(price)) return toFeatureItem({ ent: ent! }) as ProductItem; - if (nullish(ent)) return toPriceItem({ price: price! }) as ProductItem; + if (nullish(price)) return toFeatureItem({ ent: ent! }) as ProductItem; + if (nullish(ent)) return toPriceItem({ price: price! }) as ProductItem; - return toFeaturePriceItem({ ent: ent!, price: price! }) as ProductItem; + return toFeaturePriceItem({ ent: ent!, price: price! }) as ProductItem; }; export const toFeatureItem = ({ ent }: { ent: EntitlementWithFeature }) => { - if (ent.feature.type == FeatureType.Boolean) { - return { - feature_id: ent.feature.id, - entitlement_id: ent.id, - entity_feature_id: ent.entity_feature_id, - }; - } + if (ent.feature.type == FeatureType.Boolean) { + return { + feature_id: ent.feature.id, + entitlement_id: ent.id, + entity_feature_id: ent.entity_feature_id, + }; + } - const itemConfig = ent.rollover ? { rollover: ent.rollover } : undefined; + const itemConfig = ent.rollover ? { rollover: ent.rollover } : undefined; - const item = { - feature_id: ent.feature.id, - included_usage: - ent.allowance_type == AllowanceType.Unlimited ? Infinite : ent.allowance, - interval: entToItemInterval(ent.interval!), - interval_count: ent.interval_count ?? 1, + const item = { + feature_id: ent.feature.id, + included_usage: + ent.allowance_type == AllowanceType.Unlimited ? Infinite : ent.allowance, + interval: entToItemInterval(ent.interval!), + interval_count: ent.interval_count ?? 1, - entity_feature_id: ent.entity_feature_id, - reset_usage_when_enabled: !ent.carry_from_previous, + entity_feature_id: ent.entity_feature_id, + reset_usage_when_enabled: !ent.carry_from_previous, - // Include rollover config - config: itemConfig, + // Include rollover config + config: itemConfig, - // Stored in backend - entitlement_id: ent.id, - created_at: ent.created_at, - }; + // Stored in backend + entitlement_id: ent.id, + created_at: ent.created_at, + }; - return item; + return item; }; export const toFeaturePriceItem = ({ - ent, - price, + ent, + price, }: { - ent: EntitlementWithFeature; - price: Price; + ent: EntitlementWithFeature; + price: Price; }) => { - let config = price.config as UsagePriceConfig; - let tiers = config.usage_tiers.map((tier) => { - return { - amount: tier.amount, - to: tier.to == -1 ? TierInfinite : tier.to, - }; - }); + let config = price.config as UsagePriceConfig; + let tiers = config.usage_tiers.map((tier) => { + return { + amount: tier.amount, + to: tier.to == -1 ? TierInfinite : tier.to, + }; + }); - // Build the item config from both price proration config and entitlement rollover - let itemConfig: any = {}; - if (price.proration_config) { - itemConfig = { ...price.proration_config }; - } - if (ent.rollover) { - itemConfig.rollover = ent.rollover; - } + // Build the item config from both price proration config and entitlement rollover + let itemConfig: any = {}; + if (price.proration_config) { + itemConfig = { ...price.proration_config }; + } + if (ent.rollover) { + itemConfig.rollover = ent.rollover; + } - let item: ProductItem = { - feature_id: ent.feature.id, - feature_type: - ent.feature.config?.usage_type || ProductItemFeatureType.SingleUse, + let item: ProductItem = { + feature_id: ent.feature.id, + feature_type: + ent.feature.config?.usage_type || ProductItemFeatureType.SingleUse, - included_usage: ent.allowance, + included_usage: ent.allowance, - interval: billingToItemInterval(config.interval!), - interval_count: config.interval_count ?? 1, + interval: billingToItemInterval(config.interval!), + interval_count: config.interval_count ?? 1, - price: null, - tiers, - billing_units: config.billing_units, + price: null, + tiers, + billing_units: config.billing_units, - entity_feature_id: ent.entity_feature_id, - reset_usage_when_enabled: !ent.carry_from_previous, - usage_model: - config.bill_when == BillWhen.StartOfPeriod || - config.bill_when == BillWhen.InAdvance - ? UsageModel.Prepaid - : UsageModel.PayPerUse, + entity_feature_id: ent.entity_feature_id, + reset_usage_when_enabled: !ent.carry_from_previous, + usage_model: + config.bill_when == BillWhen.StartOfPeriod || + config.bill_when == BillWhen.InAdvance + ? UsageModel.Prepaid + : UsageModel.PayPerUse, - // Stored in backend - created_at: ent.created_at, - entitlement_id: ent.id, - price_id: price.id, + // Stored in backend + created_at: ent.created_at, + entitlement_id: ent.id, + price_id: price.id, - price_config: price.config, - config: Object.keys(itemConfig).length > 0 ? itemConfig : undefined, - usage_limit: ent.usage_limit, - }; + price_config: price.config, + config: Object.keys(itemConfig).length > 0 ? itemConfig : undefined, + usage_limit: ent.usage_limit, + }; - return item; + return item; }; export const toPriceItem = ({ price }: { price: Price }) => { - let config = price.config as FixedPriceConfig; - return { - feature_id: null, + let config = price.config as FixedPriceConfig; + return { + feature_id: null, - interval: billingToItemInterval(config.interval!), - interval_count: config.interval_count ?? 1, - price: config.amount, + interval: billingToItemInterval(config.interval!), + interval_count: config.interval_count ?? 1, + price: config.amount, - price_id: price.id, - created_at: price.created_at, + price_id: price.id, + created_at: price.created_at, - price_config: price.config, - }; + price_config: price.config, + }; }; diff --git a/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts b/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts index 214317b76..054d23e47 100644 --- a/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts +++ b/shared/utils/productV2Utils/productItemUtils/productItemUtils.ts @@ -3,44 +3,44 @@ import { Feature } from "../../../models/featureModels/featureModels.js"; import { EntInterval } from "../../../models/productModels/entModels/entEnums.js"; import { BillingInterval } from "../../../models/productModels/priceModels/priceEnums.js"; import { - ProductItem, - ProductItemFeatureType, - ProductItemInterval, + ProductItem, + ProductItemFeatureType, + ProductItemInterval, } from "../../../models/productV2Models/productItemModels/productItemModels.js"; export const entToItemInterval = (entInterval: EntInterval) => { - if (entInterval == EntInterval.Lifetime) { - return null; - } - return entInterval as unknown as ProductItemInterval; + if (entInterval == EntInterval.Lifetime) { + return null; + } + return entInterval as unknown as ProductItemInterval; }; 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 getItemFeatureType = ({ - item, - features, + item, + features, }: { - item: ProductItem; - features: Feature[]; + item: ProductItem; + features: Feature[]; }) => { - let feature = features.find((f) => f.id == item.feature_id); + let feature = features.find((f) => f.id == item.feature_id); - if (feature) { - if (feature.type == FeatureType.Boolean) { - return ProductItemFeatureType.Static; - } else if (feature.type == FeatureType.CreditSystem) { - return ProductItemFeatureType.SingleUse; - } else { - return feature.config?.usage_type; - } - } + if (feature) { + if (feature.type == FeatureType.Boolean) { + return ProductItemFeatureType.Static; + } else if (feature.type == FeatureType.CreditSystem) { + return ProductItemFeatureType.SingleUse; + } else { + return feature.config?.usage_type; + } + } - return undefined; + return undefined; }; diff --git a/shared/utils/rewardUtils/rewardMigrationUtils.ts b/shared/utils/rewardUtils/rewardMigrationUtils.ts index 372565c1d..e65efd536 100644 --- a/shared/utils/rewardUtils/rewardMigrationUtils.ts +++ b/shared/utils/rewardUtils/rewardMigrationUtils.ts @@ -1,9 +1,9 @@ import type { - FixedPriceConfig, - Price, - Reward, - RewardType, - UsagePriceConfig, + FixedPriceConfig, + Price, + Reward, + RewardType, + UsagePriceConfig, } from "../../index.js"; import type { UsageTier } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; import { isFixedPrice, isUsagePrice } from "../productUtils/priceUtils.js"; diff --git a/vite/biome.json b/vite/biome.json index 53487eb49..9ab8d16be 100644 --- a/vite/biome.json +++ b/vite/biome.json @@ -20,6 +20,9 @@ "recommended": true, "complexity": { "noStaticOnlyClass": "off" + }, + "suspicious": { + "noArrayIndexKey": "off" } } }, @@ -36,4 +39,4 @@ } } } -} \ No newline at end of file +} diff --git a/vite/components.json b/vite/components.json index 2082f482a..5599e04e3 100644 --- a/vite/components.json +++ b/vite/components.json @@ -1,21 +1,21 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "new-york", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "", - "css": "src/index.css", - "baseColor": "zinc", - "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": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/vite/eslint.config.js b/vite/eslint.config.js index 08a8e0ac7..3899db104 100644 --- a/vite/eslint.config.js +++ b/vite/eslint.config.js @@ -1,33 +1,32 @@ -import js from '@eslint/js' -import globals from 'globals' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import tseslint from 'typescript-eslint' +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - '@typescript-eslint/no-explicit-any': 'off', // Disable the "any" type warning - '@typescript-eslint/no-unused-vars': 'off', - 'no-unused-vars': 'off', // Also disable the core ESLint unused vars rule - 'react-hooks/exhaustive-deps': 'off', // Disable exhaustive deps rule project-wide - - }, - }, -) + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + "@typescript-eslint/no-explicit-any": "off", // Disable the "any" type warning + "@typescript-eslint/no-unused-vars": "off", + "no-unused-vars": "off", // Also disable the core ESLint unused vars rule + "react-hooks/exhaustive-deps": "off", // Disable exhaustive deps rule project-wide + }, + }, +); diff --git a/vite/package.json b/vite/package.json index b6a90531b..0eca64b3e 100644 --- a/vite/package.json +++ b/vite/package.json @@ -1,103 +1,103 @@ { - "name": "@autumn/vite", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite --port 3000 --host", - "build": "tsc && vite build", - "start": "serve -s dist", - "lint": "eslint .", - "preview": "vite preview", - "dev:bun": "bunx --bun vite --port 3000 --host", - "build:bun": "tsc && bunx --bun vite build", - "start:bun": "bunx --bun serve -s dist" - }, - "author": "Recase Inc.", - "license": "Apache-2.0", - "dependencies": { - "@autumn/shared": "workspace:*", - "@better-auth/stripe": "^1.2.12", - "@clerk/clerk-react": "^5.24.2", - "@fortawesome/free-brands-svg-icons": "^6.7.2", - "@fortawesome/react-fontawesome": "^0.2.2", - "@heroicons/react": "^2.2.0", - "@radix-ui/react-accordion": "^1.2.11", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-context-menu": "^2.2.16", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-popover": "^1.1.14", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.2.3", - "@radix-ui/react-switch": "^1.2.5", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-tooltip": "^1.1.8", - "@supabase/ssr": "^0.5.2", - "@tailwindcss/vite": "^4.0.13", - "@tanstack/react-query": "^5.85.6", - "@tanstack/react-query-devtools": "^5.85.6", - "@tanstack/react-table": "^8.21.3", - "@types/recharts": "^2.0.1", - "@wooorm/starry-night": "^3.6.0", - "ag-charts-community": "^12.0.2", - "ag-grid-community": "^34.0.2", - "ag-grid-react": "^34.0.2", - "autumn-js": "^0.1.8", - "axios": "^1.8.3", - "better-auth": "^1.2.9", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "date-fns": "^3.6.0", - "decimal.js": "^10.5.0", - "hast-util-to-html": "^9.0.5", - "input-otp": "^1.4.2", - "lodash": "^4.17.21", - "lucide-react": "^0.525.0", - "motion": "^12.16.0", - "next-themes": "^0.4.6", - "nuqs": "^2.4.3", - "posthog-js": "^1.230.4", - "prism-themes": "^1.9.0", - "prismjs": "^1.29.0", - "react": "^18.2.0", - "react-confetti-explosion": "^2.1.2", - "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", - "react-dropzone": "^14.3.8", - "react-hot-toast": "^2.5.2", - "react-hotkeys-hook": "^4.6.1", - "react-router": "^7.3.0", - "react-router-dom": "^7.6.2", - "recharts": "^3.1.0", - "sonner": "^2.0.1", - "stripe": "^18.3.0", - "svix-react": "^1.13.3", - "swr": "^2.3.3", - "tailwind-merge": "^3.0.2", - "tailwindcss": "^4.0.13", - "tailwindcss-animate": "^1.0.7", - "vaul": "^1.1.2", - "zod": "^3.25.23" - }, - "devDependencies": { - "@eslint/js": "^9.21.0", - "@types/lodash": "^4.17.16", - "@types/node": "^22.13.10", - "@types/prismjs": "^1.26.5", - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "@types/react-router-dom": "^5.3.3", - "@vitejs/plugin-react": "^4.3.4", - "eslint": "^9.21.0", - "eslint-plugin-react-hooks": "^5.1.0", - "eslint-plugin-react-refresh": "^0.4.19", - "globals": "^15.15.0", - "typescript": "~5.7.2", - "typescript-eslint": "^8.24.1", - "vite": "^6.2.0" - } + "name": "@autumn/vite", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --port 3000 --host", + "build": "tsc && vite build", + "start": "serve -s dist", + "lint": "eslint .", + "preview": "vite preview", + "dev:bun": "bunx --bun vite --port 3000 --host", + "build:bun": "tsc && bunx --bun vite build", + "start:bun": "bunx --bun serve -s dist" + }, + "author": "Recase Inc.", + "license": "Apache-2.0", + "dependencies": { + "@autumn/shared": "workspace:*", + "@better-auth/stripe": "^1.2.12", + "@clerk/clerk-react": "^5.24.2", + "@fortawesome/free-brands-svg-icons": "^6.7.2", + "@fortawesome/react-fontawesome": "^0.2.2", + "@heroicons/react": "^2.2.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-context-menu": "^2.2.16", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-tooltip": "^1.1.8", + "@supabase/ssr": "^0.5.2", + "@tailwindcss/vite": "^4.0.13", + "@tanstack/react-query": "^5.85.6", + "@tanstack/react-query-devtools": "^5.85.6", + "@tanstack/react-table": "^8.21.3", + "@types/recharts": "^2.0.1", + "@wooorm/starry-night": "^3.6.0", + "ag-charts-community": "^12.0.2", + "ag-grid-community": "^34.0.2", + "ag-grid-react": "^34.0.2", + "autumn-js": "^0.1.8", + "axios": "^1.8.3", + "better-auth": "^1.2.9", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "decimal.js": "^10.5.0", + "hast-util-to-html": "^9.0.5", + "input-otp": "^1.4.2", + "lodash": "^4.17.21", + "lucide-react": "^0.525.0", + "motion": "^12.16.0", + "next-themes": "^0.4.6", + "nuqs": "^2.4.3", + "posthog-js": "^1.230.4", + "prism-themes": "^1.9.0", + "prismjs": "^1.29.0", + "react": "^18.2.0", + "react-confetti-explosion": "^2.1.2", + "react-day-picker": "^8.10.1", + "react-dom": "^18.2.0", + "react-dropzone": "^14.3.8", + "react-hot-toast": "^2.5.2", + "react-hotkeys-hook": "^4.6.1", + "react-router": "^7.3.0", + "react-router-dom": "^7.6.2", + "recharts": "^3.1.0", + "sonner": "^2.0.1", + "stripe": "^18.3.0", + "svix-react": "^1.13.3", + "swr": "^2.3.3", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.0.13", + "tailwindcss-animate": "^1.0.7", + "vaul": "^1.1.2", + "zod": "^3.25.23" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/lodash": "^4.17.16", + "@types/node": "^22.13.10", + "@types/prismjs": "^1.26.5", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } } diff --git a/vite/src/App.tsx b/vite/src/App.tsx index fb8f4043d..e2512d756 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -20,62 +20,62 @@ import { MainLayout } from "./app/layout"; import CustomersPage from "./views/customers/CustomersPage"; export default function App() { - return ( - - - } /> - } /> - } /> - }> - } /> - } /> - } /> - } /> - } /> - } /> + return ( + + + } /> + } /> + } /> + }> + } /> + } /> + } /> + } /> + } /> + } /> - } - /> - } - /> - } /> - } - /> + } + /> + } + /> + } /> + } + /> - } /> - } /> - } /> - } - /> - } - /> - } - /> - } /> - } /> - } - /> - } - /> - } /> - - - - ); + } /> + } /> + } /> + } + /> + } + /> + } + /> + } /> + } /> + } + /> + } + /> + } /> + + + + ); } diff --git a/vite/src/app/AppContext.tsx b/vite/src/app/AppContext.tsx index 1685d967b..3345edb29 100644 --- a/vite/src/app/AppContext.tsx +++ b/vite/src/app/AppContext.tsx @@ -3,11 +3,11 @@ import { createContext, useContext } from "react"; export const AppContext = createContext(null); export const useAppContext = () => { - const context = useContext(AppContext); + const context = useContext(AppContext); - if (context === undefined) { - throw new Error("useDevContext must be used within a DevContextProvider"); - } + if (context === undefined) { + throw new Error("useDevContext must be used within a DevContextProvider"); + } - return context; + return context; }; diff --git a/vite/src/app/layout.tsx b/vite/src/app/layout.tsx index 55c7e460f..14c1fe31e 100644 --- a/vite/src/app/layout.tsx +++ b/vite/src/app/layout.tsx @@ -27,145 +27,145 @@ import { InviteNotifications } from "@/views/general/notifications/InviteNotific import { ChatWidget } from "@/components/general/ChatWidget"; export function MainLayout() { - const env = useEnv(); - const { data, isPending } = useSession(); - const { handleApiError } = useGlobalErrorHandler(); + const env = useEnv(); + const { data, isPending } = useSession(); + const { handleApiError } = useGlobalErrorHandler(); - const navigate = useNavigate(); - const posthog = usePostHog(); + const navigate = useNavigate(); + const posthog = usePostHog(); - // Global error handler for API errors - useEffect(() => { - const handleGlobalError = (event: ErrorEvent) => { - if (event.error && event.error.response) { - handleApiError(event.error); - } - }; + // Global error handler for API errors + useEffect(() => { + const handleGlobalError = (event: ErrorEvent) => { + if (event.error && event.error.response) { + handleApiError(event.error); + } + }; - window.addEventListener("error", handleGlobalError); - return () => window.removeEventListener("error", handleGlobalError); - }, [handleApiError]); + window.addEventListener("error", handleGlobalError); + return () => window.removeEventListener("error", handleGlobalError); + }, [handleApiError]); - useEffect(() => { - // Identify user - if (data && process.env.NODE_ENV !== "development") { - const email = data.user.email; + useEffect(() => { + // Identify user + if (data && process.env.NODE_ENV !== "development") { + const email = data.user.email; - posthog?.identify(email, { - email, - name: data.user.name, - id: data.user.id, - }); - } - }, [data, posthog]); + posthog?.identify(email, { + email, + name: data.user.name, + id: data.user.id, + }); + } + }, [data, posthog]); - // 1. If not loaded, show loading screen - if (isPending) { - return ( - -
    - -
    -
    - {env === AppEnv.Sandbox && ( -
    -

    - You're in sandbox -

    - -
    - )} -
    - -
    -
    -
    -
    -
    - ); - } + // 1. If not loaded, show loading screen + if (isPending) { + return ( + +
    + +
    +
    + {env === AppEnv.Sandbox && ( +
    +

    + You're in sandbox +

    + +
    + )} +
    + +
    +
    +
    +
    +
    + ); + } - // 2. If no user, redirect to sign in - if (!data) { - navigate("/sign-in"); - return; - } + // 2. If no user, redirect to sign in + if (!data) { + navigate("/sign-in"); + return; + } - return ( - - -
    - - - - - -
    -
    -
    - ); + return ( + + +
    + + + + + +
    +
    +
    + ); } const MainContent = () => { - const env = useEnv(); - const navigate = useNavigate(); + const env = useEnv(); + const navigate = useNavigate(); - useDevQuery(); - useAutumnFlags(); - useProductsQuery(); - useFeaturesQuery(); - useRewardsQuery(); - useCusSearchQuery(); - useOrg(); + useDevQuery(); + useAutumnFlags(); + useProductsQuery(); + useFeaturesQuery(); + useRewardsQuery(); + useCusSearchQuery(); + useOrg(); - return ( - -
    -
    - {env === AppEnv.Sandbox && ( -
    -

    You're in sandbox

    - {!window.location.pathname.includes("/onboarding") && ( - - )} -
    - )} -
    -
    - -
    - {/*
    + return ( + +
    +
    + {env === AppEnv.Sandbox && ( +
    +

    You're in sandbox

    + {!window.location.pathname.includes("/onboarding") && ( + + )} +
    + )} +
    +
    + +
    + {/*

    Autumn is coming to mobile soon @@ -176,9 +176,9 @@ const MainContent = () => {

    */} -
    -
    -
    -
    - ); +
    +
    +
    + + ); }; diff --git a/vite/src/components/autumn/attach-dialog.tsx b/vite/src/components/autumn/attach-dialog.tsx index 14df62931..12f5d2852 100644 --- a/vite/src/components/autumn/attach-dialog.tsx +++ b/vite/src/components/autumn/attach-dialog.tsx @@ -6,287 +6,279 @@ import { Button } from "@/components/ui/button"; import { ArrowRight, Loader2 } from "lucide-react"; import { type CheckProductPreview } from "autumn-js"; import { - Dialog, - DialogContent, - DialogFooter, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogTitle, } from "@/components/ui/dialog"; import { getAttachContent } from "@/lib/autumn/attach-content"; import { useCustomer } from "autumn-js/react"; export interface AttachDialogProps { - open: boolean; - setOpen: (open: boolean) => void; - preview: CheckProductPreview; - onClick: (options?: any) => Promise; + open: boolean; + setOpen: (open: boolean) => void; + preview: CheckProductPreview; + onClick: (options?: any) => Promise; } export default function AttachDialog(params?: AttachDialogProps) { - const { attach } = useCustomer(); - const [loading, setLoading] = useState(false); - const [optionsInput, setOptionsInput] = useState( - params?.preview?.options || [] - ); + const { attach } = useCustomer(); + const [loading, setLoading] = useState(false); + const [optionsInput, setOptionsInput] = useState( + params?.preview?.options || [], + ); - const getTotalPrice = () => { - let sum = due_today?.price || 0; - optionsInput.forEach((option) => { - if (option.price && option.quantity) { - sum += option.price * (option.quantity / option.billing_units); - } - }); - return sum; - }; + const getTotalPrice = () => { + let sum = due_today?.price || 0; + optionsInput.forEach((option) => { + if (option.price && option.quantity) { + sum += option.price * (option.quantity / option.billing_units); + } + }); + return sum; + }; - useEffect(() => { - setOptionsInput(params?.preview?.options || []); - }, [params?.preview?.options]); + useEffect(() => { + setOptionsInput(params?.preview?.options || []); + }, [params?.preview?.options]); - if (!params || !params.preview) { - return <>; - } + if (!params || !params.preview) { + return <>; + } - const { open, setOpen, preview } = params; - const { items, due_today } = preview; - const { title, message } = getAttachContent(preview); + const { open, setOpen, preview } = params; + const { items, due_today } = preview; + const { title, message } = getAttachContent(preview); - return ( - - - {title} -
    - {message} -
    - {(items || optionsInput.length > 0) && ( -
    - {items?.map((item) => ( - - - {item.description} - - {item.price} - - ))} + return ( + + + {title} +
    + {message} +
    + {(items || optionsInput.length > 0) && ( +
    + {items?.map((item) => ( + + {item.description} + {item.price} + + ))} - {optionsInput?.map((option, index) => { - return ( - - ); - })} -
    - )} + {optionsInput?.map((option, index) => { + return ( + + ); + })} +
    + )} - - {due_today && ( - - Due Today - - {new Intl.NumberFormat("en-US", { - style: "currency", - currency: due_today.currency, - }).format(getTotalPrice())} - - - )} - - -
    -
    - ); + + {due_today && ( + + Due Today + + {new Intl.NumberFormat("en-US", { + style: "currency", + currency: due_today.currency, + }).format(getTotalPrice())} + + + )} + + + + + ); } export const PriceItem = ({ - children, - className, - ...props + children, + className, + ...props }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; } & React.HTMLAttributes) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; interface FeatureOption { - feature_id: string; - feature_name: string; - billing_units: number; - price?: number; - quantity?: number; + feature_id: string; + feature_name: string; + billing_units: number; + price?: number; + quantity?: number; } interface FeatureOptionWithRequiredPrice - extends Omit { - price: number; - quantity: number; + extends Omit { + price: number; + quantity: number; } export const OptionsInput = ({ - className, - option, - optionsInput, - setOptionsInput, - index, - ...props + className, + option, + optionsInput, + setOptionsInput, + index, + ...props }: { - className?: string; - option: FeatureOptionWithRequiredPrice; - optionsInput: FeatureOption[]; - setOptionsInput: (options: FeatureOption[]) => void; - index: number; + className?: string; + option: FeatureOptionWithRequiredPrice; + optionsInput: FeatureOption[]; + setOptionsInput: (options: FeatureOption[]) => void; + index: number; } & React.HTMLAttributes) => { - const { feature_name, billing_units, quantity, price } = option; - return ( - - {feature_name} - ) => { - const newOptions = [...optionsInput]; - newOptions[index].quantity = parseInt(e.target.value) * billing_units; - setOptionsInput(newOptions); - }} - > - - × ${price} per {billing_units === 1 ? " " : billing_units}{" "} - {feature_name} - - - - ); + const { feature_name, billing_units, quantity, price } = option; + return ( + + {feature_name} + ) => { + const newOptions = [...optionsInput]; + newOptions[index].quantity = parseInt(e.target.value) * billing_units; + setOptionsInput(newOptions); + }} + > + + × ${price} per {billing_units === 1 ? " " : billing_units}{" "} + {feature_name} + + + + ); }; export const QuantityInput = ({ - children, - onChange, - value, - className, - ...props + children, + onChange, + value, + className, + ...props }: { - children: React.ReactNode; - value: string | number; - onChange: (e: React.ChangeEvent) => void; - className?: string; + children: React.ReactNode; + value: string | number; + onChange: (e: React.ChangeEvent) => void; + className?: string; } & React.HTMLAttributes) => { - const currentValue = Number(value) || 0; + const currentValue = Number(value) || 0; - const handleValueChange = (newValue: number) => { - const syntheticEvent = { - target: { value: String(newValue) }, - } as React.ChangeEvent; - onChange(syntheticEvent); - }; + const handleValueChange = (newValue: number) => { + const syntheticEvent = { + target: { value: String(newValue) }, + } as React.ChangeEvent; + onChange(syntheticEvent); + }; - return ( -
    -
    - - - {currentValue} - - -
    - {children} -
    - ); + return ( +
    +
    + + {currentValue} + +
    + {children} +
    + ); }; export const TotalPrice = ({ children }: { children: React.ReactNode }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const PricingDialogButton = ({ - children, - size, - onClick, - disabled, - className, + children, + size, + onClick, + disabled, + className, }: { - children: React.ReactNode; - size?: "sm" | "lg" | "default" | "icon"; - onClick: () => void; - disabled?: boolean; - className?: string; + children: React.ReactNode; + size?: "sm" | "lg" | "default" | "icon"; + onClick: () => void; + disabled?: boolean; + className?: string; }) => { - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/autumn/checkout-dialog.tsx b/vite/src/components/autumn/checkout-dialog.tsx index ddc89155c..7e855c634 100644 --- a/vite/src/components/autumn/checkout-dialog.tsx +++ b/vite/src/components/autumn/checkout-dialog.tsx @@ -4,458 +4,458 @@ import React, { useEffect, useState } from "react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogTitle, } from "@/components/ui/dialog"; import { getCheckoutContent } from "@/lib/autumn/checkout-content"; import { useCustomer } from "autumn-js/react"; import { ArrowRight, ChevronDown, Loader2 } from "lucide-react"; import type { CheckoutResult, ProductItem } from "autumn-js"; import { - Accordion, - AccordionContent, - AccordionItem, + Accordion, + AccordionContent, + AccordionItem, } from "@/components/ui/accordion"; import { - Popover, - PopoverContent, - PopoverTrigger, + Popover, + PopoverContent, + PopoverTrigger, } from "@/components/ui/popover"; import { Input } from "@/components/ui/input"; import { useModelPricingContext } from "@/views/onboarding2/model-pricing/ModelPricingContext"; export interface CheckoutDialogProps { - open: boolean; - setOpen: (open: boolean) => void; - checkoutResult: CheckoutResult; + open: boolean; + setOpen: (open: boolean) => void; + checkoutResult: CheckoutResult; } const formatCurrency = ({ - amount, - currency, + amount, + currency, }: { - amount: number; - currency: string; + amount: number; + currency: string; }) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: currency, - }).format(amount); + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency, + }).format(amount); }; export default function CheckoutDialog(params: CheckoutDialogProps) { - const { attach } = useCustomer(); - const [checkoutResult, setCheckoutResult] = useState< - CheckoutResult | undefined - >(params?.checkoutResult); - const { refetch } = useModelPricingContext(); + const { attach } = useCustomer(); + const [checkoutResult, setCheckoutResult] = useState< + CheckoutResult | undefined + >(params?.checkoutResult); + const { refetch } = useModelPricingContext(); - useEffect(() => { - if (params.checkoutResult) { - setCheckoutResult(params.checkoutResult); - } - }, [params.checkoutResult]); + useEffect(() => { + if (params.checkoutResult) { + setCheckoutResult(params.checkoutResult); + } + }, [params.checkoutResult]); - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - if (!checkoutResult) { - return <>; - } + if (!checkoutResult) { + return <>; + } - const { open, setOpen } = params; - const { title, message } = getCheckoutContent(checkoutResult); + const { open, setOpen } = params; + const { title, message } = getCheckoutContent(checkoutResult); - const isFree = checkoutResult?.product.properties?.is_free; - const isPaid = isFree === false; + const isFree = checkoutResult?.product.properties?.is_free; + const isPaid = isFree === false; - return ( - - - {title} -
    {message}
    + return ( + + + {title} +
    {message}
    - {isPaid && checkoutResult && ( - - )} + {isPaid && checkoutResult && ( + + )} - - - -
    -
    - ); + await attach({ + productId: checkoutResult.product.id, + options, + openInNewTab: true, + }); + await refetch(); + setOpen(false); + setLoading(false); + }} + disabled={loading} + className="min-w-16 flex items-center gap-2 !bg-zinc-900 hover:!bg-zinc-600" + > + {loading ? ( + + ) : ( + <> + Confirm + + )} + + +
    +
    + ); } function PriceInformation({ - checkoutResult, - setCheckoutResult, + checkoutResult, + setCheckoutResult, }: { - checkoutResult: CheckoutResult; - setCheckoutResult: (checkoutResult: CheckoutResult) => void; + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; }) { - return ( -
    - + return ( +
    + -
    - {checkoutResult?.has_prorations && checkoutResult.lines.length > 0 && ( - - )} - -
    -
    - ); +
    + {checkoutResult?.has_prorations && checkoutResult.lines.length > 0 && ( + + )} + +
    +
    + ); } function DueAmounts({ checkoutResult }: { checkoutResult: CheckoutResult }) { - const { next_cycle, product } = checkoutResult; - const nextCycleAtStr = next_cycle - ? new Date(next_cycle.starts_at).toLocaleDateString() - : undefined; + const { next_cycle, product } = checkoutResult; + const nextCycleAtStr = next_cycle + ? new Date(next_cycle.starts_at).toLocaleDateString() + : undefined; - const hasUsagePrice = product.items.some( - (item) => item.usage_model === "pay_per_use" - ); + const hasUsagePrice = product.items.some( + (item) => item.usage_model === "pay_per_use", + ); - const showNextCycle = next_cycle && next_cycle.total !== checkoutResult.total; + const showNextCycle = next_cycle && next_cycle.total !== checkoutResult.total; - return ( -
    -
    -
    -

    Total due today

    -
    + return ( +
    +
    +
    +

    Total due today

    +
    -

    - {formatCurrency({ - amount: checkoutResult?.total, - currency: checkoutResult?.currency, - })} -

    -
    - {showNextCycle && ( -
    -
    -

    Due next cycle ({nextCycleAtStr})

    -
    -

    - {formatCurrency({ - amount: next_cycle.total, - currency: checkoutResult?.currency, - })} - {hasUsagePrice && + usage prices} -

    -
    - )} -
    - ); +

    + {formatCurrency({ + amount: checkoutResult?.total, + currency: checkoutResult?.currency, + })} +

    +
    + {showNextCycle && ( +
    +
    +

    Due next cycle ({nextCycleAtStr})

    +
    +

    + {formatCurrency({ + amount: next_cycle.total, + currency: checkoutResult?.currency, + })} + {hasUsagePrice && + usage prices} +

    +
    + )} +
    + ); } function ProductItems({ - checkoutResult, - setCheckoutResult, + checkoutResult, + setCheckoutResult, }: { - checkoutResult: CheckoutResult; - setCheckoutResult: (checkoutResult: CheckoutResult) => void; + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; }) { - const isUpdateQuantity = - checkoutResult?.product.scenario === "active" && - checkoutResult.product.properties.updateable; - return ( -
    -

    Price

    - {checkoutResult?.product.items - .filter((item) => item.type !== "feature") - .map((item, index) => { - if (item.usage_model == "prepaid") { - return ( - - ); - } + const isUpdateQuantity = + checkoutResult?.product.scenario === "active" && + checkoutResult.product.properties.updateable; + return ( +
    +

    Price

    + {checkoutResult?.product.items + .filter((item) => item.type !== "feature") + .map((item, index) => { + if (item.usage_model == "prepaid") { + return ( + + ); + } - if (isUpdateQuantity) { - return null; - } + if (isUpdateQuantity) { + return null; + } - return ( -
    -

    - {item.feature ? item.feature.name : "Subscription"} -

    -

    - {item.display?.primary_text} {item.display?.secondary_text} -

    -
    - ); - })} -
    - ); + return ( +
    +

    + {item.feature ? item.feature.name : "Subscription"} +

    +

    + {item.display?.primary_text} {item.display?.secondary_text} +

    +
    + ); + })} +
    + ); } function CheckoutLines({ checkoutResult }: { checkoutResult: CheckoutResult }) { - return ( - - - -
    -

    View details

    - -
    -
    - - {checkoutResult?.lines - .filter((line) => line.amount != 0) - .map((line, index) => { - return ( -
    -

    {line.description}

    -

    - {new Intl.NumberFormat("en-US", { - style: "currency", - currency: checkoutResult?.currency, - }).format(line.amount)} -

    -
    - ); - })} -
    -
    -
    - ); + return ( + + + +
    +

    View details

    + +
    +
    + + {checkoutResult?.lines + .filter((line) => line.amount != 0) + .map((line, index) => { + return ( +
    +

    {line.description}

    +

    + {new Intl.NumberFormat("en-US", { + style: "currency", + currency: checkoutResult?.currency, + }).format(line.amount)} +

    +
    + ); + })} +
    +
    +
    + ); } function CustomAccordionTrigger({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); } const PrepaidItem = ({ - item, - checkoutResult, - setCheckoutResult, + item, + checkoutResult, + setCheckoutResult, }: { - item: ProductItem; - checkoutResult: CheckoutResult; - setCheckoutResult: (checkoutResult: CheckoutResult) => void; + item: ProductItem; + checkoutResult: CheckoutResult; + setCheckoutResult: (checkoutResult: CheckoutResult) => void; }) => { - const { quantity = 0, billing_units: billingUnits = 1 } = item; - const [quantityInput, setQuantityInput] = useState( - (quantity / billingUnits).toString() - ); - const { checkout } = useCustomer(); - const [loading, setLoading] = useState(false); - const [open, setOpen] = useState(false); - const scenario = checkoutResult.product.scenario; + const { quantity = 0, billing_units: billingUnits = 1 } = item; + const [quantityInput, setQuantityInput] = useState( + (quantity / billingUnits).toString(), + ); + const { checkout } = useCustomer(); + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); + const scenario = checkoutResult.product.scenario; - const handleSave = async () => { - setLoading(true); - try { - const newOptions = checkoutResult.options - .filter((option) => option.feature_id !== item.feature_id) - .map((option) => { - return { - featureId: option.feature_id, - quantity: option.quantity, - }; - }); + const handleSave = async () => { + setLoading(true); + try { + const newOptions = checkoutResult.options + .filter((option) => option.feature_id !== item.feature_id) + .map((option) => { + return { + featureId: option.feature_id, + quantity: option.quantity, + }; + }); - newOptions.push({ - featureId: item.feature_id!, - quantity: Number(quantityInput) * billingUnits, - }); + newOptions.push({ + featureId: item.feature_id!, + quantity: Number(quantityInput) * billingUnits, + }); - const { data, error } = await checkout({ - productId: checkoutResult.product.id, - options: newOptions, - dialog: CheckoutDialog, - openInNewTab: true, - }); + const { data, error } = await checkout({ + productId: checkoutResult.product.id, + options: newOptions, + dialog: CheckoutDialog, + openInNewTab: true, + }); - if (error) { - console.error(error); - return; - } - setCheckoutResult(data!); - } catch (error) { - console.error(error); - } finally { - setLoading(false); - setOpen(false); - } - }; + if (error) { + console.error(error); + return; + } + setCheckoutResult(data!); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + setOpen(false); + } + }; - const disableSelection = scenario === "renew"; + const disableSelection = scenario === "renew"; - return ( -
    -
    -

    - {item.feature?.name} -

    - - - Qty: {quantity} - - - -
    -

    {item.feature?.name}

    -

    - {item.display?.primary_text} {item.display?.secondary_text} -

    -
    + return ( +
    +
    +

    + {item.feature?.name} +

    + + + Qty: {quantity} + + + +
    +

    {item.feature?.name}

    +

    + {item.display?.primary_text} {item.display?.secondary_text} +

    +
    -
    -
    - setQuantityInput(e.target.value)} - /> -

    - {billingUnits > 1 && `x ${billingUnits} `} - {item.feature?.name} -

    -
    +
    +
    + setQuantityInput(e.target.value)} + /> +

    + {billingUnits > 1 && `x ${billingUnits} `} + {item.feature?.name} +

    +
    - -
    - - -
    -

    - {item.display?.primary_text} {item.display?.secondary_text} -

    -
    - ); + +
    +
    +
    +
    +

    + {item.display?.primary_text} {item.display?.secondary_text} +

    +
    + ); }; export const PriceItem = ({ - children, - className, - ...props + children, + className, + ...props }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; } & React.HTMLAttributes) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const PricingDialogButton = ({ - children, - size, - onClick, - disabled, - className, + children, + size, + onClick, + disabled, + className, }: { - children: React.ReactNode; - size?: "sm" | "lg" | "default" | "icon"; - onClick: () => void; - disabled?: boolean; - className?: string; + children: React.ReactNode; + size?: "sm" | "lg" | "default" | "icon"; + onClick: () => void; + disabled?: boolean; + className?: string; }) => { - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index 3de70caf1..84522ef67 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -13,384 +13,384 @@ import { useCustomer } from "autumn-js/react"; import { useOrg } from "@/hooks/common/useOrg"; export default function PricingTable({ - products, - setConnectStripeOpen, + products, + setConnectStripeOpen, }: { - products?: Product[]; - setConnectStripeOpen: (open: boolean) => void; + products?: Product[]; + setConnectStripeOpen: (open: boolean) => void; }) { - const { org } = useOrg(); - const { checkout } = useCustomer(); - const [isAnnual, setIsAnnual] = useState(false); + const { org } = useOrg(); + const { checkout } = useCustomer(); + const [isAnnual, setIsAnnual] = useState(false); - const intervals = Array.from( - new Set( - products?.map((p) => p.properties?.interval_group).filter((i) => !!i) - ) - ); + const intervals = Array.from( + new Set( + products?.map((p) => p.properties?.interval_group).filter((i) => !!i), + ), + ); - const multiInterval = intervals.length > 1; + const multiInterval = intervals.length > 1; - const intervalFilter = (product: Product) => { - if (!product.properties?.interval_group) { - return true; - } + const intervalFilter = (product: Product) => { + if (!product.properties?.interval_group) { + return true; + } - if (multiInterval) { - if (isAnnual) { - return product.properties?.interval_group === "year"; - } else { - return product.properties?.interval_group === "month"; - } - } + if (multiInterval) { + if (isAnnual) { + return product.properties?.interval_group === "year"; + } else { + return product.properties?.interval_group === "month"; + } + } - return true; - }; + return true; + }; - return ( -
    - {products && ( - - {products.filter(intervalFilter).map((product, index) => ( - + {products && ( + + {products.filter(intervalFilter).map((product, index) => ( + { - if (!org.stripe_connected) { - setConnectStripeOpen(true); - return; - } + onClick: async () => { + if (!org.stripe_connected) { + setConnectStripeOpen(true); + return; + } - if (product.id) { - const result = await checkout({ - productId: product.id, - dialog: CheckoutDialog, - openInNewTab: true, - successUrl: `${window.location.origin}`, - }); - } else if (product.display?.button_url) { - window.open(product.display?.button_url, "_blank"); - } - }, - }} - /> - ))} - - )} -
    - ); + if (product.id) { + const result = await checkout({ + productId: product.id, + dialog: CheckoutDialog, + openInNewTab: true, + successUrl: `${window.location.origin}`, + }); + } else if (product.display?.button_url) { + window.open(product.display?.button_url, "_blank"); + } + }, + }} + /> + ))} + + )} +
    + ); } const PricingTableContext = createContext<{ - isAnnualToggle: boolean; - setIsAnnualToggle: (isAnnual: boolean) => void; - products: Product[]; - showFeatures: boolean; + isAnnualToggle: boolean; + setIsAnnualToggle: (isAnnual: boolean) => void; + products: Product[]; + showFeatures: boolean; }>({ - isAnnualToggle: false, - setIsAnnualToggle: () => {}, - products: [], - showFeatures: true, + isAnnualToggle: false, + setIsAnnualToggle: () => {}, + products: [], + showFeatures: true, }); export const usePricingTableContext = (componentName: string) => { - const context = useContext(PricingTableContext); + const context = useContext(PricingTableContext); - if (context === undefined) { - throw new Error(`${componentName} must be used within `); - } + if (context === undefined) { + throw new Error(`${componentName} must be used within `); + } - return context; + return context; }; export const PricingTableContainer = ({ - children, - products, - showFeatures = true, - className, - isAnnualToggle, - setIsAnnualToggle, - multiInterval, + children, + products, + showFeatures = true, + className, + isAnnualToggle, + setIsAnnualToggle, + multiInterval, }: { - children?: React.ReactNode; - products?: Product[]; - showFeatures?: boolean; - className?: string; - isAnnualToggle: boolean; - setIsAnnualToggle: (isAnnual: boolean) => void; - multiInterval: boolean; + children?: React.ReactNode; + products?: Product[]; + showFeatures?: boolean; + className?: string; + isAnnualToggle: boolean; + setIsAnnualToggle: (isAnnual: boolean) => void; + multiInterval: boolean; }) => { - if (!products) { - throw new Error("products is required in "); - } + if (!products) { + throw new Error("products is required in "); + } - if (products.length === 0) { - return <>; - } + if (products.length === 0) { + return <>; + } - const hasRecommended = products?.some((p) => p.display?.recommend_text); - return ( - -
    - {multiInterval && ( -
    p.display?.recommend_text) && "mb-8" - )} - > - -
    - )} -
    p.display?.recommend_text); + return ( + +
    + {multiInterval && ( +
    p.display?.recommend_text) && "mb-8", + )} + > + +
    + )} +
    - {children} -
    -
    -
    - ); + className, + )} + > + {children} +
    +
    +
    + ); }; interface PricingCardProps { - productId: string; - showFeatures?: boolean; - className?: string; - onButtonClick?: (event: React.MouseEvent) => void; - buttonProps?: React.ComponentProps<"button">; + productId: string; + showFeatures?: boolean; + className?: string; + onButtonClick?: (event: React.MouseEvent) => void; + buttonProps?: React.ComponentProps<"button">; } export const PricingCard = ({ - productId, - className, - buttonProps, + productId, + className, + buttonProps, }: PricingCardProps) => { - const { products, showFeatures } = usePricingTableContext("PricingCard"); + const { products, showFeatures } = usePricingTableContext("PricingCard"); - const product = products.find((p) => p.id === productId); + const product = products.find((p) => p.id === productId); - if (!product) { - throw new Error(`Product with id ${productId} not found`); - } + if (!product) { + throw new Error(`Product with id ${productId} not found`); + } - const { name, display: productDisplay } = product; + const { name, display: productDisplay } = product; - const { buttonText } = getPricingTableContent(product); + const { buttonText } = getPricingTableContent(product); - const isRecommended = productDisplay?.recommend_text ? true : false; - const mainPriceDisplay = product.properties?.is_free - ? { - primary_text: "Free", - } - : product.items[0].display; + const isRecommended = productDisplay?.recommend_text ? true : false; + const mainPriceDisplay = product.properties?.is_free + ? { + primary_text: "Free", + } + : product.items[0].display; - const featureItems = product.properties?.is_free - ? product.items - : product.items.slice(1); + const featureItems = product.properties?.is_free + ? product.items + : product.items.slice(1); - return ( -
    - {productDisplay?.recommend_text && ( - - )} -
    -
    -
    -
    -

    - {productDisplay?.name || name || ( - - Name this product - - )} -

    - {productDisplay?.description && ( -
    -

    {productDisplay?.description}

    -
    - )} -
    -
    -

    -
    - {mainPriceDisplay?.primary_text}{" "} - {mainPriceDisplay?.secondary_text && ( - - {mainPriceDisplay?.secondary_text} - - )} -
    -

    -
    -
    - {showFeatures && featureItems.length > 0 && ( -
    - -
    - )} -
    -
    - - {productDisplay?.button_text || buttonText} - -
    -
    -
    - ); + return ( +
    + {productDisplay?.recommend_text && ( + + )} +
    +
    +
    +
    +

    + {productDisplay?.name || name || ( + + Name this product + + )} +

    + {productDisplay?.description && ( +
    +

    {productDisplay?.description}

    +
    + )} +
    +
    +

    +
    + {mainPriceDisplay?.primary_text}{" "} + {mainPriceDisplay?.secondary_text && ( + + {mainPriceDisplay?.secondary_text} + + )} +
    +

    +
    +
    + {showFeatures && featureItems.length > 0 && ( +
    + +
    + )} +
    +
    + + {productDisplay?.button_text || buttonText} + +
    +
    +
    + ); }; // Pricing Feature List export const PricingFeatureList = ({ - items, - everythingFrom, - className, + items, + everythingFrom, + className, }: { - items: ProductItem[]; - everythingFrom?: string; - className?: string; + items: ProductItem[]; + everythingFrom?: string; + className?: string; }) => { - return ( -
    - {everythingFrom && ( -

    Everything from {everythingFrom}, plus:

    - )} -
    - {items.map((item, index) => ( -
    -
    - {item.display?.primary_text} - {item.display?.secondary_text && ( - - {item.display?.secondary_text} - - )} -
    -
    - ))} -
    -
    - ); + return ( +
    + {everythingFrom && ( +

    Everything from {everythingFrom}, plus:

    + )} +
    + {items.map((item, index) => ( +
    +
    + {item.display?.primary_text} + {item.display?.secondary_text && ( + + {item.display?.secondary_text} + + )} +
    +
    + ))} +
    +
    + ); }; // Pricing Card Button export interface PricingCardButtonProps extends React.ComponentProps<"button"> { - recommended?: boolean; - buttonUrl?: string; + recommended?: boolean; + buttonUrl?: string; } export const PricingCardButton = React.forwardRef< - HTMLButtonElement, - PricingCardButtonProps + HTMLButtonElement, + PricingCardButtonProps >(({ recommended, children, className, onClick, ...props }, ref) => { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); - const handleClick = async (e: React.MouseEvent) => { - setLoading(true); - try { - await onClick?.(e); - } catch (error) { - console.error(error); - } finally { - setLoading(false); - } - }; + const handleClick = async (e: React.MouseEvent) => { + setLoading(true); + try { + await onClick?.(e); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; - return ( - - ); + return ( + + ); }); PricingCardButton.displayName = "PricingCardButton"; // Annual Switch export const AnnualSwitch = ({ - isAnnualToggle, - setIsAnnualToggle, + isAnnualToggle, + setIsAnnualToggle, }: { - isAnnualToggle: boolean; - setIsAnnualToggle: (isAnnual: boolean) => void; + isAnnualToggle: boolean; + setIsAnnualToggle: (isAnnual: boolean) => void; }) => { - return ( -
    - Monthly - - Annual -
    - ); + return ( +
    + Monthly + + Annual +
    + ); }; export const RecommendedBadge = ({ recommended }: { recommended: string }) => { - return ( -
    - {recommended} -
    - ); + return ( +
    + {recommended} +
    + ); }; diff --git a/vite/src/components/general/AdminHover.tsx b/vite/src/components/general/AdminHover.tsx index 6dfb551d5..296a66ece 100644 --- a/vite/src/components/general/AdminHover.tsx +++ b/vite/src/components/general/AdminHover.tsx @@ -1,8 +1,8 @@ import React, { - useState, - forwardRef, - cloneElement, - isValidElement, + useState, + forwardRef, + cloneElement, + isValidElement, } from "react"; import { Check } from "lucide-react"; import { Tooltip, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; @@ -13,94 +13,94 @@ import { useSession } from "@/lib/auth-client"; import { useAdmin } from "@/views/admin/hooks/useAdmin"; export const AdminHover = forwardRef< - HTMLElement, - { - children: React.ReactNode; - texts: (string | { key: string; value: string } | undefined | null)[]; - hide?: boolean; - asChild?: boolean; - } + HTMLElement, + { + children: React.ReactNode; + texts: (string | { key: string; value: string } | undefined | null)[]; + hide?: boolean; + asChild?: boolean; + } >(({ children, texts, hide = false, asChild = false }, ref) => { - const { isAdmin } = useAdmin(); + const { isAdmin } = useAdmin(); - if (!isAdmin || hide) return <>{children}; + if (!isAdmin || hide) return <>{children}; - // Try to forward the ref to the child if possible - let triggerChild = children; - if (isValidElement(children)) { - triggerChild = cloneElement(children as React.ReactElement, { ref }); - } + // Try to forward the ref to the child if possible + let triggerChild = children; + if (isValidElement(children)) { + triggerChild = cloneElement(children as React.ReactElement, { ref }); + } - return ( - - - - {triggerChild} - - {isAdmin && ( - -
    - {texts.map((text: any) => { - if (!text) return; - if (typeof text === "object") { - return ( -
    -

    - {text.key} -

    - -
    - ); - } else { - return ; - } - })} -
    -
    - )} -
    -
    - ); + return ( + + + + {triggerChild} + + {isAdmin && ( + +
    + {texts.map((text: any) => { + if (!text) return; + if (typeof text === "object") { + return ( +
    +

    + {text.key} +

    + +
    + ); + } else { + return ; + } + })} +
    +
    + )} +
    +
    + ); }); const CopyText = ({ text }: { text: string }) => { - const [isHover, setIsHover] = useState(false); - const [isCopied, setIsCopied] = useState(false); + const [isHover, setIsHover] = useState(false); + const [isCopied, setIsCopied] = useState(false); - return ( -
    -

    setIsHover(true)} - onMouseLeave={() => setIsHover(false)} - className="flex flex-col items-start gap-1 font-mono hover:underline" - onClick={(e) => { - e.stopPropagation(); - e.preventDefault(); - navigator.clipboard.writeText(text); - setIsCopied(true); - setTimeout(() => { - setIsCopied(false); - }, 1000); - }} - > - {text && text.split("\n").map((line, i) => {line})} -

    - {isCopied || isHover ? ( -
    { - navigator.clipboard.writeText(text); - setIsCopied(true); - }} - > - {isCopied ? : } -
    - ) : ( - - )} -
    - ); + return ( +
    +

    setIsHover(true)} + onMouseLeave={() => setIsHover(false)} + className="flex flex-col items-start gap-1 font-mono hover:underline" + onClick={(e) => { + e.stopPropagation(); + e.preventDefault(); + navigator.clipboard.writeText(text); + setIsCopied(true); + setTimeout(() => { + setIsCopied(false); + }, 1000); + }} + > + {text && text.split("\n").map((line, i) => {line})} +

    + {isCopied || isHover ? ( +
    { + navigator.clipboard.writeText(text); + setIsCopied(true); + }} + > + {isCopied ? : } +
    + ) : ( + + )} +
    + ); }; diff --git a/vite/src/components/general/ChatWidget.tsx b/vite/src/components/general/ChatWidget.tsx index e819f65c1..8a3937ef3 100644 --- a/vite/src/components/general/ChatWidget.tsx +++ b/vite/src/components/general/ChatWidget.tsx @@ -4,46 +4,46 @@ import { useEffect } from "react"; import { useSession } from "@/lib/auth-client"; declare global { - interface Window { - patch?: { - config: { - organizationId: string; - email?: string | null; - name?: string | null; - avatar_url?: string | null; - }; - }; - } + interface Window { + patch?: { + config: { + organizationId: string; + email?: string | null; + name?: string | null; + avatar_url?: string | null; + }; + }; + } } let widgetInitialized = false; export function ChatWidget() { - const { data: session, isPending } = useSession(); + const { data: session, isPending } = useSession(); - useEffect(() => { - if (isPending || widgetInitialized) return; + useEffect(() => { + if (isPending || widgetInitialized) return; - widgetInitialized = true; + widgetInitialized = true; - // Set config - window.patch = { - config: { - organizationId: import.meta.env.VITE_PATCH_ORG_ID, - email: session?.user?.email, - name: session?.user?.name, - avatar_url: session?.user?.image, - }, - }; + // Set config + window.patch = { + config: { + organizationId: import.meta.env.VITE_PATCH_ORG_ID, + email: session?.user?.email, + name: session?.user?.name, + avatar_url: session?.user?.image, + }, + }; - // Load script - const script = document.createElement("script"); - script.src = "https://chat.patch.bot/widget/loader.js"; - script.async = true; - document.head.appendChild(script); + // Load script + const script = document.createElement("script"); + script.src = "https://chat.patch.bot/widget/loader.js"; + script.async = true; + document.head.appendChild(script); - return; - }, [session, isPending]); + return; + }, [session, isPending]); - return null; + return null; } diff --git a/vite/src/components/general/CodeDisplay.tsx b/vite/src/components/general/CodeDisplay.tsx index a78fa93ce..7e8419d26 100644 --- a/vite/src/components/general/CodeDisplay.tsx +++ b/vite/src/components/general/CodeDisplay.tsx @@ -7,62 +7,62 @@ import "@wooorm/starry-night/style/dark"; import CopyButton from "@/components/general/CopyButton"; interface CodeDisplayProps { - code: string; - language: string; + code: string; + language: string; } const getStarryNight = async () => { - return await createStarryNight(common); + return await createStarryNight(common); }; export const CodeDisplay: React.FC = ({ code, language }) => { - const [highlightedCode, setHighlightedCode] = React.useState(""); + const [highlightedCode, setHighlightedCode] = React.useState(""); - // Format language name to look nicer - const formatLanguage = (lang: string) => { - return lang.charAt(0).toUpperCase() + lang.slice(1).toLowerCase(); - }; + // Format language name to look nicer + const formatLanguage = (lang: string) => { + return lang.charAt(0).toUpperCase() + lang.slice(1).toLowerCase(); + }; - React.useEffect(() => { - const highlight = async () => { - const starryNight = await getStarryNight(); - const scope = starryNight.flagToScope(language); + React.useEffect(() => { + const highlight = async () => { + const starryNight = await getStarryNight(); + const scope = starryNight.flagToScope(language); - if (scope) { - const tree = starryNight.highlight(code, scope); - const html = toHtml(tree as any); - // Wrap the HTML in a div with inline styles - const styledHtml = `
    ${html}
    `; - setHighlightedCode(styledHtml); - } else { - setHighlightedCode( - `
    ${code}
    ` - ); - } - }; + if (scope) { + const tree = starryNight.highlight(code, scope); + const html = toHtml(tree as any); + // Wrap the HTML in a div with inline styles + const styledHtml = `
    ${html}
    `; + setHighlightedCode(styledHtml); + } else { + setHighlightedCode( + `
    ${code}
    `, + ); + } + }; - highlight(); - }, [code, language]); + highlight(); + }, [code, language]); - return ( -
    -
    -
    - {formatLanguage(language)} -
    - -
    -
    -        
    - -
    -
    -
    - ); + return ( +
    +
    +
    + {formatLanguage(language)} +
    + +
    +
    +				
    + +
    +
    +
    + ); }; diff --git a/vite/src/components/general/CopyButton.tsx b/vite/src/components/general/CopyButton.tsx index 69bec1b43..a6979c20e 100644 --- a/vite/src/components/general/CopyButton.tsx +++ b/vite/src/components/general/CopyButton.tsx @@ -4,53 +4,53 @@ import { Check, Copy } from "lucide-react"; import { cn } from "@/lib/utils"; function CopyButton({ - text, - className, - children, - copySize, - variant = "outline", - icon, + text, + className, + children, + copySize, + variant = "outline", + icon, }: { - text: string; - className?: string; - children?: React.ReactNode; - copySize?: number; - variant?: string; - icon?: React.ReactNode; + text: string; + className?: string; + children?: React.ReactNode; + copySize?: number; + variant?: string; + icon?: React.ReactNode; }) { - const [copied, setCopied] = useState(false); + const [copied, setCopied] = useState(false); - useEffect(() => { - setTimeout(() => { - setCopied(false); - }, 1000); - }, [copied]); + useEffect(() => { + setTimeout(() => { + setCopied(false); + }, 1000); + }, [copied]); - return ( - - ); + return ( + + ); } export default CopyButton; diff --git a/vite/src/components/general/CopyablePre.tsx b/vite/src/components/general/CopyablePre.tsx index e6f377e56..6d1593df7 100644 --- a/vite/src/components/general/CopyablePre.tsx +++ b/vite/src/components/general/CopyablePre.tsx @@ -3,17 +3,14 @@ import { Copy } from "lucide-react"; import CopyButton from "./CopyButton"; export function CopyablePre({ text }: { text: string }) { - return ( -
    -
    -        
    -        
    -          {text}
    -        
    -      
    -
    - ); + return ( +
    +
    +				
    +				
    +					{text}
    +				
    +			
    +
    + ); } diff --git a/vite/src/components/general/CustomToaster.tsx b/vite/src/components/general/CustomToaster.tsx index d266a3abc..2262a2605 100644 --- a/vite/src/components/general/CustomToaster.tsx +++ b/vite/src/components/general/CustomToaster.tsx @@ -1,22 +1,22 @@ import { Toaster as ToasterComponent } from "sonner"; export const CustomToaster = () => { - return ( - - ); + }, + }} + /> + ); }; diff --git a/vite/src/components/general/DateInputUnix.tsx b/vite/src/components/general/DateInputUnix.tsx index 402808001..6e82316dd 100644 --- a/vite/src/components/general/DateInputUnix.tsx +++ b/vite/src/components/general/DateInputUnix.tsx @@ -1,7 +1,7 @@ import { - Popover, - PopoverTrigger, - PopoverContent, + Popover, + PopoverTrigger, + PopoverContent, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { CalendarIcon } from "lucide-react"; @@ -11,48 +11,48 @@ import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; export const DateInputUnix = ({ - unixDate, - setUnixDate, - disabled, + unixDate, + setUnixDate, + disabled, }: { - unixDate: number | null; - setUnixDate: (unixDate: number | null) => void; - disabled?: boolean; + unixDate: number | null; + setUnixDate: (unixDate: number | null) => void; + disabled?: boolean; }) => { - const [popoverOpen, setPopoverOpen] = useState(false); - return ( - - - - - - { - setUnixDate(date?.getTime() || null); - setPopoverOpen(false); - }} - initialFocus - /> - - - ); + const [popoverOpen, setPopoverOpen] = useState(false); + return ( + + + + + + { + setUnixDate(date?.getTime() || null); + setPopoverOpen(false); + }} + initialFocus + /> + + + ); }; diff --git a/vite/src/components/general/OnboardingStep.tsx b/vite/src/components/general/OnboardingStep.tsx index 7bb51faa1..3f8b3849f 100644 --- a/vite/src/components/general/OnboardingStep.tsx +++ b/vite/src/components/general/OnboardingStep.tsx @@ -3,40 +3,40 @@ import { cn } from "@/lib/utils"; import { Wallet } from "lucide-react"; interface StepProps { - title: string; - children: React.ReactNode; - className?: string; - description?: React.ReactNode; - number?: number; + title: string; + children: React.ReactNode; + className?: string; + description?: React.ReactNode; + number?: number; } function Step({ title, children, className, description, number }: StepProps) { - return ( -
    -
    - {/* */} -
    - {number || "1"} -
    -
    -
    -
    -

    {title}

    - {description && ( -
    - {description} -
    - )} -
    -
    {children}
    -
    -
    - ); + return ( +
    +
    + {/* */} +
    + {number || "1"} +
    +
    +
    +
    +

    {title}

    + {description && ( +
    + {description} +
    + )} +
    +
    {children}
    +
    +
    + ); } export default Step; diff --git a/vite/src/components/general/PageSectionHeader.tsx b/vite/src/components/general/PageSectionHeader.tsx index bdb0a7a0a..847e93750 100644 --- a/vite/src/components/general/PageSectionHeader.tsx +++ b/vite/src/components/general/PageSectionHeader.tsx @@ -1,60 +1,60 @@ import { cn } from "@/lib/utils"; export const PageSectionHeader = ({ - title, - titleComponent, - endContent, - isOnboarding = false, - addButton, - className, - classNames, - menuComponent, - isSecondary = false, + title, + titleComponent, + endContent, + isOnboarding = false, + addButton, + className, + classNames, + menuComponent, + isSecondary = false, }: { - title?: string; - titleComponent?: React.ReactNode; - endContent?: React.ReactNode; - isOnboarding?: boolean; - addButton?: React.ReactNode; - className?: string; - classNames?: { - title?: string; - }; - menuComponent?: React.ReactNode; - isSecondary?: boolean; + title?: string; + titleComponent?: React.ReactNode; + endContent?: React.ReactNode; + isOnboarding?: boolean; + addButton?: React.ReactNode; + className?: string; + classNames?: { + title?: string; + }; + menuComponent?: React.ReactNode; + isSecondary?: boolean; }) => { - return ( -
    -
    - {title && ( -

    - {title} -

    - )} - {titleComponent} -
    -
    - {endContent} - {addButton && ( -
    {addButton}
    - )} - {menuComponent && ( -
    {menuComponent}
    - )} -
    -
    - ); + return ( +
    +
    + {title && ( +

    + {title} +

    + )} + {titleComponent} +
    +
    + {endContent} + {addButton && ( +
    {addButton}
    + )} + {menuComponent && ( +
    {menuComponent}
    + )} +
    +
    + ); }; diff --git a/vite/src/components/general/SelectType.tsx b/vite/src/components/general/SelectType.tsx index 73fc434ca..72355cef1 100644 --- a/vite/src/components/general/SelectType.tsx +++ b/vite/src/components/general/SelectType.tsx @@ -1,42 +1,42 @@ import { cn } from "@/lib/utils"; export const SelectType = ({ - title, - description, - icon, - isSelected, - onClick, - disabled = false, + title, + description, + icon, + isSelected, + onClick, + disabled = false, }: { - title: string; - description: string; - icon: React.ReactNode; - isSelected: boolean; - onClick: () => void; - disabled?: boolean; + title: string; + description: string; + icon: React.ReactNode; + isSelected: boolean; + onClick: () => void; + disabled?: boolean; }) => { - return ( - - ); + {icon} + + {title} + + +

    {description}

    + + ); }; diff --git a/vite/src/components/general/SideAccordion.tsx b/vite/src/components/general/SideAccordion.tsx index 82f5a93c9..ef40fb855 100644 --- a/vite/src/components/general/SideAccordion.tsx +++ b/vite/src/components/general/SideAccordion.tsx @@ -1,8 +1,8 @@ import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, } from "@/components/ui/accordion"; import { cn } from "@/lib/utils"; import { Button } from "../ui/button"; @@ -10,85 +10,85 @@ import { Plus, PlusIcon } from "lucide-react"; import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; interface SideAccordionProps { - title: string; - value: string; - children: React.ReactNode; - className?: string; - onClick?: () => void; - buttonIcon?: React.ReactNode; - isOpen?: boolean; - onToggle?: (value: string) => void; - disabled?: boolean; - disabledReason?: string; + title: string; + value: string; + children: React.ReactNode; + className?: string; + onClick?: () => void; + buttonIcon?: React.ReactNode; + isOpen?: boolean; + onToggle?: (value: string) => void; + disabled?: boolean; + disabledReason?: string; } export function SideAccordion({ - title, - value, - children, - className, - onClick, - buttonIcon, - isOpen = false, - onToggle, - disabled = false, - disabledReason, + title, + value, + children, + className, + onClick, + buttonIcon, + isOpen = false, + onToggle, + disabled = false, + disabledReason, }: SideAccordionProps) { - const handleButtonClick = () => { - if (!isOpen && onToggle) { - onToggle(value); - } + const handleButtonClick = () => { + if (!isOpen && onToggle) { + onToggle(value); + } - if (onClick) { - onClick(); - } - }; + if (onClick) { + onClick(); + } + }; - const SideButton = ( - - ); + const SideButton = ( + + ); - return ( -
    - -
    -
    - - {title} - -
    - {onClick && - (disabledReason ? ( - - {SideButton} - {disabledReason} - - ) : ( - SideButton - ))} -
    - -
    - {children} -
    -
    -
    -
    - ); + return ( +
    + +
    +
    + + {title} + +
    + {onClick && + (disabledReason ? ( + + {SideButton} + {disabledReason} + + ) : ( + SideButton + ))} +
    + +
    + {children} +
    +
    +
    +
    + ); } diff --git a/vite/src/components/general/SmallSpinner.tsx b/vite/src/components/general/SmallSpinner.tsx index de5407491..b539c1d71 100644 --- a/vite/src/components/general/SmallSpinner.tsx +++ b/vite/src/components/general/SmallSpinner.tsx @@ -1,13 +1,13 @@ import { LucideLoaderCircle } from "lucide-react"; function SmallSpinner({ size = 18 }: { size?: number }) { - return ( - - ); + return ( + + ); } export default SmallSpinner; diff --git a/vite/src/components/general/TableGrid.tsx b/vite/src/components/general/TableGrid.tsx index a40559721..10e7e445b 100644 --- a/vite/src/components/general/TableGrid.tsx +++ b/vite/src/components/general/TableGrid.tsx @@ -1,52 +1,52 @@ import { cn } from "@/lib/utils"; export const Row = ({ - children, - className, - type, - onClick, - isOnboarding = false, - ...props + children, + className, + type, + onClick, + isOnboarding = false, + ...props }: { - children?: React.ReactNode; - className?: string; - type?: "header" | "body"; - onClick?: () => void; - isOnboarding?: boolean; - props?: React.ComponentProps<"div">; + children?: React.ReactNode; + className?: string; + type?: "header" | "body"; + onClick?: () => void; + isOnboarding?: boolean; + props?: React.ComponentProps<"div">; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const Item = ({ - children, - className, + children, + className, }: { - children?: React.ReactNode; - className?: string; + children?: React.ReactNode; + className?: string; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; diff --git a/vite/src/components/general/ToggleButton.tsx b/vite/src/components/general/ToggleButton.tsx index 1704af6b6..e5868a6c6 100644 --- a/vite/src/components/general/ToggleButton.tsx +++ b/vite/src/components/general/ToggleButton.tsx @@ -6,58 +6,58 @@ import { Switch } from "../ui/switch"; import { InfoTooltip } from "./modal-components/InfoTooltip"; export const ToggleButton = ({ - value, - setValue, - tooltipContent, - buttonText, - className, - disabled, - infoContent, - switchClassName, + value, + setValue, + tooltipContent, + buttonText, + className, + disabled, + infoContent, + switchClassName, }: { - value: boolean; - setValue: (value: boolean) => void; - tooltipContent?: string; - buttonText?: string | React.ReactNode; - className?: string; - disabled?: boolean; - infoContent?: string; - switchClassName?: string; + value: boolean; + setValue: (value: boolean) => void; + tooltipContent?: string; + buttonText?: string | React.ReactNode; + className?: string; + disabled?: boolean; + infoContent?: string; + switchClassName?: string; }) => { - const MainButton = ( - - ); + const MainButton = ( + + ); - if (tooltipContent) { - return ( - - {MainButton} - {tooltipContent} - - ); - } + if (tooltipContent) { + return ( + + {MainButton} + {tooltipContent} + + ); + } - return MainButton; + return MainButton; }; diff --git a/vite/src/components/general/ToggleDisplayButton.tsx b/vite/src/components/general/ToggleDisplayButton.tsx index 0e22dceec..5c3c03d0b 100644 --- a/vite/src/components/general/ToggleDisplayButton.tsx +++ b/vite/src/components/general/ToggleDisplayButton.tsx @@ -3,25 +3,25 @@ import { cn } from "@/lib/utils"; import { Button } from "../ui/button"; export const ToggleDisplayButton = ({ - show, - disabled, - onClick, - children, - className, + show, + disabled, + onClick, + children, + className, }: any) => { - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/general/modal-components/DialogContentWrapper.tsx b/vite/src/components/general/modal-components/DialogContentWrapper.tsx index b3dc688ac..6a3db9ed1 100644 --- a/vite/src/components/general/modal-components/DialogContentWrapper.tsx +++ b/vite/src/components/general/modal-components/DialogContentWrapper.tsx @@ -2,59 +2,59 @@ import { DialogContent } from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; export const CustomDialogContent = ({ - children, - fromTop = true, - setStep, - className, + children, + fromTop = true, + setStep, + className, }: { - children: React.ReactNode; - fromTop?: boolean; - setStep?: (step: any) => void; - className?: string; + children: React.ReactNode; + fromTop?: boolean; + setStep?: (step: any) => void; + className?: string; }) => { - return ( - - {children} - - ); + return ( + + {children} + + ); }; export const CustomDialogBody = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const CustomDialogFooter = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; diff --git a/vite/src/components/general/modal-components/InfoTooltip.tsx b/vite/src/components/general/modal-components/InfoTooltip.tsx index 217658195..0e476c210 100644 --- a/vite/src/components/general/modal-components/InfoTooltip.tsx +++ b/vite/src/components/general/modal-components/InfoTooltip.tsx @@ -1,27 +1,27 @@ import { - Tooltip, - TooltipContent, - TooltipTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, } from "@/components/ui/tooltip"; import { TooltipContentProps } from "@radix-ui/react-tooltip"; import { InfoIcon } from "lucide-react"; export const InfoTooltip = ({ - children, - className, - ...props + children, + className, + ...props }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; } & TooltipContentProps) => { - return ( - - - - - - {children} - - - ); + return ( + + + + + + {children} + + + ); }; diff --git a/vite/src/components/general/modal-components/WarningBox.tsx b/vite/src/components/general/modal-components/WarningBox.tsx index 315714e9c..5b0c18894 100644 --- a/vite/src/components/general/modal-components/WarningBox.tsx +++ b/vite/src/components/general/modal-components/WarningBox.tsx @@ -1,20 +1,20 @@ import { cn } from "@/lib/utils"; export const WarningBox = ({ - className, - children, + className, + children, }: { - className?: string; - children: React.ReactNode; + className?: string; + children: React.ReactNode; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; diff --git a/vite/src/components/general/sidebar/sidebar-label.tsx b/vite/src/components/general/sidebar/sidebar-label.tsx index 3b49f2e94..b19b06684 100644 --- a/vite/src/components/general/sidebar/sidebar-label.tsx +++ b/vite/src/components/general/sidebar/sidebar-label.tsx @@ -1,17 +1,17 @@ import { cn } from "@/lib/utils"; export const SidebarLabel = ({ - className, - children, + className, + children, }: { - className?: string; - children: React.ReactNode; + className?: string; + children: React.ReactNode; }) => { - return ( - - {children} - - ); + return ( + + {children} + + ); }; diff --git a/vite/src/components/general/table-components/ContextMenuWrapper.tsx b/vite/src/components/general/table-components/ContextMenuWrapper.tsx index e58503275..18759d44b 100644 --- a/vite/src/components/general/table-components/ContextMenuWrapper.tsx +++ b/vite/src/components/general/table-components/ContextMenuWrapper.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; export const useContextMenu = () => { - const [openKeyId, setOpenKeyId] = useState(null); + const [openKeyId, setOpenKeyId] = useState(null); - return { - openKeyId, - setOpenKeyId, - getContextMenuProps: (keyId: string) => ({ - onOpenChange: (open: boolean) => setOpenKeyId(open ? keyId : null), - }), - }; + return { + openKeyId, + setOpenKeyId, + getContextMenuProps: (keyId: string) => ({ + onOpenChange: (open: boolean) => setOpenKeyId(open ? keyId : null), + }), + }; }; diff --git a/vite/src/components/general/table-components/HamburgerMenu.tsx b/vite/src/components/general/table-components/HamburgerMenu.tsx index 0ed7287ab..365decfc0 100644 --- a/vite/src/components/general/table-components/HamburgerMenu.tsx +++ b/vite/src/components/general/table-components/HamburgerMenu.tsx @@ -12,7 +12,7 @@ import { Button } from "@/components/ui/button"; import { EllipsisVertical } from "lucide-react"; export interface MenuAction { - type: 'item' | 'sub'; + type: "item" | "sub"; label: string; onClick?: () => void; tooltip?: string; @@ -35,7 +35,7 @@ export const HamburgerMenu = ({ contentAlign?: "start" | "center" | "end"; }) => { const renderMenuAction = (action: MenuAction) => { - if (action.type === 'sub') { + if (action.type === "sub") { return ( @@ -60,7 +60,7 @@ export const HamburgerMenu = ({ action.onClick?.(); setDropdownOpen(false); }} - className="cursor-pointer" + className="cursor-pointer" > {action.label} @@ -77,7 +77,10 @@ export const HamburgerMenu = ({ className={`w-10 h-10 p-0 text-purple-600 hover:text-purple-700 hover:bg-purple-50 ${triggerClassName || ""}`} /> - + {actions.map(renderMenuAction)} diff --git a/vite/src/components/general/table-components/TableButton.tsx b/vite/src/components/general/table-components/TableButton.tsx index b54a38b0f..946258d65 100644 --- a/vite/src/components/general/table-components/TableButton.tsx +++ b/vite/src/components/general/table-components/TableButton.tsx @@ -2,33 +2,33 @@ import { Button, ButtonProps } from "@/components/ui/button"; import { cn } from "@/lib/utils"; export const TableButton = ({ - className, - children, - icon, - onClick, + className, + children, + icon, + onClick, }: { - className?: string; - children?: any; - icon?: React.ReactNode; - onClick?: any; + className?: string; + children?: any; + icon?: React.ReactNode; + onClick?: any; }) => { - return ( - - ); + return ( + + ); }; diff --git a/vite/src/components/general/table-components/ToolbarButton.tsx b/vite/src/components/general/table-components/ToolbarButton.tsx index 5897adc44..d5e17ed6b 100644 --- a/vite/src/components/general/table-components/ToolbarButton.tsx +++ b/vite/src/components/general/table-components/ToolbarButton.tsx @@ -4,20 +4,20 @@ import { forwardRef } from "react"; import { EllipsisVertical } from "lucide-react"; export const ToolbarButton = forwardRef( - (props, ref) => { - return ( - - ); - } + (props, ref) => { + return ( + + ); + }, ); diff --git a/vite/src/components/pricing/attach-pricing-dialog.tsx b/vite/src/components/pricing/attach-pricing-dialog.tsx index 230bd4461..09a774804 100644 --- a/vite/src/components/pricing/attach-pricing-dialog.tsx +++ b/vite/src/components/pricing/attach-pricing-dialog.tsx @@ -1,80 +1,80 @@ import { cn } from "@/lib/utils"; import { Button } from "../ui/button"; export const PriceItem = ({ - children, - className, - ...props + children, + className, + ...props }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; } & React.HTMLAttributes) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const QuantityInput = ({ - children, - onChange, - value, - className, - ...props + children, + onChange, + value, + className, + ...props }: { - children: React.ReactNode; - value: string | number; - onChange: (e: React.ChangeEvent) => void; - className?: string; + children: React.ReactNode; + value: string | number; + onChange: (e: React.ChangeEvent) => void; + className?: string; } & React.HTMLAttributes) => { - const currentValue = Number(value) || 0; + const currentValue = Number(value) || 0; - const handleValueChange = (newValue: number) => { - const syntheticEvent = { - target: { value: String(newValue) }, - } as React.ChangeEvent; - onChange(syntheticEvent); - }; + const handleValueChange = (newValue: number) => { + const syntheticEvent = { + target: { value: String(newValue) }, + } as React.ChangeEvent; + onChange(syntheticEvent); + }; - return ( -
    -
    - - {currentValue} - -
    - {children} -
    - ); + return ( +
    +
    + + {currentValue} + +
    + {children} +
    + ); }; export const TotalPrice = ({ children }: { children: React.ReactNode }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; diff --git a/vite/src/components/pricing/pricing-dialog.tsx b/vite/src/components/pricing/pricing-dialog.tsx index 861a049bd..346ff7310 100644 --- a/vite/src/components/pricing/pricing-dialog.tsx +++ b/vite/src/components/pricing/pricing-dialog.tsx @@ -1,183 +1,183 @@ import { cn } from "@/lib/utils"; import { - Dialog, - DialogContent, - DialogFooter, - DialogTitle, + Dialog, + DialogContent, + DialogFooter, + DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ArrowRight } from "lucide-react"; import React from "react"; export const PricingDialog = ({ - open, - setOpen, - children, - className, + open, + setOpen, + children, + className, }: { - open: boolean; - setOpen: (open: boolean) => void; - children: React.ReactNode; - className?: string; + open: boolean; + setOpen: (open: boolean) => void; + children: React.ReactNode; + className?: string; }) => { - return ( - - - {children} - - - ); + return ( + + + {children} + + + ); }; export const PricingDialogTitle = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return ( - - {children} - - ); + return ( + + {children} + + ); }; export const Information = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const PriceItem = ({ - children, - className, - ...props + children, + className, + ...props }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; } & React.HTMLAttributes) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const QuantityInput = ({ - children, - onChange, - value, - className, - ...props + children, + onChange, + value, + className, + ...props }: { - children: React.ReactNode; - value: string | number; - onChange: (e: React.ChangeEvent) => void; - className?: string; + children: React.ReactNode; + value: string | number; + onChange: (e: React.ChangeEvent) => void; + className?: string; } & React.HTMLAttributes) => { - const currentValue = Number(value) || 0; + const currentValue = Number(value) || 0; - const handleValueChange = (newValue: number) => { - const syntheticEvent = { - target: { value: String(newValue) }, - } as React.ChangeEvent; - onChange(syntheticEvent); - }; + const handleValueChange = (newValue: number) => { + const syntheticEvent = { + target: { value: String(newValue) }, + } as React.ChangeEvent; + onChange(syntheticEvent); + }; - return ( -
    -
    - - {currentValue} - -
    - {children} -
    - ); + return ( +
    +
    + + {currentValue} + +
    + {children} +
    + ); }; export const PricingDialogFooter = ({ - children, - className, + children, + className, }: { - children: React.ReactNode; - className?: string; + children: React.ReactNode; + className?: string; }) => { - return ( - - {children} - - ); + return ( + + {children} + + ); }; export const TotalPrice = ({ children }: { children: React.ReactNode }) => { - return ( -
    - {children} -
    - ); + return ( +
    + {children} +
    + ); }; export const PricingDialogButton = React.forwardRef< - HTMLButtonElement, - React.ComponentProps + HTMLButtonElement, + React.ComponentProps >(({ children, onClick, disabled, size, className, ...props }, ref) => { - return ( - - ); + return ( + + ); }); PricingDialogButton.displayName = "PricingDialogButton"; diff --git a/vite/src/components/ui/accordion.tsx b/vite/src/components/ui/accordion.tsx index 930ec67b4..0854075b9 100644 --- a/vite/src/components/ui/accordion.tsx +++ b/vite/src/components/ui/accordion.tsx @@ -5,60 +5,60 @@ import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; function Accordion({ - ...props + ...props }: React.ComponentProps) { - return ; + return ; } function AccordionItem({ - className, - ...props + className, + ...props }: React.ComponentProps) { - return ( - - ); + return ( + + ); } function AccordionTrigger({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - - ); + return ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + + ); } function AccordionContent({ - className, - children, - ...props + className, + children, + ...props }: React.ComponentProps) { - return ( - -
    {children}
    -
    - ); + return ( + +
    {children}
    +
    + ); } export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/vite/src/components/ui/alert.tsx b/vite/src/components/ui/alert.tsx index 3b8ee79eb..7ca07c249 100644 --- a/vite/src/components/ui/alert.tsx +++ b/vite/src/components/ui/alert.tsx @@ -1,66 +1,66 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "text-destructive-foreground [&>svg]:text-current *:data-[slot=alert-description]:text-destructive-foreground/80", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); function Alert({ - className, - variant, - ...props + className, + variant, + ...props }: React.ComponentProps<"div"> & VariantProps) { - return ( -
    - ) + return ( +
    + ); } function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
    - ) + return ( +
    + ); } function AlertDescription({ - className, - ...props + className, + ...props }: React.ComponentProps<"div">) { - return ( -
    - ) + return ( +
    + ); } -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/vite/src/components/ui/badge.tsx b/vite/src/components/ui/badge.tsx index 5313a7445..373fcaaa3 100644 --- a/vite/src/components/ui/badge.tsx +++ b/vite/src/components/ui/badge.tsx @@ -5,49 +5,49 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "@/lib/utils"; const badgeVariants = cva( - "inline-flex items-center rounded-full border border-zinc-200 px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 dark:border-zinc-800 dark:focus:ring-zinc-300", - { - variants: { - variant: { - default: - "border-transparent bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-900/80 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/80", - secondary: - "border-transparent bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80", - destructive: - "border-transparent bg-red-500 text-zinc-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/80", - outline: "text-zinc-950 dark:text-zinc-50", - purple: "bg-purple-50 text-purple-500 border-purple-200", - blue: "bg-blue-50 text-blue-500 border-blue-200", + "inline-flex items-center rounded-full border border-zinc-200 px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-zinc-950 focus:ring-offset-2 dark:border-zinc-800 dark:focus:ring-zinc-300", + { + variants: { + variant: { + default: + "border-transparent bg-zinc-900 text-zinc-50 shadow hover:bg-zinc-900/80 dark:bg-zinc-50 dark:text-zinc-900 dark:hover:bg-zinc-50/80", + secondary: + "border-transparent bg-zinc-100 text-zinc-900 hover:bg-zinc-100/80 dark:bg-zinc-800 dark:text-zinc-50 dark:hover:bg-zinc-800/80", + destructive: + "border-transparent bg-red-500 text-zinc-50 shadow hover:bg-red-500/80 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/80", + outline: "text-zinc-950 dark:text-zinc-50", + purple: "bg-purple-50 text-purple-500 border-purple-200", + blue: "bg-blue-50 text-blue-500 border-blue-200", - green: - "bg-green-50 text-green-500 border-green-200 !hover:bg-green-100", - yellow: "bg-yellow-50 text-yellow-500 border-yellow-200", - red: "bg-red-50 text-red-500 border-red-200", - status: "px-1.5 py-0 text-white border-none", - }, - }, - defaultVariants: { - variant: "default", - }, - } + green: + "bg-green-50 text-green-500 border-green-200 !hover:bg-green-100", + yellow: "bg-yellow-50 text-yellow-500 border-yellow-200", + red: "bg-red-50 text-red-500 border-red-200", + status: "px-1.5 py-0 text-white border-none", + }, + }, + defaultVariants: { + variant: "default", + }, + }, ); function Badge({ - className, - variant, - asChild = false, - ...props + className, + variant, + asChild = false, + ...props }: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span"; + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; - return ( - - ); + return ( + + ); } export { Badge, badgeVariants }; diff --git a/vite/src/components/ui/breadcrumb.tsx b/vite/src/components/ui/breadcrumb.tsx index eb88f3212..9392996e1 100644 --- a/vite/src/components/ui/breadcrumb.tsx +++ b/vite/src/components/ui/breadcrumb.tsx @@ -1,109 +1,109 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { ChevronRight, MoreHorizontal } from "lucide-react" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; function Breadcrumb({ ...props }: React.ComponentProps<"nav">) { - return