From dc1c7ecd5fc2abfec1cd382b265c1cae1feaad1a Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:32:16 +0100 Subject: [PATCH 01/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lockfiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pnpm-lock.yaml | 15047 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 15047 insertions(+) create mode 100644 pnpm-lock.yaml diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 000000000..fb7b64c60 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,15047 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@wooorm/starry-night': + specifier: ^3.8.0 + version: 3.8.0 + ag-charts-react: + specifier: ^12.0.1 + version: 12.1.2(react@18.3.1) + chalk: + specifier: ^5.3.0 + version: 5.6.0 + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.4 + tailwind-scrollbar-hide: + specifier: ^4.0.0 + version: 4.0.0(tailwindcss@4.1.12) + devDependencies: + '@types/node': + specifier: ^24.0.3 + version: 24.3.0 + concurrently: + specifier: ^9.1.2 + version: 9.2.1 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + inquirer: + specifier: ^12.6.3 + version: 12.9.4(@types/node@24.3.0) + + server: + dependencies: + '@ai-sdk/anthropic': + specifier: ^1.2.10 + version: 1.2.12(zod@3.25.76) + '@anthropic-ai/sdk': + specifier: ^0.32.1 + version: 0.32.1 + '@autumn/shared': + specifier: workspace:* + version: link:../shared + '@axiomhq/pino': + specifier: ^1.3.1 + version: 1.3.1 + '@browserbasehq/sdk': + specifier: ^2.6.0 + version: 2.6.0 + '@clerk/express': + specifier: ^1.3.22 + version: 1.7.26(express@4.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clickhouse/client': + specifier: ^1.11.2 + version: 1.12.1 + '@date-fns/tz': + specifier: ^1.2.0 + version: 1.4.1 + '@date-fns/utc': + specifier: ^2.1.0 + version: 2.1.1 + '@hyperbrowser/sdk': + specifier: ^0.54.0 + version: 0.54.0 + '@hyperdx/node-opentelemetry': + specifier: ^0.8.2 + version: 0.8.2 + '@logtail/node': + specifier: ^0.5.2 + version: 0.5.5 + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/auto-instrumentations-node': + specifier: ^0.60.1 + version: 0.60.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)) + '@opentelemetry/exporter-trace-otlp-proto': + specifier: ^0.202.0 + version: 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^2.0.1 + version: 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': + specifier: ^2.0.1 + version: 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': + specifier: ^0.202.0 + version: 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^2.0.1 + version: 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^2.0.1 + version: 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.34.0 + version: 1.36.0 + '@react-email/components': + specifier: ^0.0.42 + version: 0.0.42(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@sentry/node': + specifier: ^9.30.0 + version: 9.46.0 + '@supabase/supabase-js': + specifier: ^2.46.2 + version: 2.56.1 + '@upstash/redis': + specifier: ^1.35.1 + version: 1.35.3 + ai: + specifier: ^4.3.10 + version: 4.3.19(react@18.3.1)(zod@3.25.76) + autumn-js: + specifier: ^0.1.8 + version: 0.1.28(better-auth@1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76))(better-call@1.0.16)(convex@1.26.2(@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1))(react@18.3.1) + axios: + specifier: ^1.8.3 + version: 1.11.0 + better-auth: + specifier: ^1.2.9 + version: 1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + body-parser: + specifier: ^1.20.3 + version: 1.20.3 + bullmq: + specifier: ^5.56.2 + version: 5.58.4 + chai: + specifier: ^5.1.2 + version: 5.3.3 + chai-http: + specifier: ^5.1.1 + version: 5.1.2 + chalk: + specifier: ^5.3.0 + version: 5.6.0 + cloudflare: + specifier: ^4.4.1 + version: 4.5.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 + cron: + specifier: ^3.5.0 + version: 3.5.0 + csv-parse: + specifier: ^5.6.0 + version: 5.6.0 + csv-parser: + specifier: ^3.2.0 + version: 3.2.0 + currency-symbol-map: + specifier: ^5.1.0 + version: 5.1.0 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + decimal.js: + specifier: ^10.5.0 + version: 10.6.0 + detect-content-type: + specifier: ^1.2.0 + version: 1.2.0 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + drizzle-orm: + specifier: ^0.43.1 + version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7) + express: + specifier: ^4.21.1 + version: 4.21.2 + express-rate-limit: + specifier: ^7.5.1 + version: 7.5.1(express@4.21.2) + fetch-retry: + specifier: ^6.0.0 + version: 6.0.0 + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 + ioredis: + specifier: ^5.5.0 + version: 5.7.0 + ksuid: + specifier: ^3.0.0 + version: 3.0.0 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + loops: + specifier: ^5.0.1 + version: 5.0.1 + mime-detect: + specifier: ^1.3.0 + version: 1.3.0 + openai: + specifier: ^4.85.2 + version: 4.104.0(ws@8.18.3)(zod@3.25.76) + pg: + specifier: ^8.13.1 + version: 8.16.3 + pino: + specifier: ^9.6.0 + version: 9.9.0 + pino-pretty: + specifier: ^13.0.0 + version: 13.1.1 + postgres: + specifier: ^3.4.7 + version: 3.4.7 + posthog-node: + specifier: ^4.17.2 + version: 4.18.0 + puppeteer-core: + specifier: ^24.14.0 + version: 24.18.0 + react: + specifier: ^18.2.0 + version: 18.3.1 + recaseai: + specifier: ^0.0.37 + version: 0.0.37(ws@8.18.3)(zod@3.25.76) + resend: + specifier: ^4.1.1 + version: 4.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + stripe: + specifier: ^18.4.0 + version: 18.5.0(@types/node@22.18.0) + svix: + specifier: ^1.45.1 + version: 1.75.0 + tsc-alias: + specifier: ^1.8.16 + version: 1.8.16 + ws: + specifier: ^8.18.0 + version: 8.18.3 + zod: + specifier: ^3.25.23 + version: 3.25.76 + devDependencies: + '@types/chai': + specifier: ^5.0.1 + version: 5.2.2 + '@types/chai-http': + specifier: ^3.0.5 + version: 3.0.5 + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.3 + version: 5.0.3 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/mocha': + specifier: ^10.0.10 + version: 10.0.10 + '@types/node': + specifier: ^22.13.4 + version: 22.18.0 + '@types/pg': + specifier: ^8.11.10 + version: 8.15.5 + '@types/react': + specifier: ^18.3.18 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.7(@types/react@18.3.24) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.4 + mocha: + specifier: ^11.1.0 + version: 11.7.1 + nodemon: + specifier: ^3.1.10 + version: 3.1.10 + react-email: + specifier: 4.0.16 + version: 4.0.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.18.0)(typescript@5.7.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + tsx: + specifier: ^4.19.4 + version: 4.20.5 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + + shared: + dependencies: + date-fns: + specifier: ^4.1.0 + version: 4.1.0 + decimal.js: + specifier: ^10.5.0 + version: 10.6.0 + dotenv: + specifier: ^16.5.0 + version: 16.6.1 + drizzle-kit: + specifier: ^0.31.1 + version: 0.31.4 + drizzle-orm: + specifier: ^0.43.1 + version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7) + drizzle-zod: + specifier: ^0.8.2 + version: 0.8.3(drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7))(zod@3.25.76) + zod: + specifier: ^3.25.23 + version: 3.25.76 + devDependencies: + '@types/bun': + specifier: latest + version: 1.2.21(@types/react@18.3.24) + '@types/node': + specifier: ^24.0.3 + version: 24.3.0 + '@typescript/native-preview': + specifier: ^7.0.0-dev.20250731.1 + version: 7.0.0-dev.20250901.1 + cross-env: + specifier: ^7.0.3 + version: 7.0.3 + nodemon: + specifier: ^3.1.7 + version: 3.1.10 + tsx: + specifier: ^4.19.4 + version: 4.20.5 + typescript: + specifier: ^5.7.2 + version: 5.7.3 + + vite: + dependencies: + '@autumn/shared': + specifier: workspace:* + version: link:../shared + '@better-auth/stripe': + specifier: ^1.2.12 + version: 1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + '@clerk/clerk-react': + specifier: ^5.24.2 + version: 5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@fortawesome/free-brands-svg-icons': + specifier: ^6.7.2 + version: 6.7.2 + '@fortawesome/react-fontawesome': + specifier: ^0.2.2 + version: 0.2.6(@fortawesome/fontawesome-svg-core@7.0.0)(react@18.3.1) + '@heroicons/react': + specifier: ^2.2.0 + version: 2.2.0(react@18.3.1) + '@radix-ui/react-accordion': + specifier: ^1.2.11 + version: 1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.1.4 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-context-menu': + specifier: ^2.2.16 + version: 2.2.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.14 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-scroll-area': + specifier: ^1.2.3 + version: 1.2.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.1.6 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.2 + version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.5 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.3 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.1.8 + version: 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@supabase/ssr': + specifier: ^0.5.2 + version: 0.5.2(@supabase/supabase-js@2.56.1) + '@tailwindcss/vite': + specifier: ^4.0.13 + version: 4.1.12(vite@6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)) + '@tanstack/react-query': + specifier: ^5.85.6 + version: 5.87.1(react@18.3.1) + '@tanstack/react-query-devtools': + specifier: ^5.85.6 + version: 5.87.1(@tanstack/react-query@5.87.1(react@18.3.1))(react@18.3.1) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/recharts': + specifier: ^2.0.1 + version: 2.0.1(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(redux@5.0.1) + '@wooorm/starry-night': + specifier: ^3.6.0 + version: 3.8.0 + ag-charts-community: + specifier: ^12.0.2 + version: 12.1.2 + ag-grid-community: + specifier: ^34.0.2 + version: 34.1.2 + ag-grid-react: + specifier: ^34.0.2 + version: 34.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + autumn-js: + specifier: ^0.1.8 + version: 0.1.28(better-auth@1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76))(better-call@1.0.16)(convex@1.26.2(@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1))(react@18.3.1) + axios: + specifier: ^1.8.3 + version: 1.11.0 + better-auth: + specifier: ^1.2.9 + version: 1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + decimal.js: + specifier: ^10.5.0 + version: 10.6.0 + hast-util-to-html: + specifier: ^9.0.5 + version: 9.0.5 + input-otp: + specifier: ^1.4.2 + version: 1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + lucide-react: + specifier: ^0.525.0 + version: 0.525.0(react@18.3.1) + motion: + specifier: ^12.16.0 + version: 12.23.12(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + nuqs: + specifier: ^2.4.3 + version: 2.5.2(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router-dom@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + posthog-js: + specifier: ^1.230.4 + version: 1.261.0 + prism-themes: + specifier: ^1.9.0 + version: 1.9.0 + prismjs: + specifier: ^1.29.0 + version: 1.30.0 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-confetti-explosion: + specifier: ^2.1.2 + version: 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-day-picker: + specifier: ^8.10.1 + version: 8.10.1(date-fns@3.6.0)(react@18.3.1) + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@18.3.1) + react-hot-toast: + specifier: ^2.5.2 + version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-hotkeys-hook: + specifier: ^4.6.1 + version: 4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: + specifier: ^7.3.0 + version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.6.2 + version: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + recharts: + specifier: ^3.1.0 + version: 3.1.2(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(redux@5.0.1) + sonner: + specifier: ^2.0.1 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + stripe: + specifier: ^18.3.0 + version: 18.5.0(@types/node@22.18.0) + svix-react: + specifier: ^1.13.3 + version: 1.13.6(@babel/core@7.28.3)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svix@1.75.0) + swr: + specifier: ^2.3.3 + version: 2.3.6(react@18.3.1) + tailwind-merge: + specifier: ^3.0.2 + version: 3.3.1 + tailwindcss: + specifier: ^4.0.13 + version: 4.1.12 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.1.12) + vaul: + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + zod: + specifier: ^3.25.23 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.21.0 + version: 9.34.0 + '@types/lodash': + specifier: ^4.17.16 + version: 4.17.20 + '@types/node': + specifier: ^22.13.10 + version: 22.18.0 + '@types/prismjs': + specifier: ^1.26.5 + version: 1.26.5 + '@types/react': + specifier: ^18.3.18 + version: 18.3.24 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.7(@types/react@18.3.24) + '@types/react-router-dom': + specifier: ^5.3.3 + version: 5.3.3 + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5)) + eslint: + specifier: ^9.21.0 + version: 9.34.0(jiti@2.5.1) + eslint-plugin-react-hooks: + specifier: ^5.1.0 + version: 5.2.0(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-react-refresh: + specifier: ^0.4.19 + version: 0.4.20(eslint@9.34.0(jiti@2.5.1)) + globals: + specifier: ^15.15.0 + version: 15.15.0 + typescript: + specifier: ~5.7.2 + version: 5.7.3 + typescript-eslint: + specifier: ^8.24.1 + version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + vite: + specifier: ^6.2.0 + version: 6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + +packages: + + '@ai-sdk/anthropic@1.2.12': + resolution: {integrity: sha512-YSzjlko7JvuiyQFmI9RN1tNZdEiZxc+6xld/0tq/VkJaHpEzGAb1yiNxxvmYVcjvfu/PcvCxAAYXmTYQQ63IHQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.0.0 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@anthropic-ai/sdk@0.32.1': + resolution: {integrity: sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg==} + + '@axiomhq/js@1.3.1': + resolution: {integrity: sha512-Ytf5V3wKz8FKNiqJxnqZmUhjgJ7TItKUoyHVNE/H2V9dN1ozD6NNnsueenOjKdA48cm2sGRyP432nworst18aA==} + engines: {node: '>=16'} + + '@axiomhq/pino@1.3.1': + resolution: {integrity: sha512-zf6p2rU+b5XAk8Nj6EdjqdXTCuWQlf+C8UGdumD9xbtDWBYvk/EYkxXKMqK8mo2Gp+Fi+p5eHgjuOtbeop2XBQ==} + engines: {node: '>=16'} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-private-property-in-object@7.21.11': + resolution: {integrity: sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==} + engines: {node: '>=6.9.0'} + deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead. + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} + engines: {node: '>=6.9.0'} + + '@better-auth/stripe@1.3.7': + resolution: {integrity: sha512-hmcWJu9RB1IOf+Mb8izQyLOAkRFKes8O0j/VoIFwLR0l2zWTknXKzkLUjadFPeKXHtV3i2n2++prjuVev6F6zw==} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + '@better-auth/utils@0.2.6': + resolution: {integrity: sha512-3y/vaL5Ox33dBwgJ6ub3OPkVqr6B5xL2kgxNHG8eHZuryLyG/4JSPGqjbdRSgjuy9kALUZYDFl+ORIAxlWMSuA==} + + '@better-fetch/fetch@1.1.18': + resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + + '@browserbasehq/sdk@2.6.0': + resolution: {integrity: sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA==} + + '@clerk/backend@2.10.1': + resolution: {integrity: sha512-6UV9OblBhApQolpTBYg9jRHT5LGEwbAkC10nQoycWs6+3r2LiBE5DAGFp3R0NX0VzqJt4Es3d3e691V/waNAPw==} + engines: {node: '>=18.17.0'} + + '@clerk/clerk-react@5.45.0': + resolution: {integrity: sha512-TgEEUDToeBZlEi+FuzbibAIed7+UazLWmzZZ0Z1ztvI7U8iGYaGrC8+VIE4PosMjrWUy11aW4C5nCwQdq2o5hw==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + + '@clerk/express@1.7.26': + resolution: {integrity: sha512-qVNg5hHUTZsN0Cs5s6InQllz3P1LLCkDA5DOCGuvp3EJ7+tFil1yUTmpn3QyXg3j1IlP8QMGPeUiYev7Mt+ivA==} + engines: {node: '>=18.17.0'} + peerDependencies: + express: ^4.17.0 || ^5.0.0 + + '@clerk/shared@3.23.0': + resolution: {integrity: sha512-HzUzkpjoCMGq3RlpFlWmWYUzD7b2Arqqz5EgPJ5+FsQhCLbq1kHqoTQByTJDX0Ahf8Pvp9E+fbkovaMzBVbuow==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + '@clerk/types@4.83.0': + resolution: {integrity: sha512-WEILYgTGkwdB7OUoxHPrdHe/UAUPYvwutJxrwPyHfTejBmRD0RcKmB8VwTqwOxlfQgcKVDfLQ9jLW8m7NeJHIA==} + engines: {node: '>=18.17.0'} + + '@clickhouse/client-common@1.12.1': + resolution: {integrity: sha512-ccw1N6hB4+MyaAHIaWBwGZ6O2GgMlO99FlMj0B0UEGfjxM9v5dYVYql6FpP19rMwrVAroYs/IgX2vyZEBvzQLg==} + + '@clickhouse/client@1.12.1': + resolution: {integrity: sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A==} + engines: {node: '>=16'} + + '@colors/colors@1.6.0': + resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@date-fns/tz@1.4.1': + resolution: {integrity: sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==} + + '@date-fns/utc@2.1.1': + resolution: {integrity: sha512-SlJDfG6RPeEX8wEVv6ZB3kak4MmbtyiI2qX/5zuKdordbrhB/iaJ58GVMZgJ6P1sJaM1gMgENFYYeg1JWrCFrA==} + + '@drizzle-team/brocli@0.10.2': + resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + + '@emotion/is-prop-valid@0.7.3': + resolution: {integrity: sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==} + + '@emotion/memoize@0.7.1': + resolution: {integrity: sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==} + + '@esbuild-kit/core-utils@3.3.2': + resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild-kit/esm-loader@2.6.5': + resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} + deprecated: 'Merged into tsx: https://tsx.is' + + '@esbuild/aix-ppc64@0.25.4': + resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.18.20': + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.4': + resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.18.20': + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.4': + resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.18.20': + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.4': + resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.18.20': + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.4': + resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.18.20': + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.4': + resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.18.20': + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.4': + resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.18.20': + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.4': + resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.18.20': + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.4': + resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.18.20': + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.4': + resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.18.20': + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.4': + resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.18.20': + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.4': + resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.18.20': + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.4': + resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.18.20': + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.4': + resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.18.20': + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.4': + resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.18.20': + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.4': + resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.18.20': + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.4': + resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.4': + resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.18.20': + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.4': + resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.4': + resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.18.20': + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.4': + resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.18.20': + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.4': + resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.18.20': + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.4': + resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.18.20': + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.4': + resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.18.20': + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.4': + resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.34.0': + resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@fortawesome/fontawesome-common-types@6.7.2': + resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-common-types@7.0.0': + resolution: {integrity: sha512-PGMrIYXLGA5K8RWy8zwBkd4vFi4z7ubxtet6Yn13Plf6krRTwPbdlCwlcfmoX0R7B4Z643QvrtHmdQ5fNtfFCg==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-svg-core@7.0.0': + resolution: {integrity: sha512-obBEF+zd98r/KtKVW6A+8UGWeaOoyMpl6Q9P3FzHsOnsg742aXsl8v+H/zp09qSSu/a/Hxe9LNKzbBaQq1CEbA==} + engines: {node: '>=6'} + + '@fortawesome/free-brands-svg-icons@6.7.2': + resolution: {integrity: sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==} + engines: {node: '>=6'} + + '@fortawesome/react-fontawesome@0.2.6': + resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==} + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 + react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@grpc/grpc-js@1.13.4': + resolution: {integrity: sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==} + engines: {node: '>=12.10.0'} + + '@grpc/proto-loader@0.7.15': + resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} + engines: {node: '>=6'} + hasBin: true + + '@heroicons/react@2.2.0': + resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} + peerDependencies: + react: '>= 16 || ^19.0.0-rc' + + '@hexagon/base64@1.1.28': + resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@hyperbrowser/sdk@0.54.0': + resolution: {integrity: sha512-QmbwMG6niqInlS0FfUWWzvv+DfyINHGnF8i/0oNuyyuiIdqSXnp+UG/74Ci+yz8QuyezmuKzBSp6dBXlVG0Glw==} + + '@hyperdx/instrumentation-exception@0.1.0': + resolution: {integrity: sha512-Jgk7JY5J07Mq9fgXApGVhSkS4+WdzzRcWLReAZhxgo46KShxE6w614mFqUSnuo+z6ghlehsy4ForViUfxrFyew==} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@hyperdx/instrumentation-sentry-node@0.1.0': + resolution: {integrity: sha512-n8d/K/8M2owL2w4FNfV+lSVW6yoznEj5SdRCysV/ZIfyrZwpijiiSn7gkRcrOfKHmrxrupyp7DVg5L19cGuH6A==} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@hyperdx/node-opentelemetry@0.8.2': + resolution: {integrity: sha512-ryL8nqgyqKcYQ6KRTHiwPOI540inxo0yPHoy+tIDkJrqpOG+K0GyFu99CGg38dx3ZgT8dPlxHYzhnFLHHEEpsw==} + hasBin: true + + '@img/sharp-darwin-arm64@0.34.3': + resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.3': + resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.0': + resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.0': + resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.0': + resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.2.0': + resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-ppc64@1.2.0': + resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + cpu: [ppc64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.0': + resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.2.0': + resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.34.3': + resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.34.3': + resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-ppc64@0.34.3': + resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.3': + resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.34.3': + resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.34.3': + resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.34.3': + resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.34.3': + resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.3': + resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.3': + resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.3': + resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/checkbox@4.2.2': + resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.16': + resolution: {integrity: sha512-j1a5VstaK5KQy8Mu8cHmuQvN1Zc62TbLhjJxwHvKPPKEoowSF6h/0UdOpA9DNdWZ+9Inq73+puRq1df6OJ8Sag==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.2.0': + resolution: {integrity: sha512-NyDSjPqhSvpZEMZrLCYUquWNl+XC/moEcVFqS55IEYIYsY0a1cUCevSqk7ctOlnm/RaSBU5psFryNlxcmGrjaA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.18': + resolution: {integrity: sha512-yeQN3AXjCm7+Hmq5L6Dm2wEDeBRdAZuyZ4I7tWSSanbxDzqM0KqzoDbKM7p4ebllAYdoQuPJS6N71/3L281i6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.18': + resolution: {integrity: sha512-xUjteYtavH7HwDMzq4Cn2X4Qsh5NozoDHCJTdoXg9HfZ4w3R6mxV1B9tL7DGJX2eq/zqtsFjhm0/RJIMGlh3ag==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.1': + resolution: {integrity: sha512-Oau4yL24d2B5IL4ma4UpbQigkVhzPDXLoqy1ggK4gnHg/stmkffJE4oOXHXF3uz0UEpywG68KcyXsyYpA1Re/Q==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.13': + resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==} + engines: {node: '>=18'} + + '@inquirer/input@4.2.2': + resolution: {integrity: sha512-hqOvBZj/MhQCpHUuD3MVq18SSoDNHy7wEnQ8mtvs71K8OPZVXJinOzcvQna33dNYLYE4LkA9BlhAhK6MJcsVbw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.18': + resolution: {integrity: sha512-7exgBm52WXZRczsydCVftozFTrrwbG5ySE0GqUd2zLNSBXyIucs2Wnm7ZKLe/aUu6NUg9dg7Q80QIHCdZJiY4A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.18': + resolution: {integrity: sha512-zXvzAGxPQTNk/SbT3carAD4Iqi6A2JS2qtcqQjsL22uvD+JfQzUrDEtPjLL7PLn8zlSNyPdY02IiQjzoL9TStA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.8.4': + resolution: {integrity: sha512-MuxVZ1en1g5oGamXV3DWP89GEkdD54alcfhHd7InUW5BifAdKQEK9SLFa/5hlWbvuhMPlobF0WAx7Okq988Jxg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.6': + resolution: {integrity: sha512-KOZqa3QNr3f0pMnufzL7K+nweFFCCBs6LCXZzXDrVGTyssjLeudn5ySktZYv1XiSqobyHRYYK0c6QsOxJEhXKA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.1.1': + resolution: {integrity: sha512-TkMUY+A2p2EYVY3GCTItYGvqT6LiLzHBnqsU1rJbrpXUijFfM6zvUx0R4civofVwFCmJZcKqOVwwWAjplKkhxA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.3.2': + resolution: {integrity: sha512-nwous24r31M+WyDEHV+qckXkepvihxhnyIaod2MG7eCE6G0Zm/HUF6jgN8GXgf4U7AU6SLseKdanY195cwvU6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.8': + resolution: {integrity: sha512-lg9Whz8onIHRthWaN1Q9EGLa/0LFJjyM8mEUbL1eTi6yMGvBf8gvyDLtxSXztQsxMvhxxNpJYrwa1YHdq+w4Jw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@1.3.1': + resolution: {integrity: sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==} + + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@js-sdsl/ordered-map@4.4.2': + resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} + + '@levischuck/tiny-cbor@0.2.11': + resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} + + '@logtail/core@0.5.4': + resolution: {integrity: sha512-UlbxvUg5MDroqk6gEhtfKuuBShrvD/VaXl3T3jGU1U6I19Y1OwfB7e681LvH7RbsCAnOJFziM/7rMQib/CJTdg==} + + '@logtail/node@0.5.5': + resolution: {integrity: sha512-hSZDjl2wFRxe/+pxSlYNSpm0V3a2ZsiWqBifvtCBHTriwrNkd0LjQtedRlrjoBBumVxIMGdDOCPayAIbcux5YQ==} + + '@logtail/tools@0.5.4': + resolution: {integrity: sha512-S0PtfWOVVgvb36q8mLksMw910mO2TahDaqIYnUM/ZEYX+AwqKRGov4zt/X33zHRzCFVpSKoZFv7bFs8TBie/pQ==} + + '@logtail/types@0.5.3': + resolution: {integrity: sha512-5uKf1xQ7wRqSW0lYIwDpXdXcFHxPKH5EM22MoEjwTpd8suymOYuEdx9qCCHCYey4uOhPq7x6OkZf86bTO83JsA==} + + '@msgpack/msgpack@2.8.0': + resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} + engines: {node: '>= 10'} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} + cpu: [x64] + os: [win32] + + '@next/env@15.5.2': + resolution: {integrity: sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==} + + '@next/swc-darwin-arm64@15.5.2': + resolution: {integrity: sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@next/swc-darwin-x64@15.5.2': + resolution: {integrity: sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@next/swc-linux-arm64-gnu@15.5.2': + resolution: {integrity: sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-arm64-musl@15.5.2': + resolution: {integrity: sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@next/swc-linux-x64-gnu@15.5.2': + resolution: {integrity: sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-linux-x64-musl@15.5.2': + resolution: {integrity: sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@next/swc-win32-arm64-msvc@15.5.2': + resolution: {integrity: sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@next/swc-win32-x64-msvc@15.5.2': + resolution: {integrity: sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@0.6.0': + resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api-logs@0.202.0': + resolution: {integrity: sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.51.1': + resolution: {integrity: sha512-E3skn949Pk1z2XtXu/lxf6QAZpawuTM/IUEXcAzpiUkTd73Hmvw26FiN3cJuTmkpM5hZzHwkomVdtrh/n/zzwA==} + engines: {node: '>=14'} + + '@opentelemetry/api-logs@0.57.2': + resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/auto-instrumentations-node@0.56.1': + resolution: {integrity: sha512-4cK0+unfkXRRbQQg2r9K3ki8JlE0j9Iw8+4DZEkChShAnmviiE+/JMgHGvK+VVcLrSlgV6BBHv4+ZTLukQwhkA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + + '@opentelemetry/auto-instrumentations-node@0.60.1': + resolution: {integrity: sha512-oMBVXiun0qWhj693Y24Ie+75q45YXHRFeH9vX/XBWKRNJIM/02ufjmNvmOdoHY0EPxU9rBmWCW82Uidf54iSPA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.4.1 + '@opentelemetry/core': ^2.0.0 + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/context-async-hooks@2.0.1': + resolution: {integrity: sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-grpc@0.202.0': + resolution: {integrity: sha512-Y84L8Yja/A2qjGEzC/To0yrMUXHrtwJzHtZ2za1/ulZplRe5QFsLNyHixIS42ZYUKuNyWMDgOFhnN2Pz5uThtg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2': + resolution: {integrity: sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.202.0': + resolution: {integrity: sha512-mJWLkmoG+3r+SsYQC+sbWoy1rjowJhMhFvFULeIPTxSI+EZzKPya0+NZ3+vhhgx2UTybGQlye3FBtCH3o6Rejg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-http@0.57.2': + resolution: {integrity: sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.202.0': + resolution: {integrity: sha512-qYwbmNWPkP7AbzX8o4DRu5bb/a0TWYNcpZc1NEAOhuV7pgBpAUPEClxRWPN94ulIia+PfQjzFGMaRwmLGmNP6g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2': + resolution: {integrity: sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0': + resolution: {integrity: sha512-/dq/rf4KCkTYoP+NyPXTE+5wjvfhAHSqK62vRsJ/IalG61VPQvwaL18yWcavbI+44ImQwtMeZxfIJSox7oQL0w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2': + resolution: {integrity: sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.202.0': + resolution: {integrity: sha512-ooYcrf/m9ZuVGpQnER7WRH+JZbDPD389HG7VS/EnvIEF5WpNYEqf+NdmtaAcs51d81QrytTYAubc5bVWi//28w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2': + resolution: {integrity: sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.202.0': + resolution: {integrity: sha512-X0RpPpPjyCAmIq9tySZm0Hk3Ltw8KWsqeNq5I7gS9AR9RzbVHb/l+eiMI1CqSRvW9R47HXcUu/epmEzY8ebFAg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2': + resolution: {integrity: sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.202.0': + resolution: {integrity: sha512-6RvQqZHAPFiwL1OKRJe4ta6SgJx/g8or41B+OovVVEie3HeCDhDGL9S1VJNkBozUz6wTY8a47fQwdMrCOUdMhQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-prometheus@0.57.2': + resolution: {integrity: sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.202.0': + resolution: {integrity: sha512-d5wLdbNA3ahpSeD0I34vbDFMTh4vPsXemH0bKDXLeCVULCAjOJXuZmEiuRammiDgVvvX7CAb/IGLDz8d2QHvoA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2': + resolution: {integrity: sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.202.0': + resolution: {integrity: sha512-/hKE8DaFCJuaQqE1IxpgkcjOolUIwgi3TgHElPVKGdGRBSmJMTmN/cr6vWa55pCJIXPyhKvcMrbrya7DZ3VmzA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.57.2': + resolution: {integrity: sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.202.0': + resolution: {integrity: sha512-z3vzdMclCETGIn8uUBgpz7w651ftCiH2qh3cewhBk+rF0EYPNQ3mJvyxktLnKIBZ/ci0zUknAzzYC7LIIZmggQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2': + resolution: {integrity: sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-zipkin@1.30.1': + resolution: {integrity: sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/exporter-zipkin@2.0.1': + resolution: {integrity: sha512-a9eeyHIipfdxzCfc2XPrE+/TI3wmrZUDFtG2RRXHSbZZULAny7SyybSvaDvS77a7iib5MPiAvluwVvbGTsHxsw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1': + resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-amqplib@0.49.0': + resolution: {integrity: sha512-OCGkE+1JoUN+gOzs3u0GSa7GV//KX6NMKzaPchedae7ZwFVyyBQ8VECJngHgW3k/FLABFnq9Oiym2WZGiWugVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.50.3': + resolution: {integrity: sha512-kotm/mRvSWUauudxcylc5YCDei+G/r+jnOH6q5S99aPLQ/Ms8D2yonMIxEJUILIPlthEmwLYxkw3ualWzMjm/A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-lambda@0.53.1': + resolution: {integrity: sha512-canWSwigcvxq2mIrrxAdj6Cf21ysoxpRoe1T4UGINlKuyvrFiYAKLVZlPqVgP2Dwh9w/5+6yGBouoDhO/dQ0Kg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.49.1': + resolution: {integrity: sha512-Vbj4BYeV/1K4Pbbfk+gQ8gwYL0w+tBeUwG88cOxnF7CLPO1XnskGV8Q3Gzut2Ah/6Dg17dBtlzEqL3UiFP2Z6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-aws-sdk@0.54.0': + resolution: {integrity: sha512-4XnXfpACX8fpOnt/D8d/1AFg3uOwBTG9TopQBuikDZJYUrLUSdT7UiotCFqAM/Z6hQJh72Jy3591C/OrmKct7A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.45.1': + resolution: {integrity: sha512-T9POV9ccS41UjpsjLrJ4i0m8LfplBiN3dMeH9XZ2btiDrjoaWtDrst6tNb1avetBjkeshOuBp1EWKP22EVSr0g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-bunyan@0.48.0': + resolution: {integrity: sha512-Q6ay5CXIKuyejadPoLboz+jKumB3Zuxyk35ycFh9vfIeww3+mNRyMVj6KxHRS0Imbv9zhNbP3uyrUpvEMMyHuw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1': + resolution: {integrity: sha512-RqnP0rK2hcKK1AKcmYvedLiL6G5TvFGiSUt2vI9wN0cCBdTt9Y9+wxxY19KoGxq7e9T/aHow6P5SUhCVI1sHvQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cassandra-driver@0.48.0': + resolution: {integrity: sha512-0dcX8Kx0S6ZAOknrbA+BBh1j5lg5F20W18m5VYoGUxkuLIUbWkQA3uaqeTfqbOwmnBmb1upDPUWPR+g5N12B4Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.43.1': + resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.46.0': + resolution: {integrity: sha512-YNq/7M1JXnWRkpKPC9dbYZA36cg547gY0p1bijW7vuZJ9t5f3alo6w8TWtZwV/hOFtBGHDXVhKVfp2Mh6zVHjQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-cucumber@0.14.1': + resolution: {integrity: sha512-ybO+tmH85pDO0ywTskmrMtZcccKyQr7Eb7wHy1keR2HFfx46SzZbjHo1AuGAX//Hook3gjM7+w211gJ2bwKe1Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-cucumber@0.17.1': + resolution: {integrity: sha512-s/18qxBzjEWcJ8BIPJx5oP6GCB7huOaQai3lpJRXe9AcRTOUn6Jzp6oOeY52GE2uvTebo9/3CurM8So2J96fag==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/instrumentation-dataloader@0.16.1': + resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.19.0': + resolution: {integrity: sha512-zIVRnRs3zDZCqStQcpIdRx3Dz9WXFSVj9qimqI7CRuKao9qnrZYUVQHvvVlLZX3JAg+nDC6JRS95zvbq50hj4A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.43.1': + resolution: {integrity: sha512-e/tMZYU1nc+k+J3259CQtqVZIPsPRSLNoAQbGEmSKrjLEY/KJSbpBZ17lu4dFVBzqoF1cZYIZxn9WPQxy4V9ng==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dns@0.46.0': + resolution: {integrity: sha512-m8u72x2fSIjhP1ITJX9Ims3eR4Qn8ze+QWy9NHYO01JlmiMamoc9TfIOd4dyOtxVja4tjnkWceKQdlEH9F9BoA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.47.1': + resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-express@0.51.1': + resolution: {integrity: sha512-cKmzev7RolYGedQ82hVUoH+74BP4E0xmhUCEagOxmW3aKilXYx01KwsNN6wnx3IXR7u2nlYugQsEsLLA4d829A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.44.2': + resolution: {integrity: sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fastify@0.47.1': + resolution: {integrity: sha512-zTVFju7I67wA7Y2ZJ9Gb5u3zqiXIx5Zcaa2KSapal6VZ6gS8OkoC3t35M/6iazfBIPd9e1uCsonbm8jz8v+x1A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.19.1': + resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.22.0': + resolution: {integrity: sha512-ktQVFD6pd8eAIW6t2DtDuXj2lxq+wnQ8WUkJLNZzl3rEE2TZEiHg7wIkWVoxl4Cz4pJ2YZJbdU2fHAizuDebDw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.43.1': + resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.46.1': + resolution: {integrity: sha512-4rH/7nqxY2rnAodfP5nHMpwC6aFJUGRq9PZ44L5nUf+dxNyeuSPkuvxUr1tOf+qduqkhs2sZDP8/53n9/YmNzQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.47.1': + resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.50.0': + resolution: {integrity: sha512-Nn3vBS5T0Dv4+9WF1dGR0Lgsxuz6ztQmTsxoHvesm6YAAXiHffnwsxBEJUKEJcjxfXzjO1SVuLDkv1bAeQ3NFw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.202.0': + resolution: {integrity: sha512-dWvefHNAyAfaHVmxQ/ySLQSI2hGKLgK1sBtvae4w9xruqU08bBMtvmVeGMA/5whfiUDU8ftp1/84U4Zoe5N56A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-grpc@0.57.2': + resolution: {integrity: sha512-TR6YQA67cLSZzdxbf2SrbADJy2Y8eBW1+9mF15P0VK2MYcpdoUSmQTF1oMkBwa3B9NwqDFA2fq7wYTTutFQqaQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.45.2': + resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.49.0': + resolution: {integrity: sha512-d4BcCjbW7Pfg4FpbAAF0cK/ue3dN02WMw0uO2G792KzDjxj05MtZm3eBTz672j3ejV9hM0HvPPhUHUsIC0H6Gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.202.0': + resolution: {integrity: sha512-oX+jyY2KBg4/nVH3vZhSWDbhywkHgE0fq3YinhUBx0jv+YUWC2UKA7qLkxr/CSzfKsFi/Km0NKV+llH17yYGKw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.57.2': + resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.47.1': + resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.50.1': + resolution: {integrity: sha512-HKrWKOM23qNwqNjWfzkw7mePversmcH5ac6T1dUdiRyJVYaLr4qfydyYkgKIGWHOF2TKvQGobXo3CjvxABQWVw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.11.0': + resolution: {integrity: sha512-+i9VqVEPNObB1tkwcLV6zAafnve72h2Iwo48E11M/kVXMNXlgGhiYckYCmzba8c2u5XD/V98XZDrCIyO8CLCNA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.7.1': + resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.44.1': + resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.47.0': + resolution: {integrity: sha512-OjqjnzXD5+FXVGkOznbRAz9yByb4UWzIUhXjuHvOQ50IUY8mv3rM2Gj6Ar7m5JsENiS5DtAy2Vfwk4e9zNC0ng==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.47.1': + resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.50.2': + resolution: {integrity: sha512-+XTWg7a+u6lu4bm6HN0ItirTF0bBFUJlayqe+iVE87Cwpha7W47DV0aoNL8AzGPczlF1UQVVO0kvcfI8bnrkHw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1': + resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.47.0': + resolution: {integrity: sha512-UJ2UlCAIF+N4zNkiHdMr4O0caN0K6YboAso3/zaFdG1QiPR2zqZcbWAGFBikZ9HSByU+NwbxTXDzlpkcDZIqWg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.43.1': + resolution: {integrity: sha512-rK5YWC22gmsLp2aEbaPk5F+9r6BFFZuc9GTnW/ErrWpz2XNHUgeFInoPDg4t+Trs8OttIfn8XwkfFkSKqhxanw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-memcached@0.46.0': + resolution: {integrity: sha512-FFDcOVJUxZQqbg57gVskZGXRfEsZXwOvCaPv6/qIZRw5glLXPTulpnfG/s8NAltsj2buXSvS4eKFo+0HKH0apw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.52.0': + resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.55.1': + resolution: {integrity: sha512-Wb13YixWm8nB27ZSQW3h070UWkivoh6bjeyDUY6lLimSUulALr+YHBn0t71U1aTcUeaZv3IBNaPRimFXhz6gBA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.46.1': + resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.49.0': + resolution: {integrity: sha512-nF+43QFe8IoW20TmTJZdxZhnVZGEglODUvzAo3fRmaBFAkwUXRGzRgABS255PCjIbScEaRRDCXc6EAsSkwRNPg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.45.2': + resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.48.1': + resolution: {integrity: sha512-deJbaCC595WYhqDoXR5UfRx0bPmYyr1WhiFv6BjHRndMEUStHYYnsF6+mjpa3f0nebVj8Nv8eDcke4iTs7AfBw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.45.1': + resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.48.1': + resolution: {integrity: sha512-wP5+wIfQXmnKY4riKlx+1PiHpMSkG8Zu2cMXkiX6u84HqAWY62+N/Wv3fx9FU+/DU+fz3BdQQbo0EpVWziqqvg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.44.1': + resolution: {integrity: sha512-4TXaqJK27QXoMqrt4+hcQ6rKFd8B6V4JfrTJKnqBmWR1cbaqd/uwyl9yxhNH1JEkyo8GaBfdpBC4ZE4FuUhPmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-nestjs-core@0.48.1': + resolution: {integrity: sha512-rH0IUQRf9wjxEkiPfltM17DVqgSe/rgeIlg1CKRDAhuxWkbXlcHwdOnBfngGKhJNGOlGUo6HzZesavPAlmm3Fw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.43.1': + resolution: {integrity: sha512-TaMqP6tVx9/SxlY81dHlSyP5bWJIKq+K7vKfk4naB/LX4LBePPY3++1s0edpzH+RfwN+tEGVW9zTb9ci0up/lQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-net@0.46.1': + resolution: {integrity: sha512-r7Buqem+odrTTPlWfT7EqS24QnDAL4U+c4e38RzcRtdZF00Z34oqEpge7TZcQLo0vEASWbHQ/WjWNR7ZYKFKBA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-oracledb@0.28.0': + resolution: {integrity: sha512-VObbQRd3g8nDLLOeGjm5l6TnB9dtEaJoedLfLwMGrlD6lkai+hdfalYh6FOF5dce+dJouZdW6NUUAaBj4f4KcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.51.1': + resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.54.1': + resolution: {integrity: sha512-vy2WbrB76iRpJccGCC7HNICVm+f5zg20dYBuB4r1X7TaXYsi1txrjvemOEH0h9lTpm2vVFoheHsVoClxleXvvQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.46.1': + resolution: {integrity: sha512-HB8gD/9CNAKlTV+mdZehnFC4tLUtQ7e+729oGq88e4WipxzZxmMYuRwZ2vzOA9/APtq+MRkERJ9PcoDqSIjZ+g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pino@0.49.1': + resolution: {integrity: sha512-QTD4HQA0fhtu4Hvw9iIlnroDiQju8nmEmnTQrS3xrb6g9AsdBMYnXXyzNBlLpSvtqwfPvgAxfY4DY1IYtnNwDg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.46.1': + resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis-4@0.49.0': + resolution: {integrity: sha512-i+Wsl7M2LXEDA2yXouNJ3fttSzzb5AhlehvSBVRIFuinY51XrrKSH66biO0eox+pYQMwAlPxJ778XcMQffN78A==} + engines: {node: ^18.19.0 || >=20.6.0} + deprecated: Use "@opentelemetry/instrumentation-redis", which (as of v0.50.0) includes support for instrumenting redis v4. + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.46.1': + resolution: {integrity: sha512-AN7OvlGlXmlvsgbLHs6dS1bggp6Fcki+GxgYZdSrb/DB692TyfjR7sVILaCe0crnP66aJuXsg9cge3hptHs9UA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.49.1': + resolution: {integrity: sha512-Ds5Ke9qE9kTlDThqLSJJntkIvuMQCBPiFKwHntocb/3q/9q5D47BNwawO5Mj9sVMV6zkld5M5Pb9Av39iieuOg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.45.1': + resolution: {integrity: sha512-Zd6Go9iEa+0zcoA2vDka9r/plYKaT3BhD3ESIy4JNIzFWXeQBGbH3zZxQIsz0jbNTMEtonlymU7eTLeaGWiApA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-restify@0.48.2': + resolution: {integrity: sha512-V+Bac9zMkQI7p1BsJV/jqg175nBs5aIpeh5HRHxrvlnDaH7TDlHDpax2i+sSrTHjSqxF/jeaIDDjuCW5C5WKbg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.44.1': + resolution: {integrity: sha512-l4T/S7ByjpY5TCUPeDe1GPns02/5BpR0jroSMexyH3ZnXJt9PtYqx1IKAlOjaFEGEOQF2tGDsMi4PY5l+fSniQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-router@0.47.0': + resolution: {integrity: sha512-U0zA1LTDqtTWyd5e4SdoqQA/8QUOhc4LDv9U7b+8FMFTty95OF84apUdatl09Dzc51XeWPWIV7VutmSCd/zsUg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.12.2': + resolution: {integrity: sha512-HNBW1rJiHDBTHQlh5oH1IAcV8O5VR7/L5BBOfGAMpGno3Jq9cNqTh96uUp0qBXBuxD8Yl1eoI5N+B5TdmjLteQ==} + engines: {node: '>=17.4.0'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-runtime-node@0.16.0': + resolution: {integrity: sha512-Q/GB9LsKLrRCEIPLAQTDQvydnLmLXBSRkYkWzwKzY/LCkOs+Cl8YiJG08p6D4CaJ6lvP0iG4kwPHk1ydNbdehg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.46.1': + resolution: {integrity: sha512-9AsCVUAHOqvfe2RM/2I0DsDnx2ihw1d5jIN4+Bly1YPFTJIbk4+bXjAkr9+X6PUfhiV5urQHZkiYYPU1Q4yzPA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-socket.io@0.49.0': + resolution: {integrity: sha512-DpMtNBEcaLCcbP1WVBPCSgRiBs31igTQkal1gUm40VL/XAv5GUqRAUnvHZrQh3yPipOqzV65pdb0jJXdps/tug==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.18.1': + resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.21.1': + resolution: {integrity: sha512-RDQyesAIJ3JHw8w5vthJjrzI3jTm/l6aeEQzIZ6cMG5hcW7ySPSyaxWbtmmp0FHPkdoGwc3Li+UQfV77+xLA1Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.10.1': + resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-undici@0.13.2': + resolution: {integrity: sha512-rO8CNuHnVN13rKrXayvtuXMXwPQkem3H0r/UWZhyNGQeHlqlQgpgtu5mR9dzSuv9kLRrxZb/WjK+sGOP5kwetg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation-winston@0.44.1': + resolution: {integrity: sha512-iexblTsT3fP0hHUz/M1mWr+Ylg3bsYN2En/jvKXZtboW3Qkvt17HrQJYTF9leVIkXAfN97QxAcTE99YGbQW7vQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-winston@0.47.0': + resolution: {integrity: sha512-r+GqnZU/aFldQyB5QdOlxsMlH9KZ4+zJfnYplz3lbC9f9ozAIlVAeoshvWTtbv7Oxp2NnK64EfnNP1pClaGEqA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.202.0': + resolution: {integrity: sha512-Uz3BxZWPgDwgHM2+vCKEQRh0R8WKrd/q6Tus1vThRClhlPO39Dyz7mDrOr6KuqGXAlBQ1e5Tnymzri4RMZNaWA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.51.1': + resolution: {integrity: sha512-JIrvhpgqY6437QIqToyozrUG1h5UhwHkaGK/WAX+fkrpyPtc+RO5FkRtUd9BH0MibabHHvqsnBGKfKVijbmp8w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.57.2': + resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.202.0': + resolution: {integrity: sha512-nMEOzel+pUFYuBJg2znGmHJWbmvMbdX5/RhoKNKowguMbURhz0fwik5tUKplLcUtl8wKPL1y9zPnPxeBn65N0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.57.2': + resolution: {integrity: sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.202.0': + resolution: {integrity: sha512-yIEHVxFA5dmYif7lZbbB66qulLLhrklj6mI2X3cuGW5hYPyUErztEmbroM+6teu/XobBi9bLHid2VT4NIaRuGg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2': + resolution: {integrity: sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.202.0': + resolution: {integrity: sha512-5XO77QFzs9WkexvJQL9ksxL8oVFb/dfi9NWQSq7Sv0Efr9x3N+nb1iklP1TeVgxqJ7m1xWiC/Uv3wupiQGevMw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.57.2': + resolution: {integrity: sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagation-utils@0.30.16': + resolution: {integrity: sha512-ZVQ3Z/PQ+2GQlrBfbMMMT0U7MzvYZLCPP800+ooyaBqm4hMvuQHfP028gB9/db0mwkmyEAMad9houukUVxhwcw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/propagation-utils@0.31.3': + resolution: {integrity: sha512-ZI6LKjyo+QYYZY5SO8vfoCQ9A69r1/g+pyjvtu5RSK38npINN1evEmwqbqhbg2CdcIK3a4PN6pDAJz/yC5/gAA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-b3@2.0.1': + resolution: {integrity: sha512-Hc09CaQ8Tf5AGLmf449H726uRoBNGPBL4bjr7AnnUpzWMvhdn61F78z9qb6IqB737TffBsokGAK1XykFEZ1igw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@2.0.1': + resolution: {integrity: sha512-7PMdPBmGVH2eQNb/AtSJizQNgeNTfh6jQFqys6lfhd6P4r+m/nTh3gKPPpaCXVdRQ+z93vfKk+4UGty390283w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/redis-common@0.36.2': + resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} + engines: {node: '>=14'} + + '@opentelemetry/redis-common@0.37.0': + resolution: {integrity: sha512-tJwgE6jt32bLs/9J6jhQRKU2EZnsD8qaO13aoFyXwF6s4LhpT7YFHf3Z03MqdILk6BA2BFUhoyh7k9fj9i032A==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/redis-common@0.38.0': + resolution: {integrity: sha512-4Wc0AWURII2cfXVVoZ6vDqK+s5n4K5IssdrlVrvGsx6OEOKdghKtJZqXAHWFiZv4nTDLH2/2fldjIHY8clMOjQ==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1': + resolution: {integrity: sha512-9l0FVP3F4+Z6ax27vMzkmhZdNtxAbDqEfy7rduzya3xFLaRiJSvOpw6cru6Edl5LwO+WvgNui+VzHa9ViE8wCg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.3': + resolution: {integrity: sha512-I556LHcLVsBXEgnbPgQISP/JezDt5OfpgOaJNR1iVJl202r+K145OSSOxnH5YOc/KvrydBD0FOE03F7x0xnVTw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@1.12.0': + resolution: {integrity: sha512-Cvi7ckOqiiuWlHBdA1IjS0ufr3sltex2Uws2RK6loVp4gzIJyOijsddAI6IZ5kiO8h/LgCWe8gxPmwkTKImd+Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-aws@2.3.0': + resolution: {integrity: sha512-PkD/lyXG3B3REq1Y6imBLckljkJYXavtqGYSryAeJYvGOf5Ds3doR+BCGjmKeF6ObAtI5MtpBeUStTDtGtBsWA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.6.1': + resolution: {integrity: sha512-Djr31QCExVfWViaf9cGJnH+bUInD72p0GEfgDGgjCAztyvyji6WJvKjs6qmkpPN+Ig6KLk0ho2VgzT5Kdl4L2Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-azure@0.9.0': + resolution: {integrity: sha512-5wJwAAW2vhbqIhgaRisU1y0F5mUco59F/dKgmnnnT6YNbxjrbdUZYxKF5Wl7deJoACVdL5wi/3N97GCXPEwwCQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.6.1': + resolution: {integrity: sha512-o4sLzx149DQXDmVa8pgjBDEEKOj9SuQnkSLbjUVOpQNnn10v0WNR6wLwh30mFsK26xOJ6SpqZBGKZiT7i5MjlA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-container@0.7.3': + resolution: {integrity: sha512-SK+xUFw6DKYbQniaGmIFsFxAZsr8RpRSRWxKi5/ZJAoqqPnjcyGI/SeUx8zzPk4XLO084zyM4pRHgir0hRTaSQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.33.1': + resolution: {integrity: sha512-/aZJXI1rU6Eus04ih2vU0hxXAibXXMzH1WlDZ8bXcTJmhwmTY8cP392+6l7cWeMnTQOibBUz8UKV3nhcCBAefw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resource-detector-gcp@0.36.0': + resolution: {integrity: sha512-mWnEcg4tA+IDPrkETWo42psEsDN20dzYZSm4ZH8m8uiQALnNksVmf5C3An0GUEj5zrrxMasjSuv4zEH1gI40XQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.202.0': + resolution: {integrity: sha512-pv8QiQLQzk4X909YKm0lnW4hpuQg4zHwJ4XBd5bZiXcd9urvrJNoNVKnxGHPiDVX/GiLFvr5DMYsDBQbZCypRQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.57.2': + resolution: {integrity: sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.30.1': + resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-node@0.202.0': + resolution: {integrity: sha512-SF9vXWVd9I5CZ69mW3GfwfLI2SHgyvEqntcg0en5y8kRp5+2PPoa3Mkgj0WzFLrbSgTw4PsXn7c7H6eSdrtV0w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-node@0.57.2': + resolution: {integrity: sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.0.1': + resolution: {integrity: sha512-UhdbPF19pMpBtCWYP5lHbTogLWx9N0EBxtdagvkn5YtsAnCBZzL7SjktG+ZmupRgifsHMjwUaCCaVmqGfSADmA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.40.1': + resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@opentelemetry/sql-common@0.41.0': + resolution: {integrity: sha512-pmzXctVbEERbqSfiAgdes9Y63xjoOyXcD7B6IXBkVb+vbM7M9U98mn33nGXxPf4dfYR0M+vhcKRZmbSJ7HfqFA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@paralleldrive/cuid2@2.2.2': + resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==} + + '@peculiar/asn1-android@2.4.0': + resolution: {integrity: sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==} + + '@peculiar/asn1-ecc@2.4.0': + resolution: {integrity: sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==} + + '@peculiar/asn1-rsa@2.4.0': + resolution: {integrity: sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==} + + '@peculiar/asn1-schema@2.4.0': + resolution: {integrity: sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==} + + '@peculiar/asn1-x509@2.4.0': + resolution: {integrity: sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@posthog/core@1.0.2': + resolution: {integrity: sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==} + + '@prisma/instrumentation@6.11.1': + resolution: {integrity: sha512-mrZOev24EDhnefmnZX7WVVT7v+r9LttPRqf54ONvj6re4XMF7wFTpK2tLJi4XHB7fFp/6xhYbgRel8YV7gQiyA==} + peerDependencies: + '@opentelemetry/api': ^1.8 + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@puppeteer/browsers@2.10.8': + resolution: {integrity: sha512-f02QYEnBDE0p8cteNoPYHHjbDuwyfbe4cCIVlNi8/MRicIxFW4w4CfgU0LNgWEID6s06P+hRJ1qjpBLMhPRCiQ==} + engines: {node: '>=18'} + hasBin: true + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context-menu@2.2.16': + resolution: {integrity: sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@react-email/body@0.0.11': + resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/button@0.0.19': + resolution: {integrity: sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-block@0.0.13': + resolution: {integrity: sha512-4DE4yPSgKEOnZMzcrDvRuD6mxsNxOex0hCYEG9F9q23geYgb2WCCeGBvIUXVzK69l703Dg4Vzrd5qUjl+JfcwA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-inline@0.0.5': + resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/column@0.0.13': + resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/components@0.0.42': + resolution: {integrity: sha512-KZtf7RjCoLgEwa5swrsbEF/liGrmfMlZCDNDXqaAjlgF3iRq0h5KY/HNMs6LbLmW7fEneRhnynrwApwbkEwe+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/container@0.0.15': + resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/font@0.0.9': + resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/head@0.0.12': + resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/heading@0.0.15': + resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/hr@0.0.11': + resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/html@0.0.11': + resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/img@0.0.11': + resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/link@0.0.12': + resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/markdown@0.0.15': + resolution: {integrity: sha512-UQA9pVm5sbflgtg3EX3FquUP4aMBzmLReLbGJ6DZQZnAskBF36aI56cRykDq1o+1jT+CKIK1CducPYziaXliag==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/preview@0.0.13': + resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@1.1.2': + resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/row@0.0.12': + resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/section@0.0.16': + resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/tailwind@1.0.5': + resolution: {integrity: sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/text@0.1.5': + resolution: {integrity: sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@reduxjs/toolkit@2.8.2': + resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rolldown/pluginutils@1.0.0-beta.27': + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} + + '@rollup/rollup-android-arm-eabi@4.50.0': + resolution: {integrity: sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.50.0': + resolution: {integrity: sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.50.0': + resolution: {integrity: sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.50.0': + resolution: {integrity: sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.50.0': + resolution: {integrity: sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.50.0': + resolution: {integrity: sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': + resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.50.0': + resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.50.0': + resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.50.0': + resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': + resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.0': + resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.50.0': + resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.50.0': + resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.0': + resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.50.0': + resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.50.0': + resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.50.0': + resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.0': + resolution: {integrity: sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.50.0': + resolution: {integrity: sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.50.0': + resolution: {integrity: sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==} + cpu: [x64] + os: [win32] + + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + + '@sentry/core@8.55.0': + resolution: {integrity: sha512-6g7jpbefjHYs821Z+EBJ8r4Z7LT5h80YSWRJaylGS4nW5W5Z2KXzpdnyFarv37O7QjauzVC2E+PABmpkw5/JGA==} + engines: {node: '>=14.18'} + + '@sentry/core@9.46.0': + resolution: {integrity: sha512-it7JMFqxVproAgEtbLgCVBYtQ9fIb+Bu0JD+cEplTN/Ukpe6GaolyYib5geZqslVxhp2sQgT+58aGvfd/k0N8Q==} + engines: {node: '>=18'} + + '@sentry/node-core@9.46.0': + resolution: {integrity: sha512-XRVu5pqoklZeh4wqhxCLZkz/ipoKhitctgEFXX9Yh1e1BoHM2pIxT52wf+W6hHM676TFmFXW3uKBjsmRM3AjgA==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/resources': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@sentry/node@9.46.0': + resolution: {integrity: sha512-pRLqAcd7GTGvN8gex5FtkQR5Mcol8gOy1WlyZZFq4rBbVtMbqKOQRhohwqnb+YrnmtFpj7IZ7KNDo077MvNeOQ==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@9.46.0': + resolution: {integrity: sha512-w2zTxqrdmwRok0cXBoh+ksXdGRUHUZhlpfL/H2kfTodOL+Mk8rW72qUmfqQceXoqgbz8UyK8YgJbyt+XS5H4Qg==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/context-async-hooks': ^1.30.1 || ^2.0.0 + '@opentelemetry/core': ^1.30.1 || ^2.0.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.0.0 + '@opentelemetry/semantic-conventions': ^1.34.0 + + '@sentry/types@8.55.0': + resolution: {integrity: sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog==} + engines: {node: '>=14.18'} + + '@sentry/utils@8.55.0': + resolution: {integrity: sha512-cYcl39+xcOivBpN9d8ZKbALl+DxZKo/8H0nueJZ0PO4JA+MJGhSm6oHakXxLPaiMoNLTX7yor8ndnQIuFg+vmQ==} + engines: {node: '>=14.18'} + + '@simplewebauthn/browser@13.1.2': + resolution: {integrity: sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw==} + + '@simplewebauthn/server@13.1.2': + resolution: {integrity: sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g==} + engines: {node: '>=20.0.0'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@supabase/auth-js@2.71.1': + resolution: {integrity: sha512-mMIQHBRc+SKpZFRB2qtupuzulaUhFYupNyxqDj5Jp/LyPvcWvjaJzZzObv6URtL/O6lPxkanASnotGtNpS3H2Q==} + + '@supabase/functions-js@2.4.5': + resolution: {integrity: sha512-v5GSqb9zbosquTo6gBwIiq7W9eQ7rE5QazsK/ezNiQXdCbY+bH8D9qEaBIkhVvX4ZRW5rP03gEfw5yw9tiq4EQ==} + + '@supabase/node-fetch@2.6.15': + resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==} + engines: {node: 4.x || >=6.0.0} + + '@supabase/postgrest-js@1.21.3': + resolution: {integrity: sha512-rg3DmmZQKEVCreXq6Am29hMVe1CzemXyIWVYyyua69y6XubfP+DzGfLxME/1uvdgwqdoaPbtjBDpEBhqxq1ZwA==} + + '@supabase/realtime-js@2.15.4': + resolution: {integrity: sha512-e/FYIWjvQJHOCNACWehnKvg26zosju3694k0NMUNb+JGLdvHJzEa29ZVVLmawd2kvx4hdbv8mxSqfttRnH3+DA==} + + '@supabase/ssr@0.5.2': + resolution: {integrity: sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==} + peerDependencies: + '@supabase/supabase-js': ^2.43.4 + + '@supabase/storage-js@2.11.0': + resolution: {integrity: sha512-Y+kx/wDgd4oasAgoAq0bsbQojwQ+ejIif8uczZ9qufRHWFLMU5cODT+ApHsSrDufqUcVKt+eyxtOXSkeh2v9ww==} + + '@supabase/supabase-js@2.56.1': + resolution: {integrity: sha512-cb/kS0d6G/qbcmUFItkqVrQbxQHWXzfRZuoiSDv/QiU6RbGNTn73XjjvmbBCZ4MMHs+5teihjhpEVluqbXISEg==} + + '@swc/helpers@0.5.15': + resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + + '@tailwindcss/node@4.1.12': + resolution: {integrity: sha512-3hm9brwvQkZFe++SBt+oLjo4OLDtkvlE8q2WalaD/7QWaeM7KEJbAiY/LJZUaCs7Xa8aUu4xy3uoyX4q54UVdQ==} + + '@tailwindcss/oxide-android-arm64@4.1.12': + resolution: {integrity: sha512-oNY5pq+1gc4T6QVTsZKwZaGpBb2N1H1fsc1GD4o7yinFySqIuRZ2E4NvGasWc6PhYJwGK2+5YT1f9Tp80zUQZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + resolution: {integrity: sha512-cq1qmq2HEtDV9HvZlTtrj671mCdGB93bVY6J29mwCyaMYCP/JaUBXxrQQQm7Qn33AXXASPUb2HFZlWiiHWFytw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.12': + resolution: {integrity: sha512-6UCsIeFUcBfpangqlXay9Ffty9XhFH1QuUFn0WV83W8lGdX8cD5/+2ONLluALJD5+yJ7k8mVtwy3zMZmzEfbLg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + resolution: {integrity: sha512-JOH/f7j6+nYXIrHobRYCtoArJdMJh5zy5lr0FV0Qu47MID/vqJAY3r/OElPzx1C/wdT1uS7cPq+xdYYelny1ww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + resolution: {integrity: sha512-v4Ghvi9AU1SYgGr3/j38PD8PEe6bRfTnNSUE3YCMIRrrNigCFtHZ2TCm8142X8fcSqHBZBceDx+JlFJEfNg5zQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + resolution: {integrity: sha512-YP5s1LmetL9UsvVAKusHSyPlzSRqYyRB0f+Kl/xcYQSPLEw/BvGfxzbH+ihUciePDjiXwHh+p+qbSP3SlJw+6g==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + resolution: {integrity: sha512-V8pAM3s8gsrXcCv6kCHSuwyb/gPsd863iT+v1PGXC4fSL/OJqsKhfK//v8P+w9ThKIoqNbEnsZqNy+WDnwQqCA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + resolution: {integrity: sha512-xYfqYLjvm2UQ3TZggTGrwxjYaLB62b1Wiysw/YE3Yqbh86sOMoTn0feF98PonP7LtjsWOWcXEbGqDL7zv0uW8Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + resolution: {integrity: sha512-ha0pHPamN+fWZY7GCzz5rKunlv9L5R8kdh+YNvP5awe3LtuXb5nRi/H27GeL2U+TdhDOptU7T6Is7mdwh5Ar3A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + resolution: {integrity: sha512-4tSyu3dW+ktzdEpuk6g49KdEangu3eCYoqPhWNsZgUhyegEda3M9rG0/j1GV/JjVVsj+lG7jWAyrTlLzd/WEBg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + resolution: {integrity: sha512-iGLyD/cVP724+FGtMWslhcFyg4xyYyM+5F4hGvKA7eifPkXHRAUDFaimu53fpNg9X8dfP75pXx/zFt/jlNF+lg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + resolution: {integrity: sha512-NKIh5rzw6CpEodv/++r0hGLlfgT/gFN+5WNdZtvh6wpU2BpGNgdjvj6H2oFc8nCM839QM1YOhjpgbAONUb4IxA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.12': + resolution: {integrity: sha512-gM5EoKHW/ukmlEtphNwaGx45fGoEmP10v51t9unv55voWh6WrOL19hfuIdo2FjxIaZzw776/BUQg7Pck++cIVw==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.12': + resolution: {integrity: sha512-4pt0AMFDx7gzIrAOIYgYP0KCBuKWqyW8ayrdiLEjoJTT4pKTjrzG/e4uzWtTLDziC+66R9wbUqZBccJalSE5vQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/query-core@5.87.1': + resolution: {integrity: sha512-HOFHVvhOCprrWvtccSzc7+RNqpnLlZ5R6lTmngb8aq7b4rc2/jDT0w+vLdQ4lD9bNtQ+/A4GsFXy030Gk4ollA==} + + '@tanstack/query-devtools@5.86.0': + resolution: {integrity: sha512-/JDw9BP80eambEK/EsDMGAcsL2VFT+8F5KCOwierjPU7QP8Wt1GT32yJpn3qOinBM8/zS3Jy36+F0GiyJp411A==} + + '@tanstack/react-query-devtools@5.87.1': + resolution: {integrity: sha512-YPuEub8RQrrsXOxoiMJn33VcGPIeuVINWBgLu9RLSQB8ueXaKlGLZ3NJkahGpbt2AbWf749FQ6R+1jBFk3kdCA==} + peerDependencies: + '@tanstack/react-query': ^5.87.1 + react: ^18 || ^19 + + '@tanstack/react-query@5.87.1': + resolution: {integrity: sha512-YKauf8jfMowgAqcxj96AHs+Ux3m3bWT1oSVKamaRPXSnW2HqSznnTCEkAVqctF1e/W9R/mPcyzzINIgpOH94qg==} + peerDependencies: + react: ^18 || ^19 + + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/aws-lambda@8.10.147': + resolution: {integrity: sha512-nD0Z9fNIZcxYX5Mai2CTmFD7wX7UldCkW2ezCF8D1T5hdiLsnTWDGRpfRYntU6VjTdLQjOvyszru7I1c1oCQew==} + + '@types/aws-lambda@8.10.150': + resolution: {integrity: sha512-AX+AbjH/rH5ezX1fbK8onC/a+HyQHo7QGmvoxAE42n22OsciAxvZoZNEr22tbXs8WfP1nIsBjKDpgPm3HjOZbA==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/bun@1.2.21': + resolution: {integrity: sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A==} + + '@types/bunyan@1.8.11': + resolution: {integrity: sha512-758fRH7umIMk5qt5ELmRMff4mLDlN+xyYzC+dkPTdKwbSkJFvz6xwyScrytPU0QIBbRRwbiE8/BIg8bpajerNQ==} + + '@types/chai-http@3.0.5': + resolution: {integrity: sha512-nJ/oIvYley9+1Fec8xzDHsWrWSu5VTuhuF8iLabnRTQwRfSbAke6VuI7qBXvBN9Qqo/+gwsSaGyYndofTpiJ9A==} + + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.0.7': + resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/history@4.7.11': + resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/memcached@2.2.10': + resolution: {integrity: sha512-AM9smvZN55Gzs2wRrqeMHVP7KE8KWgCJO/XL5yCly2xF6EKa4YlbpK+cLSAH4NG/Ah64HrlegmGqW8kYws7Vxg==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/mocha@10.0.10': + resolution: {integrity: sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==} + + '@types/mysql@2.15.26': + resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} + + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.123': + resolution: {integrity: sha512-K7DIaHnh0mzVxreCR9qwgNxp3MH9dltPNIEddW9MYUlcKAzm+3grKNSTe2vCJHI1FaLpvpL5JGJrz1UZDKYvDg==} + + '@types/node@22.18.0': + resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} + + '@types/node@24.3.0': + resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} + + '@types/oracledb@6.5.2': + resolution: {integrity: sha512-kK1eBS/Adeyis+3OlBDMeQQuasIDLUYXsi2T15ccNJ0iyUpQ4xDF7svFu3+bGVrI0CMBUclPciz+lsQR3JX3TQ==} + + '@types/pg-pool@2.0.6': + resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} + + '@types/pg@8.15.4': + resolution: {integrity: sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==} + + '@types/pg@8.15.5': + resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==} + + '@types/pg@8.6.1': + resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + + '@types/phoenix@1.6.6': + resolution: {integrity: sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==} + + '@types/prismjs@1.26.5': + resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react-router-dom@5.3.3': + resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} + + '@types/react-router@5.1.20': + resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} + + '@types/react@18.3.24': + resolution: {integrity: sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==} + + '@types/recharts@2.0.1': + resolution: {integrity: sha512-/cFs7oiafzByUwBSWA1IzE6FW+ppPwQAWsDTadSgVOwzveY9MESpyLHyyHY0SfPPKLW4+4qVNYHPXd0rFiC8vg==} + deprecated: This is a stub types definition. recharts provides its own type definitions, so you do not need this installed. + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + + '@types/stack-trace@0.0.33': + resolution: {integrity: sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==} + + '@types/superagent@8.1.9': + resolution: {integrity: sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/triple-beam@1.3.5': + resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@typescript-eslint/eslint-plugin@8.41.0': + resolution: {integrity: sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.41.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.41.0': + resolution: {integrity: sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.41.0': + resolution: {integrity: sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.41.0': + resolution: {integrity: sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.41.0': + resolution: {integrity: sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.41.0': + resolution: {integrity: sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.41.0': + resolution: {integrity: sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.41.0': + resolution: {integrity: sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.41.0': + resolution: {integrity: sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.41.0': + resolution: {integrity: sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-x5fzycDS72mJyekZR2oITvBUJ7eBV3xNh21smMNE/OpkhdNTcMPSj9TqXRk7kmIPKu+d6f+WaQvuV4xp6BekFw==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-6Wn/FoTeEFPLPYk3BsYlWtcu8bbHu85W51K1kMmuLaAwc4OWlT3xcY72uz1QThZK3EDF457n7hvO5TQIE64ycg==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [darwin] + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-WF2MRR5miHDk93UT8Nz/sdpDjyrpYk3cwidPgYLY4hwXa4s4D0JUY9vN0Rh0eAHZjHM1oWnPfk7Tuzl77d+ytA==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [linux] + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-qBOfntj8fT3L4xk3JEPj1c6oIgJ9Kf2hsHX8+zKScnXkZFIJZtnssa0EIcsjQoCbwp75bEmrglbVLXydrjz9xA==} + engines: {node: '>=20.6.0'} + cpu: [arm] + os: [linux] + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-9eAb4fHCBmo0pZNu3sziV6g+ohgwUk/z/TSvLDNBaZHMOTWR3iRhTzDWRCjOrxA6A4IPWTgVacnRoPFFnfbrUg==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [linux] + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-XfLw5OMWLg5VByYm/Gj0iU/rFFMgrzT6bL4zr2jw/gJkJv0Mi9dOAUsQbBlF4yCFFNb9lscgAPUDI1lIT63CVg==} + engines: {node: '>=20.6.0'} + cpu: [arm64] + os: [win32] + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-mK9SK17t7yr4QO8L+tB6n/iMRvQkiD7ypCdD07n1i8VapFOJrlQhDX0NLQueTJJRwwHWcUXHUWdz75tKdJdDhA==} + engines: {node: '>=20.6.0'} + cpu: [x64] + os: [win32] + + '@typescript/native-preview@7.0.0-dev.20250901.1': + resolution: {integrity: sha512-J3VKeImoPYF7kGHWXoJHk8BonWBj8R6DlQT/CXOvN7YnYH8CoOZXOysGlPhAg+S3SfF7BtU9HjvjlnhTGENn8g==} + engines: {node: '>=20.6.0'} + hasBin: true + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@upstash/redis@1.35.3': + resolution: {integrity: sha512-hSjv66NOuahW3MisRGlSgoszU2uONAY2l5Qo3Sae8OT3/Tng9K+2/cBRuyPBX8egwEGcNNCF9+r0V6grNnhL+w==} + + '@vitejs/plugin-react@4.7.0': + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + + '@wooorm/starry-night@3.8.0': + resolution: {integrity: sha512-BWRm0tCzWCmv1ucBh6frL2uFRvYFj/LWmJzr+rJsdF/JsJ1+bBkeiyExH1iWQS18IH22HFu7f4QgG99OU1nklA==} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ag-charts-community@12.1.2: + resolution: {integrity: sha512-Z3oSsbIyAqaiB4PdnMQ2gt3s+oY19xTHJUee5TKjsTThX8wVX8PXRGMOZaFWbN/HNaZCWkUTgkuLrasw2w+x/w==} + + ag-charts-core@12.1.2: + resolution: {integrity: sha512-9AfMX+HSpr4zmp88PBWcOqO9jW1HTKxC+b/7B/xYOE6K1hBFvnuLbqdeNGfGoiyWqw5TUlY1lirr/9xrFyZFsQ==} + + ag-charts-locale@12.1.2: + resolution: {integrity: sha512-GFAq4KZit3CHT3Br/LGruas75HqsvV+VZV6qz5hLrmiovJSSVfzPirzf/hUGDTCHqIOS7CdxmpdKUu8pKv1LGw==} + + ag-charts-react@12.1.2: + resolution: {integrity: sha512-ungoxsE/sLaJ+bYWK2/cXts83jNRMogvTHysjvoxxFcU7vLat6FM01f4+NCE8szDSg+d6em2xI5NAcOROapBUg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + ag-charts-types@12.1.2: + resolution: {integrity: sha512-B5IEMzaDxpJRko9sREIBEZOn44tlMX353tMJ1RGYHZrjDFx4on/p6q3YxBvway3nViAM2POmFOS45+1fF9YC8g==} + + ag-grid-community@34.1.2: + resolution: {integrity: sha512-PN79zG/tigZaVKkQZXaithLO3j4JWp+9CjqEFth6kFUwMJ0E6OcNzNtn4NFEKM38fEdpGsr+k+gFk5KUE2h5fA==} + + ag-grid-react@34.1.2: + resolution: {integrity: sha512-6l5+hp3O4VSZ1nc2IZSQyhNeUOZ4TDg4UVrpxsqV3xXBO1TRqmOhcsYBDJ95JZ8zSiAEvy7XKT3MSI7sJG+MFg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ai@4.3.19: + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.0: + resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asn1js@3.0.6: + resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} + engines: {node: '>=12.0.0'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-listen@3.1.0: + resolution: {integrity: sha512-TkOhqze98lP+6e7SPbrBpyhTpfvqqX8VYKGn4uckrgPan4WQIHnTaUD2zZzZS18eVVDj4rHPcIZa1PGgvo1DfA==} + engines: {node: '>= 14'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + + autumn-js@0.1.28: + resolution: {integrity: sha512-f40FLvmZtY/3t41HZtUZmi5/191DhEE7IATI+8388yGPBSa4Pk46J5zeExfhBJEiphYirFoDxh4jxwpw5cJQdw==} + peerDependencies: + better-auth: ^1.3.4 + better-call: ^1.0.12 + convex: ^1.25.4 + react: '*' + peerDependenciesMeta: + better-auth: + optional: true + better-call: + optional: true + react: + optional: true + + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + + b4a@1.6.7: + resolution: {integrity: sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bare-events@2.6.1: + resolution: {integrity: sha512-AuTJkq9XmE6Vk0FJVNq5QxETrSA/vKHarWVBG5l/JbdCL1prJemiyJqUS0jrlXO0MftuPq4m3YVYhoNc5+aE/g==} + + bare-fs@4.2.1: + resolution: {integrity: sha512-mELROzV0IhqilFgsl1gyp48pnZsaV9xhQapHLDsvn4d4ZTfbFhcghQezl7FTEDNBcGqLUnNI3lUlm6ecrLWdFA==} + engines: {bare: '>=1.16.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-os@3.6.2: + resolution: {integrity: sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==} + engines: {bare: '>=1.14.0'} + + bare-path@3.0.0: + resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} + + bare-stream@2.7.0: + resolution: {integrity: sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==} + peerDependencies: + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + bare-events: + optional: true + + base-convert-int-array@1.0.1: + resolution: {integrity: sha512-NWqzaoXx8L/SS32R+WmKqnQkVXVYl2PwNJ68QV3RAlRRL1uV+yxJT66abXI1cAvqCXQTyXr7/9NN4Af90/zDVw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + better-auth@1.3.7: + resolution: {integrity: sha512-/1fEyx2SGgJQM5ujozDCh9eJksnVkNU/J7Fk/tG5Y390l8nKbrPvqiFlCjlMM+scR+UABJbQzA6An7HT50LHyQ==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + + better-call@1.0.16: + resolution: {integrity: sha512-42dgJ1rOtc0anOoxjXPOWuel/Z/4aeO7EJ2SiXNwvlkySSgjXhNjAjTMWa8DL1nt6EXS3jl3VKC3mPsU/lUgVA==} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + browserslist@4.25.4: + resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bullmq@5.58.4: + resolution: {integrity: sha512-1EE7pbEw2YNc64nVW+RYVvA6I1ijkhCacsZ/zWbjhDtN8sxZ+Wy3WQ/rUSMOOk/PGAGEQRTgngbTpYbvGYijHQ==} + + bun-types@1.2.21: + resolution: {integrity: sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw==} + peerDependencies: + '@types/react': ^19 + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001739: + resolution: {integrity: sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chai-http@5.1.2: + resolution: {integrity: sha512-UFup7mUGkkjmi9bGA7F6vfp3lzGQZjtL//CEd+a4C+vlynSv756XHDUK8PoYk/UpTBBXqSghjQaJOUMUxJXNaA==} + engines: {node: '>=18.20.0'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.0: + resolution: {integrity: sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + chardet@2.1.0: + resolution: {integrity: sha512-bNFETTG/pM5ryzQ9Ad0lJOTa6HWD/YsScAR3EnCPZRPlQh77JocYktSHOUHelyhm8IARL+o4c4F1bP5KVOjiRA==} + + charset@1.0.1: + resolution: {integrity: sha512-6dVyOOYjpfFcL1Y4qChrAoQLRHvj2ziyhcm0QJlhOcAhykL/k1kTUPbeo+87MNRTRdk2OIIsIXbuF3x2wi5EXg==} + engines: {node: '>=4.0.0'} + + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} + engines: {node: '>= 16'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chromium-bidi@8.0.0: + resolution: {integrity: sha512-d1VmE0FD7lxZQHzcDUCKZSNRtRwISXDsdg4HjdTR5+Ll5nQ/vzU12JeNmupD6VWffrPSlrnGhEWlLESKH3VO+g==} + peerDependencies: + devtools-protocol: '*' + + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + client-only@0.0.1: + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-regexp@3.0.0: + resolution: {integrity: sha512-ujdnoq2Kxb8s3ItNBtnYeXdm07FcU0u8ARAT1lQ2YdMwQC+cdiXX8KoqMVuglztILivceTtp4ivqGSmEmhBUJw==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cloudflare@4.5.0: + resolution: {integrity: sha512-fPcbPKx4zF45jBvQ0z7PCdgejVAPBBCZxwqk1k7krQNfpM07Cfj97/Q6wBzvYqlWXx/zt1S9+m8vnfCe06umbQ==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concurrently@9.2.1: + resolution: {integrity: sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==} + engines: {node: '>=18'} + hasBin: true + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + convert-hrtime@5.0.0: + resolution: {integrity: sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==} + engines: {node: '>=12'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + convex@1.26.2: + resolution: {integrity: sha512-QG3zvZ9GTTeeBS+N5PJj73TULHYu99eVSOI3KU2hMnB9q40fHxkMFJ+AYox+K81roTakciBvNTvGlhfz+M10fQ==} + engines: {node: '>=18.0.0', npm: '>=7.0.0'} + hasBin: true + peerDependencies: + '@auth0/auth0-react': ^2.0.1 + '@clerk/clerk-react': ^4.12.8 || ^5.0.0 + react: ^18.0.0 || ^19.0.0-0 || ^19.0.0 + peerDependenciesMeta: + '@auth0/auth0-react': + optional: true + '@clerk/clerk-react': + optional: true + react: + optional: true + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + core-js@3.45.1: + resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + + cron@3.5.0: + resolution: {integrity: sha512-0eYZqCnapmxYcV06uktql93wNWdlTmmBFP2iYz+JPVcQqlyFYcn1lFuIk4R54pkOmE7mcldTAPZv6X5XA4Q46A==} + + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-jss@10.10.0: + resolution: {integrity: sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==} + + css-vendor@2.0.8: + resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + csv-parse@5.6.0: + resolution: {integrity: sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==} + + csv-parser@3.2.0: + resolution: {integrity: sha512-fgKbp+AJbn1h2dcAHKIdKNSSjfp43BZZykXsCjzALjKy80VXQNHPFJ6T9Afwdzoj24aMkq8GwDS7KGcDPpejrA==} + engines: {node: '>= 10'} + hasBin: true + + currency-symbol-map@5.1.0: + resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debounce@2.2.0: + resolution: {integrity: sha512-Xks6RUDLZFdz8LIdR6q0MTH44k7FikOmnh5xkSjMig6ch45afc8sjTjRQf3P6ax8dMgcQrYO/AR2RGWURrruqw==} + engines: {node: '>=18'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + decode-uri-component@0.4.1: + resolution: {integrity: sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==} + engines: {node: '>=14.16'} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-content-type@1.2.0: + resolution: {integrity: sha512-YCBxuqJLY9rMxV44Ict2kNgjYFN3v1dnsn6sJvd6sUwwU1TWP3D+K2dr/S9AF/fio2/RsAKYdRiEOtNoRbmiag==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + devtools-protocol@0.0.1475386: + resolution: {integrity: sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA==} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + drizzle-kit@0.31.4: + resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==} + hasBin: true + + drizzle-orm@0.43.1: + resolution: {integrity: sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + + drizzle-zod@0.8.3: + resolution: {integrity: sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww==} + peerDependencies: + drizzle-orm: '>=0.36.0' + zod: ^3.25.0 || ^4.0.0 + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.211: + resolution: {integrity: sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==} + + emoji-regex@10.5.0: + resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.39.10: + resolution: {integrity: sha512-E0iGnTtbDhkeczB0T+mxmoVlT4YNweEKBLq7oaU4p11mecdsZpNWOglI4895Vh4usbQ+LsJiuLuI2L0Vdmfm2w==} + + es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.25.4: + resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + eslint-plugin-react-hooks@5.2.0: + resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 + + eslint-plugin-react-refresh@0.4.20: + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} + peerDependencies: + eslint: '>=8.40' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.34.0: + resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-copy@3.0.2: + resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==} + + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + + fetch-retry@6.0.0: + resolution: {integrity: sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==} + + fflate@0.4.8: + resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + framer-motion@12.23.12: + resolution: {integrity: sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function-timeout@0.1.1: + resolution: {integrity: sha512-0NVVC0TaP7dSTvn1yMiy6d6Q8gifzbvQafO46RtLG/kHJUBNd+pVRGOBoK44wNBvtSPUJRfdVvkFdD3p0xvyZg==} + engines: {node: '>=14.16'} + + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.3.1: + resolution: {integrity: sha512-R1QfovbPsKmosqTnPoRFiJ7CF9MLRgb53ChvMZm+r4p76/+8yKDy17qLL2PKInORy2RkZZekuK0efYgmzTkXyQ==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + glob@11.0.3: + resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} + engines: {node: 20 || >=22} + hasBin: true + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + iframe-resizer-react@1.1.1: + resolution: {integrity: sha512-s0EUUekv58FGbuWBanSCVsEKmmyBdWmdwQ8qx8g04jlNlCR7pNuDz7fw7zo5T5sdssl6yRMDl97NBdwD1gfDyQ==} + engines: {node: '>=16', npm: '>=5'} + peerDependencies: + prop-types: ^15.7.2 + react: ^16.13.1 || ^18.0.0 + react-dom: ^16.13.1 || ^18.0.0 + + iframe-resizer@4.4.5: + resolution: {integrity: sha512-U8bCywf/Gh07O69RXo6dXAzTtODQrxaHGHRI7Nt4ipXsuq6EMxVsOP/jjaP43YtXz/ibESS0uSVDN3sOGCzSmw==} + engines: {node: '>=0.8.0'} + + ignore-by-default@1.0.1: + resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + + import-in-the-middle@1.7.4: + resolution: {integrity: sha512-Lk+qzWmiQuRPPulGQeK5qq0v32k2bHnWrRPFgqyvhw7Kkov5L6MOLOIU3pcWeujc9W4q54Cp3Q2WV16eQkc7Bg==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + input-otp@1.4.2: + resolution: {integrity: sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + inquirer@12.9.4: + resolution: {integrity: sha512-5bV3LOgLtMAiJq1QpaUddfRrvaX59wiMYppS7z2jNRSQ64acI0yqx7WMxWhgymenSXOyD657g9tlsTjqGYM8sg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + ioredis@5.7.0: + resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} + engines: {node: '>=12.22.0'} + + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + + ip-regex@5.0.0: + resolution: {integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-browser@1.1.3: + resolution: {integrity: sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-ip@5.0.1: + resolution: {integrity: sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw==} + engines: {node: '>=14.16'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-regexp@3.1.0: + resolution: {integrity: sha512-rbku49cWloU5bSMI+zaRaXdQHXnthP6DZ/vLnfdSKyL4zUzuWnomtOEiZZOd+ioQ+avFo/qau3KPTc7Fjy1uPA==} + engines: {node: '>=12'} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-unicode-supported@1.3.0: + resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==} + engines: {node: '>=12'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jackspeak@4.1.1: + resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} + engines: {node: 20 || >=22} + + jiti@2.5.1: + resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==} + hasBin: true + + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + jss-plugin-camel-case@10.10.0: + resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==} + + jss-plugin-compose@10.10.0: + resolution: {integrity: sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==} + + jss-plugin-default-unit@10.10.0: + resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==} + + jss-plugin-expand@10.10.0: + resolution: {integrity: sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==} + + jss-plugin-extend@10.10.0: + resolution: {integrity: sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==} + + jss-plugin-global@10.10.0: + resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==} + + jss-plugin-nested@10.10.0: + resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==} + + jss-plugin-props-sort@10.10.0: + resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==} + + jss-plugin-rule-value-function@10.10.0: + resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==} + + jss-plugin-rule-value-observable@10.10.0: + resolution: {integrity: sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==} + + jss-plugin-template@10.10.0: + resolution: {integrity: sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==} + + jss-plugin-vendor-prefixer@10.10.0: + resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==} + + jss-preset-default@10.10.0: + resolution: {integrity: sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==} + + jss@10.10.0: + resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==} + + jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + ksuid@3.0.0: + resolution: {integrity: sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA==} + + kysely@0.28.5: + resolution: {integrity: sha512-rlB0I/c6FBDWPcQoDtkxi9zIvpmnV5xoIalfCMSMCa7nuA6VGA3F54TW9mEgX4DVf10sXAWCF5fDbamI/5ZpKA==} + engines: {node: '>=20.0.0'} + + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + + lodash.isobject@3.0.2: + resolution: {integrity: sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-symbols@6.0.0: + resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} + engines: {node: '>=18'} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + + logform@2.7.0: + resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} + engines: {node: '>= 12.0.0'} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loops@5.0.1: + resolution: {integrity: sha512-xM1c9mnlr8Hr4cHW944TQoK6ApynjinUWOgYZd9/B0/3lwTThq24BQ7+XLjgbFAP5kJzqDTRDQi3t+Diy51Udw==} + engines: {node: '>=18'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.1.0: + resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lucide-react@0.525.0: + resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + + luxon@3.7.1: + resolution: {integrity: sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==} + engines: {node: '>=12'} + + magic-string@0.30.18: + resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + marked@7.0.4: + resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} + engines: {node: '>= 16'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + md-to-react-email@5.0.5: + resolution: {integrity: sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==} + peerDependencies: + react: ^18.0 || ^19.0 + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-detect@1.3.0: + resolution: {integrity: sha512-SFFkIWONYQdDoiA3iq2abd68kCy+ifzsgWyytMDa/v8Jiu5UhVhgb4upoKDVKTlCz663jqM9PksyM99m+MnSaQ==} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mocha@11.7.1: + resolution: {integrity: sha512-5EK+Cty6KheMS/YLPPMJC64g5V61gIR25KsRItHw6x4hEKT6Njp1n9LOlH4gpevuwMVS66SXaBBpg+RWZkza4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + motion-dom@12.23.12: + resolution: {integrity: sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==} + + motion-utils@12.23.6: + resolution: {integrity: sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==} + + motion@12.23.12: + resolution: {integrity: sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.3: + resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} + hasBin: true + + msgpackr@1.11.5: + resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + mylas@2.1.13: + resolution: {integrity: sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg==} + engines: {node: '>=12.0.0'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + + nanostores@0.11.4: + resolution: {integrity: sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ==} + engines: {node: ^18.0.0 || >=20.0.0} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + next@15.5.2: + resolution: {integrity: sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==} + engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + nodemon@3.1.10: + resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} + engines: {node: '>=10'} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + nuqs@2.5.2: + resolution: {integrity: sha512-vzKeoYlMRmNYPWECdn53Nmh/jM+r/iSezEin342EVXPogT6KzALwdnYbZxASE5vTdXRUtOymtPkgsarLipKetg==} + peerDependencies: + '@remix-run/react': '>=2' + '@tanstack/react-router': ^1 + next: '>=14.2.0' + react: '>=18.2.0 || ^19.0.0-0' + react-router: ^6 || ^7 + react-router-dom: ^6 || ^7 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@tanstack/react-router': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + ora@8.2.0: + resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} + engines: {node: '>=18'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pino-abstract-transport@1.2.0: + resolution: {integrity: sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-pretty@13.1.1: + resolution: {integrity: sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==} + hasBin: true + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.9.0: + resolution: {integrity: sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ==} + hasBin: true + + plimit-lit@1.6.1: + resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==} + engines: {node: '>=12'} + + postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + engines: {node: ^10 || ^12 || >=14} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + posthog-js@1.261.0: + resolution: {integrity: sha512-jyiXqyrCU+VlpbNNVRA6OQYAVut0XZMYNELCZH+XvTd981VqbE4jXn4XCBreo7XCL2gdPgDVxUVOuzNvEuKcmw==} + peerDependencies: + '@rrweb/types': 2.0.0-alpha.17 + rrweb-snapshot: 2.0.0-alpha.17 + peerDependenciesMeta: + '@rrweb/types': + optional: true + rrweb-snapshot: + optional: true + + posthog-node@4.18.0: + resolution: {integrity: sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw==} + engines: {node: '>=15.0.0'} + + preact@10.27.1: + resolution: {integrity: sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + prism-themes@1.9.0: + resolution: {integrity: sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw==} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pstree.remy@1.1.8: + resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + puppeteer-core@24.18.0: + resolution: {integrity: sha512-As0BvfXxek2MbV0m7iqBmQKFnfSrzSvTM4zGipjd4cL+9f2Ccgut6RvHlc8+qBieKHqCMFy9BSI4QyveoYXTug==} + engines: {node: '>=18'} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.1.3: + resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==} + engines: {node: '>=6.0.0'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + query-string@9.2.2: + resolution: {integrity: sha512-pDSIZJ9sFuOp6VnD+5IkakSVf+rICAuuU88Hcsr6AKL0QtxSIfVuKiVP2oahFI7tk3CRSexwV+Ya6MOoTxzg9g==} + engines: {node: '>=18'} + + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + + queue-lit@1.5.2: + resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==} + engines: {node: '>=12'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + react-confetti-explosion@2.1.2: + resolution: {integrity: sha512-4UzDFBajAGXmF9TSJoRMO2QOBCIXc66idTxH8l7Mkul48HLGtk+tMzK9HYDYsy7Zmw5sEGchi2fbn4AJUuLrZw==} + peerDependencies: + react: ^18.x + react-dom: ^18.x + + react-day-picker@8.10.1: + resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==} + peerDependencies: + date-fns: ^2.28.0 || ^3.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + + react-display-name@0.2.5: + resolution: {integrity: sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-email@4.0.16: + resolution: {integrity: sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg==} + engines: {node: '>=18.0.0'} + hasBin: true + + react-hot-toast@2.6.0: + resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-hotkeys-hook@4.6.2: + resolution: {integrity: sha512-FmP+ZriY3EG59Ug/lxNfrObCnW9xQShgk7Nb83+CkpfkcCpfS95ydv+E9JuXA5cp8KtskU7LGlIARpkc92X22Q==} + peerDependencies: + react: '>=16.8.1' + react-dom: '>=16.8.1' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-jss@10.10.0: + resolution: {integrity: sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==} + peerDependencies: + react: '>=16.8.6' + + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.8.2: + resolution: {integrity: sha512-Z4VM5mKDipal2jQ385H6UBhiiEDlnJPx6jyWsTYoZQdl5TrjxEV2a9yl3Fi60NBJxYzOTGTTHXPi0pdizvTwow==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.8.2: + resolution: {integrity: sha512-7M2fR1JbIZ/jFWqelpvSZx+7vd7UlBTfdZqf6OSdF9g6+sfdqJDAWcak6ervbHph200ePlu+7G8LdoiC3ReyAQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + recaseai@0.0.37: + resolution: {integrity: sha512-cKVMWTGBnGtm8K+uD2vfMXOzxdHj1U3++vvTePpOoZABI/SY/jjDpW997vM5xeDnzz7Vk/qqVkLQUQ/ZMDXpsQ==} + hasBin: true + + recharts@3.1.2: + resolution: {integrity: sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resend@4.8.0: + resolution: {integrity: sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==} + engines: {node: '>=18'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.50.0: + resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rou3@0.5.1: + resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + + rou3@0.6.3: + resolution: {integrity: sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA==} + + run-async@4.0.6: + resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + secure-json-parse@4.0.0: + resolution: {integrity: sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==} + + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + + sharp@0.34.3: + resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.3: + resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} + engines: {node: '>= 0.4'} + + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + sonner@2.0.7: + resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + + stdin-discarder@0.2.2: + resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} + engines: {node: '>=18'} + + streamx@2.22.1: + resolution: {integrity: sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + stripe@18.5.0: + resolution: {integrity: sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA==} + engines: {node: '>=12.*'} + peerDependencies: + '@types/node': '>=12.x.x' + peerDependenciesMeta: + '@types/node': + optional: true + + styled-jsx@5.1.6: + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} + peerDependencies: + '@babel/core': '*' + babel-plugin-macros: '*' + react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' + peerDependenciesMeta: + '@babel/core': + optional: true + babel-plugin-macros: + optional: true + + super-regex@0.2.0: + resolution: {integrity: sha512-WZzIx3rC1CvbMDloLsVw0lkZVKJWbrkJ0k1ghKFmcnPrW1+jWbgTkTEWVtD9lMdmI4jZEz40+naBxl1dCUhXXw==} + engines: {node: '>=14.16'} + + superagent@10.2.3: + resolution: {integrity: sha512-y/hkYGeXAj7wUMjxRbB21g/l6aAEituGXM9Rwl4o20+SX3e8YOSV6BxFXl+dL3Uk0mjSL3kCbNkwURm8/gEDig==} + engines: {node: '>=14.18.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svix-react@1.13.6: + resolution: {integrity: sha512-6WACy36/UNAkpYQp6N6lWH7tNAwBzNSMcv1OCXmTMcWyq5CdTZ1IYdMEqElUObeDf67iIGf/HdZ0rmDhg3NgDA==} + peerDependencies: + react: '>=16' + react-dom: '>=16' + svix: '>=1.26.0' + + svix@1.75.0: + resolution: {integrity: sha512-3GLI1DN4Pfd8Ze+7yxoV3Ocp8hW07/WRDsoIBdjDvR916Fx3gRHaYSoa0WWRdKSqJNj+OTCeDSSv47mSp9ibqw==} + + swr@2.3.4: + resolution: {integrity: sha512-bYd2lrhc+VarcpkgWclcUi92wYCpOgMws9Sd1hG1ntAu0NEy+14CbotuFjshBU2kt9rYj9TSmDcybpxpeTU1fg==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + swr@2.3.6: + resolution: {integrity: sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + symbol-observable@1.2.0: + resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} + engines: {node: '>=0.10.0'} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwind-scrollbar-hide@4.0.0: + resolution: {integrity: sha512-gobtvVcThB2Dxhy0EeYSS1RKQJ5baDFkamkhwBvzvevwX6L4XQfpZ3me9s25Ss1ecFVT5jPYJ50n+7xTBJG9WQ==} + peerDependencies: + tailwindcss: '>=3.0.0 || >= 4.0.0 || >= 4.0.0-beta.8 || >= 4.0.0-alpha.20' + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@4.1.12: + resolution: {integrity: sha512-DzFtxOi+7NsFf7DBtI3BJsynR+0Yp6etH+nRPTbpWnS2pZBaSksv/JGctNwSWzbFjp0vxSqknaUylseZqMDGrA==} + + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} + + tar-stream@3.1.7: + resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + text-decoder@1.2.3: + resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} + + theming@3.3.0: + resolution: {integrity: sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==} + engines: {node: '>=8'} + peerDependencies: + react: '>=16.3' + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + + time-span@5.1.0: + resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} + engines: {node: '>=12'} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + touch@3.1.1: + resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} + hasBin: true + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + triple-beam@1.4.1: + resolution: {integrity: sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==} + engines: {node: '>= 14.0.0'} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsc-alias@1.8.16: + resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==} + engines: {node: '>=16.20.2'} + hasBin: true + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + typed-query-selector@2.12.0: + resolution: {integrity: sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==} + + typescript-eslint@8.41.0: + resolution: {integrity: sha512-n66rzs5OBXW3SFSnZHr2T685q1i4ODm2nulFJhMZBotaTavsS8TrI3d7bDlRSs9yWo7HmyWrN9qDu14Qv7Y0Dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + typescript@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undefsafe@2.0.5: + resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.10.0: + resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vscode-oniguruma@2.0.1: + resolution: {integrity: sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==} + + vscode-textmate@9.2.0: + resolution: {integrity: sha512-rkvG4SraZQaPSN/5XjwKswdU0OP9MF28QjrYzUBbhb8QyG3ljB1Ky996m++jiI7KdiAP2CkBiQZd9pqEDTClqA==} + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + web-vitals@4.2.4: + resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + winston-transport@4.9.0: + resolution: {integrity: sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==} + engines: {node: '>= 12.0.0'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + workerpool@9.3.3: + resolution: {integrity: sha512-slxCaKbYjEdFT/o2rH9xS1hf4uRDch1w7Uo+apxhZ+sf/1d9e0ZVkn42kPNGP2dgjIx6YFvSevj0zHvbWe2jdw==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod-to-json-schema@3.24.6: + resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.1.5: + resolution: {integrity: sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@ai-sdk/anthropic@1.2.12(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@18.3.1)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 18.3.1 + swr: 2.3.6(react@18.3.1) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@anthropic-ai/sdk@0.32.1': + dependencies: + '@types/node': 18.19.123 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@axiomhq/js@1.3.1': + dependencies: + fetch-retry: 6.0.0 + uuid: 11.1.0 + + '@axiomhq/pino@1.3.1': + dependencies: + '@axiomhq/js': 1.3.1 + pino-abstract-transport: 1.2.0 + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.3': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + convert-source-map: 2.0.0 + debug: 4.4.1(supports-color@5.5.0) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.4 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.2 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.3 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.3': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + + '@babel/parser@7.28.3': + dependencies: + '@babel/types': 7.28.2 + + '@babel/plugin-proposal-private-property-in-object@7.21.11(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.28.3) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.3)': + dependencies: + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.28.3': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@babel/traverse@7.28.3': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.2': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@better-auth/stripe@1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76)': + dependencies: + better-auth: 1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - react + - react-dom + + '@better-auth/utils@0.2.6': + dependencies: + uncrypto: 0.1.3 + + '@better-fetch/fetch@1.1.18': {} + + '@browserbasehq/sdk@2.6.0': + dependencies: + '@types/node': 18.19.123 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@clerk/backend@2.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.83.0 + cookie: 1.0.2 + standardwebhooks: 1.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/shared': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.83.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@clerk/express@1.7.26(express@4.21.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/backend': 2.10.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/shared': 3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@clerk/types': 4.83.0 + express: 4.21.2 + tslib: 2.8.1 + transitivePeerDependencies: + - react + - react-dom + + '@clerk/shared@3.23.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@clerk/types': 4.83.0 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.9.0 + swr: 2.3.4(react@18.3.1) + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@clerk/types@4.83.0': + dependencies: + csstype: 3.1.3 + + '@clickhouse/client-common@1.12.1': {} + + '@clickhouse/client@1.12.1': + dependencies: + '@clickhouse/client-common': 1.12.1 + + '@colors/colors@1.6.0': {} + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@date-fns/tz@1.4.1': {} + + '@date-fns/utc@2.1.1': {} + + '@drizzle-team/brocli@0.10.2': {} + + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/is-prop-valid@0.7.3': + dependencies: + '@emotion/memoize': 0.7.1 + + '@emotion/memoize@0.7.1': {} + + '@esbuild-kit/core-utils@3.3.2': + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + + '@esbuild-kit/esm-loader@2.6.5': + dependencies: + '@esbuild-kit/core-utils': 3.3.2 + get-tsconfig: 4.10.1 + + '@esbuild/aix-ppc64@0.25.4': + optional: true + + '@esbuild/aix-ppc64@0.25.9': + optional: true + + '@esbuild/android-arm64@0.18.20': + optional: true + + '@esbuild/android-arm64@0.25.4': + optional: true + + '@esbuild/android-arm64@0.25.9': + optional: true + + '@esbuild/android-arm@0.18.20': + optional: true + + '@esbuild/android-arm@0.25.4': + optional: true + + '@esbuild/android-arm@0.25.9': + optional: true + + '@esbuild/android-x64@0.18.20': + optional: true + + '@esbuild/android-x64@0.25.4': + optional: true + + '@esbuild/android-x64@0.25.9': + optional: true + + '@esbuild/darwin-arm64@0.18.20': + optional: true + + '@esbuild/darwin-arm64@0.25.4': + optional: true + + '@esbuild/darwin-arm64@0.25.9': + optional: true + + '@esbuild/darwin-x64@0.18.20': + optional: true + + '@esbuild/darwin-x64@0.25.4': + optional: true + + '@esbuild/darwin-x64@0.25.9': + optional: true + + '@esbuild/freebsd-arm64@0.18.20': + optional: true + + '@esbuild/freebsd-arm64@0.25.4': + optional: true + + '@esbuild/freebsd-arm64@0.25.9': + optional: true + + '@esbuild/freebsd-x64@0.18.20': + optional: true + + '@esbuild/freebsd-x64@0.25.4': + optional: true + + '@esbuild/freebsd-x64@0.25.9': + optional: true + + '@esbuild/linux-arm64@0.18.20': + optional: true + + '@esbuild/linux-arm64@0.25.4': + optional: true + + '@esbuild/linux-arm64@0.25.9': + optional: true + + '@esbuild/linux-arm@0.18.20': + optional: true + + '@esbuild/linux-arm@0.25.4': + optional: true + + '@esbuild/linux-arm@0.25.9': + optional: true + + '@esbuild/linux-ia32@0.18.20': + optional: true + + '@esbuild/linux-ia32@0.25.4': + optional: true + + '@esbuild/linux-ia32@0.25.9': + optional: true + + '@esbuild/linux-loong64@0.18.20': + optional: true + + '@esbuild/linux-loong64@0.25.4': + optional: true + + '@esbuild/linux-loong64@0.25.9': + optional: true + + '@esbuild/linux-mips64el@0.18.20': + optional: true + + '@esbuild/linux-mips64el@0.25.4': + optional: true + + '@esbuild/linux-mips64el@0.25.9': + optional: true + + '@esbuild/linux-ppc64@0.18.20': + optional: true + + '@esbuild/linux-ppc64@0.25.4': + optional: true + + '@esbuild/linux-ppc64@0.25.9': + optional: true + + '@esbuild/linux-riscv64@0.18.20': + optional: true + + '@esbuild/linux-riscv64@0.25.4': + optional: true + + '@esbuild/linux-riscv64@0.25.9': + optional: true + + '@esbuild/linux-s390x@0.18.20': + optional: true + + '@esbuild/linux-s390x@0.25.4': + optional: true + + '@esbuild/linux-s390x@0.25.9': + optional: true + + '@esbuild/linux-x64@0.18.20': + optional: true + + '@esbuild/linux-x64@0.25.4': + optional: true + + '@esbuild/linux-x64@0.25.9': + optional: true + + '@esbuild/netbsd-arm64@0.25.4': + optional: true + + '@esbuild/netbsd-arm64@0.25.9': + optional: true + + '@esbuild/netbsd-x64@0.18.20': + optional: true + + '@esbuild/netbsd-x64@0.25.4': + optional: true + + '@esbuild/netbsd-x64@0.25.9': + optional: true + + '@esbuild/openbsd-arm64@0.25.4': + optional: true + + '@esbuild/openbsd-arm64@0.25.9': + optional: true + + '@esbuild/openbsd-x64@0.18.20': + optional: true + + '@esbuild/openbsd-x64@0.25.4': + optional: true + + '@esbuild/openbsd-x64@0.25.9': + optional: true + + '@esbuild/openharmony-arm64@0.25.9': + optional: true + + '@esbuild/sunos-x64@0.18.20': + optional: true + + '@esbuild/sunos-x64@0.25.4': + optional: true + + '@esbuild/sunos-x64@0.25.9': + optional: true + + '@esbuild/win32-arm64@0.18.20': + optional: true + + '@esbuild/win32-arm64@0.25.4': + optional: true + + '@esbuild/win32-arm64@0.25.9': + optional: true + + '@esbuild/win32-ia32@0.18.20': + optional: true + + '@esbuild/win32-ia32@0.25.4': + optional: true + + '@esbuild/win32-ia32@0.25.9': + optional: true + + '@esbuild/win32-x64@0.18.20': + optional: true + + '@esbuild/win32-x64@0.25.4': + optional: true + + '@esbuild/win32-x64@0.25.9': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.34.0(jiti@2.5.1))': + dependencies: + eslint: 9.34.0(jiti@2.5.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1(supports-color@5.5.0) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.1': {} + + '@eslint/core@0.15.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1(supports-color@5.5.0) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.34.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.5': + dependencies: + '@eslint/core': 0.15.2 + levn: 0.4.1 + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@fortawesome/fontawesome-common-types@6.7.2': {} + + '@fortawesome/fontawesome-common-types@7.0.0': {} + + '@fortawesome/fontawesome-svg-core@7.0.0': + dependencies: + '@fortawesome/fontawesome-common-types': 7.0.0 + + '@fortawesome/free-brands-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@7.0.0)(react@18.3.1)': + dependencies: + '@fortawesome/fontawesome-svg-core': 7.0.0 + prop-types: 15.8.1 + react: 18.3.1 + + '@grpc/grpc-js@1.13.4': + dependencies: + '@grpc/proto-loader': 0.7.15 + '@js-sdsl/ordered-map': 4.4.2 + + '@grpc/proto-loader@0.7.15': + dependencies: + lodash.camelcase: 4.3.0 + long: 5.3.2 + protobufjs: 7.5.4 + yargs: 17.7.2 + + '@heroicons/react@2.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@hexagon/base64@1.1.28': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@hyperbrowser/sdk@0.54.0': + dependencies: + form-data: 4.0.4 + node-fetch: 2.7.0 + zod: 3.25.76 + zod-to-json-schema: 3.24.6(zod@3.25.76) + transitivePeerDependencies: + - encoding + + '@hyperdx/instrumentation-exception@0.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@hyperdx/instrumentation-sentry-node': 0.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 8.55.0 + '@sentry/types': 8.55.0 + '@sentry/utils': 8.55.0 + json-stringify-safe: 5.0.1 + shimmer: 1.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@hyperdx/instrumentation-sentry-node@0.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + json-stringify-safe: 5.0.1 + shimmer: 1.2.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@hyperdx/node-opentelemetry@0.8.2': + dependencies: + '@hyperdx/instrumentation-exception': 0.1.0(@opentelemetry/api@1.9.0) + '@hyperdx/instrumentation-sentry-node': 0.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/auto-instrumentations-node': 0.56.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': 0.12.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + cli-spinners: 2.9.2 + json-stringify-safe: 5.0.1 + lodash.isobject: 3.0.2 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + node-fetch: 2.7.0 + open: 8.4.2 + ora: 5.4.1 + pino-abstract-transport: 1.2.0 + semver: 7.7.2 + shimmer: 1.2.1 + tslib: 2.8.1 + winston-transport: 4.9.0 + transitivePeerDependencies: + - encoding + - supports-color + + '@img/sharp-darwin-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.0 + optional: true + + '@img/sharp-darwin-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.0 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.0': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.0': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.0': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.0': + optional: true + + '@img/sharp-linux-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.0 + optional: true + + '@img/sharp-linux-arm@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.0 + optional: true + + '@img/sharp-linux-ppc64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.0 + optional: true + + '@img/sharp-linux-s390x@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.0 + optional: true + + '@img/sharp-linux-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.3': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + optional: true + + '@img/sharp-wasm32@0.34.3': + dependencies: + '@emnapi/runtime': 1.5.0 + optional: true + + '@img/sharp-win32-arm64@0.34.3': + optional: true + + '@img/sharp-win32-ia32@0.34.3': + optional: true + + '@img/sharp-win32-x64@0.34.3': + optional: true + + '@inquirer/checkbox@4.2.2(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.18.0) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/checkbox@4.2.2(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.3.0) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/confirm@5.1.16(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/confirm@5.1.16(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/core@10.2.0(@types/node@22.18.0)': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.18.0) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/core@10.2.0(@types/node@24.3.0)': + dependencies: + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.3.0) + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/editor@4.2.18(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/external-editor': 1.0.1(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/editor@4.2.18(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/external-editor': 1.0.1(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/expand@4.0.18(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/expand@4.0.18(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/external-editor@1.0.1(@types/node@22.18.0)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.6.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/external-editor@1.0.1(@types/node@24.3.0)': + dependencies: + chardet: 2.1.0 + iconv-lite: 0.6.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/figures@1.0.13': {} + + '@inquirer/input@4.2.2(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/input@4.2.2(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/number@3.0.18(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/number@3.0.18(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/password@4.0.18(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/password@4.0.18(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + ansi-escapes: 4.3.2 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/prompts@7.8.4(@types/node@22.18.0)': + dependencies: + '@inquirer/checkbox': 4.2.2(@types/node@22.18.0) + '@inquirer/confirm': 5.1.16(@types/node@22.18.0) + '@inquirer/editor': 4.2.18(@types/node@22.18.0) + '@inquirer/expand': 4.0.18(@types/node@22.18.0) + '@inquirer/input': 4.2.2(@types/node@22.18.0) + '@inquirer/number': 3.0.18(@types/node@22.18.0) + '@inquirer/password': 4.0.18(@types/node@22.18.0) + '@inquirer/rawlist': 4.1.6(@types/node@22.18.0) + '@inquirer/search': 3.1.1(@types/node@22.18.0) + '@inquirer/select': 4.3.2(@types/node@22.18.0) + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/prompts@7.8.4(@types/node@24.3.0)': + dependencies: + '@inquirer/checkbox': 4.2.2(@types/node@24.3.0) + '@inquirer/confirm': 5.1.16(@types/node@24.3.0) + '@inquirer/editor': 4.2.18(@types/node@24.3.0) + '@inquirer/expand': 4.0.18(@types/node@24.3.0) + '@inquirer/input': 4.2.2(@types/node@24.3.0) + '@inquirer/number': 3.0.18(@types/node@24.3.0) + '@inquirer/password': 4.0.18(@types/node@24.3.0) + '@inquirer/rawlist': 4.1.6(@types/node@24.3.0) + '@inquirer/search': 3.1.1(@types/node@24.3.0) + '@inquirer/select': 4.3.2(@types/node@24.3.0) + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/rawlist@4.1.6(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/rawlist@4.1.6(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/search@3.1.1(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.18.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/search@3.1.1(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.3.0) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/select@4.3.2(@types/node@22.18.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@22.18.0) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/select@4.3.2(@types/node@24.3.0)': + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/figures': 1.0.13 + '@inquirer/type': 3.0.8(@types/node@24.3.0) + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.3.0 + + '@inquirer/type@3.0.8(@types/node@22.18.0)': + optionalDependencies: + '@types/node': 22.18.0 + + '@inquirer/type@3.0.8(@types/node@24.3.0)': + optionalDependencies: + '@types/node': 24.3.0 + + '@ioredis/commands@1.3.1': {} + + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.30': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@js-sdsl/ordered-map@4.4.2': {} + + '@levischuck/tiny-cbor@0.2.11': {} + + '@logtail/core@0.5.4': + dependencies: + '@logtail/tools': 0.5.4 + '@logtail/types': 0.5.3 + serialize-error: 8.1.0 + + '@logtail/node@0.5.5': + dependencies: + '@logtail/core': 0.5.4 + '@logtail/types': 0.5.3 + '@msgpack/msgpack': 2.8.0 + '@types/stack-trace': 0.0.33 + minimatch: 9.0.5 + stack-trace: 0.0.10 + + '@logtail/tools@0.5.4': + dependencies: + '@logtail/types': 0.5.3 + + '@logtail/types@0.5.3': {} + + '@msgpack/msgpack@2.8.0': {} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': + optional: true + + '@next/env@15.5.2': {} + + '@next/swc-darwin-arm64@15.5.2': + optional: true + + '@next/swc-darwin-x64@15.5.2': + optional: true + + '@next/swc-linux-arm64-gnu@15.5.2': + optional: true + + '@next/swc-linux-arm64-musl@15.5.2': + optional: true + + '@next/swc-linux-x64-gnu@15.5.2': + optional: true + + '@next/swc-linux-x64-musl@15.5.2': + optional: true + + '@next/swc-win32-arm64-msvc@15.5.2': + optional: true + + '@next/swc-win32-x64-msvc@15.5.2': + optional: true + + '@noble/ciphers@0.6.0': {} + + '@noble/hashes@1.8.0': {} + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@opentelemetry/api-logs@0.202.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.51.1': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.57.2': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/auto-instrumentations-node@0.56.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.50.3(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.49.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.14.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.44.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 1.12.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.6.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.33.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/auto-instrumentations-node@0.60.1(@opentelemetry/api@1.9.0)(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-lambda': 0.53.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-aws-sdk': 0.54.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-bunyan': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cassandra-driver': 0.48.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-cucumber': 0.17.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.19.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dns': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fastify': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.22.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.50.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-grpc': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.50.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.11.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.50.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-memcached': 0.46.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.55.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.48.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.48.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-nestjs-core': 0.48.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-net': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-oracledb': 0.28.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.54.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pino': 0.49.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis': 0.49.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-restify': 0.48.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-router': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-runtime-node': 0.16.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-socket.io': 0.49.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.21.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.13.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-winston': 0.47.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-alibaba-cloud': 0.31.3(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-aws': 2.3.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-azure': 0.9.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-container': 0.7.3(@opentelemetry/api@1.9.0) + '@opentelemetry/resource-detector-gcp': 0.36.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-node': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/context-async-hooks@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/exporter-logs-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-logs-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-metrics-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-prometheus@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-grpc-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-proto@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-zipkin@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-zipkin@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-amqplib@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.50.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/aws-lambda': 8.10.147 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-lambda@0.53.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/aws-lambda': 8.10.150 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.49.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/propagation-utils': 0.30.16(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-aws-sdk@0.54.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagation-utils': 0.31.3(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-bunyan@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@types/bunyan': 1.8.11 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cassandra-driver@0.48.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.14.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-cucumber@0.17.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.19.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dns@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-express@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.44.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fastify@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.22.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.50.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-grpc@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + forwarded-parse: 2.1.2 + semver: 7.7.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.50.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.38.0 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.11.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.50.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-memcached@0.46.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/memcached': 2.2.10 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.55.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.48.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/mysql': 2.15.26 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.48.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-nestjs-core@0.48.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.43.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-net@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-oracledb@0.28.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/oracledb': 6.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) + '@types/pg': 8.6.1 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.54.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@opentelemetry/sql-common': 0.41.0(@opentelemetry/api@1.9.0) + '@types/pg': 8.15.4 + '@types/pg-pool': 2.0.6 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pino@0.49.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis-4@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.37.0 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.36.2 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.49.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/redis-common': 0.37.0 + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.45.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-restify@0.48.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-router@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.12.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-runtime-node@0.16.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.46.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-socket.io@0.49.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.21.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.13.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.44.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-winston@0.47.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.51.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.51.1 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.7.4 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.202.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-grpc-exporter-base@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@grpc/grpc-js': 1.13.4 + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.57.2(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/propagation-utils@0.30.16(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/propagation-utils@0.31.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-b3@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/redis-common@0.36.2': {} + + '@opentelemetry/redis-common@0.37.0': {} + + '@opentelemetry/redis-common@0.38.0': {} + + '@opentelemetry/resource-detector-alibaba-cloud@0.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-alibaba-cloud@0.31.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-aws@1.12.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-aws@2.3.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-azure@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-azure@0.9.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-container@0.6.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-container@0.7.3(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/resource-detector-gcp@0.33.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resource-detector-gcp@0.36.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + gcp-metadata: 6.1.1 + transitivePeerDependencies: + - encoding + - supports-color + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/sdk-logs@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-logs@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-node@0.202.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.202.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.202.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-node@0.57.2(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.57.2 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-metrics-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-prometheus': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-grpc': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-proto': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-zipkin': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.2 + + '@opentelemetry/sdk-trace-node@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.36.0': {} + + '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sql-common@0.41.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + + '@paralleldrive/cuid2@2.2.2': + dependencies: + '@noble/hashes': 1.8.0 + + '@peculiar/asn1-android@2.4.0': + dependencies: + '@peculiar/asn1-schema': 2.4.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-ecc@2.4.0': + dependencies: + '@peculiar/asn1-schema': 2.4.0 + '@peculiar/asn1-x509': 2.4.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-rsa@2.4.0': + dependencies: + '@peculiar/asn1-schema': 2.4.0 + '@peculiar/asn1-x509': 2.4.0 + asn1js: 3.0.6 + tslib: 2.8.1 + + '@peculiar/asn1-schema@2.4.0': + dependencies: + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@peculiar/asn1-x509@2.4.0': + dependencies: + '@peculiar/asn1-schema': 2.4.0 + asn1js: 3.0.6 + pvtsutils: 1.3.6 + tslib: 2.8.1 + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@posthog/core@1.0.2': {} + + '@prisma/instrumentation@6.11.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + transitivePeerDependencies: + - supports-color + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@puppeteer/browsers@2.10.8': + dependencies: + debug: 4.4.1(supports-color@5.5.0) + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.7.2 + tar-fs: 3.1.0 + yargs: 17.7.2 + transitivePeerDependencies: + - bare-buffer + - supports-color + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-context-menu@2.2.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-context@1.1.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.24)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.24)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.24 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + '@types/react-dom': 18.3.7(@types/react@18.3.24) + + '@radix-ui/rect@1.1.1': {} + + '@react-email/body@0.0.11(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/button@0.0.19(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/code-block@0.0.13(react@18.3.1)': + dependencies: + prismjs: 1.30.0 + react: 18.3.1 + + '@react-email/code-inline@0.0.5(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/column@0.0.13(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/components@0.0.42(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@react-email/body': 0.0.11(react@18.3.1) + '@react-email/button': 0.0.19(react@18.3.1) + '@react-email/code-block': 0.0.13(react@18.3.1) + '@react-email/code-inline': 0.0.5(react@18.3.1) + '@react-email/column': 0.0.13(react@18.3.1) + '@react-email/container': 0.0.15(react@18.3.1) + '@react-email/font': 0.0.9(react@18.3.1) + '@react-email/head': 0.0.12(react@18.3.1) + '@react-email/heading': 0.0.15(react@18.3.1) + '@react-email/hr': 0.0.11(react@18.3.1) + '@react-email/html': 0.0.11(react@18.3.1) + '@react-email/img': 0.0.11(react@18.3.1) + '@react-email/link': 0.0.12(react@18.3.1) + '@react-email/markdown': 0.0.15(react@18.3.1) + '@react-email/preview': 0.0.13(react@18.3.1) + '@react-email/render': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@react-email/row': 0.0.12(react@18.3.1) + '@react-email/section': 0.0.16(react@18.3.1) + '@react-email/tailwind': 1.0.5(react@18.3.1) + '@react-email/text': 0.1.5(react@18.3.1) + react: 18.3.1 + transitivePeerDependencies: + - react-dom + + '@react-email/container@0.0.15(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/font@0.0.9(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/head@0.0.12(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/heading@0.0.15(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/hr@0.0.11(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/html@0.0.11(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/img@0.0.11(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/link@0.0.12(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/markdown@0.0.15(react@18.3.1)': + dependencies: + md-to-react-email: 5.0.5(react@18.3.1) + react: 18.3.1 + + '@react-email/preview@0.0.13(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/render@1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.6.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-promise-suspense: 0.3.4 + + '@react-email/row@0.0.12(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/section@0.0.16(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/tailwind@1.0.5(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@react-email/text@0.1.5(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.24)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 18.3.1 + react-redux: 9.2.0(@types/react@18.3.24)(react@18.3.1)(redux@5.0.1) + + '@rolldown/pluginutils@1.0.0-beta.27': {} + + '@rollup/rollup-android-arm-eabi@4.50.0': + optional: true + + '@rollup/rollup-android-arm64@4.50.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.0': + optional: true + + '@rollup/rollup-darwin-x64@4.50.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.50.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.50.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.50.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.50.0': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.50.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.50.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.50.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.50.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.50.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.0': + optional: true + + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + + '@sentry/core@8.55.0': {} + + '@sentry/core@9.46.0': {} + + '@sentry/node-core@9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.46.0 + '@sentry/opentelemetry': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + import-in-the-middle: 1.14.2 + + '@sentry/node@9.46.0': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@prisma/instrumentation': 6.11.1(@opentelemetry/api@1.9.0) + '@sentry/core': 9.46.0 + '@sentry/node-core': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/opentelemetry': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0) + import-in-the-middle: 1.14.2 + minimatch: 9.0.5 + transitivePeerDependencies: + - supports-color + + '@sentry/opentelemetry@9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.36.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@sentry/core': 9.46.0 + + '@sentry/types@8.55.0': + dependencies: + '@sentry/core': 8.55.0 + + '@sentry/utils@8.55.0': + dependencies: + '@sentry/core': 8.55.0 + + '@simplewebauthn/browser@13.1.2': {} + + '@simplewebauthn/server@13.1.2': + dependencies: + '@hexagon/base64': 1.1.28 + '@levischuck/tiny-cbor': 0.2.11 + '@peculiar/asn1-android': 2.4.0 + '@peculiar/asn1-ecc': 2.4.0 + '@peculiar/asn1-rsa': 2.4.0 + '@peculiar/asn1-schema': 2.4.0 + '@peculiar/asn1-x509': 2.4.0 + + '@socket.io/component-emitter@3.1.2': {} + + '@stablelib/base64@1.0.1': {} + + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@supabase/auth-js@2.71.1': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/functions-js@2.4.5': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/node-fetch@2.6.15': + dependencies: + whatwg-url: 5.0.0 + + '@supabase/postgrest-js@1.21.3': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/realtime-js@2.15.4': + dependencies: + '@supabase/node-fetch': 2.6.15 + '@types/phoenix': 1.6.6 + '@types/ws': 8.18.1 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@supabase/ssr@0.5.2(@supabase/supabase-js@2.56.1)': + dependencies: + '@supabase/supabase-js': 2.56.1 + '@types/cookie': 0.6.0 + cookie: 0.7.2 + + '@supabase/storage-js@2.11.0': + dependencies: + '@supabase/node-fetch': 2.6.15 + + '@supabase/supabase-js@2.56.1': + dependencies: + '@supabase/auth-js': 2.71.1 + '@supabase/functions-js': 2.4.5 + '@supabase/node-fetch': 2.6.15 + '@supabase/postgrest-js': 1.21.3 + '@supabase/realtime-js': 2.15.4 + '@supabase/storage-js': 2.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@swc/helpers@0.5.15': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.1.12': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + magic-string: 0.30.18 + source-map-js: 1.2.1 + tailwindcss: 4.1.12 + + '@tailwindcss/oxide-android-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.12': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.12': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.12': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.12': + optional: true + + '@tailwindcss/oxide@4.1.12': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-arm64': 4.1.12 + '@tailwindcss/oxide-darwin-x64': 4.1.12 + '@tailwindcss/oxide-freebsd-x64': 4.1.12 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.12 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.12 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.12 + '@tailwindcss/oxide-linux-x64-musl': 4.1.12 + '@tailwindcss/oxide-wasm32-wasi': 4.1.12 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.12 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.12 + + '@tailwindcss/vite@4.1.12(vite@6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5))': + dependencies: + '@tailwindcss/node': 4.1.12 + '@tailwindcss/oxide': 4.1.12 + tailwindcss: 4.1.12 + vite: 6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + + '@tanstack/query-core@5.87.1': {} + + '@tanstack/query-devtools@5.86.0': {} + + '@tanstack/react-query-devtools@5.87.1(@tanstack/react-query@5.87.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/query-devtools': 5.86.0 + '@tanstack/react-query': 5.87.1(react@18.3.1) + react: 18.3.1 + + '@tanstack/react-query@5.87.1(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.87.1 + react: 18.3.1 + + '@tanstack/react-table@8.21.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@tanstack/table-core@8.21.3': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/aws-lambda@8.10.147': {} + + '@types/aws-lambda@8.10.150': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.2 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.28.2 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.18.0 + + '@types/bun@1.2.21(@types/react@18.3.24)': + dependencies: + bun-types: 1.2.21(@types/react@18.3.24) + transitivePeerDependencies: + - '@types/react' + + '@types/bunyan@1.8.11': + dependencies: + '@types/node': 22.18.0 + + '@types/chai-http@3.0.5': + dependencies: + '@types/chai': 5.2.2 + '@types/node': 22.18.0 + '@types/superagent': 8.1.9 + + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.18.0 + + '@types/cookie@0.6.0': {} + + '@types/cookiejar@2.1.5': {} + + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.18.0 + + '@types/d3-array@3.2.1': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/deep-eql@4.0.2': {} + + '@types/diff-match-patch@1.0.36': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.0.7': + dependencies: + '@types/node': 22.18.0 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@5.0.3': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.0.7 + '@types/serve-static': 1.15.8 + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/history@4.7.11': {} + + '@types/http-errors@2.0.5': {} + + '@types/json-schema@7.0.15': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/luxon@3.4.2': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/memcached@2.2.10': + dependencies: + '@types/node': 22.18.0 + + '@types/methods@1.1.4': {} + + '@types/mime@1.3.5': {} + + '@types/mocha@10.0.10': {} + + '@types/mysql@2.15.26': + dependencies: + '@types/node': 22.18.0 + + '@types/mysql@2.15.27': + dependencies: + '@types/node': 22.18.0 + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 22.18.0 + form-data: 4.0.4 + + '@types/node@18.19.123': + dependencies: + undici-types: 5.26.5 + + '@types/node@22.18.0': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.3.0': + dependencies: + undici-types: 7.10.0 + + '@types/oracledb@6.5.2': + dependencies: + '@types/node': 22.18.0 + + '@types/pg-pool@2.0.6': + dependencies: + '@types/pg': 8.15.5 + + '@types/pg@8.15.4': + dependencies: + '@types/node': 22.18.0 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/pg@8.15.5': + dependencies: + '@types/node': 22.18.0 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/pg@8.6.1': + dependencies: + '@types/node': 22.18.0 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + + '@types/phoenix@1.6.6': {} + + '@types/prismjs@1.26.5': {} + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/react-dom@18.3.7(@types/react@18.3.24)': + dependencies: + '@types/react': 18.3.24 + + '@types/react-router-dom@5.3.3': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.24 + '@types/react-router': 5.1.20 + + '@types/react-router@5.1.20': + dependencies: + '@types/history': 4.7.11 + '@types/react': 18.3.24 + + '@types/react@18.3.24': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/recharts@2.0.1(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(redux@5.0.1)': + dependencies: + recharts: 3.1.2(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(redux@5.0.1) + transitivePeerDependencies: + - '@types/react' + - react + - react-dom + - react-is + - redux + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.18.0 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.18.0 + '@types/send': 0.17.5 + + '@types/shimmer@1.2.0': {} + + '@types/stack-trace@0.0.33': {} + + '@types/superagent@8.1.9': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 22.18.0 + form-data: 4.0.4 + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 22.18.0 + + '@types/triple-beam@1.3.5': {} + + '@types/unist@3.0.3': {} + + '@types/use-sync-external-store@0.0.6': {} + + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.18.0 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.18.0 + optional: true + + '@typescript-eslint/eslint-plugin@8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3))(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/type-utils': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.41.0 + eslint: 9.34.0(jiti@2.5.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1(supports-color@5.5.0) + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.41.0(typescript@5.7.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.7.3) + '@typescript-eslint/types': 8.41.0 + debug: 4.4.1(supports-color@5.5.0) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.41.0': + dependencies: + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + + '@typescript-eslint/tsconfig-utils@8.41.0(typescript@5.7.3)': + dependencies: + typescript: 5.7.3 + + '@typescript-eslint/type-utils@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + debug: 4.4.1(supports-color@5.5.0) + eslint: 9.34.0(jiti@2.5.1) + ts-api-utils: 2.1.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.41.0': {} + + '@typescript-eslint/typescript-estree@8.41.0(typescript@5.7.3)': + dependencies: + '@typescript-eslint/project-service': 8.41.0(typescript@5.7.3) + '@typescript-eslint/tsconfig-utils': 8.41.0(typescript@5.7.3) + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/visitor-keys': 8.41.0 + debug: 4.4.1(supports-color@5.5.0) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) + '@typescript-eslint/scope-manager': 8.41.0 + '@typescript-eslint/types': 8.41.0 + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.7.3) + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.41.0': + dependencies: + '@typescript-eslint/types': 8.41.0 + eslint-visitor-keys: 4.2.1 + + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-darwin-x64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-linux-arm64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-linux-arm@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-linux-x64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-win32-arm64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview-win32-x64@7.0.0-dev.20250901.1': + optional: true + + '@typescript/native-preview@7.0.0-dev.20250901.1': + optionalDependencies: + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250901.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250901.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20250901.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250901.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20250901.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250901.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20250901.1 + + '@ungap/structured-clone@1.3.0': {} + + '@upstash/redis@1.35.3': + dependencies: + uncrypto: 0.1.3 + + '@vitejs/plugin-react@4.7.0(vite@6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5))': + dependencies: + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.3) + '@rolldown/pluginutils': 1.0.0-beta.27 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5) + transitivePeerDependencies: + - supports-color + + '@wooorm/starry-night@3.8.0': + dependencies: + '@types/hast': 3.0.4 + import-meta-resolve: 4.2.0 + vscode-oniguruma: 2.0.1 + vscode-textmate: 9.2.0 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ag-charts-community@12.1.2: + dependencies: + ag-charts-core: 12.1.2 + ag-charts-locale: 12.1.2 + ag-charts-types: 12.1.2 + + ag-charts-core@12.1.2: + dependencies: + ag-charts-types: 12.1.2 + + ag-charts-locale@12.1.2: {} + + ag-charts-react@12.1.2(react@18.3.1): + dependencies: + ag-charts-community: 12.1.2 + react: 18.3.1 + + ag-charts-types@12.1.2: {} + + ag-grid-community@34.1.2: + dependencies: + ag-charts-types: 12.1.2 + + ag-grid-react@34.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + ag-grid-community: 34.1.2 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ai@4.3.19(react@18.3.1)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@18.3.1)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 18.3.1 + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + asap@2.0.6: {} + + asn1js@3.0.6: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.1.3 + tslib: 2.8.1 + + assertion-error@2.0.1: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async-listen@3.1.0: {} + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + attr-accept@2.2.5: {} + + autumn-js@0.1.28(better-auth@1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76))(better-call@1.0.16)(convex@1.26.2(@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + convex: 1.26.2(@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + query-string: 9.2.2 + rou3: 0.6.3 + swr: 2.3.6(react@18.3.1) + zod: 4.1.5 + optionalDependencies: + better-auth: 1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76) + better-call: 1.0.16 + react: 18.3.1 + + axios@1.11.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + b4a@1.6.7: {} + + balanced-match@1.0.2: {} + + bare-events@2.6.1: + optional: true + + bare-fs@4.2.1: + dependencies: + bare-events: 2.6.1 + bare-path: 3.0.0 + bare-stream: 2.7.0(bare-events@2.6.1) + optional: true + + bare-os@3.6.2: + optional: true + + bare-path@3.0.0: + dependencies: + bare-os: 3.6.2 + optional: true + + bare-stream@2.7.0(bare-events@2.6.1): + dependencies: + streamx: 2.22.1 + optionalDependencies: + bare-events: 2.6.1 + optional: true + + base-convert-int-array@1.0.1: {} + + base64-js@1.5.1: {} + + base64id@2.0.0: {} + + basic-ftp@5.0.5: {} + + better-auth@1.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.25.76): + dependencies: + '@better-auth/utils': 0.2.6 + '@better-fetch/fetch': 1.1.18 + '@noble/ciphers': 0.6.0 + '@noble/hashes': 1.8.0 + '@simplewebauthn/browser': 13.1.2 + '@simplewebauthn/server': 13.1.2 + better-call: 1.0.16 + defu: 6.1.4 + jose: 5.10.0 + kysely: 0.28.5 + nanostores: 0.11.4 + zod: 3.25.76 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + better-call@1.0.16: + dependencies: + '@better-fetch/fetch': 1.1.18 + rou3: 0.5.1 + set-cookie-parser: 2.7.1 + uncrypto: 0.1.3 + + bignumber.js@9.3.1: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browser-stdout@1.3.1: {} + + browserslist@4.25.4: + dependencies: + caniuse-lite: 1.0.30001739 + electron-to-chromium: 1.5.211 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.4) + + buffer-crc32@0.2.13: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bullmq@5.58.4: + dependencies: + cron-parser: 4.9.0 + ioredis: 5.7.0 + msgpackr: 1.11.5 + node-abort-controller: 3.1.1 + semver: 7.7.2 + tslib: 2.8.1 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + bun-types@1.2.21(@types/react@18.3.24): + dependencies: + '@types/node': 24.3.0 + '@types/react': 18.3.24 + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001739: {} + + ccount@2.0.1: {} + + chai-http@5.1.2: + dependencies: + '@types/superagent': 8.1.9 + charset: 1.0.1 + cookiejar: 2.1.4 + is-ip: 5.0.1 + methods: 1.1.2 + qs: 6.14.0 + superagent: 10.2.3 + transitivePeerDependencies: + - supports-color + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.0: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + chardet@2.1.0: {} + + charset@1.0.1: {} + + check-error@2.1.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + chromium-bidi@8.0.0(devtools-protocol@0.0.1475386): + dependencies: + devtools-protocol: 0.0.1475386 + mitt: 3.0.1 + zod: 3.25.76 + + cjs-module-lexer@1.4.3: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@2.9.2: {} + + cli-width@4.1.0: {} + + client-only@0.0.1: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-regexp@3.0.0: + dependencies: + is-regexp: 3.1.0 + + clone@1.0.4: {} + + cloudflare@4.5.0: + dependencies: + '@types/node': 18.19.123 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + clsx@2.1.1: {} + + cluster-key-slot@1.1.2: {} + + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.24)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + optional: true + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + optional: true + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + comma-separated-tokens@2.0.3: {} + + commander@12.1.0: {} + + commander@13.1.0: {} + + commander@9.5.0: {} + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + concurrently@9.2.1: + dependencies: + chalk: 4.1.2 + rxjs: 7.8.2 + shell-quote: 1.8.3 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + convert-hrtime@5.0.0: {} + + convert-source-map@2.0.0: {} + + convex@1.26.2(@clerk/clerk-react@5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + esbuild: 0.25.4 + jwt-decode: 4.0.0 + prettier: 3.6.2 + optionalDependencies: + '@clerk/clerk-react': 5.45.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + cookie@1.0.2: {} + + cookiejar@2.1.4: {} + + core-js@3.45.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cron-parser@4.9.0: + dependencies: + luxon: 3.7.1 + + cron@3.5.0: + dependencies: + '@types/luxon': 3.4.2 + luxon: 3.5.0 + + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-jss@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + jss-preset-default: 10.10.0 + + css-vendor@2.0.8: + dependencies: + '@babel/runtime': 7.28.3 + is-in-browser: 1.1.3 + + csstype@3.1.3: {} + + csv-parse@5.6.0: {} + + csv-parser@3.2.0: {} + + currency-symbol-map@5.1.0: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.0: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + data-uri-to-buffer@6.0.2: {} + + date-fns@3.6.0: {} + + date-fns@4.1.0: {} + + dateformat@4.6.3: {} + + debounce@2.2.0: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.1(supports-color@5.5.0): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 5.5.0 + + debug@4.4.1(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + decode-uri-component@0.4.1: {} + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + define-lazy-prop@2.0.0: {} + + defu@6.1.4: {} + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-content-type@1.2.0: {} + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + devtools-protocol@0.0.1475386: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff-match-patch@1.0.5: {} + + diff@4.0.2: {} + + diff@7.0.0: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dotenv@16.6.1: {} + + drizzle-kit@0.31.4: + dependencies: + '@drizzle-team/brocli': 0.10.2 + '@esbuild-kit/esm-loader': 2.6.5 + esbuild: 0.25.9 + esbuild-register: 3.6.0(esbuild@0.25.9) + transitivePeerDependencies: + - supports-color + + drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7): + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/pg': 8.15.5 + bun-types: 1.2.21(@types/react@18.3.24) + kysely: 0.28.5 + pg: 8.16.3 + postgres: 3.4.7 + + drizzle-zod@0.8.3(drizzle-orm@0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7))(zod@3.25.76): + dependencies: + drizzle-orm: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(bun-types@1.2.21(@types/react@18.3.24))(kysely@0.28.5)(pg@8.16.3)(postgres@3.4.7) + zod: 3.25.76 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.211: {} + + emoji-regex@10.5.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 22.18.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.3 + + entities@4.5.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-toolkit@1.39.10: {} + + es6-promise@4.2.8: {} + + esbuild-register@3.6.0(esbuild@0.25.9): + dependencies: + debug: 4.4.1(supports-color@5.5.0) + esbuild: 0.25.9 + transitivePeerDependencies: + - supports-color + + esbuild@0.18.20: + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + + esbuild@0.25.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.4 + '@esbuild/android-arm': 0.25.4 + '@esbuild/android-arm64': 0.25.4 + '@esbuild/android-x64': 0.25.4 + '@esbuild/darwin-arm64': 0.25.4 + '@esbuild/darwin-x64': 0.25.4 + '@esbuild/freebsd-arm64': 0.25.4 + '@esbuild/freebsd-x64': 0.25.4 + '@esbuild/linux-arm': 0.25.4 + '@esbuild/linux-arm64': 0.25.4 + '@esbuild/linux-ia32': 0.25.4 + '@esbuild/linux-loong64': 0.25.4 + '@esbuild/linux-mips64el': 0.25.4 + '@esbuild/linux-ppc64': 0.25.4 + '@esbuild/linux-riscv64': 0.25.4 + '@esbuild/linux-s390x': 0.25.4 + '@esbuild/linux-x64': 0.25.4 + '@esbuild/netbsd-arm64': 0.25.4 + '@esbuild/netbsd-x64': 0.25.4 + '@esbuild/openbsd-arm64': 0.25.4 + '@esbuild/openbsd-x64': 0.25.4 + '@esbuild/sunos-x64': 0.25.4 + '@esbuild/win32-arm64': 0.25.4 + '@esbuild/win32-ia32': 0.25.4 + '@esbuild/win32-x64': 0.25.4 + + esbuild@0.25.9: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + eslint-plugin-react-hooks@5.2.0(eslint@9.34.0(jiti@2.5.1)): + dependencies: + eslint: 9.34.0(jiti@2.5.1) + + eslint-plugin-react-refresh@0.4.20(eslint@9.34.0(jiti@2.5.1)): + dependencies: + eslint: 9.34.0(jiti@2.5.1) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.34.0(jiti@2.5.1): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.34.0(jiti@2.5.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.34.0 + '@eslint/plugin-kit': 0.3.5 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1(supports-color@5.5.0) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.5.1 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} + + express-rate-limit@7.5.1(express@4.21.2): + dependencies: + express: 4.21.2 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-copy@3.0.2: {} + + fast-deep-equal@2.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-redact@3.5.0: {} + + fast-safe-stringify@2.1.1: {} + + fast-sha256@1.3.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fecha@4.2.3: {} + + fetch-retry@6.0.0: {} + + fflate@0.4.8: {} + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + filter-obj@5.1.0: {} + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flat@5.0.2: {} + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data-encoder@1.7.2: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.2.2 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded-parse@2.1.2: {} + + forwarded@0.2.0: {} + + framer-motion@12.23.12(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.23.12 + motion-utils: 12.23.6 + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.7.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + fresh@0.5.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function-timeout@0.1.1: {} + + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.3.1: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@11.0.3: + dependencies: + foreground-child: 3.3.1 + jackspeak: 4.1.1 + minimatch: 10.0.3 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globby@11.1.0: + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + + google-logging-utils@0.0.2: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + he@1.2.0: {} + + help-me@5.0.0: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + + html-void-elements@3.0.0: {} + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + http-status-codes@2.3.0: {} + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + hyphenate-style-name@1.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + iframe-resizer-react@1.1.1(@babel/core@7.28.3)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/plugin-proposal-private-property-in-object': 7.21.11(@babel/core@7.28.3) + iframe-resizer: 4.4.5 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + warning: 4.0.3 + transitivePeerDependencies: + - '@babel/core' + - supports-color + + iframe-resizer@4.4.5: {} + + ignore-by-default@1.0.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + immer@10.1.1: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + import-in-the-middle@1.7.4: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + + import-meta-resolve@4.2.0: {} + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + input-otp@1.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + inquirer@12.9.4(@types/node@22.18.0): + dependencies: + '@inquirer/core': 10.2.0(@types/node@22.18.0) + '@inquirer/prompts': 7.8.4(@types/node@22.18.0) + '@inquirer/type': 3.0.8(@types/node@22.18.0) + ansi-escapes: 4.3.2 + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 22.18.0 + + inquirer@12.9.4(@types/node@24.3.0): + dependencies: + '@inquirer/core': 10.2.0(@types/node@24.3.0) + '@inquirer/prompts': 7.8.4(@types/node@24.3.0) + '@inquirer/type': 3.0.8(@types/node@24.3.0) + ansi-escapes: 4.3.2 + mute-stream: 2.0.0 + run-async: 4.0.6 + rxjs: 7.8.2 + optionalDependencies: + '@types/node': 24.3.0 + + internmap@2.0.3: {} + + ioredis@5.7.0: + dependencies: + '@ioredis/commands': 1.3.1 + cluster-key-slot: 1.1.2 + debug: 4.4.1(supports-color@5.5.0) + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ip-address@10.0.1: {} + + ip-regex@5.0.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.3.2: + optional: true + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-docker@2.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-browser@1.1.3: {} + + is-interactive@1.0.0: {} + + is-interactive@2.0.0: {} + + is-ip@5.0.1: + dependencies: + ip-regex: 5.0.0 + super-regex: 0.2.0 + + is-number@7.0.0: {} + + is-plain-obj@2.1.0: {} + + is-regexp@3.1.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + is-unicode-supported@1.3.0: {} + + is-unicode-supported@2.1.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + + isexe@2.0.0: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jackspeak@4.1.1: + dependencies: + '@isaacs/cliui': 8.0.2 + + jiti@2.5.1: {} + + jose@5.10.0: {} + + joycon@3.1.1: {} + + js-cookie@3.0.5: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.6.0 + diff-match-patch: 1.0.5 + + jss-plugin-camel-case@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + hyphenate-style-name: 1.1.0 + jss: 10.10.0 + + jss-plugin-compose@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + tiny-warning: 1.0.3 + + jss-plugin-default-unit@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + + jss-plugin-expand@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + + jss-plugin-extend@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + tiny-warning: 1.0.3 + + jss-plugin-global@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + + jss-plugin-nested@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + tiny-warning: 1.0.3 + + jss-plugin-props-sort@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + + jss-plugin-rule-value-function@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + tiny-warning: 1.0.3 + + jss-plugin-rule-value-observable@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + symbol-observable: 1.2.0 + + jss-plugin-template@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + tiny-warning: 1.0.3 + + jss-plugin-vendor-prefixer@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + css-vendor: 2.0.8 + jss: 10.10.0 + + jss-preset-default@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + jss: 10.10.0 + jss-plugin-camel-case: 10.10.0 + jss-plugin-compose: 10.10.0 + jss-plugin-default-unit: 10.10.0 + jss-plugin-expand: 10.10.0 + jss-plugin-extend: 10.10.0 + jss-plugin-global: 10.10.0 + jss-plugin-nested: 10.10.0 + jss-plugin-props-sort: 10.10.0 + jss-plugin-rule-value-function: 10.10.0 + jss-plugin-rule-value-observable: 10.10.0 + jss-plugin-template: 10.10.0 + jss-plugin-vendor-prefixer: 10.10.0 + + jss@10.10.0: + dependencies: + '@babel/runtime': 7.28.3 + csstype: 3.1.3 + is-in-browser: 1.1.3 + tiny-warning: 1.0.3 + + jwt-decode@4.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + ksuid@3.0.0: + dependencies: + base-convert-int-array: 1.0.1 + + kysely@0.28.5: {} + + leac@0.6.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.camelcase@4.3.0: {} + + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + + lodash.isobject@3.0.2: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-symbols@6.0.0: + dependencies: + chalk: 5.6.0 + is-unicode-supported: 1.3.0 + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.1.2 + + logform@2.7.0: + dependencies: + '@colors/colors': 1.6.0 + '@types/triple-beam': 1.3.5 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.5.0 + triple-beam: 1.4.1 + + long@5.3.2: {} + + loops@5.0.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.1.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lru-cache@7.18.3: {} + + lucide-react@0.525.0(react@18.3.1): + dependencies: + react: 18.3.1 + + luxon@3.5.0: {} + + luxon@3.7.1: {} + + magic-string@0.30.18: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-error@1.3.6: {} + + marked@7.0.4: {} + + math-intrinsics@1.1.0: {} + + md-to-react-email@5.0.5(react@18.3.1): + dependencies: + marked: 7.0.4 + react: 18.3.1 + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-detect@1.3.0: + dependencies: + mime-db: 1.54.0 + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mkdirp@3.0.1: {} + + mocha@11.7.1: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.1(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.4.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 9.0.5 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.3 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + module-details-from-path@1.0.4: {} + + motion-dom@12.23.12: + dependencies: + motion-utils: 12.23.6 + + motion-utils@12.23.6: {} + + motion@12.23.12(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.23.12(@emotion/is-prop-valid@0.7.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@emotion/is-prop-valid': 0.7.3 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + ms@2.0.0: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.3: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + optionalDependencies: + '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 + '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 + optional: true + + msgpackr@1.11.5: + optionalDependencies: + msgpackr-extract: 3.0.3 + + mute-stream@2.0.0: {} + + mylas@2.1.13: {} + + nanoid@3.3.11: {} + + nanoid@5.1.5: {} + + nanostores@0.11.4: {} + + natural-compare@1.4.0: {} + + negotiator@0.6.3: {} + + netmask@2.0.2: {} + + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 15.5.2 + '@swc/helpers': 0.5.15 + caniuse-lite: 1.0.30001739 + postcss: 8.4.31 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.28.3)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 15.5.2 + '@next/swc-darwin-x64': 15.5.2 + '@next/swc-linux-arm64-gnu': 15.5.2 + '@next/swc-linux-arm64-musl': 15.5.2 + '@next/swc-linux-x64-gnu': 15.5.2 + '@next/swc-linux-x64-musl': 15.5.2 + '@next/swc-win32-arm64-msvc': 15.5.2 + '@next/swc-win32-x64-msvc': 15.5.2 + '@opentelemetry/api': 1.9.0 + sharp: 0.34.3 + transitivePeerDependencies: + - '@babel/core' + - babel-plugin-macros + + node-abort-controller@3.1.1: {} + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.0.4 + optional: true + + node-releases@2.0.19: {} + + nodemon@3.1.10: + dependencies: + chokidar: 3.6.0 + debug: 4.4.1(supports-color@5.5.0) + ignore-by-default: 1.0.1 + minimatch: 3.1.2 + pstree.remy: 1.1.8 + semver: 7.7.2 + simple-update-notifier: 2.0.0 + supports-color: 5.5.0 + touch: 3.1.1 + undefsafe: 2.0.5 + + normalize-path@3.0.0: {} + + nuqs@2.5.2(next@15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router-dom@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-router@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1): + dependencies: + '@standard-schema/spec': 1.0.0 + react: 18.3.1 + optionalDependencies: + next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + + openai@4.104.0(ws@8.18.3)(zod@3.25.76): + dependencies: + '@types/node': 18.19.123 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.18.3 + zod: 3.25.76 + transitivePeerDependencies: + - encoding + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@8.2.0: + dependencies: + chalk: 5.6.0 + cli-cursor: 5.0.0 + cli-spinners: 2.9.2 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 6.0.0 + stdin-discarder: 0.2.2 + string-width: 7.2.0 + strip-ansi: 7.1.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.1.0 + minipass: 7.1.2 + + path-to-regexp@0.1.12: {} + + path-type@4.0.0: {} + + pathval@2.0.1: {} + + peberminta@0.9.0: {} + + pend@1.2.0: {} + + pg-cloudflare@1.2.7: + optional: true + + pg-connection-string@2.9.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.10.1(pg@8.16.3): + dependencies: + pg: 8.16.3 + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.3: + dependencies: + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.7 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pino-abstract-transport@1.2.0: + dependencies: + readable-stream: 4.7.0 + split2: 4.2.0 + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.1: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 3.0.2 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pump: 3.0.3 + secure-json-parse: 4.0.0 + sonic-boom: 4.2.0 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.0.0: {} + + pino@9.9.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + plimit-lit@1.6.1: + dependencies: + queue-lit: 1.5.2 + + postcss@8.4.31: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + postgres@3.4.7: {} + + posthog-js@1.261.0: + dependencies: + '@posthog/core': 1.0.2 + core-js: 3.45.1 + fflate: 0.4.8 + preact: 10.27.1 + web-vitals: 4.2.4 + + posthog-node@4.18.0: + dependencies: + axios: 1.11.0 + transitivePeerDependencies: + - debug + + preact@10.27.1: {} + + prelude-ls@1.2.1: {} + + prettier@3.6.2: {} + + prism-themes@1.9.0: {} + + prismjs@1.30.0: {} + + process-warning@5.0.0: {} + + process@0.11.10: {} + + progress@2.0.3: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + property-information@7.1.0: {} + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 22.18.0 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pstree.remy@1.1.8: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + puppeteer-core@24.18.0: + dependencies: + '@puppeteer/browsers': 2.10.8 + chromium-bidi: 8.0.0(devtools-protocol@0.0.1475386) + debug: 4.4.1(supports-color@5.5.0) + devtools-protocol: 0.0.1475386 + typed-query-selector: 2.12.0 + ws: 8.18.3 + transitivePeerDependencies: + - bare-buffer + - bufferutil + - supports-color + - utf-8-validate + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.1.3: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + query-string@9.2.2: + dependencies: + decode-uri-component: 0.4.1 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + + querystringify@2.2.0: {} + + queue-lit@1.5.2: {} + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + react-confetti-explosion@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + lodash: 4.17.21 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-jss: 10.10.0(react@18.3.1) + + react-day-picker@8.10.1(date-fns@3.6.0)(react@18.3.1): + dependencies: + date-fns: 3.6.0 + react: 18.3.1 + + react-display-name@0.2.5: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@14.3.8(react@18.3.1): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 18.3.1 + + react-email@4.0.16(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/parser': 7.28.3 + '@babel/traverse': 7.28.3 + chalk: 5.6.0 + chokidar: 4.0.3 + commander: 13.1.0 + debounce: 2.2.0 + esbuild: 0.25.9 + glob: 11.0.3 + log-symbols: 7.0.1 + mime-types: 3.0.1 + next: 15.5.2(@babel/core@7.28.3)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + normalize-path: 3.0.0 + ora: 8.2.0 + socket.io: 4.8.1 + transitivePeerDependencies: + - '@babel/core' + - '@opentelemetry/api' + - '@playwright/test' + - babel-plugin-macros + - babel-plugin-react-compiler + - bufferutil + - react + - react-dom + - sass + - supports-color + - utf-8-validate + + react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.1.3 + goober: 2.1.16(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-hotkeys-hook@4.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-is@16.13.1: {} + + react-jss@10.10.0(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.3 + '@emotion/is-prop-valid': 0.7.3 + css-jss: 10.10.0 + hoist-non-react-statics: 3.3.2 + is-in-browser: 1.1.3 + jss: 10.10.0 + jss-preset-default: 10.10.0 + prop-types: 15.8.1 + react: 18.3.1 + shallow-equal: 1.2.1 + theming: 3.3.0(react@18.3.1) + tiny-warning: 1.0.3 + + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + + react-redux@9.2.0(@types/react@18.3.24)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + redux: 5.0.1 + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + react-remove-scroll@2.7.1(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.24)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.24)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.24)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.24)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.24 + + react-router-dom@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.8.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + real-require@0.2.0: {} + + recaseai@0.0.37(ws@8.18.3)(zod@3.25.76): + dependencies: + '@anthropic-ai/sdk': 0.32.1 + '@supabase/supabase-js': 2.56.1 + '@types/cors': 2.8.19 + '@types/express': 5.0.3 + '@types/node': 22.18.0 + async-listen: 3.1.0 + axios: 1.11.0 + commander: 12.1.0 + cors: 2.8.5 + dotenv: 16.6.1 + express: 4.21.2 + figures: 6.1.0 + inquirer: 12.9.4(@types/node@22.18.0) + ksuid: 3.0.0 + nanoid: 5.1.5 + openai: 4.104.0(ws@8.18.3)(zod@3.25.76) + ora: 8.2.0 + picocolors: 1.1.1 + pino: 9.9.0 + tsx: 4.20.5 + typescript: 5.7.3 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - supports-color + - utf-8-validate + - ws + - zod + + recharts@3.1.2(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react-is@16.13.1)(react@18.3.1)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@18.3.24)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.39.10 + eventemitter3: 5.0.1 + immer: 10.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 16.13.1 + react-redux: 9.2.0(@types/react@18.3.24)(react@18.3.1)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.5.0(react@18.3.1) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + require-directory@2.1.1: {} + + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.1(supports-color@5.5.0) + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + requires-port@1.0.0: {} + + reselect@5.1.1: {} + + resend@4.8.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@react-email/render': 1.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + transitivePeerDependencies: + - react + - react-dom + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rollup@4.50.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.50.0 + '@rollup/rollup-android-arm64': 4.50.0 + '@rollup/rollup-darwin-arm64': 4.50.0 + '@rollup/rollup-darwin-x64': 4.50.0 + '@rollup/rollup-freebsd-arm64': 4.50.0 + '@rollup/rollup-freebsd-x64': 4.50.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.0 + '@rollup/rollup-linux-arm-musleabihf': 4.50.0 + '@rollup/rollup-linux-arm64-gnu': 4.50.0 + '@rollup/rollup-linux-arm64-musl': 4.50.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.0 + '@rollup/rollup-linux-ppc64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-musl': 4.50.0 + '@rollup/rollup-linux-s390x-gnu': 4.50.0 + '@rollup/rollup-linux-x64-gnu': 4.50.0 + '@rollup/rollup-linux-x64-musl': 4.50.0 + '@rollup/rollup-openharmony-arm64': 4.50.0 + '@rollup/rollup-win32-arm64-msvc': 4.50.0 + '@rollup/rollup-win32-ia32-msvc': 4.50.0 + '@rollup/rollup-win32-x64-msvc': 4.50.0 + fsevents: 2.3.3 + + rou3@0.5.1: {} + + rou3@0.6.3: {} + + run-async@4.0.6: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + secure-json-parse@2.7.0: {} + + secure-json-parse@4.0.0: {} + + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + + semver@6.3.1: {} + + semver@7.7.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serialize-error@8.1.0: + dependencies: + type-fest: 0.20.2 + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.1: {} + + setprototypeof@1.2.0: {} + + shallow-equal@1.2.1: {} + + sharp@0.34.3: + dependencies: + color: 4.2.3 + detect-libc: 2.0.4 + semver: 7.7.2 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.3 + '@img/sharp-darwin-x64': 0.34.3 + '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-linux-arm': 0.34.3 + '@img/sharp-linux-arm64': 0.34.3 + '@img/sharp-linux-ppc64': 0.34.3 + '@img/sharp-linux-s390x': 0.34.3 + '@img/sharp-linux-x64': 0.34.3 + '@img/sharp-linuxmusl-arm64': 0.34.3 + '@img/sharp-linuxmusl-x64': 0.34.3 + '@img/sharp-wasm32': 0.34.3 + '@img/sharp-win32-arm64': 0.34.3 + '@img/sharp-win32-ia32': 0.34.3 + '@img/sharp-win32-x64': 0.34.3 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.3: {} + + shimmer@1.2.1: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + optional: true + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.7.2 + + slash@3.0.0: {} + + smart-buffer@4.2.0: {} + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1(supports-color@5.5.0) + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.0.1 + smart-buffer: 4.2.0 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + space-separated-tokens@2.0.2: {} + + split-on-first@3.0.0: {} + + split2@4.2.0: {} + + stack-trace@0.0.10: {} + + standard-as-callback@2.1.0: {} + + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + + statuses@2.0.1: {} + + std-env@3.9.0: {} + + stdin-discarder@0.2.2: {} + + streamx@2.22.1: + dependencies: + fast-fifo: 1.3.2 + text-decoder: 1.2.3 + optionalDependencies: + bare-events: 2.6.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.5.0 + get-east-asian-width: 1.3.1 + strip-ansi: 7.1.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.2.0 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-json-comments@5.0.3: {} + + stripe@18.5.0(@types/node@22.18.0): + dependencies: + qs: 6.14.0 + optionalDependencies: + '@types/node': 22.18.0 + + styled-jsx@5.1.6(@babel/core@7.28.3)(react@18.3.1): + dependencies: + client-only: 0.0.1 + react: 18.3.1 + optionalDependencies: + '@babel/core': 7.28.3 + + super-regex@0.2.0: + dependencies: + clone-regexp: 3.0.0 + function-timeout: 0.1.1 + time-span: 5.1.0 + + superagent@10.2.3: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.1(supports-color@5.5.0) + fast-safe-stringify: 2.1.1 + form-data: 4.0.4 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.14.0 + transitivePeerDependencies: + - supports-color + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svix-react@1.13.6(@babel/core@7.28.3)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svix@1.75.0): + dependencies: + iframe-resizer-react: 1.1.1(@babel/core@7.28.3)(prop-types@15.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + svix: 1.75.0 + transitivePeerDependencies: + - '@babel/core' + - prop-types + - supports-color + + svix@1.75.0: + dependencies: + '@stablelib/base64': 1.0.1 + '@types/node': 22.18.0 + es6-promise: 4.2.8 + fast-sha256: 1.3.0 + url-parse: 1.5.10 + uuid: 10.0.0 + + swr@2.3.4(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + + swr@2.3.6(react@18.3.1): + dependencies: + dequal: 2.0.3 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + + symbol-observable@1.2.0: {} + + tailwind-merge@3.3.1: {} + + tailwind-scrollbar-hide@4.0.0(tailwindcss@4.1.12): + dependencies: + tailwindcss: 4.1.12 + + tailwindcss-animate@1.0.7(tailwindcss@4.1.12): + dependencies: + tailwindcss: 4.1.12 + + tailwindcss@4.1.12: {} + + tapable@2.2.3: {} + + tar-fs@3.1.0: + dependencies: + pump: 3.0.3 + tar-stream: 3.1.7 + optionalDependencies: + bare-fs: 4.2.1 + bare-path: 3.0.0 + transitivePeerDependencies: + - bare-buffer + + tar-stream@3.1.7: + dependencies: + b4a: 1.6.7 + fast-fifo: 1.3.2 + streamx: 2.22.1 + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + text-decoder@1.2.3: + dependencies: + b4a: 1.6.7 + + theming@3.3.0(react@18.3.1): + dependencies: + hoist-non-react-statics: 3.3.2 + prop-types: 15.8.1 + react: 18.3.1 + react-display-name: 0.2.5 + tiny-warning: 1.0.3 + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + throttleit@2.1.0: {} + + time-span@5.1.0: + dependencies: + convert-hrtime: 5.0.0 + + tiny-invariant@1.3.3: {} + + tiny-warning@1.0.3: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + touch@3.1.1: {} + + tr46@0.0.3: {} + + tree-kill@1.2.2: {} + + trim-lines@3.0.1: {} + + triple-beam@1.4.1: {} + + ts-api-utils@2.1.0(typescript@5.7.3): + dependencies: + typescript: 5.7.3 + + ts-node@10.9.2(@types/node@22.18.0)(typescript@5.7.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.18.0 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsc-alias@1.8.16: + dependencies: + chokidar: 3.6.0 + commander: 9.5.0 + get-tsconfig: 4.10.1 + globby: 11.1.0 + mylas: 2.1.13 + normalize-path: 3.0.0 + plimit-lit: 1.6.1 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + tsx@4.20.5: + dependencies: + esbuild: 0.25.9 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + typed-query-selector@2.12.0: {} + + typescript-eslint@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.41.0(@typescript-eslint/parser@8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3))(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + '@typescript-eslint/parser': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + '@typescript-eslint/typescript-estree': 8.41.0(typescript@5.7.3) + '@typescript-eslint/utils': 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.7.3) + eslint: 9.34.0(jiti@2.5.1) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + typescript@5.7.3: {} + + uncrypto@0.1.3: {} + + undefsafe@2.0.5: {} + + undici-types@5.26.5: {} + + undici-types@6.21.0: {} + + undici-types@7.10.0: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.4): + dependencies: + browserslist: 4.25.4 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + use-callback-ref@1.3.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + use-sidecar@1.1.3(@types/react@18.3.24)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.24 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + uuid@10.0.0: {} + + uuid@11.1.0: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + vaul@1.1.2(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.24))(@types/react@18.3.24)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@6.3.5(@types/node@22.18.0)(jiti@2.5.1)(lightningcss@1.30.1)(tsx@4.20.5): + dependencies: + esbuild: 0.25.9 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.50.0 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 22.18.0 + fsevents: 2.3.3 + jiti: 2.5.1 + lightningcss: 1.30.1 + tsx: 4.20.5 + + vscode-oniguruma@2.0.1: {} + + vscode-textmate@9.2.0: {} + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + web-streams-polyfill@4.0.0-beta.3: {} + + web-vitals@4.2.4: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + winston-transport@4.9.0: + dependencies: + logform: 2.7.0 + readable-stream: 3.6.2 + triple-beam: 1.4.1 + + word-wrap@1.2.5: {} + + workerpool@9.3.3: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + wrappy@1.0.2: {} + + ws@8.17.1: {} + + ws@8.18.3: {} + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + yoctocolors@2.1.2: {} + + zod-to-json-schema@3.24.6(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zod@4.1.5: {} + + zwitch@2.0.4: {} From 50504472e8cc43e0c93bffab453a4be657ccb59d Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:32:33 +0100 Subject: [PATCH 02/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20bun=20finally=20wor?= =?UTF-8?q?ks=20in=20shared,=20via=20tsc=20outDir?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/package.json b/shared/package.json index 1dc7842fd..624b58bae 100644 --- a/shared/package.json +++ b/shared/package.json @@ -14,9 +14,9 @@ "license": "Apache-2.0", "scripts": { "build:tsc": "tsc", - "build": "bun build ./index.ts --outdir dist --target bun --external zod", + "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 --declaration --declarationMap false --emitDeclarationOnly true --outFile dist/index.d.ts", + "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", From 941c4ce41ff46a3ae0bb563a4df730ef452ca10e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:54:27 +0100 Subject: [PATCH 03/60] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20clean=20up=20b?= =?UTF-8?q?efore=20changes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../referrals/handleCreateRewardProgram.ts | 85 +++++++ .../handlers/referrals/handleCreateTrigger.ts | 85 +++++++ .../referrals/handleDeleteRewardProgram.ts | 22 ++ .../api/rewards/handlers/referrals/index.ts | 3 + .../handlers/rewards/handleCreateCoupon.ts | 68 +++++ .../handlers/rewards/handleDeleteCoupon.ts | 55 ++++ .../handlers/rewards/handleGetCoupon.ts | 21 ++ .../handlers/rewards/handleUpdateCoupon.ts | 79 ++++++ .../api/rewards/handlers/rewards/index.ts | 6 + .../api/rewards/rewardProgramRouter.ts | 111 +------- .../src/internal/api/rewards/rewardRouter.ts | 239 +----------------- 11 files changed, 435 insertions(+), 339 deletions(-) create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts create mode 100644 server/src/internal/api/rewards/handlers/referrals/index.ts create mode 100644 server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts create mode 100644 server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts create mode 100644 server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts create mode 100644 server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts create mode 100644 server/src/internal/api/rewards/handlers/rewards/index.ts diff --git a/server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts b/server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts new file mode 100644 index 000000000..150d5f04d --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts @@ -0,0 +1,85 @@ +import { + CreateRewardProgram, + ErrCode, + RewardTriggerEvent, +} from "@autumn/shared"; +import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; +import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { nullish } from "@/utils/genUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "create reward trigger", + handler: async (req, res) => { + const { orgId, env, db } = req; + 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.id) { + throw new RecaseError({ + message: "Please give this program an ID", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const existingProgram = await RewardProgramService.get({ + db, + id: body.id, + orgId, + env, + }); + + if (existingProgram) { + throw new RecaseError({ + message: `Program with ID ${body.id} already exists`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const rewardProgram = constructRewardProgram({ + rewardProgramData: CreateRewardProgram.parse(req.body), + orgId, + env, + }); + + // Fetch reward ID + // let reward = await RewardService.get({ + // db, + // id: rewardProgram.internal_reward_id, + // orgId, + // env, + // }); + + if ( + rewardProgram.when === RewardTriggerEvent.Checkout && + (nullish(rewardProgram.product_ids) || + rewardProgram.product_ids!.length === 0) + ) { + throw new RecaseError({ + message: "If redeem on checkout, must specify at least one product", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const createdRewardProgram = await RewardProgramService.create({ + db, + data: rewardProgram, + }); + + return res.status(200).json(createdRewardProgram); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts b/server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts new file mode 100644 index 000000000..722e21d64 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts @@ -0,0 +1,85 @@ +import { + CreateRewardProgram, + ErrCode, + RewardTriggerEvent, +} from "@autumn/shared"; +import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; +import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { nullish } from "@/utils/genUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "create trigger", + handler: async (req, res) => { + const { orgId, env, db } = req; + 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.id) { + throw new RecaseError({ + message: "Please give this program an ID", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const existingProgram = await RewardProgramService.get({ + db, + id: body.id, + orgId, + env, + }); + + if (existingProgram) { + throw new RecaseError({ + message: `Program with ID ${body.id} already exists`, + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const rewardProgram = constructRewardProgram({ + rewardProgramData: CreateRewardProgram.parse(req.body), + orgId, + env, + }); + + // Fetch reward ID + // let reward = await RewardService.get({ + // db, + // id: rewardProgram.internal_reward_id, + // orgId, + // env, + // }); + + if ( + rewardProgram.when === RewardTriggerEvent.Checkout && + (nullish(rewardProgram.product_ids) || + rewardProgram.product_ids!.length === 0) + ) { + throw new RecaseError({ + message: "If redeem on checkout, must specify at least one product", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } + + const createdRewardProgram = await RewardProgramService.create({ + db, + data: rewardProgram, + }); + + return res.status(200).json(createdRewardProgram); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts b/server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts new file mode 100644 index 000000000..343d9eb1b --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts @@ -0,0 +1,22 @@ +import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "delete reward scheme", + handler: async (req, res) => { + const { orgId, env, db } = req; + const { id } = req.params; + + const rewardProgram = await RewardProgramService.delete({ + db, + id, + orgId, + env, + }); + + return res.status(200).json(rewardProgram); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/index.ts b/server/src/internal/api/rewards/handlers/referrals/index.ts new file mode 100644 index 000000000..2f35e4645 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/index.ts @@ -0,0 +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 diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts new file mode 100644 index 000000000..560524ea9 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts @@ -0,0 +1,68 @@ +import { CreateRewardSchema, RewardCategory } from "@autumn/shared"; +import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { + 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); + + const org = await OrgService.getFromReq(req); + + const newReward = constructReward({ + reward: rewardData, + orgId, + env, + // internalId: rewardBody.internal_id, + }); + + 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 || [], + }) + ]); + + await initRewardStripePrices({ + db, + prices, + org, + env, + logger, + }); + + await createStripeCoupon({ + reward: newReward, + org, + env, + prices, + logger, + legacyVersion: req.query.legacyStripe === "true", + }); + } + + const insertedCoupon = await RewardService.insert({ + db, + data: newReward, + }); + + res.status(200).json(insertedCoupon); + }, +}) \ No newline at end of file diff --git a/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts new file mode 100644 index 000000000..da02f9751 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts @@ -0,0 +1,55 @@ +import { ErrCode } from "@autumn/shared"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "delete reward", + handler: async (req, res) => { + const { id } = req.params; + const { orgId, env, db } = req; + + const org = await OrgService.getFromReq(req); + const stripeCli = createStripeCli({ + org, + env, + }); + + const reward = await RewardService.get({ + db, + idOrInternalId: id, + orgId, + env, + }); + + if (!reward) { + throw new RecaseError({ + message: `Reward ${id} not found`, + code: ErrCode.InvalidRequest, + }); + } + + try { + await stripeCli.coupons.del(reward.id); + } catch (error) { + console.log(`Failed to delete coupon from stripe: ${(error as { message: string }).message}`); + } + + await RewardService.delete({ + db, + internalId: reward.internal_id, + env, + orgId, + }); + + res.status(200).json({ + success: true, + message: "Reward deleted successfully", + }); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts new file mode 100644 index 000000000..f4a694555 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewards/handleGetCoupon.ts @@ -0,0 +1,21 @@ +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; + + const reward = await RewardService.get({ + db, + idOrInternalId: id, + orgId, + env, + }); + + res.status(200).json(reward); + } +}); \ No newline at end of file diff --git a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts new file mode 100644 index 000000000..76ab77ad8 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts @@ -0,0 +1,79 @@ +import { ErrCode, notNullish, RewardCategory } from "@autumn/shared"; +import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { getRewardCat } from "@/internal/rewards/rewardUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "update coupon", + handler: async (req, res) => { + const { internalId } = req.params; + const { orgId, env, db, logtail: logger } = req; + const rewardBody = req.body; + + const org = await OrgService.getFromReq(req); + const stripeCli = createStripeCli({ + org, + env, + }); + + const reward = await RewardService.get({ + db, + idOrInternalId: internalId, + orgId, + env, + }); + + if (!reward) { + throw new RecaseError({ + message: `Reward ${internalId} not found`, + code: ErrCode.InvalidRequest, + }); + } + + const prices = await PriceService.getInIds({ + db, + ids: notNullish(rewardBody.price_ids) + ? rewardBody.price_ids + : reward.discount_config?.price_ids, + }); + + // 1. Delete old prices from stripe + try { + await stripeCli.coupons.del(reward.id); + await stripeCli.coupons.del(reward.internal_id); + } catch (_) { + // console.log(`Failed to delete coupon from stripe: ${error.message}`); + } + + const rewardCat = getRewardCat(rewardBody); + if (rewardCat === RewardCategory.Discount) { + await createStripeCoupon({ + reward: rewardBody, + org, + env, + prices, + logger, + legacyVersion: req.query.legacyStripe === "true", + }); + } + + // 3. Update coupon in db + const updatedCoupon = await RewardService.update({ + db, + internalId: reward.internal_id, + env, + orgId, + update: rewardBody, + }); + + 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 new file mode 100644 index 000000000..29b1d2695 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewards/index.ts @@ -0,0 +1,6 @@ +import handleCreateCoupon from "./handleCreateCoupon.js"; +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 diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 70db20ddc..15611b1c8 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -1,111 +1,8 @@ -import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; -import { RewardService } from "@/internal/rewards/RewardService.js"; -import { constructRewardProgram } from "@/internal/rewards/rewardTriggerUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { nullish } from "@/utils/genUtils.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { - CreateRewardProgram, - ErrCode, - RewardTriggerEvent, -} from "@autumn/shared"; -import express, { Router } from "express"; +import express, { type Router } from "express"; +import { handleCreateRewardProgram, handleDeleteRewardProgram } from "./handlers/referrals/index.js"; export const rewardProgramRouter: Router = express.Router(); -rewardProgramRouter.post("", (req, res) => - routeHandler({ - req, - res, - action: "create reward trigger", - handler: async (req: any, res: any) => { - const { orgId, env, db } = req; - const body = req.body; +rewardProgramRouter.post("", handleCreateRewardProgram); - 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.id) { - throw new RecaseError({ - message: "Please give this program an ID", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } - - let existingProgram = await RewardProgramService.get({ - db, - id: body.id, - orgId, - env, - }); - - if (existingProgram) { - throw new RecaseError({ - message: `Program with ID ${body.id} already exists`, - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } - - const rewardProgram = constructRewardProgram({ - rewardProgramData: CreateRewardProgram.parse(req.body), - orgId, - env, - }); - - // Fetch reward ID - // let reward = await RewardService.get({ - // db, - // id: rewardProgram.internal_reward_id, - // orgId, - // env, - // }); - - if ( - rewardProgram.when == RewardTriggerEvent.Checkout && - (nullish(rewardProgram.product_ids) || - rewardProgram.product_ids!.length == 0) - ) { - throw new RecaseError({ - message: "If redeem on checkout, must specify at least one product", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } - - let createdRewardProgram = await RewardProgramService.create({ - db, - data: rewardProgram, - }); - - return res.status(200).json(createdRewardProgram); - }, - }), -); - -rewardProgramRouter.delete("/:id", (req, res) => - routeHandler({ - req, - res, - action: "delete reward scheme", - handler: async (req: any, res: any) => { - const { orgId, env, db } = req; - const { id } = req.params; - - let rewardProgram = await RewardProgramService.delete({ - db, - id, - orgId, - env, - }); - - return res.status(200).json(rewardProgram); - }, - }), -); +rewardProgramRouter.delete("/:id", handleDeleteRewardProgram); \ No newline at end of file diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts index 6bb45de32..86aff9652 100644 --- a/server/src/internal/api/rewards/rewardRouter.ts +++ b/server/src/internal/api/rewards/rewardRouter.ts @@ -1,239 +1,14 @@ -import express from "express"; -import { CreateRewardSchema, ErrCode, RewardCategory } from "@autumn/shared"; -import { Router } from "express"; -import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; - -import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; -import { RewardService } from "@/internal/rewards/RewardService.js"; -import { PriceService } from "@/internal/products/prices/PriceService.js"; -import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; -import { - constructReward, - getRewardCat, - initRewardStripePrices, -} from "@/internal/rewards/rewardUtils.js"; -import { notNullish } from "@/utils/genUtils.js"; +import express, { type Router } from "express"; +import { handleCreateCoupon, handleDeleteCoupon, handleGetCoupon, handleUpdateCoupon } from "./handlers/rewards/index.js"; const rewardRouter: Router = express.Router(); -rewardRouter.post("", async (req: any, res: any) => { - try { - const { db, orgId, env, logtail: logger } = req; - const rewardBody = req.body; - const rewardData = CreateRewardSchema.parse(rewardBody); +rewardRouter.post("", handleCreateCoupon); - const org = await OrgService.getFromReq(req); +rewardRouter.delete("/:id", handleDeleteCoupon); - const newReward = constructReward({ - reward: rewardData, - orgId, - env, - // internalId: rewardBody.internal_id, - }); +rewardRouter.post("/:internalId", handleUpdateCoupon); - if (getRewardCat(newReward) === RewardCategory.Discount) { - const stripeCli = createStripeCli({ - org, - env, - }); +rewardRouter.get("/:id", handleGetCoupon); - let discountConfig = newReward.discount_config; - - // Get prices for coupon - const [prices, entitlements] = await Promise.all([ - PriceService.getInIds({ - db, - ids: discountConfig!.price_ids || [], - }), - EntitlementService.getByOrg({ - db, - orgId, - env, - }), - ]); - - await initRewardStripePrices({ - db, - prices, - org, - env, - logger, - }); - - await createStripeCoupon({ - reward: newReward, - org, - env, - prices, - logger, - legacyVersion: req.query.legacyStripe === "true", - }); - } - - const insertedCoupon = await RewardService.insert({ - db, - data: newReward, - }); - - res.status(200).json(insertedCoupon); - } catch (error) { - handleRequestError({ - error, - res, - req, - action: "create coupon", - }); - } -}); - -rewardRouter.delete("/:id", async (req: any, res: any) => { - try { - const { id } = req.params; - const { orgId, env, db } = req; - - const org = await OrgService.getFromReq(req); - const stripeCli = createStripeCli({ - org, - env, - }); - - let reward = await RewardService.get({ - db, - idOrInternalId: id, - orgId, - env, - }); - - if (!reward) { - throw new RecaseError({ - message: `Reward ${id} not found`, - code: ErrCode.InvalidRequest, - }); - } - - try { - await stripeCli.coupons.del(reward.id); - } catch (error: any) { - console.log(`Failed to delete coupon from stripe: ${error.message}`); - } - - await RewardService.delete({ - db, - internalId: reward.internal_id, - env, - orgId, - }); - - res.status(200).json({ - success: true, - message: "Reward deleted successfully", - }); - } catch (error) { - handleRequestError({ - error, - res, - req, - action: "delete coupon", - }); - } -}); - -rewardRouter.post("/:internalId", async (req: any, res: any) => { - try { - const { internalId } = req.params; - const { orgId, env, db, logtail: logger } = req; - const rewardBody = req.body; - - const org = await OrgService.getFromReq(req); - const stripeCli = createStripeCli({ - org, - env, - }); - - const reward = await RewardService.get({ - db, - idOrInternalId: internalId, - orgId, - env, - }); - - if (!reward) { - throw new RecaseError({ - message: `Reward ${internalId} not found`, - code: ErrCode.InvalidRequest, - }); - } - - const prices = await PriceService.getInIds({ - db, - ids: notNullish(rewardBody.price_ids) - ? rewardBody.price_ids - : reward.discount_config?.price_ids, - }); - - // 1. Delete old prices from stripe - try { - await stripeCli.coupons.del(reward.id); - await stripeCli.coupons.del(reward.internal_id); - } catch (error) { - // console.log(`Failed to delete coupon from stripe: ${error.message}`); - } - - let rewardCat = getRewardCat(rewardBody); - if (rewardCat == RewardCategory.Discount) { - await createStripeCoupon({ - reward: rewardBody, - org, - env, - prices, - logger, - legacyVersion: req.query.legacyStripe === "true", - }); - } - - // 3. Update coupon in db - const updatedCoupon = await RewardService.update({ - db, - internalId: reward.internal_id, - env, - orgId, - update: rewardBody, - }); - - res.status(200).json(updatedCoupon); - } catch (error) { - handleRequestError({ - error, - res, - req, - action: "update coupon", - }); - } -}); - -rewardRouter.get("/:id", async (req: any, res: any) => { - try { - const { id } = req.params; - const { orgId, env, db } = req; - - const reward = await RewardService.get({ - db, - idOrInternalId: id, - orgId, - env, - }); - - res.status(200).json(reward); - } catch (error) { - handleRequestError({ - error, - res, - req, - action: "get reward", - }); - } -}); - -export default rewardRouter; +export default rewardRouter; \ No newline at end of file From e7acf7d3af32987cefa58552742985a6e579c04e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:11:01 +0100 Subject: [PATCH 04/60] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20more=20cleanup?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handlers/referrals/handleGetRedemption.ts | 20 ++ .../referrals/handleGetReferralCode.ts | 85 +++++ .../referrals/handleRedeemReferral.ts | 197 ++++++++++++ .../api/rewards/handlers/referrals/index.ts | 6 +- .../handleCreateRewardProgram.ts | 0 .../handleCreateTrigger.ts | 0 .../handleDeleteRewardProgram.ts | 0 .../rewards/handlers/rewardPrograms/index.ts | 3 + .../internal/api/rewards/referralRouter.ts | 300 +----------------- .../api/rewards/rewardProgramRouter.ts | 2 +- 10 files changed, 317 insertions(+), 296 deletions(-) create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleGetRedemption.ts create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts create mode 100644 server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts rename server/src/internal/api/rewards/handlers/{referrals => rewardPrograms}/handleCreateRewardProgram.ts (100%) rename server/src/internal/api/rewards/handlers/{referrals => rewardPrograms}/handleCreateTrigger.ts (100%) rename server/src/internal/api/rewards/handlers/{referrals => rewardPrograms}/handleDeleteRewardProgram.ts (100%) create mode 100644 server/src/internal/api/rewards/handlers/rewardPrograms/index.ts diff --git a/server/src/internal/api/rewards/handlers/referrals/handleGetRedemption.ts b/server/src/internal/api/rewards/handlers/referrals/handleGetRedemption.ts new file mode 100644 index 000000000..0f3a29e6d --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleGetRedemption.ts @@ -0,0 +1,20 @@ +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "get redemption by id", + handler: async (req, res) => { + const { db } = req; + const { redemptionId } = req.params; + + const redemption = await RewardRedemptionService.getById({ + db, + id: redemptionId, + }); + + res.status(200).json(redemption); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts b/server/src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts new file mode 100644 index 000000000..31c8b2e09 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleGetReferralCode.ts @@ -0,0 +1,85 @@ +import { ErrCode } from "@autumn/shared"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; +import { generateReferralCode } from "@/internal/rewards/referralUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { generateId } from "@/utils/genUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; + +export default async (req: any, res: any) => + routeHandler({ + req, + res, + action: "get referral code", + handler: async (req, res) => { + const { orgId, env, db } = req; + const { program_id: rewardProgramId, customer_id: customerId } = req.body; + + const [rewardProgram, customer] = await Promise.all([ + RewardProgramService.get({ + db, + id: rewardProgramId, + orgId, + env, + errorIfNotFound: true, + }), + CusService.get({ + db: req.db, + orgId, + env, + idOrInternalId: customerId, + }), + ]); + + if (!customer) { + throw new RecaseError({ + message: "Customer not found", + statusCode: 404, + code: ErrCode.CustomerNotFound, + }); + } + + if (!rewardProgram) { + throw new RecaseError({ + message: "Reward program not found", + statusCode: 404, + code: ErrCode.RewardProgramNotFound, + }); + } + + // Get referral code by customer and reward trigger + let referralCode = + await RewardProgramService.getCodeByCustomerAndRewardProgram({ + db, + orgId, + env, + internalCustomerId: customer.internal_id, + internalRewardProgramId: rewardProgram.internal_id, + }); + + if (!referralCode) { + const code = generateReferralCode(); + + referralCode = { + code, + org_id: orgId, + env, + internal_customer_id: customer.internal_id, + internal_reward_program_id: rewardProgram.internal_id, + id: generateId("rc"), + created_at: Date.now(), + }; + + referralCode = await RewardProgramService.createReferralCode({ + db, + data: referralCode, + }); + } + + res.status(200).json({ + code: referralCode.code, + customer_id: customer.id, + created_at: referralCode.created_at, + }); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts new file mode 100644 index 000000000..895694a75 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts @@ -0,0 +1,197 @@ +import { + ErrCode, + RewardCategory, + type RewardRedemption, + RewardTriggerEvent, +} from "@autumn/shared"; +import { parseReqForAction } from "@/internal/analytics/actionUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { + triggerFreeProduct, + triggerRedemption, +} from "@/internal/rewards/referralUtils.js"; +import { getRewardCat } from "@/internal/rewards/rewardUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { generateId, notNullish } from "@/utils/genUtils.js"; +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; + + // 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, + }); + } + + // 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, + }); + } + + // 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, + }); + } + + // 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.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(), + }; + + redemption = await RewardRedemptionService.insert({ + db, + rewardRedemption: redemption, + }); + + // 5. If reward trigger when is immediate: + const { reward_program } = referralCode; + + if ( + referralCode.reward_program.when === RewardTriggerEvent.CustomerCreation + ) { + 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, + }); + } + + 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, + }); + } + } + + // Add coupon to customer? + 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, + }, + }); + }, + }); diff --git a/server/src/internal/api/rewards/handlers/referrals/index.ts b/server/src/internal/api/rewards/handlers/referrals/index.ts index 2f35e4645..4fd0e2a2c 100644 --- a/server/src/internal/api/rewards/handlers/referrals/index.ts +++ b/server/src/internal/api/rewards/handlers/referrals/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 handleGetRedemption } from "./handleGetRedemption.js"; +export { default as handleGetReferralCode } from "./handleGetReferralCode.js"; +export { default as handleRedeemReferral } from "./handleRedeemReferral.js"; diff --git a/server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts similarity index 100% rename from server/src/internal/api/rewards/handlers/referrals/handleCreateRewardProgram.ts rename to server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateRewardProgram.ts diff --git a/server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts similarity index 100% rename from server/src/internal/api/rewards/handlers/referrals/handleCreateTrigger.ts rename to server/src/internal/api/rewards/handlers/rewardPrograms/handleCreateTrigger.ts diff --git a/server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/handleDeleteRewardProgram.ts similarity index 100% rename from server/src/internal/api/rewards/handlers/referrals/handleDeleteRewardProgram.ts rename to server/src/internal/api/rewards/handlers/rewardPrograms/handleDeleteRewardProgram.ts diff --git a/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts b/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts new file mode 100644 index 000000000..2f35e4645 --- /dev/null +++ b/server/src/internal/api/rewards/handlers/rewardPrograms/index.ts @@ -0,0 +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 diff --git a/server/src/internal/api/rewards/referralRouter.ts b/server/src/internal/api/rewards/referralRouter.ts index bcd0d3290..98021711b 100644 --- a/server/src/internal/api/rewards/referralRouter.ts +++ b/server/src/internal/api/rewards/referralRouter.ts @@ -1,301 +1,17 @@ -import { CusService } from "@/internal/customers/CusService.js"; +import express, { type Router } from "express"; import { - generateReferralCode, - triggerFreeProduct, - triggerRedemption, -} from "@/internal/rewards/referralUtils.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; - -import RecaseError from "@/utils/errorUtils.js"; -import { generateId, notNullish } from "@/utils/genUtils.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { ErrCode, RewardCategory, RewardTriggerEvent } from "@autumn/shared"; -import express, { Router } from "express"; -import { RewardRedemption } from "@autumn/shared"; -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; -import { getRewardCat } from "@/internal/rewards/rewardUtils.js"; -import { RewardService } from "@/internal/rewards/RewardService.js"; -import { parseReqForAction } from "@/internal/analytics/actionUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; + handleGetRedemption, + handleGetReferralCode, + handleRedeemReferral, +} from "./handlers/referrals/index.js"; export const referralRouter: Router = express.Router(); // 1. Get referral code -referralRouter.post("/code", (req, res) => - routeHandler({ - req, - res, - action: "get referral code", - handler: async (req: any, res: any) => { - const { orgId, env, logtail: logger, db } = req; - const { program_id: rewardProgramId, customer_id: customerId } = req.body; +referralRouter.post("/code", handleGetReferralCode); - let [rewardProgram, customer] = await Promise.all([ - RewardProgramService.get({ - db, - id: rewardProgramId, - orgId, - env, - errorIfNotFound: true, - }), - CusService.get({ - db: req.db, - orgId, - env, - idOrInternalId: customerId, - }), - ]); - - if (!customer) { - throw new RecaseError({ - message: "Customer not found", - statusCode: 404, - code: ErrCode.CustomerNotFound, - }); - } - - if (!rewardProgram) { - throw new RecaseError({ - message: "Reward program not found", - statusCode: 404, - code: ErrCode.RewardProgramNotFound, - }); - } - - // Get referral code by customer and reward trigger - let referralCode = - await RewardProgramService.getCodeByCustomerAndRewardProgram({ - db, - orgId, - env, - internalCustomerId: customer.internal_id, - internalRewardProgramId: rewardProgram.internal_id, - }); - - if (!referralCode) { - const code = generateReferralCode(); - - referralCode = { - code, - org_id: orgId, - env, - internal_customer_id: customer.internal_id, - internal_reward_program_id: rewardProgram.internal_id, - id: generateId("rc"), - created_at: Date.now(), - }; - - referralCode = await RewardProgramService.createReferralCode({ - db, - data: referralCode, - }); - } - - res.status(200).json({ - code: referralCode.code, - customer_id: customer.id, - created_at: referralCode.created_at, - }); - }, - }) -); - -referralRouter.post("/redeem", (req, res) => - routeHandler({ - req, - res, - action: "redeem referral code", - handler: async (req: any, res: any) => { - const { orgId, env, logtail: logger, db } = req; - // const { referral_id: rewardTriggerId } = req.params; - const { code, customer_id: customerId } = req.body; - - // 1. Get redeemed by customer, and referral code - let [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, - }); - } - - // 2. Check that code has not reached max redemptions - let 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, - }); - } - - // 3. Check that customer has not already redeemed a code in this referral program - let 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, - }); - } - - // Don't let customer redeem their own code - let 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.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(), - }; - - redemption = await RewardRedemptionService.insert({ - db, - rewardRedemption: redemption, - }); - - // 5. If reward trigger when is immediate: - let { reward_program } = referralCode; - - if ( - referralCode.reward_program.when === RewardTriggerEvent.CustomerCreation - ) { - 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, - }); - } - - let 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, - }); - } - } - - // Add coupon to customer? - res.status(200).json({ - id: redemption.id, - customer_id: customer.id, - // triggered: redemption?.applied, - // applied: redemption?.applied, - reward_id: reward_program.reward.id, - referrer: { - id: codeCustomer.id, - name: codeCustomer.name, - email: codeCustomer.email, - created_at: codeCustomer.created_at, - }, - }); - }, - }) -); +referralRouter.post("/redeem", handleRedeemReferral); export const redemptionRouter: Router = express.Router(); -redemptionRouter.get("/:redemptionId", (req, res) => - routeHandler({ - req, - res, - action: "get redemption by id", - handler: async (req: any, res: any) => { - const { db } = req; - const { redemptionId } = req.params; - - let redemption = await RewardRedemptionService.getById({ - db, - id: redemptionId, - }); - - res.status(200).json(redemption); - }, - }) -); +redemptionRouter.get("/:redemptionId", handleGetRedemption); diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 15611b1c8..725b9ac99 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -1,5 +1,5 @@ import express, { type Router } from "express"; -import { handleCreateRewardProgram, handleDeleteRewardProgram } from "./handlers/referrals/index.js"; +import { handleCreateRewardProgram, handleDeleteRewardProgram } from "./handlers/rewardPrograms/index.js"; export const rewardProgramRouter: Router = express.Router(); From 441f6d43beb38b9f47b02e8568aa930a265ab2b0 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:54:16 +0100 Subject: [PATCH 05/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../referrals/handleRedeemReferral.ts | 12 +- .../handlers/rewards/handleCreateCoupon.ts | 2 +- .../src/internal/invoices/invoiceMemoUtils.ts | 200 +++++---- server/src/utils/scriptUtils/initCustomer.ts | 397 +++++++++--------- 4 files changed, 305 insertions(+), 306 deletions(-) diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts index 895694a75..85e484c93 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"; @@ -11,8 +11,8 @@ import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { - triggerFreeProduct, - triggerRedemption, + triggerFreeProduct, + triggerRedemption, } from "@/internal/rewards/referralUtils.js"; import { getRewardCat } from "@/internal/rewards/rewardUtils.js"; import RecaseError from "@/utils/errorUtils.js"; diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts index 560524ea9..29b1da644 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts @@ -57,7 +57,7 @@ export default async (req: any, res: any) => routeHandler({ legacyVersion: req.query.legacyStripe === "true", }); } - + const insertedCoupon = await RewardService.insert({ db, data: newReward, diff --git a/server/src/internal/invoices/invoiceMemoUtils.ts b/server/src/internal/invoices/invoiceMemoUtils.ts index 3745dcb08..111066cb4 100644 --- a/server/src/internal/invoices/invoiceMemoUtils.ts +++ b/server/src/internal/invoices/invoiceMemoUtils.ts @@ -1,130 +1,124 @@ import { - Organization, - Feature, - FullProduct, - EntitlementWithFeature, - AllowanceType, - getFeatureName, - FeatureType, - Price, + type EntitlementWithFeature, + type Feature, type FullProduct, type Organization, + type Price } from "@autumn/shared"; import { mapToProductItems } from "@/internal/products/productV2Utils.js"; -import { getProductItemDisplay } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js"; -import { getEntRelatedPrice } from "../products/entitlements/entitlementUtils.js"; import { isFeatureItem } from "../products/product-items/productItemUtils/getItemType.js"; +import { getProductItemDisplay } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js"; export const buildInvoiceMemo = async ({ - org, - product, - products, - features, + org, + product, + products, + features, }: { - org: Organization; - product?: FullProduct; - products?: FullProduct[]; - features: Feature[]; + org: Organization; + product?: FullProduct; + products?: FullProduct[]; + features: Feature[]; }): Promise => { - if (product) { - const items = mapToProductItems({ - prices: product.prices, - entitlements: product.entitlements, - features, - }); + if (product) { + const items = mapToProductItems({ + prices: product.prices, + entitlements: product.entitlements, + features, + }); - const itemsToDisplay = ["Included features:"]; - for (const item of items) { - if (!item.feature_id) continue; - const display = getProductItemDisplay({ - item, - features, - currency: org.default_currency, - }); - if (display?.primary_text) itemsToDisplay.push(display.primary_text); - } + const itemsToDisplay = ["Included features:"]; + for (const item of items) { + if (!item.feature_id) continue; + const display = getProductItemDisplay({ + item, + features, + currency: org.default_currency, + }); + if (display?.primary_text) itemsToDisplay.push(display.primary_text); + } - return itemsToDisplay.join("\n"); - } else if (products) { - const itemsToDisplay = ["Included features:"]; + return itemsToDisplay.join("\n"); + } else if (products) { + const itemsToDisplay = ["Included features:"]; - for (const p of products) { - const items = mapToProductItems({ - prices: p.prices, - entitlements: p.entitlements, - features, - }); + for (const p of products) { + const items = mapToProductItems({ + prices: p.prices, + entitlements: p.entitlements, + features, + }); - console.log( - "Items: %s", - items.map((i) => i.feature_id) - ); + console.log( + "Items: %s", + items.map((i) => i.feature_id), + ); - for (const item of items) { - if (!item.feature_id) continue; - const display = getProductItemDisplay({ - item, - features, - currency: org.default_currency, - }); - console.log( - "Display for item %s: %s", - item.feature_id, - display?.primary_text - ); - if (display?.primary_text) itemsToDisplay.push(display.primary_text); - } - } + for (const item of items) { + if (!item.feature_id) continue; + const display = getProductItemDisplay({ + item, + features, + currency: org.default_currency, + }); + console.log( + "Display for item %s: %s", + item.feature_id, + display?.primary_text, + ); + if (display?.primary_text) itemsToDisplay.push(display.primary_text); + } + } - console.log("Items to display: %s", itemsToDisplay); + console.log("Items to display: %s", itemsToDisplay); - let memo = itemsToDisplay.join("\n"); - if (memo.length > 490) { - memo = memo.slice(0, 490) + "..."; - } - return memo; - } else return ""; + let memo = itemsToDisplay.join("\n"); + if (memo.length > 490) { + memo = memo.slice(0, 490) + "..."; + } + return memo; + } else return ""; }; export const buildInvoiceMemoFromEntitlements = async ({ - org, - entitlements, - prices, - features, - logger, + org, + entitlements, + prices, + features, + logger, }: { - org: Organization; - entitlements: EntitlementWithFeature[]; - prices: Price[]; - features: Feature[]; - logger: any; + org: Organization; + entitlements: EntitlementWithFeature[]; + prices: Price[]; + features: Feature[]; + logger: any; }) => { - // Get item from price and ent - const items = mapToProductItems({ - entitlements, - prices, - features, - }); + // Get item from price and ent + const items = mapToProductItems({ + entitlements, + prices, + features, + }); - if (items.filter(isFeatureItem).length === 0) return ""; + if (items.filter(isFeatureItem).length === 0) return ""; - const itemsToDisplay = ["Included:"]; + const itemsToDisplay = ["Included:"]; - for (const item of items) { - if (!isFeatureItem(item)) continue; + for (const item of items) { + if (!isFeatureItem(item)) continue; - const display = getProductItemDisplay({ - item, - features, - currency: org.default_currency, - }); + const display = getProductItemDisplay({ + item, + features, + currency: org.default_currency, + }); - itemsToDisplay.push( - `- ${display?.primary_text}${display?.secondary_text ? ` ${display?.secondary_text}` : ""}` - ); - } + itemsToDisplay.push( + `- ${display?.primary_text}${display?.secondary_text ? ` ${display?.secondary_text}` : ""}`, + ); + } - let memo = itemsToDisplay.join("\n"); - if (memo.length > 490) { - memo = memo.slice(0, 490) + "..."; - } - return memo; + let memo = itemsToDisplay.join("\n"); + if (memo.length > 490) { + memo = `${memo.slice(0, 490)}...`; + } + return memo; }; diff --git a/server/src/utils/scriptUtils/initCustomer.ts b/server/src/utils/scriptUtils/initCustomer.ts index bc0759141..880a7b472 100644 --- a/server/src/utils/scriptUtils/initCustomer.ts +++ b/server/src/utils/scriptUtils/initCustomer.ts @@ -1,239 +1,244 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { AppEnv, Customer, Organization, ProcessorType } from "@autumn/shared"; -import { createStripeCli } from "../../external/stripe/utils.js"; -import { Autumn } from "autumn-js"; +import { + type AppEnv, + type Customer, + type Organization, + ProcessorType, +} from "@autumn/shared"; +import type { Autumn } from "autumn-js"; +import type Stripe from "stripe"; +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, } from "../../external/stripe/stripeCusUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import Stripe from "stripe"; -import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import { createStripeCli } from "../../external/stripe/utils.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; - customerId: string; - fingerprint?: string; - org: Organization; - env: AppEnv; - db: DrizzleCli; - attachPm?: "success" | "fail"; - withTestClock?: boolean; + autumn: Autumn; + customerId: string; + fingerprint?: string; + org: Organization; + env: AppEnv; + db: DrizzleCli; + attachPm?: "success" | "fail"; + withTestClock?: boolean; }) => { - let customerData = { - id: customerId, - name: customerId, - email: `${customerId}@example.com`, - fingerprint, - }; + const customerData = { + id: customerId, + name: customerId, + email: `${customerId}@example.com`, + fingerprint, + }; - let 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); - let 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 { - let 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; }) => { - let name = customerId; - let email = `${customerId}@example.com`; - let fingerprint_ = ""; - const stripeCli = createStripeCli({ org, env }); + const name = customerId; + const email = `${customerId}@example.com`; + const fingerprint_ = ""; + const stripeCli = createStripeCli({ org, env }); - let testClockId = undefined; + 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-ignore - 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 || "", + }; }; From 72ca1815c523df4a9d2fd95ee86c2774721e811d Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:55:34 +0100 Subject: [PATCH 06/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20exclusion=20map?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite/src/utils/formatUtils/formatTextUtils.ts | 10 +- vite/src/utils/product/productItemUtils.ts | 146 +++++++++--------- 2 files changed, 80 insertions(+), 76 deletions(-) diff --git a/vite/src/utils/formatUtils/formatTextUtils.ts b/vite/src/utils/formatUtils/formatTextUtils.ts index 99d121179..9f440eb42 100644 --- a/vite/src/utils/formatUtils/formatTextUtils.ts +++ b/vite/src/utils/formatUtils/formatTextUtils.ts @@ -1,10 +1,12 @@ import { BillingInterval, - EntInterval, - ProductItemInterval, + EntInterval } from "@autumn/shared"; -export const keyToTitle = (key: string) => { +export const keyToTitle = (key: string, options?: { exclusionMap?: Record }) => { + if(options?.exclusionMap?.[key]) { + return options.exclusionMap[key]; + } return key .replace(/[_-]/g, " ") .replace(/\b\w/g, (char) => char.toUpperCase()); @@ -23,7 +25,7 @@ export const slugify = ( ) => { return text .toLowerCase() - .replace(/ /g, type == "underscore" ? "_" : "-") + .replace(/ /g, type === "underscore" ? "_" : "-") .replace(/[^\w\s-]/g, ""); }; diff --git a/vite/src/utils/product/productItemUtils.ts b/vite/src/utils/product/productItemUtils.ts index d912e6295..033ae7bc9 100644 --- a/vite/src/utils/product/productItemUtils.ts +++ b/vite/src/utils/product/productItemUtils.ts @@ -1,9 +1,9 @@ import { BillingInterval, EntInterval, - Feature, + type Feature, Infinite, - ProductItem, + type ProductItem, ProductItemFeatureType, ProductItemType, UsageModel, @@ -13,103 +13,105 @@ import { isFeatureItem, isFeaturePriceItem, isPriceItem } from "./getItemType"; import { itemToUsageType } from "./productItemUtils/convertItem"; export const itemIsUnlimited = (item: ProductItem) => { - return item.included_usage == Infinite; + return item.included_usage === Infinite; }; export const formatAmount = ({ - defaultCurrency, - amount, - maxFractionDigits = 6, + defaultCurrency, + amount, + maxFractionDigits = 6, }: { - defaultCurrency: string; - amount: number; - maxFractionDigits?: number; + defaultCurrency: string; + amount: number; + maxFractionDigits?: number; }) => { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: defaultCurrency, - minimumFractionDigits: 0, - maximumFractionDigits: maxFractionDigits || 6, - }).format(amount); + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: defaultCurrency, + minimumFractionDigits: 0, + maximumFractionDigits: maxFractionDigits || 6, + }).format(amount); }; export const getItemType = (item: ProductItem) => { - if (isPriceItem(item)) { - return ProductItemType.Price; - } else if (isFeatureItem(item)) { - return ProductItemType.Feature; - } + if (isPriceItem(item)) { + return ProductItemType.Price; + } else if (isFeatureItem(item)) { + return ProductItemType.Feature; + } - return ProductItemType.FeaturePrice; + return ProductItemType.FeaturePrice; }; -export const intervalIsNone = (interval: any) => { - return ( - nullish(interval) || - interval == EntInterval.Lifetime || - interval == BillingInterval.OneOff - ); +export const intervalIsNone = ( + interval: EntInterval | BillingInterval | null | undefined, +) => { + return ( + nullish(interval) || + interval === EntInterval.Lifetime || + interval === BillingInterval.OneOff + ); }; export const getShowParams = (item: ProductItem | null) => { - if (!item) { - return { - price: false, - feature: false, - allowance: false, - perEntity: false, - cycle: false, - }; - } + if (!item) { + return { + price: false, + feature: false, + allowance: false, + perEntity: false, + cycle: false, + }; + } - return { - price: notNullish(item.price) || notNullish(item.tiers), - feature: !isPriceItem(item), - allowance: true, - perEntity: notNullish(item.entity_feature_id), - cycle: true, - }; + return { + price: notNullish(item.price) || notNullish(item.tiers), + feature: !isPriceItem(item), + allowance: true, + perEntity: notNullish(item.entity_feature_id), + cycle: true, + }; }; export const shouldShowProrationConfig = ({ - item, - features, + item, + features, }: { - item: ProductItem; - features: Feature[]; + item: ProductItem; + features: Feature[]; }) => { - if (!isFeaturePriceItem(item)) return false; + if (!isFeaturePriceItem(item)) return false; - // If pay per use single use - const usageType = itemToUsageType({ item, features }); + // If pay per use single use + const usageType = itemToUsageType({ item, features }); - if (item.usage_model == UsageModel.Prepaid) return true; + if (item.usage_model === UsageModel.Prepaid) return true; - // if ( - // usageType == ProductItemFeatureType.SingleUse && - // item.usage_model == UsageModel.Prepaid - // ) { - // return true; - // } else + // if ( + // usageType == ProductItemFeatureType.SingleUse && + // item.usage_model == UsageModel.Prepaid + // ) { + // return true; + // } else - if ( - usageType == ProductItemFeatureType.ContinuousUse - // &&item.usage_model !== UsageModel.Prepaid - ) { - return true; - } - return false; + if ( + usageType === ProductItemFeatureType.ContinuousUse + // &&item.usage_model !== UsageModel.Prepaid + ) { + return true; + } + return false; }; export const itemsHaveSameInterval = ({ - item1, - item2, + item1, + item2, }: { - item1: ProductItem; - item2: ProductItem; + item1: ProductItem; + item2: ProductItem; }) => { - return ( - item1.interval == item2.interval && - (item1.interval_count || 1) == (item2.interval_count || 1) - ); + return ( + item1.interval === item2.interval && + (item1.interval_count || 1) === (item2.interval_count || 1) + ); }; From 04d72184721a5cd43da3762fac2115fe25610326 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:55:45 +0100 Subject: [PATCH 07/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- biome.json | 5 +- vite/biome.json | 5 +- .../CusProductStatus.tsx | 100 +++++++++--------- 3 files changed, 57 insertions(+), 53 deletions(-) diff --git a/biome.json b/biome.json index 6c514ad12..3a9e72855 100644 --- a/biome.json +++ b/biome.json @@ -15,7 +15,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noStaticOnlyClass": "off" + } } }, "javascript": { diff --git a/vite/biome.json b/vite/biome.json index fcbfad054..53487eb49 100644 --- a/vite/biome.json +++ b/vite/biome.json @@ -17,7 +17,10 @@ "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "complexity": { + "noStaticOnlyClass": "off" + } } }, "javascript": { diff --git a/vite/src/views/customers/customer/customer-product-list/CusProductStatus.tsx b/vite/src/views/customers/customer/customer-product-list/CusProductStatus.tsx index e6aa8821e..a2d84e866 100644 --- a/vite/src/views/customers/customer/customer-product-list/CusProductStatus.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CusProductStatus.tsx @@ -1,68 +1,66 @@ +import { CusProductStatus, type FullCusProduct } from "@autumn/shared"; +import { differenceInDays } from "date-fns"; import { Badge } from "@/components/ui/badge"; import { cn } from "@/lib/utils"; -import { CusProductStatus, FullCusProduct } from "@autumn/shared"; -import { CusProductStripeLink } from "./CusProductStripeLink"; import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; -import { formatUnixToDateTime } from "@/utils/formatUtils/formatDateUtils"; -import { notNullish } from "@/utils/genUtils"; -import { differenceInDays, subDays } from "date-fns"; +import { CusProductStripeLink } from "./CusProductStripeLink"; export const CusProductStatusItem = ({ - cusProduct, + cusProduct, }: { - cusProduct: FullCusProduct; + cusProduct: FullCusProduct; }) => { - const getStatus = () => { - if (cusProduct.status == CusProductStatus.Expired) { - return CusProductStatus.Expired; - } + const getStatus = () => { + if (cusProduct.status == CusProductStatus.Expired) { + return CusProductStatus.Expired; + } - 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 canceled = cusProduct.canceled; + const canceled = cusProduct.canceled; - if (canceled) return "canceled"; + if (canceled) return "canceled"; - if (trialing) { - return CusProductStatus.Trialing; - } + if (trialing) { + return CusProductStatus.Trialing; + } - return cusProduct.status; - }; + return cusProduct.status; + }; - const getTitle = () => { - const status = getStatus(); - if (status == CusProductStatus.Trialing) { - const daysTillEnd = differenceInDays( - new Date(cusProduct.trial_ends_at!), - new Date() - ); - return `trial (${daysTillEnd}d)`; - } - return keyToTitle(getStatus()).toLowerCase(); - }; + const getTitle = () => { + const status = getStatus(); + if (status === CusProductStatus.Trialing) { + const daysTillEnd = differenceInDays( + new Date(cusProduct.trial_ends_at!), + new Date(), + ); + return `trial (${daysTillEnd}d)`; + } + return keyToTitle(getStatus()).toLowerCase(); + }; - const statusToColor: Record = { - [CusProductStatus.Active]: "bg-lime-500", - [CusProductStatus.Expired]: "bg-stone-800", - [CusProductStatus.PastDue]: "bg-red-500", - [CusProductStatus.Scheduled]: "bg-blue-500", - [CusProductStatus.Trialing]: "bg-blue-400", - canceled: "bg-gray-500", - [CusProductStatus.Unknown]: "bg-gray-500", - }; + const statusToColor: Record = { + [CusProductStatus.Active]: "bg-lime-500", + [CusProductStatus.Expired]: "bg-stone-800", + [CusProductStatus.PastDue]: "bg-red-500", + [CusProductStatus.Scheduled]: "bg-blue-500", + [CusProductStatus.Trialing]: "bg-blue-400", + canceled: "bg-gray-500", + [CusProductStatus.Unknown]: "bg-gray-500", + }; - return ( -
- - {getTitle()} - + return ( +
+ + {getTitle()} + - -
- ); + +
+ ); }; From 4eafb982925fde8291fe78bde38944355aa2215e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:55:54 +0100 Subject: [PATCH 08/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite/src/services/products/RewardService.tsx | 62 ++++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/vite/src/services/products/RewardService.tsx b/vite/src/services/products/RewardService.tsx index 4ffd34c5b..8fa29c49e 100644 --- a/vite/src/services/products/RewardService.tsx +++ b/vite/src/services/products/RewardService.tsx @@ -1,37 +1,37 @@ -import { Reward, CreateReward } from "@autumn/shared"; +import type { CreateReward, Reward } from "@autumn/shared"; -import { AxiosInstance } from "axios"; +import type { AxiosInstance } from "axios"; export class RewardService { - static async createReward({ - axiosInstance, - data, - }: { - axiosInstance: AxiosInstance; - data: CreateReward; - }) { - await axiosInstance.post("/v1/rewards", data); - } + static async createReward({ + axiosInstance, + data, + }: { + axiosInstance: AxiosInstance; + data: CreateReward; + }) { + await axiosInstance.post("/v1/rewards", data); + } - static async deleteReward({ - axiosInstance, - internalId, - }: { - axiosInstance: AxiosInstance; - internalId: string; - }) { - await axiosInstance.delete(`/v1/rewards/${internalId}`); - } + static async deleteReward({ + axiosInstance, + internalId, + }: { + axiosInstance: AxiosInstance; + internalId: string; + }) { + await axiosInstance.delete(`/v1/rewards/${internalId}`); + } - static async updateReward({ - axiosInstance, - internalId, - data, - }: { - axiosInstance: AxiosInstance; - internalId: string; - data: Reward; - }) { - await axiosInstance.post(`/v1/rewards/${internalId}`, data); - } + static async updateReward({ + axiosInstance, + internalId, + data, + }: { + axiosInstance: AxiosInstance; + internalId: string; + data: Reward; + }) { + await axiosInstance.post(`/v1/rewards/${internalId}`, data); + } } From fcc81a4fe26888b5c85bfb5e04f83486652726e9 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:56:04 +0100 Subject: [PATCH 09/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/biome.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/biome.json b/server/biome.json index aa8a3e2cd..76170ffa5 100644 --- a/server/biome.json +++ b/server/biome.json @@ -26,6 +26,9 @@ "noExportsInTest": "off", "noExplicitAny": "off", "noImplicitAnyLet": "off" + }, + "complexity": { + "noStaticOnlyClass": "off" } } }, From f96ad748b3ac10a067f53064c920d0e49527654d Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 9 Sep 2025 13:56:46 +0100 Subject: [PATCH 10/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20db=20cols=20for=20?= =?UTF-8?q?reward=20trials?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rewardModels/rewardModels/rewardModels.ts | 8 +++ .../rewardModels/rewardModels/rewardTable.ts | 57 ++++++++++--------- .../rewards/utils/defaultRewardModels.ts | 43 ++++++++------ 3 files changed, 65 insertions(+), 43 deletions(-) diff --git a/shared/models/rewardModels/rewardModels/rewardModels.ts b/shared/models/rewardModels/rewardModels/rewardModels.ts index 1781fc753..12adfb5d4 100644 --- a/shared/models/rewardModels/rewardModels/rewardModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardModels.ts @@ -14,6 +14,11 @@ export const DiscountConfigSchema = z.object({ price_ids: z.array(z.string()).optional(), }); +export const FreeProductConfigSchema = z.object({ + duration_type: z.nativeEnum(CouponDurationType), + duration_value: z.number(), +}); + const RewardSchema = z.object({ name: z.string().nullish(), @@ -23,6 +28,7 @@ const RewardSchema = z.object({ free_product_id: z.string().nullish(), discount_config: DiscountConfigSchema.nullish(), + free_product_config: FreeProductConfigSchema.nullish(), internal_id: z.string(), org_id: z.string(), @@ -36,6 +42,7 @@ export const CreateRewardSchema = z.object({ id: z.string(), type: z.nativeEnum(RewardType).nullish(), discount_config: DiscountConfigSchema.nullish(), + free_product_config: FreeProductConfigSchema.nullish(), free_product_id: z.string().nullish(), }); @@ -43,3 +50,4 @@ export type PromoCode = z.infer; export type CreateReward = z.infer; export type Reward = z.infer; export type DiscountConfig = z.infer; +export type FreeProductConfig = z.infer; diff --git a/shared/models/rewardModels/rewardModels/rewardTable.ts b/shared/models/rewardModels/rewardModels/rewardTable.ts index 097c0aa46..b8248ef6e 100644 --- a/shared/models/rewardModels/rewardModels/rewardTable.ts +++ b/shared/models/rewardModels/rewardModels/rewardTable.ts @@ -1,32 +1,37 @@ -import { foreignKey, jsonb, text } from "drizzle-orm/pg-core"; -import { numeric } from "drizzle-orm/pg-core"; -import { pgTable } from "drizzle-orm/pg-core"; +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 { DiscountConfig, PromoCode } from "./rewardModels.js"; -import { InferSelectModel, InferInsertModel } from "drizzle-orm"; +import type { + DiscountConfig, + FreeProductConfig, + PromoCode, +} from "./rewardModels.js"; export const rewards = pgTable( - "rewards", - { - internal_id: text("internal_id").primaryKey().notNull(), - id: text(), - org_id: text("org_id"), - env: text(), - created_at: numeric({ mode: "number" }), - name: text(), - discount_config: jsonb("discount_config").$type(), - free_product_id: text("free_product_id"), - promo_codes: jsonb("promo_codes").$type().array(), - type: text(), - }, - (table) => [ - foreignKey({ - columns: [table.org_id], - foreignColumns: [organizations.id], - name: "coupons_org_id_fkey", - }).onDelete("cascade"), - ], + "rewards", + { + internal_id: text("internal_id").primaryKey().notNull(), + id: text(), + org_id: text("org_id"), + env: text(), + created_at: numeric({ mode: "number" }), + name: text(), + discount_config: jsonb("discount_config").$type(), + free_product_config: jsonb( + "free_product_config", + ).$type(), + free_product_id: text("free_product_id"), + promo_codes: jsonb("promo_codes").$type().array(), + type: text(), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "coupons_org_id_fkey", + }).onDelete("cascade"), + ], ); export type RewardRow = InferSelectModel; -export type InsertRewardRow = InferInsertModel; \ No newline at end of file +export type InsertRewardRow = InferInsertModel; diff --git a/vite/src/views/products/rewards/utils/defaultRewardModels.ts b/vite/src/views/products/rewards/utils/defaultRewardModels.ts index 50b876616..882f66036 100644 --- a/vite/src/views/products/rewards/utils/defaultRewardModels.ts +++ b/vite/src/views/products/rewards/utils/defaultRewardModels.ts @@ -1,29 +1,38 @@ import { - CreateReward, - RewardType, CouponDurationType, - DiscountConfig, + type CreateReward, + type DiscountConfig, + type FreeProductConfig, + RewardType, } from "@autumn/shared"; export const defaultDiscountConfig: DiscountConfig = { - discount_value: 0, - duration_type: CouponDurationType.Months, - duration_value: 0, - should_rollover: true, - apply_to_all: true, - price_ids: [], + discount_value: 0, + duration_type: CouponDurationType.Months, + duration_value: 0, + should_rollover: true, + apply_to_all: true, + price_ids: [], +}; + +export const defaultFreeProductConfig: FreeProductConfig = { + duration_type: CouponDurationType.Months, + duration_value: 0, }; export const defaultReward: CreateReward = { - name: "", - id: "", - promo_codes: [{ code: "" }], + name: "", + id: "", + promo_codes: [{ code: "" }], - type: RewardType.PercentageDiscount, + type: RewardType.PercentageDiscount, - // For free product coupons - free_product_id: null, + // For free product coupons + free_product_id: null, - // For discount type coupons - discount_config: defaultDiscountConfig, + // For discount type coupons + discount_config: defaultDiscountConfig, + + // For free product type coupons + free_product_config: defaultFreeProductConfig, }; From c186f2334531c9451a0a98b49d72304340fb2f58 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:42:06 +0100 Subject: [PATCH 11/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/external/autumn/autumnCli.ts | 853 ++++++++-------- server/src/external/stripe/stripeCusUtils.ts | 520 +++++----- .../add-product/createFullCusProduct.ts | 946 +++++++++--------- .../products/free-trials/freeTrialUtils.ts | 306 +++--- .../handlers/handleListProductsBeta.ts | 5 - server/src/internal/products/productUtils.ts | 930 ++++++++--------- server/src/utils/errorUtils.ts | 2 +- 7 files changed, 1790 insertions(+), 1772 deletions(-) diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index 7a9393f9c..dd6b0a55c 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -1,497 +1,494 @@ import dotenv from "dotenv"; + dotenv.config(); -import { toSnakeCase } from "@/utils/genUtils.js"; + import { - APIVersion, - CreateEntity, - CreateRewardProgram, + type APIVersion, + type AttachBody, + type CreateEntity, + type CreateRewardProgram, CusExpand, EntityExpand, - ErrCode, - Invoice, - OrgConfig, + ErrCode, type OrgConfig } from "@autumn/shared"; import { - CancelParams, - CheckoutParams, - CheckoutResult, - CheckParams, - CheckResult, - Customer, - TrackParams, - TransferProductParams, - UsageParams, + type CancelParams, + type CheckoutParams, + type CheckoutResult, + type CheckParams, + type CheckResult, + type Customer, + type TrackParams, type UsageParams } from "autumn-js"; -import { AttachBody } from "@autumn/shared"; export default class AutumnError extends Error { - message: string; - code: string; + message: string; + code: string; - constructor({ message, code }: { message: string; code: string }) { - super(message); - this.message = message; - this.code = code; - } + constructor({ message, code }: { message: string; code: string }) { + super(message); + this.message = message; + this.code = code; + } - toString(): string { - return `${this.message} (code: ${this.code})`; - } + toString(): string { + return `${this.message} (code: ${this.code})`; + } } export class AutumnInt { - private apiKey: string; - public headers: Record; - public baseUrl: string; + private apiKey: string; + public headers: Record; + public baseUrl: string; - constructor({ - apiKey, - secretKey, - baseUrl, - version, - orgConfig, - }: { - apiKey?: string; - secretKey?: string; - baseUrl?: string; - version?: string | APIVersion; - orgConfig?: Partial; - } = {}) { - // this.apiKey = apiKey || process.env.AUTUMN_API_KEY || ""; - this.apiKey = - apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || ""; + constructor({ + apiKey, + secretKey, + baseUrl, + version, + orgConfig, + }: { + apiKey?: string; + secretKey?: string; + baseUrl?: string; + version?: string | APIVersion; + orgConfig?: Partial; + } = {}) { + // this.apiKey = apiKey || process.env.AUTUMN_API_KEY || ""; + this.apiKey = + apiKey || secretKey || process.env.UNIT_TEST_AUTUMN_SECRET_KEY || ""; - this.headers = { - Authorization: `Bearer ${this.apiKey}`, - "Content-Type": "application/json", - }; + this.headers = { + Authorization: `Bearer ${this.apiKey}`, + "Content-Type": "application/json", + }; - if (version) { - this.headers["x-api-version"] = version.toString(); - } + if (version) { + this.headers["x-api-version"] = version.toString(); + } - if (orgConfig) { - this.headers["org-config"] = JSON.stringify(orgConfig); - } + if (orgConfig) { + this.headers["org-config"] = JSON.stringify(orgConfig); + } - this.baseUrl = baseUrl || "http://localhost:8080/v1"; - } + this.baseUrl = baseUrl || "http://localhost:8080/v1"; + } - async get(path: string) { - const response = await fetch(`${this.baseUrl}${path}`, { - headers: this.headers, - }); - return response.json(); - } + async get(path: string) { + const response = await fetch(`${this.baseUrl}${path}`, { + headers: this.headers, + }); + return response.json(); + } - async post(path: string, body: any) { - const response = await fetch(`${this.baseUrl}${path}`, { - method: "POST", - headers: this.headers, - body: JSON.stringify(body), - }); + async post(path: string, body: any) { + const response = await fetch(`${this.baseUrl}${path}`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(body), + }); - if (response.status != 200) { - let error: any; - try { - error = await response.json(); - } catch (error) { - throw new AutumnError({ - message: "Failed to parse Autumn API error response", - code: ErrCode.InternalError, - }); - } + if (response.status != 200) { + let error: any; + try { + error = await response.json(); + } catch (error) { + throw new AutumnError({ + message: "Failed to parse Autumn API error response", + code: ErrCode.InternalError, + }); + } - throw new AutumnError({ - message: error.message, - code: error.code, - }); - } + throw new AutumnError({ + message: error.message, + code: error.code, + }); + } - return response.json(); - } + return response.json(); + } - async delete( - path: string, - { - deleteInStripe = false, - }: { - deleteInStripe?: boolean; - } = {} - ) { - const response = await fetch( - `${this.baseUrl}${path}?${deleteInStripe ? "delete_in_stripe=true" : ""}`, - { - method: "DELETE", - headers: this.headers, - } - ); + async delete( + path: string, + { + deleteInStripe = false, + }: { + deleteInStripe?: boolean; + } = {}, + ) { + const response = await fetch( + `${this.baseUrl}${path}?${deleteInStripe ? "delete_in_stripe=true" : ""}`, + { + method: "DELETE", + headers: this.headers, + }, + ); - if (response.status != 200) { - let error: any; - try { - error = await response.json(); - } catch (error) { - throw new AutumnError({ - message: "Failed to parse Autumn API error response", - code: ErrCode.InternalError, - }); - } + if (response.status != 200) { + let error: any; + try { + error = await response.json(); + } catch (error) { + throw new AutumnError({ + message: "Failed to parse Autumn API error response", + code: ErrCode.InternalError, + }); + } - throw new AutumnError({ - message: error.message, - code: error.code, - }); - } + throw new AutumnError({ + message: error.message, + code: error.code, + }); + } - return response.json(); - } + return response.json(); + } - async createCustomer({ - id, - email, - name, - fingerprint, - }: { - id: string; - email: string; - name: string; - fingerprint?: string; - }) { - const data = await this.post("/customers", { - id, - email, - name, - fingerprint, - }); + async createCustomer({ + id, + email, + name, + fingerprint, + }: { + id: string; + email: string; + name: string; + fingerprint?: string; + }) { + const data = await this.post("/customers", { + id, + email, + name, + fingerprint, + }); - return data; - } + return data; + } - async attach(params: AttachBody) { - // const data = await this.post(`/attach`, { - // customer_id: customerId, - // product_id: productId, - // options: toSnakeCase(options), - // }); - const data = await this.post(`/attach`, params); + async attach(params: AttachBody) { + // const data = await this.post(`/attach`, { + // customer_id: customerId, + // product_id: productId, + // options: toSnakeCase(options), + // }); + const data = await this.post(`/attach`, params); - return data; - } - async checkout( - params: CheckoutParams & { invoice?: boolean; force_checkout?: boolean } - ) { - // const data = await this.post(`/attach`, { - // customer_id: customerId, - // product_id: productId, - // options: toSnakeCase(options), - // }); - const data = await this.post(`/checkout`, params); + return data; + } + async checkout( + params: CheckoutParams & { invoice?: boolean; force_checkout?: boolean }, + ) { + // const data = await this.post(`/attach`, { + // customer_id: customerId, + // product_id: productId, + // options: toSnakeCase(options), + // }); + const data = await this.post(`/checkout`, params); - return data as CheckoutResult; - } - async transfer( - customerId: string, - params: { - from_entity_id?: string; - to_entity_id: string; - product_id: string; - } - ) { - const data = await this.post(`/customers/${customerId}/transfer`, params); + return data as CheckoutResult; + } + async transfer( + customerId: string, + params: { + from_entity_id?: string; + to_entity_id: string; + product_id: string; + }, + ) { + const data = await this.post(`/customers/${customerId}/transfer`, params); - return data as CheckoutResult; - } + return data as CheckoutResult; + } - async sendEvent({ - customerId, - eventName, - properties, - customer_data, - idempotency_key, - }: { - customerId: string; - eventName: string; - properties?: any; - customer_data?: any; - idempotency_key?: string; - }) { - const data = await this.post(`/events`, { - customer_id: customerId, - event_name: eventName, - properties, - customer_data, - idempotency_key, - }); + async sendEvent({ + customerId, + eventName, + properties, + customer_data, + idempotency_key, + }: { + customerId: string; + eventName: string; + properties?: any; + customer_data?: any; + idempotency_key?: string; + }) { + const data = await this.post(`/events`, { + customer_id: customerId, + event_name: eventName, + properties, + customer_data, + idempotency_key, + }); - return data; - } + return data; + } - async entitled({ - customerId, - featureId, - quantity, - customer_data, - }: { - customerId: string; - featureId: string; - quantity?: number; - customer_data?: any; - }) { - const data = await this.post(`/entitled`, { - customer_id: customerId, - feature_id: featureId, - quantity, - customer_data, - }); + async entitled({ + customerId, + featureId, + quantity, + customer_data, + }: { + customerId: string; + featureId: string; + quantity?: number; + customer_data?: any; + }) { + const data = await this.post(`/entitled`, { + customer_id: customerId, + feature_id: featureId, + quantity, + customer_data, + }); - return data; - } + return data; + } - customers = { - get: async ( - customerId: string, - params?: { - expand?: CusExpand[]; - } - ): Promise< - Customer & { - invoices: any[]; - } - > => { - const queryParams = new URLSearchParams(); - const defaultParams = { - expand: [CusExpand.Invoices], - }; + customers = { + get: async ( + customerId: string, + params?: { + expand?: CusExpand[]; + }, + ): Promise< + Customer & { + invoices: any[]; + } + > => { + const queryParams = new URLSearchParams(); + const defaultParams = { + expand: [CusExpand.Invoices], + }; - const finalParams = { ...defaultParams, ...params }; - if (finalParams.expand) { - queryParams.append("expand", finalParams.expand.join(",")); - } + const finalParams = { ...defaultParams, ...params }; + if (finalParams.expand) { + queryParams.append("expand", finalParams.expand.join(",")); + } - const data = await this.get( - `/customers/${customerId}?${queryParams.toString()}` - ); - return data; - }, + const data = await this.get( + `/customers/${customerId}?${queryParams.toString()}`, + ); + return data; + }, - create: async (customer: { id: string; email: string; name?: string }) => { - const data = await this.post(`/customers?with_autumn_id=true`, customer); - return data; - }, - delete: async ( - customerId: string, - { - deleteInStripe = false, - }: { - deleteInStripe?: boolean; - } = {} - ) => { - const data = await this.delete(`/customers/${customerId}`, { - deleteInStripe, - }); - return data; - }, - }; + create: async (customer: { id: string; email: string; name?: string }) => { + const data = await this.post(`/customers?with_autumn_id=true`, customer); + return data; + }, + delete: async ( + customerId: string, + { + deleteInStripe = false, + }: { + deleteInStripe?: boolean; + } = {}, + ) => { + const data = await this.delete(`/customers/${customerId}`, { + deleteInStripe, + }); + return data; + }, + }; - entities = { - get: async (customerId: string, entityId: string) => { - const data = await this.get( - `/customers/${customerId}/entities/${entityId}?expand=${EntityExpand.Invoices}` - ); - return data; - }, + entities = { + get: async (customerId: string, entityId: string) => { + const data = await this.get( + `/customers/${customerId}/entities/${entityId}?expand=${EntityExpand.Invoices}`, + ); + return data; + }, - create: async ( - customerId: string, - entity: CreateEntity | CreateEntity[] - ) => { - // let entities = Array.isArray(entity) ? entity : [entity]; - const data = await this.post( - `/customers/${customerId}/entities?with_autumn_id=true`, - entity - ); + create: async ( + customerId: string, + entity: CreateEntity | CreateEntity[], + ) => { + // let entities = Array.isArray(entity) ? entity : [entity]; + const data = await this.post( + `/customers/${customerId}/entities?with_autumn_id=true`, + entity, + ); - return data; - }, + return data; + }, - list: async (customerId: string) => { - const data = await this.get(`/customers/${customerId}/entities`); - return data; - }, + list: async (customerId: string) => { + const data = await this.get(`/customers/${customerId}/entities`); + return data; + }, - delete: async (customerId: string, entityId: string) => { - const data = await this.delete( - `/customers/${customerId}/entities/${entityId}` - ); - return data; - }, - }; + delete: async (customerId: string, entityId: string) => { + const data = await this.delete( + `/customers/${customerId}/entities/${entityId}`, + ); + return data; + }, + }; - products = { - update: async (productId: string, product: any) => { - // if (product.items && typeof product.items === "object") { - // product.items = Object.values(product.items); - // } - const data = await this.post(`/products/${productId}`, product); - return data; - }, + products = { + update: async (productId: string, product: any) => { + // if (product.items && typeof product.items === "object") { + // product.items = Object.values(product.items); + // } + const data = await this.post(`/products/${productId}`, product); + return data; + }, - get: async ( - productId: string, - { v1Schema = false }: { v1Schema?: boolean } = {} - ) => { - const data = await this.get( - `/products/${productId}?${v1Schema ? "schemaVersion=1" : ""}` - ); - return data; - }, + get: async ( + productId: string, + { v1Schema = false }: { v1Schema?: boolean } = {}, + ) => { + const data = await this.get( + `/products/${productId}?${v1Schema ? "schemaVersion=1" : ""}`, + ); + return data; + }, - create: async (product: any) => { - const data = await this.post(`/products`, product); - return data; - }, + create: async (product: any) => { + const data = await this.post(`/products`, product); + return data; + }, - delete: async (productId: string) => { - const data = await this.delete(`/products/${productId}`); - return data; - }, - }; + delete: async (productId: string) => { + const data = await this.delete(`/products/${productId}`); + return data; + }, + }; - rewards = { - get: async (rewardId: string) => { - const data = await this.get(`/rewards/${rewardId}`); - return data; - }, + rewards = { + get: async (rewardId: string) => { + const data = await this.get(`/rewards/${rewardId}`); + return data; + }, - create: async (reward: any) => { - const data = await this.post(`/rewards?legacyStripe=true`, reward); - return data; - }, + create: async (reward: any) => { + const data = await this.post(`/rewards?legacyStripe=true`, reward); + return data; + }, - delete: async (rewardId: string) => { - const data = await this.delete(`/rewards/${rewardId}`); - return data; - }, - }; + delete: async (rewardId: string) => { + const data = await this.delete(`/rewards/${rewardId}`); + return data; + }, + }; - rewardPrograms = { - create: async (rewardProgram: CreateRewardProgram) => { - const data = await this.post(`/reward_programs`, rewardProgram); - return data; - }, - }; + rewardPrograms = { + create: async (rewardProgram: CreateRewardProgram) => { + const data = await this.post(`/reward_programs`, rewardProgram); + return data; + }, + }; - referrals = { - createCode: async ({ - customerId, - referralId, - }: { - customerId: string; - referralId: string; - }) => { - const data = await this.post(`/referrals/code`, { - customer_id: customerId, - program_id: referralId, - }); - return data; - }, - redeem: async ({ - customerId, - code, - }: { - customerId: string; - code: string; - }) => { - const data = await this.post(`/referrals/redeem`, { - customer_id: customerId, - code, - }); - return data; - }, - }; + referrals = { + createCode: async ({ + customerId, + referralId, + }: { + customerId: string; + referralId: string; + }) => { + const data = await this.post(`/referrals/code`, { + customer_id: customerId, + program_id: referralId, + }); + return data; + }, + redeem: async ({ + customerId, + code, + }: { + customerId: string; + code: string; + }) => { + const data = await this.post(`/referrals/redeem`, { + customer_id: customerId, + code, + }); + return data; + }, + }; - redemptions = { - get: async (redemptionId: string) => { - const data = await this.get(`/redemptions/${redemptionId}`); - return data; - }, - }; + redemptions = { + get: async (redemptionId: string) => { + const data = await this.get(`/redemptions/${redemptionId}`); + return data; + }, + }; - events = { - send: async ({ - customerId, - featureId, - value, - properties, - }: { - customerId: string; - featureId: string; - value: number; - properties?: any; - }) => { - const data = await this.post(`/events`, { - customer_id: customerId, - feature_id: featureId, - value, - properties, - }); - return data; - }, - }; + events = { + send: async ({ + customerId, + featureId, + value, + properties, + }: { + customerId: string; + featureId: string; + value: number; + properties?: any; + }) => { + const data = await this.post(`/events`, { + customer_id: customerId, + feature_id: featureId, + value, + properties, + }); + return data; + }, + }; - stripe = { - connect: async (params: { - secret_key: string; - success_url: string; - default_currency: string; - }) => { - const data = await this.post(`/organization/stripe`, params); - return data; - }, + stripe = { + connect: async (params: { + secret_key: string; + success_url: string; + default_currency: string; + }) => { + const data = await this.post(`/organization/stripe`, params); + return data; + }, - delete: async () => { - const data = await this.delete(`/organization/stripe`); - return data; - }, - }; + delete: async () => { + const data = await this.delete(`/organization/stripe`); + return data; + }, + }; - track = async (params: TrackParams & { timestamp?: number }) => { - const data = await this.post(`/track`, params); - return data; - }; + track = async (params: TrackParams & { timestamp?: number }) => { + const data = await this.post(`/track`, params); + return data; + }; - usage = async (params: UsageParams) => { - const data = await this.post(`/usage`, params); - return data; - }; + usage = async (params: UsageParams) => { + const data = await this.post(`/usage`, params); + return data; + }; - check = async (params: CheckParams): Promise => { - const data = await this.post(`/check`, params); - return data; - }; + check = async (params: CheckParams): Promise => { + const data = await this.post(`/check`, params); + return data; + }; - attachPreview = async (params: AttachBody) => { - const data = await this.post(`/attach/preview`, params); - return data; - }; + attachPreview = async (params: AttachBody) => { + const data = await this.post(`/attach/preview`, params); + return data; + }; - cancel = async (params: CancelParams) => { - const data = await this.post(`/cancel`, params); - return data; - }; + cancel = async (params: CancelParams) => { + const data = await this.post(`/cancel`, params); + return data; + }; - migrate = async (params: { - from_product_id: string; - to_product_id: string; - from_version: number; - to_version: number; - }) => { - const data = await this.post(`/migrations`, params); - return data; - }; + migrate = async (params: { + from_product_id: string; + to_product_id: string; + from_version: number; + to_version: number; + }) => { + const data = await this.post(`/migrations`, params); + return data; + }; - initStripe = async () => { - await this.post(`/products/all/init_stripe`, {}); - }; + initStripe = async () => { + await this.post(`/products/all/init_stripe`, {}); + }; } diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts index 73ce9d58b..36996b4b7 100644 --- a/server/src/external/stripe/stripeCusUtils.ts +++ b/server/src/external/stripe/stripeCusUtils.ts @@ -1,332 +1,338 @@ -import { AppEnv, Customer, Organization, ProcessorType } from "@autumn/shared"; -import { Stripe } from "stripe"; -import { createStripeCli } from "./utils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@autumn/shared"; +import { + type AppEnv, + type Customer, + ErrCode, + type Organization, + ProcessorType, +} from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +import type { Stripe } from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { createStripeCli } from "./utils.js"; export const getStripeCus = async ({ - stripeCli, - stripeId, + stripeCli, + stripeId, }: { - stripeCli: Stripe; - stripeId: string; + stripeCli: Stripe; + stripeId: string; }) => { - try { - const stripeCus = await stripeCli.customers.retrieve(stripeId); - return stripeCus as Stripe.Customer; - } catch (error) { - return undefined; - } + try { + const stripeCus = await stripeCli.customers.retrieve(stripeId); + return stripeCus as Stripe.Customer; + } catch (error) { + return undefined; + } }; export const createStripeCusIfNotExists = async ({ - db, - org, - env, - customer, - logger, + db, + org, + env, + customer, + logger, }: { - db: DrizzleCli; - org: Organization; - env: AppEnv; - customer: Customer; - logger: any; + db: DrizzleCli; + org: Organization; + env: AppEnv; + customer: Customer; + logger: any; }) => { - let createNew = false; - const stripeCli = createStripeCli({ org, env }); - if (!customer.processor || !customer.processor.id) { - createNew = true; - } else { - try { - let stripeCus = await stripeCli.customers.retrieve( - customer.processor.id, - { - expand: ["test_clock", "invoice_settings.default_payment_method"], - } - ); - if (!stripeCus.deleted) { - return stripeCus as Stripe.Customer; - } else { - createNew = true; - } - } catch (error) { - createNew = true; - } - } + let createNew = false; + const stripeCli = createStripeCli({ org, env }); + if (!customer.processor || !customer.processor.id) { + createNew = true; + } else { + try { + const stripeCus = await stripeCli.customers.retrieve( + customer.processor.id, + { + expand: ["test_clock", "invoice_settings.default_payment_method"], + }, + ); + if (!stripeCus.deleted) { + return stripeCus as Stripe.Customer; + } else { + createNew = true; + } + } catch (error) { + createNew = true; + } + } - if (createNew) { - logger.info(`Creating new stripe customer for ${customer.id}`); - const stripeCustomer = await createStripeCustomer({ - org, - env, - customer, - }); + if (createNew) { + logger.info(`Creating new stripe customer for ${customer.id}`); + const stripeCustomer = await createStripeCustomer({ + org, + env, + customer, + }); - await CusService.update({ - db, - internalCusId: customer.internal_id, - update: { - processor: { - id: stripeCustomer.id, - type: ProcessorType.Stripe, - }, - }, - }); + await CusService.update({ + db, + internalCusId: customer.internal_id, + update: { + processor: { + id: stripeCustomer.id, + type: ProcessorType.Stripe, + }, + }, + }); - customer.processor = { - id: stripeCustomer.id, - type: ProcessorType.Stripe, - }; + customer.processor = { + id: stripeCustomer.id, + type: ProcessorType.Stripe, + }; - return stripeCustomer; - } + return stripeCustomer; + } }; export const createStripeCustomer = async ({ - org, - env, - customer, - testClockId, + org, + env, + customer, + testClockId, }: { - org: Organization; - env: AppEnv; - customer: Customer; - testClockId?: string; + org: Organization; + env: AppEnv; + customer: Customer; + testClockId?: string; }) => { - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env }); - try { - const stripeCustomer = await stripeCli.customers.create({ - name: customer.name || undefined, - email: customer.email || undefined, - metadata: { - autumn_id: customer.id || null, - autumn_internal_id: customer.internal_id, - }, - test_clock: testClockId, - }); + try { + const stripeCustomer = await stripeCli.customers.create({ + name: customer.name || undefined, + email: customer.email || undefined, + metadata: { + autumn_id: customer.id || null, + autumn_internal_id: customer.internal_id, + }, + test_clock: testClockId, + }); - return stripeCustomer; - } catch (error: any) { - throw new RecaseError({ - message: `Error creating customer in Stripe. ${error.message}`, - code: ErrCode.StripeCreateCustomerFailed, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - }); - } + return stripeCustomer; + } catch (error: any) { + throw new RecaseError({ + message: `Error creating customer in Stripe. ${error.message}`, + code: ErrCode.StripeCreateCustomerFailed, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } }; export const deleteStripeCustomer = async ({ - org, - env, - stripeId, + org, + env, + stripeId, }: { - org: Organization; - env: AppEnv; - stripeId: string; + org: Organization; + env: AppEnv; + stripeId: string; }) => { - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env }); - const stripeCustomer = await stripeCli.customers.del(stripeId); + const stripeCustomer = await stripeCli.customers.del(stripeId); - return stripeCustomer; + return stripeCustomer; }; export const listCusPaymentMethods = async ({ - stripeCli, - stripeId, + stripeCli, + stripeId, }: { - stripeCli: Stripe; - stripeId: string; + stripeCli: Stripe; + stripeId: string; }) => { - let res = await stripeCli.paymentMethods.list({ - customer: stripeId, - }); + const res = await stripeCli.paymentMethods.list({ + customer: stripeId, + }); - const paymentMethods = res.data; - paymentMethods.sort((a, b) => b.created - a.created); + const paymentMethods = res.data; + paymentMethods.sort((a, b) => b.created - a.created); - return paymentMethods; + return paymentMethods; }; export const getCusPaymentMethod = async ({ - stripeCli, - stripeId, - errorIfNone = false, + stripeCli, + stripeId, + errorIfNone = false, }: { - stripeCli: Stripe; - stripeId?: string; - errorIfNone?: boolean; + stripeCli: Stripe; + stripeId?: string; + errorIfNone?: boolean; }) => { - if (!stripeId) { - return null; - } + if (!stripeId) { + return null; + } - const stripeCustomer = (await stripeCli.customers.retrieve( - stripeId - )) as Stripe.Customer; + const stripeCustomer = (await stripeCli.customers.retrieve( + stripeId, + )) as Stripe.Customer; - let paymentMethodId = stripeCustomer.invoice_settings?.default_payment_method; + const paymentMethodId = + stripeCustomer.invoice_settings?.default_payment_method; - if (!paymentMethodId) { - let res = await stripeCli.paymentMethods.list({ - customer: stripeId, - }); + if (!paymentMethodId) { + const res = await stripeCli.paymentMethods.list({ + customer: stripeId, + }); - const paymentMethods = res.data; - paymentMethods.sort((a, b) => b.created - a.created); + const paymentMethods = res.data; + paymentMethods.sort((a, b) => b.created - a.created); - if (res.data.length === 0) { - if (errorIfNone) { - throw new RecaseError({ - code: ErrCode.StripeGetPaymentMethodFailed, - message: `No payment method found for customer ${stripeId}`, - statusCode: 500, - }); - } - return null; - } + if (res.data.length === 0) { + if (errorIfNone) { + throw new RecaseError({ + code: ErrCode.StripeGetPaymentMethodFailed, + message: `No payment method found for customer ${stripeId}`, + statusCode: 500, + }); + } + return null; + } - return paymentMethods[0]; - } else { - const paymentMethod = await stripeCli.paymentMethods.retrieve( - paymentMethodId as string - ); - return paymentMethod; - } + return paymentMethods[0]; + } else { + const paymentMethod = await stripeCli.paymentMethods.retrieve( + paymentMethodId as string, + ); + return paymentMethod; + } }; // 2. Create a payment method and attach to customer export const attachPmToCus = async ({ - db, - customer, - org, - env, - willFail = false, - testClockId, + db, + customer, + org, + env, + willFail = false, + testClockId, }: { - db: DrizzleCli; - customer: Customer; - org: Organization; - env: AppEnv; - willFail?: boolean; - testClockId?: string; + db: DrizzleCli; + customer: Customer; + org: Organization; + env: AppEnv; + willFail?: boolean; + testClockId?: string; }) => { - // 1. Create stripe customer if not exists + // 1. Create stripe customer if not exists - let stripeCusId = customer.processor?.id; - if (!stripeCusId) { - const stripeCustomer = await createStripeCustomer({ - org, - env, - customer, - testClockId, - }); + let stripeCusId = customer.processor?.id; + if (!stripeCusId) { + const stripeCustomer = await createStripeCustomer({ + org, + env, + customer, + testClockId, + }); - await CusService.update({ - db, - internalCusId: customer.internal_id, - update: { - processor: { - id: stripeCustomer.id, - type: ProcessorType.Stripe, - }, - }, - }); + await CusService.update({ + db, + internalCusId: customer.internal_id, + update: { + processor: { + id: stripeCustomer.id, + type: ProcessorType.Stripe, + }, + }, + }); - stripeCusId = stripeCustomer.id; - customer.processor = { - id: stripeCustomer.id, - type: "stripe", - }; - } + stripeCusId = stripeCustomer.id; + customer.processor = { + id: stripeCustomer.id, + type: "stripe", + }; + } - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env }); - try { - let token = willFail ? "tok_chargeCustomerFail" : "tok_visa"; - const pm = await stripeCli.paymentMethods.create({ - type: "card", - card: { - token, - }, - }); - await stripeCli.paymentMethods.attach(pm.id, { - customer: stripeCusId, - }); + try { + const token = willFail ? "tok_chargeCustomerFail" : "tok_visa"; + const pm = await stripeCli.paymentMethods.create({ + type: "card", + card: { + token, + }, + }); + await stripeCli.paymentMethods.attach(pm.id, { + customer: stripeCusId, + }); - await stripeCli.customers.update(stripeCusId, { - invoice_settings: { - default_payment_method: pm.id, - }, - }); - // console.log(" - Payment method attached"); - } catch (error) { - console.log(" - Error attaching payment method", error); - } + await stripeCli.customers.update(stripeCusId, { + invoice_settings: { + default_payment_method: pm.id, + }, + }); + // console.log(" - Payment method attached"); + } catch (error) { + console.log(" - Error attaching payment method", error); + } }; export const attachFailedPaymentMethod = async ({ - stripeCli, - customer, + stripeCli, + customer, }: { - stripeCli: Stripe; - customer: Customer; + stripeCli: Stripe; + customer: Customer; }) => { - // Delete existing payment method - const paymentMethods = await stripeCli.paymentMethods.list({ - customer: customer.processor?.id, - }); - for (const pm of paymentMethods.data) { - await stripeCli.paymentMethods.detach(pm.id); - } + // Delete existing payment method + const paymentMethods = await stripeCli.paymentMethods.list({ + customer: customer.processor?.id, + }); + for (const pm of paymentMethods.data) { + await stripeCli.paymentMethods.detach(pm.id); + } - const pm = await stripeCli.paymentMethods.create({ - type: "card", - card: { - token: "tok_chargeCustomerFail", - }, - }); - await stripeCli.paymentMethods.attach(pm.id, { - customer: customer.processor?.id, - }); + const pm = await stripeCli.paymentMethods.create({ + type: "card", + card: { + token: "tok_chargeCustomerFail", + }, + }); + await stripeCli.paymentMethods.attach(pm.id, { + customer: customer.processor?.id, + }); }; export const deleteAllStripeCustomers = async ({ - org, - env, + org, + env, }: { - org: Organization; - env: AppEnv; + org: Organization; + env: AppEnv; }) => { - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env }); - const stripeCustomers = await stripeCli.customers.list({ - limit: 100, - }); + const stripeCustomers = await stripeCli.customers.list({ + limit: 100, + }); - if (stripeCustomers.data.length === 0) { - return; - } + if (stripeCustomers.data.length === 0) { + return; + } - let firstCustomer = stripeCustomers.data[0]; - if (firstCustomer.livemode) { - throw new RecaseError({ - message: "Cannot delete livemode customers", - code: ErrCode.StripeDeleteCustomerFailed, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - }); - } + const firstCustomer = stripeCustomers.data[0]; + if (firstCustomer.livemode) { + throw new RecaseError({ + message: "Cannot delete livemode customers", + code: ErrCode.StripeDeleteCustomerFailed, + statusCode: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } - let batchSize = 10; - for (let i = 0; i < stripeCustomers.data.length; i += batchSize) { - let batch = stripeCustomers.data.slice(i, i + batchSize); - await Promise.all(batch.map((c) => stripeCli.customers.del(c.id))); - console.log( - `Deleted ${i + batch.length}/${stripeCustomers.data.length} customers` - ); - } + const batchSize = 10; + for (let i = 0; i < stripeCustomers.data.length; i += batchSize) { + const batch = stripeCustomers.data.slice(i, i + batchSize); + await Promise.all(batch.map((c) => stripeCli.customers.del(c.id))); + console.log( + `Deleted ${i + batch.length}/${stripeCustomers.data.length} customers`, + ); + } }; diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts index a7b4bca95..ae0f4d8c0 100644 --- a/server/src/internal/customers/add-product/createFullCusProduct.ts +++ b/server/src/internal/customers/add-product/createFullCusProduct.ts @@ -1,558 +1,556 @@ import { - CusProductStatus, - Price, - ProcessorType, - CustomerEntitlement, - CusProduct, - FeatureOptions, - FreeTrial, + type APIVersion, CollectionMethod, - FullCusProduct, - APIVersion, - InsertReplaceable, - ProductOptions, + 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 { generateId, notNullish, nullish } from "@/utils/genUtils.js"; - -import { Customer } from "@autumn/shared"; -import { FullProduct } from "@autumn/shared"; -import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; -import { CustomerPrice } from "@autumn/shared"; -import { CusProductService } from "../cusProducts/CusProductService.js"; -import { InsertCusProductParams } from "../cusProducts/AttachParams.js"; -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js"; - -import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; -import { searchCusProducts } from "@/internal/customers/cusProducts/cusProductUtils.js"; -import { updateOneTimeCusProduct } from "./createOneTimeCusProduct.js"; -import { initCusEntitlement } from "./initCusEnt.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { searchCusProducts } from "@/internal/customers/cusProducts/cusProductUtils.js"; +import { getEntRelatedPrice } from "@/internal/products/entitlements/entitlementUtils.js"; +import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; +import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; +import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; +import type { InsertCusProductParams } from "../cusProducts/AttachParams.js"; +import { CusProductService } from "../cusProducts/CusProductService.js"; import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js"; -import { CusPriceService } from "../cusProducts/cusPrices/CusPriceService.js"; import { addExistingUsagesToCusEnts } from "../cusProducts/cusEnts/cusEntUtils/getExistingUsage.js"; -import { RepService } from "../cusProducts/cusEnts/RepService.js"; import { getNewProductRollovers } from "../cusProducts/cusEnts/cusRollovers/getNewProductRollovers.js"; import { RolloverService } from "../cusProducts/cusEnts/cusRollovers/RolloverService.js"; +import { RepService } from "../cusProducts/cusEnts/RepService.js"; +import { CusPriceService } from "../cusProducts/cusPrices/CusPriceService.js"; +import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js"; +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; }) => { - let 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()) { - let 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 { - let { 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... - let 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; + } - let 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); - let 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 - let deductedCusEnts = addExistingUsagesToCusEnts({ - cusEnts: cusEnts, - entitlements: entitlements, - curCusProduct: curCusProduct as FullCusProduct, - carryExistingUsages, - isDowngrade, - entities: attachParams.entities, - features: attachParams.features, - }); + // 3. Deduct existing usages + const deductedCusEnts = addExistingUsagesToCusEnts({ + cusEnts: cusEnts, + entitlements: entitlements, + curCusProduct: curCusProduct as FullCusProduct, + carryExistingUsages, + isDowngrade, + entities: attachParams.entities, + features: attachParams.features, + }); - // 4. Get new rollovers - let 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, + }); - let 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, + }), + ); + } - let 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, + // }), + })), + ); - let 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/products/free-trials/freeTrialUtils.ts b/server/src/internal/products/free-trials/freeTrialUtils.ts index eb4bf83dc..557f4791a 100644 --- a/server/src/internal/products/free-trials/freeTrialUtils.ts +++ b/server/src/internal/products/free-trials/freeTrialUtils.ts @@ -1,198 +1,214 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { generateId, notNullish } from "@/utils/genUtils.js"; import { - CreateFreeTrial, + type CreateFreeTrial, CreateFreeTrialSchema, ErrCode, - FreeTrial, + type FreeProductConfig, + type FreeTrial, FreeTrialDuration, - Price, + type Price } from "@autumn/shared"; import { addDays, addMinutes, addMonths, addYears } from "date-fns"; -import { FreeTrialService } from "./FreeTrialService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { isFreeProduct, isOneOff } from "../productUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { generateId } from "@/utils/genUtils.js"; +import { isOneOff } from "../productUtils.js"; +import { FreeTrialService } from "./FreeTrialService.js"; export const validateOneOffTrial = async ({ - prices, - freeTrial, + prices, + freeTrial, }: { - prices: Price[]; - freeTrial: FreeTrial | null; + prices: Price[]; + freeTrial: FreeTrial | null; }) => { - if(isOneOff(prices) && freeTrial) { - throw new RecaseError({ - message: "One-off products cannot have a free trial", - code: ErrCode.InvalidRequest, - statusCode: 400, - }); - } -} + if (isOneOff(prices) && freeTrial) { + throw new RecaseError({ + message: "One-off products cannot have a free trial", + code: ErrCode.InvalidRequest, + statusCode: 400, + }); + } +}; export const validateAndInitFreeTrial = ({ - freeTrial, - internalProductId, - isCustom = false, + freeTrial, + internalProductId, + isCustom = false, }: { - freeTrial: CreateFreeTrial; - internalProductId: string; - isCustom?: boolean; + freeTrial: CreateFreeTrial; + internalProductId: string; + isCustom?: boolean; }): FreeTrial => { - const freeTrialSchema = CreateFreeTrialSchema.parse(freeTrial); + const freeTrialSchema = CreateFreeTrialSchema.parse(freeTrial); - return { - ...freeTrialSchema, - id: generateId("ft"), - created_at: Date.now(), - duration: freeTrial.duration || FreeTrialDuration.Day, - internal_product_id: internalProductId, - is_custom: isCustom, - card_required: freeTrial.card_required ?? true, - }; + return { + ...freeTrialSchema, + id: generateId("ft"), + created_at: Date.now(), + duration: freeTrial.duration || FreeTrialDuration.Day, + internal_product_id: internalProductId, + is_custom: isCustom, + card_required: freeTrial.card_required ?? true, + }; }; export const freeTrialsAreSame = ({ - ft1, - ft2, + ft1, + ft2, }: { - ft1?: FreeTrial | CreateFreeTrial | null; - ft2?: FreeTrial | CreateFreeTrial | null; + ft1?: FreeTrial | CreateFreeTrial | null; + ft2?: FreeTrial | CreateFreeTrial | null; }) => { - if (!ft1 && !ft2) return true; - if (!ft1 || !ft2) return false; - return ( - ft1.length === ft2.length && - ft1.unique_fingerprint === ft2.unique_fingerprint && - ft1.duration === ft2.duration && - ft1.card_required === ft2.card_required - ); + if (!ft1 && !ft2) return true; + if (!ft1 || !ft2) return false; + return ( + ft1.length === ft2.length && + ft1.unique_fingerprint === ft2.unique_fingerprint && + ft1.duration === ft2.duration && + ft1.card_required === ft2.card_required + ); }; export const freeTrialToStripeTimestamp = ({ - freeTrial, - now, + freeTrial, + now, }: { - freeTrial: FreeTrial | null | undefined; - now?: number | undefined; + freeTrial: FreeTrial | null | undefined; + now?: number | undefined; }) => { - now = now || Date.now(); + now = now || Date.now(); - if (!freeTrial) return undefined; + if (!freeTrial) return undefined; - let duration = freeTrial.duration || FreeTrialDuration.Day; - let length = freeTrial.length; + const duration = freeTrial.duration || FreeTrialDuration.Day; + const length = freeTrial.length; - let trialEnd: Date; - if (duration === FreeTrialDuration.Day) { - trialEnd = addDays(new Date(now), length); - } else if (duration === FreeTrialDuration.Month) { - trialEnd = addMonths(new Date(now), length); - } else if (duration === FreeTrialDuration.Year) { - trialEnd = addYears(new Date(now), length); - } else { - throw new RecaseError({ - message: `Invalid free trial duration: ${duration}`, - code: "invalid_free_trial_duration", - statusCode: 400, - }); - } + let trialEnd: Date; + if (duration === FreeTrialDuration.Day) { + trialEnd = addDays(new Date(now), length); + } else if (duration === FreeTrialDuration.Month) { + trialEnd = addMonths(new Date(now), length); + } else if (duration === FreeTrialDuration.Year) { + trialEnd = addYears(new Date(now), length); + } else { + throw new RecaseError({ + message: `Invalid free trial duration: ${duration}`, + code: "invalid_free_trial_duration", + statusCode: 400, + }); + } - // trialEnd = addMinutes(trialEnd, 5); - trialEnd = addMinutes(trialEnd, 10); + // trialEnd = addMinutes(trialEnd, 5); + trialEnd = addMinutes(trialEnd, 10); - return Math.ceil(trialEnd.getTime() / 1000); + return Math.ceil(trialEnd.getTime() / 1000); +}; + +export const rewardTrialToStripeTimestamp = ({ + rewardTrial, + now, +}: { + rewardTrial: FreeProductConfig | null | undefined; + now?: number | undefined; +}) => { + now = now || Date.now(); + if (!rewardTrial) return undefined; + const length = rewardTrial.duration_value; + const trialEnd: Date = addMonths(new Date(now), length); + + return Math.ceil(trialEnd.getTime() / 1000); }; export const getFreeTrialAfterFingerprint = async ({ - db, - freeTrial, - productId, - fingerprint, - internalCustomerId, - multipleAllowed, + db, + freeTrial, + productId, + fingerprint, + internalCustomerId, + multipleAllowed, }: { - db: DrizzleCli; - freeTrial: FreeTrial | null | undefined; - productId: string; - fingerprint: string | null | undefined; - internalCustomerId: string; - multipleAllowed: boolean; + db: DrizzleCli; + freeTrial: FreeTrial | null | undefined; + productId: string; + fingerprint: string | null | undefined; + internalCustomerId: string; + multipleAllowed: boolean; }): Promise => { - if (!freeTrial) return null; + if (!freeTrial) return null; - if (multipleAllowed) { - return freeTrial; - } + if (multipleAllowed) { + return freeTrial; + } - let uniqueFreeTrial: FreeTrial | null = freeTrial; + let uniqueFreeTrial: FreeTrial | null = freeTrial; - const data = await CusProductService.getByFingerprint({ - db, - productId, - internalCustomerId, - fingerprint: uniqueFreeTrial.unique_fingerprint ? fingerprint! : undefined, - }); + const data = await CusProductService.getByFingerprint({ + db, + productId, + internalCustomerId, + fingerprint: uniqueFreeTrial.unique_fingerprint ? fingerprint! : undefined, + }); - const exists = data && data.length > 0; + const exists = data && data.length > 0; - if (exists) { - console.log("Free trial fingerprint exists"); - uniqueFreeTrial = null; - } + if (exists) { + console.log("Free trial fingerprint exists"); + uniqueFreeTrial = null; + } - return uniqueFreeTrial; + return uniqueFreeTrial; }; export const handleNewFreeTrial = async ({ - db, - newFreeTrial, - curFreeTrial, - internalProductId, - isCustom = false, - product, + db, + newFreeTrial, + curFreeTrial, + internalProductId, + isCustom = false, + product, }: { - db: DrizzleCli; - newFreeTrial: CreateFreeTrial | null; - curFreeTrial: FreeTrial | null | undefined; - internalProductId: string; - isCustom: boolean; - product?: any; // Add product parameter for validation + db: DrizzleCli; + newFreeTrial: CreateFreeTrial | null; + curFreeTrial: FreeTrial | null | undefined; + internalProductId: string; + isCustom: boolean; + product?: any; // Add product parameter for validation }) => { - // If new free trial is null - if (!newFreeTrial) { - if (!isCustom && curFreeTrial) { - await FreeTrialService.delete({ - db, - id: curFreeTrial.id, - }); - } - return null; - } + // If new free trial is null + if (!newFreeTrial) { + if (!isCustom && curFreeTrial) { + await FreeTrialService.delete({ + db, + id: curFreeTrial.id, + }); + } + return null; + } - if (freeTrialsAreSame({ ft1: curFreeTrial, ft2: newFreeTrial })) { - return curFreeTrial; - } + if (freeTrialsAreSame({ ft1: curFreeTrial, ft2: newFreeTrial })) { + return curFreeTrial; + } - const createdFreeTrial = validateAndInitFreeTrial({ - freeTrial: newFreeTrial, - internalProductId, - isCustom, - }); + const createdFreeTrial = validateAndInitFreeTrial({ + freeTrial: newFreeTrial, + internalProductId, + isCustom, + }); - if (isCustom && newFreeTrial) { - await FreeTrialService.insert({ - db, - data: createdFreeTrial, - }); - } else if (!isCustom) { - createdFreeTrial.id = curFreeTrial?.id || createdFreeTrial.id; + if (isCustom && newFreeTrial) { + await FreeTrialService.insert({ + db, + data: createdFreeTrial, + }); + } else if (!isCustom) { + createdFreeTrial.id = curFreeTrial?.id || createdFreeTrial.id; - await FreeTrialService.upsert({ - db, - data: createdFreeTrial, - }); - } + await FreeTrialService.upsert({ + db, + data: createdFreeTrial, + }); + } - return createdFreeTrial; + return createdFreeTrial; }; diff --git a/server/src/internal/products/handlers/handleListProductsBeta.ts b/server/src/internal/products/handlers/handleListProductsBeta.ts index 84cf742c3..1cd0eeed9 100644 --- a/server/src/internal/products/handlers/handleListProductsBeta.ts +++ b/server/src/internal/products/handlers/handleListProductsBeta.ts @@ -44,11 +44,6 @@ export const handleListProductsBeta = async (req: any, res: any) => })(), ]); - console.log( - "Products:", - products.map((p) => `${p.internal_id} - ${p.id} - ${p.archived}`) - ); - if (req.query.v1_schema === "true") { res.status(200).json({ list: products, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index 92a896b0f..4e69ebf21 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -1,577 +1,583 @@ import { - AppEnv, - BillingInterval, - BillingType, - CreateProduct, - Entitlement, - EntitlementSchema, - ErrCode, - Feature, - FixedPriceConfig, - intervalsSame, - Organization, - Price, - PriceSchema, - PriceType, - ProcessorType, - Product, - ProductOptions, - ProductSchema, - 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 { FullProduct } from "@autumn/shared"; -import { - getBillingInterval, - getBillingType, -} from "@/internal/products/prices/priceUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { ProductService } from "./ProductService.js"; -import { - AttachParams, - InsertCusProductParams, -} from "../customers/cusProducts/AttachParams.js"; -import { - getEntitlementsForProduct, - getEntsWithFeature, -} from "./entitlements/entitlementUtils.js"; import { Decimal } from "decimal.js"; -import { generateId, notNullish } from "@/utils/genUtils.js"; -import { PriceService } from "./prices/PriceService.js"; -import { EntitlementService } from "./entitlements/EntitlementService.js"; -import RecaseError from "@/utils/errorUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; -import { FreeTrialService } from "./free-trials/FreeTrialService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { compareBillingIntervals } from "./prices/priceUtils/priceIntervalUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { + 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, +} from "../customers/cusProducts/AttachParams.js"; import { isStripeConnected } from "../orgs/orgUtils.js"; +import { EntitlementService } from "./entitlements/EntitlementService.js"; +import { + getEntitlementsForProduct +} from "./entitlements/entitlementUtils.js"; +import { FreeTrialService } from "./free-trials/FreeTrialService.js"; +import { ProductService } from "./ProductService.js"; +import { PriceService } from "./prices/PriceService.js"; +import { compareBillingIntervals } from "./prices/priceUtils/priceIntervalUtils.js"; 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; }) => { - let 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; + } - let billingInterval1 = getBillingInterval(prices1); // pro quarter - let 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; - let stripeCli = createStripeCli({ - org, - env, - }); + let createNew = false; + const stripeCli = createStripeCli({ + org, + env, + }); - if (!product.processor || !product.processor.id) { - createNew = true; - } else { - try { - let 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) { - createNew = true; - } - } catch (error) { - createNew = true; - } - } + if (!stripeProduct.active) { + createNew = 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)) { - let 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 - let 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 - let 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, + }); + } - let 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; + } - let newPrices: Price[] = []; - for (const price of product.prices) { - // 1. Copy price - let newPrice = structuredClone(price); + const newPrices: Price[] = []; + for (const price of product.prices) { + // 1. Copy price + const newPrice = structuredClone(price); - let 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) { - let 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, + ); - let 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 - let 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) { - let config = p.config as UsagePriceConfig; - return config.usage_tiers.some((t) => t.amount > 0); - } else { - let 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, + ); }; 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/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index 26701ba9b..461f146f2 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -111,7 +111,7 @@ export const handleRequestError = ({ logger.warn( `RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`, { - error: error.data, + error: error.data ?? error, } ); From 69e754852aca8c78e59a32dc84c28f57d8057f27 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 10 Sep 2025 16:42:29 +0100 Subject: [PATCH 12/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20extra=20checks=20a?= =?UTF-8?q?nd=20ui=20tooltip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../general/modal-components/FieldLabel.tsx | 53 +- .../rewards/reward-config/CreateReward.tsx | 125 +++-- .../rewards/reward-config/DiscountConfig.tsx | 500 +++++++++--------- .../reward-config/FreeDurationSelect.tsx | 74 +++ .../rewards/reward-config/RewardConfig.tsx | 300 ++++++----- .../reward-programs/CreateRewardProgram.tsx | 144 ++--- .../reward-programs/RewardProgramConfig.tsx | 473 +++++++++-------- 7 files changed, 897 insertions(+), 772 deletions(-) create mode 100644 vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx diff --git a/vite/src/components/general/modal-components/FieldLabel.tsx b/vite/src/components/general/modal-components/FieldLabel.tsx index 86bc0413c..99b20e662 100644 --- a/vite/src/components/general/modal-components/FieldLabel.tsx +++ b/vite/src/components/general/modal-components/FieldLabel.tsx @@ -1,21 +1,42 @@ +import type React from "react"; import { cn } from "@/lib/utils"; -import React from "react"; +import { InfoTooltip } from "./InfoTooltip"; -function FieldLabel({ children, className, description, empty = false }: any) { - if (empty) { - children = "\u00A0"; - } - if (!description) { - return ( -
{children}
- ); - } - return ( -
- {children} - {description &&

{description}

} -
- ); +function FieldLabel({ + children, + className, + description, + empty = false, + tooltip, +}: { + children: React.ReactNode; + className?: string; + description?: string; + empty?: boolean; + tooltip?: string; +}) { + if (empty) { + children = "\u00A0"; + } + if (!description) { + return ( +
{children}
+ ); + } + return ( +
+ {children} + {description && !tooltip && ( +

{description}

+ )} + {tooltip && description && ( +
+

{description}

+ {tooltip} +
+ )} +
+ ); } export default FieldLabel; diff --git a/vite/src/views/products/rewards/reward-config/CreateReward.tsx b/vite/src/views/products/rewards/reward-config/CreateReward.tsx index 8be756b25..0c870349d 100644 --- a/vite/src/views/products/rewards/reward-config/CreateReward.tsx +++ b/vite/src/views/products/rewards/reward-config/CreateReward.tsx @@ -1,83 +1,80 @@ +import { useEffect, useState, useTransition } from "react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, - DialogTrigger, + DialogFooter, DialogHeader, DialogTitle, - DialogFooter, + DialogTrigger, } from "@/components/ui/dialog"; - -import React, { useEffect, useState } from "react"; - -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { toast } from "sonner"; - -import { getBackendErr } from "@/utils/genUtils"; -import { useProductsContext } from "../../ProductsContext"; -import { RewardService } from "@/services/products/RewardService"; -import { RewardConfig } from "./RewardConfig"; -import { defaultReward } from "../utils/defaultRewardModels"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { RewardService } from "@/services/products/RewardService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { getBackendErr } from "@/utils/genUtils"; +import { defaultReward } from "../utils/defaultRewardModels"; +import { RewardConfig } from "./RewardConfig"; function CreateReward() { - const axiosInstance = useAxiosInstance(); - const { refetch } = useRewardsQuery(); + const axiosInstance = useAxiosInstance(); + const { refetch } = useRewardsQuery(); - const [isLoading, setIsLoading] = useState(false); - const [open, setOpen] = useState(false); + const [isLoading, startTransition] = useTransition(); + const [open, setOpen] = useState(false); - const [reward, setReward] = useState(defaultReward); + const [reward, setReward] = useState(defaultReward); - useEffect(() => { - if (open) { - setReward(defaultReward); - } - }, [open]); + useEffect(() => { + if (open) { + setReward(defaultReward); + } + }, [open]); - const handleCreate = async () => { - if (!reward?.id && !reward?.name) { - toast.error("ID and name are required"); - return; - } - setIsLoading(true); - try { - await RewardService.createReward({ - axiosInstance, - data: reward, - }); + const handleCreate = () => startTransition(() => { + (async () => { + if (!reward?.id && !reward?.name) { + toast.error("ID and name are required"); + return; + } - await refetch(); - setOpen(false); - } catch (error) { - console.log("Error:", error); - toast.error(getBackendErr(error, "Failed to create coupon")); - } - setIsLoading(false); - }; + try { + await RewardService.createReward({ + axiosInstance, + data: reward, + }); - return ( - - - - - - - Create Reward - - - - - - - - ); + await refetch(); + setOpen(false); + } catch (error) { + console.log("Error:", error); + toast.error(getBackendErr(error, "Failed to create coupon")); + } + })(); + }); + + return ( + + + + + + + Create Reward + + + + + + + + ); } export default CreateReward; diff --git a/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx index 7100175b2..d402c38a7 100644 --- a/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx @@ -1,20 +1,13 @@ -import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { Input } from "@/components/ui/input"; -import { useState } from "react"; -import { Select, SelectContent, SelectItem } from "@/components/ui/select"; -import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { - Reward, CouponDurationType, + type ProductItem, + type Reward, RewardType, - ProductItem, } from "@autumn/shared"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { useState } from "react"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -23,104 +16,115 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { Check, ChevronsUpDown, Trash2, X } from "lucide-react"; - -import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { isFeatureItem } from "@/utils/product/getItemType"; import { formatProductItemText } from "@/utils/product/product-item/formatProductItem"; -import { useOrg } from "@/hooks/common/useOrg"; -import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; -import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; export const DiscountConfig = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const { org } = useOrg(); + const { org } = useOrg(); - const config = reward.discount_config!; - const setConfig = (key: any, value: any) => { - setReward({ - ...reward, - discount_config: { ...config, [key]: value }, - }); - }; + const config = reward.discount_config!; + const setConfig = (key: any, value: any) => { + setReward({ + ...reward, + discount_config: { ...config, [key]: value }, + }); + }; - return ( -
-
-
- Amount - - setConfig("discount_value", Number(e.target.value)) - } - endContent={ -

- {reward.type === RewardType.PercentageDiscount - ? "%" - : org?.default_currency || "USD"} -

- } - /> -
-
- Duration -
- {config.duration_type === CouponDurationType.Months && ( - { - setConfig("duration_value", Number(e.target.value)); - }} - type="number" - /> - )} - -
-
-
+ return ( +
+
+
+ Amount + + setConfig("discount_value", Number(e.target.value)) + } + endContent={ +

+ {reward.type === RewardType.PercentageDiscount + ? "%" + : org?.default_currency || "USD"} +

+ } + /> +
+
+ Duration +
+ {config.duration_type === CouponDurationType.Months && ( + { + setConfig("duration_value", Number(e.target.value)); + }} + type="number" + /> + )} + +
+
+
- {/* {config.duration_type !== CouponDurationType.OneOff && + {/* {config.duration_type !== CouponDurationType.OneOff && reward.type === RewardType.FixedDiscount && (
)} */} -
- {/*

Products

*/} - Products +
+ {/*

Products

*/} + Products - -
-
- ); + +
+
+ ); }; const ProductPriceSelector = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const { org } = useOrg(); - const { products } = useProductsQuery(); - const { features } = useFeaturesQuery(); + const { org } = useOrg(); + const { products } = useProductsQuery(); + const { features } = useFeaturesQuery(); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); - const config = reward.discount_config!; - const setConfig = (key: any, value: any) => { - setReward({ - ...reward, - discount_config: { ...config, [key]: value }, - }); - }; + const config = reward.discount_config!; + const setConfig = (key: any, value: any) => { + setReward({ + ...reward, + discount_config: { ...config, [key]: value }, + }); + }; - // Handle selection/deselection of a price - const handlePriceToggle = (priceId: string) => { - let newPriceIds = [...(config.price_ids || [])]; - if (config.price_ids?.includes(priceId)) { - newPriceIds = config.price_ids?.filter((id) => id !== priceId) || []; - } else { - newPriceIds = [...(config.price_ids || []), priceId]; - } - setConfig("price_ids", newPriceIds); - }; + // Handle selection/deselection of a price + const handlePriceToggle = (priceId: string) => { + let newPriceIds = [...(config.price_ids || [])]; + if (config.price_ids?.includes(priceId)) { + newPriceIds = config.price_ids?.filter((id) => id !== priceId) || []; + } else { + newPriceIds = [...(config.price_ids || []), priceId]; + } + setConfig("price_ids", newPriceIds); + }; - if (!products || products.length === 0) { - return

No products available

; - } + if (!products || products.length === 0) { + return

No products available

; + } - return ( - - - -
- ); - })} - - )} - - - - - - - - - No prices found. - - { - setConfig("apply_to_all", !config.apply_to_all); - }} - className="cursor-pointer" - > -

Apply to all products

- {config.apply_to_all && ( - - )} -
-
- {!config.apply_to_all && - products.map((product: any) => ( - - {product.items.length > 0 ? ( - product.items - ?.filter((item: ProductItem) => { - return !isFeatureItem(item); - }) - .map((item: any) => ( - handlePriceToggle(item.price_id)} - className="cursor-pointer overflow-x-hidden max-w-[380px]" - > - - {formatProductItemText({ - item, - org, - features, - })} - + const text = item + ? formatProductItemText({ + item, + org, + features, + }) + : "Deleted price"; + return ( +
+

{text}

+ +
+ ); + })} + + )} + + + + + + + + + No prices found. + + { + setConfig("apply_to_all", !config.apply_to_all); + }} + className="cursor-pointer" + > +

Apply to all products

+ {config.apply_to_all && ( + + )} +
+
+ {!config.apply_to_all && + products.map((product: any) => ( + + {product.items.length > 0 ? ( + product.items + ?.filter((item: ProductItem) => { + return !isFeatureItem(item); + }) + .map((item: any) => ( + handlePriceToggle(item.price_id)} + className="cursor-pointer overflow-x-hidden max-w-[380px]" + > + + {formatProductItemText({ + item, + org, + features, + })} + - {config.price_ids?.includes(item.price_id) && ( - - )} - - )) - ) : ( - -

No prices available

-
- )} -
- ))} -
-
-
-
- - ); + {config.price_ids?.includes(item.price_id) && ( + + )} +
+ )) + ) : ( + +

No prices available

+
+ )} +
+ ))} +
+
+
+
+ + ); }; diff --git a/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx b/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx new file mode 100644 index 000000000..174518978 --- /dev/null +++ b/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx @@ -0,0 +1,74 @@ +import { CouponDurationType, type Reward } from "@autumn/shared"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; + +export const FreeDurationSelect = ({ + reward, + setReward, +}: { + reward: Reward; + setReward: (reward: Reward) => void; +}) => { + return ( +
+ Duration +
+ {reward.free_product_config?.duration_type === + CouponDurationType.Months && ( + { + setReward({ + ...reward, + // @ts-expect-error + free_product_config: { + ...(reward.free_product_config ?? {}), + duration_value: Number(e.target.value), + }, + }); + }} + type="number" + min={1} + max={12} + /> + )} + +
+
+ ); +}; diff --git a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx index c0feeb552..797ed5741 100644 --- a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx @@ -1,146 +1,174 @@ -import FieldLabel from "@/components/general/modal-components/FieldLabel"; - -import { Input } from "@/components/ui/input"; +import { type ProductV2, type Reward, RewardType } from "@autumn/shared"; import { useEffect, useState } from "react"; -import { Select, SelectContent, SelectItem } from "@/components/ui/select"; -import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { keyToTitle, slugify } from "@/utils/formatUtils/formatTextUtils"; -import { Reward, RewardType, Product, ProductV2 } from "@autumn/shared"; -import { useProductsContext } from "../../ProductsContext"; -import { DiscountConfig } from "./DiscountConfig"; -import { notNullish } from "@/utils/genUtils"; -import { defaultDiscountConfig } from "../utils/defaultRewardModels"; -import { isFreeProduct } from "@/utils/product/priceUtils"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { keyToTitle, slugify } from "@/utils/formatUtils/formatTextUtils"; +import { notNullish } from "@/utils/genUtils"; +import { isFeaturePriceItem } from "@/utils/product/getItemType"; +import { isFreeProduct, isOneOffProduct } from "@/utils/product/priceUtils"; +import { defaultDiscountConfig } from "../utils/defaultRewardModels"; +import { DiscountConfig } from "./DiscountConfig"; +import { FreeDurationSelect } from "./FreeDurationSelect"; export const RewardConfig = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const [idChanged, setIdChanged] = useState(false); - const { products } = useProductsQuery(); + const [idChanged, setIdChanged] = useState(false); + const { products } = useProductsQuery(); - useEffect(() => { - if (!idChanged) { - setReward({ - ...reward, - id: slugify(reward.name || ""), - }); - } - }, [reward.name]); + useEffect(() => { + if (!idChanged) { + setReward({ + ...reward, + id: slugify(reward.name || ""), + }); + } + }, [reward, idChanged, setReward]); - return ( -
-
-
- Name - setReward({ ...reward, name: e.target.value })} - /> -
-
- - ID - - { - setReward({ ...reward, id: e.target.value }); - setIdChanged(true); - }} - /> -
-
-
-
- Promotional Code - 0 ? reward.promo_codes[0].code : "" - } - onChange={(e) => - setReward({ - ...reward, - promo_codes: [{ code: e.target.value }], - }) - } - /> -
-
- Type - -
-
- {reward.type === RewardType.FreeProduct ? ( -
- - Product - - setReward({ ...reward, name: e.target.value })} + /> +
+
+ + ID + + { + setReward({ ...reward, id: e.target.value }); + setIdChanged(true); + }} + /> +
+
+
+
+ Promotional Code + 0 ? reward.promo_codes[0].code : "" + } + onChange={(e) => + setReward({ + ...reward, + promo_codes: [{ code: e.target.value }], + }) + } + /> +
+
+ Type + +
+
+ {reward.type === RewardType.FreeProduct ? ( +
+
+ + Product + +
+ -
- ) : notNullish(reward.type) ? ( - - ) : null} - - ); + const empty = filteredProducts.length === 0; + return ( + <> + + + + + {filteredProducts.map((product: ProductV2) => ( + + {product.name} + + ))} + + + ); + })()} + + + ) : notNullish(reward.type) ? ( + + ) : null} + + {reward.type === RewardType.FreeProduct && + notNullish(reward.free_product_id) && + reward.free_product_id && + !isOneOffProduct( + products.find( + (product: ProductV2) => product.id === reward.free_product_id, + )?.items || [], + ) ? ( + + ) : null} + + ); }; diff --git a/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx b/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx index adda88548..59e952dad 100644 --- a/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx +++ b/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx @@ -1,90 +1,92 @@ +import type { CreateRewardProgram } from "@autumn/shared"; +import { RewardReceivedBy, RewardTriggerEvent } from "@autumn/shared"; +import { useEffect, useState, useTransition } from "react"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogTrigger, - DialogHeader, - DialogTitle, - DialogFooter, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; -import { useEffect, useState } from "react"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { toast } from "sonner"; - -import { RewardTriggerEvent, RewardReceivedBy } from "@autumn/shared"; -import { getBackendErr } from "@/utils/genUtils"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; -import { CreateRewardProgram } from "@autumn/shared"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { getBackendErr } from "@/utils/genUtils"; import { RewardProgramConfig } from "./RewardProgramConfig"; const defaultRewardProgram: CreateRewardProgram = { - id: "", - // trigger: { - // type: RewardTriggerEvent.SignUp, - // product_ids: [], - // exclude_trial: false, - // }, - when: RewardTriggerEvent.CustomerCreation, - product_ids: [], - exclude_trial: false, - internal_reward_id: "", - max_redemptions: 0, - received_by: RewardReceivedBy.Referrer, + id: "", + // trigger: { + // type: RewardTriggerEvent.SignUp, + // product_ids: [], + // exclude_trial: false, + // }, + when: RewardTriggerEvent.CustomerCreation, + product_ids: [], + exclude_trial: false, + internal_reward_id: "", + max_redemptions: 0, + received_by: RewardReceivedBy.Referrer, }; function CreateRewardProgramModal() { - const { refetch } = useRewardsQuery(); - const axiosInstance = useAxiosInstance(); + const { refetch } = useRewardsQuery(); + const axiosInstance = useAxiosInstance(); - const [isLoading, setIsLoading] = useState(false); - const [open, setOpen] = useState(false); + const [isLoading, startTransition] = useTransition(); + const [open, setOpen] = useState(false); - const [rewardProgram, setRewardProgram] = useState(defaultRewardProgram); + const [rewardProgram, setRewardProgram] = useState(defaultRewardProgram); - useEffect(() => { - if (open) { - setRewardProgram(defaultRewardProgram); - } - }, [open]); + useEffect(() => { + if (open) { + setRewardProgram(defaultRewardProgram); + } + }, [open]); - const handleCreate = async () => { - setIsLoading(true); - try { - await axiosInstance.post("/v1/reward_programs", rewardProgram); + const handleCreate = () => { + startTransition(() => { + (async () => { + try { + await axiosInstance.post("/v1/reward_programs", rewardProgram); - await refetch(); - setOpen(false); - } catch (error) { - toast.error(getBackendErr(error, "Failed to create referral program")); - } - setIsLoading(false); - }; + await refetch(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to create referral program")); + } finally { + } + })(); + }); + }; - return ( - - - - - - - Create Referral Program - - - - - - - - ); + return ( + + + + + + + Create Referral Program + + + + + + + + ); } export default CreateRewardProgramModal; diff --git a/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx b/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx index 340b5954f..ce898447a 100644 --- a/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx +++ b/vite/src/views/products/rewards/reward-programs/RewardProgramConfig.tsx @@ -1,258 +1,257 @@ +import { + type Reward, + type RewardProgram, + RewardReceivedBy, + RewardTriggerEvent, +} from "@autumn/shared"; +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { useState } from "react"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; import { Input } from "@/components/ui/input"; import { - Select, - SelectTrigger, - SelectContent, - SelectItem, - SelectValue, -} from "@/components/ui/select"; -import { - Reward, - RewardProgram, - RewardTriggerEvent, - RewardReceivedBy, -} from "@autumn/shared"; -import { useProductsContext } from "../../ProductsContext"; -import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; -import { useState } from "react"; -import { - Popover, - PopoverContent, - PopoverTrigger, + Popover, + PopoverContent, + PopoverTrigger, } from "@/components/ui/popover"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Button } from "@/components/ui/button"; -import { Check, ChevronsUpDown, X } from "lucide-react"; -import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; export const RewardProgramConfig = ({ - rewardProgram, - setRewardProgram, + rewardProgram, + setRewardProgram, }: { - rewardProgram: RewardProgram; - setRewardProgram: (rewardProgram: RewardProgram) => void; + rewardProgram: RewardProgram; + setRewardProgram: (rewardProgram: RewardProgram) => void; }) => { - const { rewards } = useRewardsQuery(); + const { rewards } = useRewardsQuery(); - return ( -
-
-
- Program ID - - setRewardProgram({ ...rewardProgram, id: e.target.value }) - } - /> -
-
- Reward - -
-
-
-
- Redeem On - -
-
- Max Redemptions - - setRewardProgram({ - ...rewardProgram, - max_redemptions: parseInt(e.target.value), - }) - } - /> -
-
-
-
- Received by - -
-
-
- {rewardProgram.when === RewardTriggerEvent.Checkout && ( -
- Products - -
- )} -
-
- ); + return ( +
+
+
+ Program ID + + setRewardProgram({ ...rewardProgram, id: e.target.value }) + } + /> +
+
+ Reward + +
+
+
+
+ Redeem On + +
+
+ Max Redemptions + + setRewardProgram({ + ...rewardProgram, + max_redemptions: parseInt(e.target.value), + }) + } + /> +
+
+
+
+ Received by + +
+
+
+ {rewardProgram.when === RewardTriggerEvent.Checkout && ( +
+ Products + +
+ )} +
+
+ ); }; const ProductSelector = ({ - rewardProgram, - setRewardProgram, + rewardProgram, + setRewardProgram, }: { - rewardProgram: RewardProgram; - setRewardProgram: (rewardProgram: RewardProgram) => void; + rewardProgram: RewardProgram; + setRewardProgram: (rewardProgram: RewardProgram) => void; }) => { - const { products } = useProductsQuery(); - const [open, setOpen] = useState(false); + const { products } = useProductsQuery(); + const [open, setOpen] = useState(false); - // Handle selection/deselection of a product - const handleProductToggle = (productId: string) => { - let newProductIds = [...(rewardProgram.product_ids || [])]; - if (newProductIds.includes(productId)) { - newProductIds = newProductIds.filter((id) => id !== productId); - } else { - newProductIds = [...newProductIds, productId]; - } - setRewardProgram({ - ...rewardProgram, - product_ids: newProductIds, - }); - }; + // Handle selection/deselection of a product + const handleProductToggle = (productId: string) => { + let newProductIds = [...(rewardProgram.product_ids || [])]; + if (newProductIds.includes(productId)) { + newProductIds = newProductIds.filter((id) => id !== productId); + } else { + newProductIds = [...newProductIds, productId]; + } + setRewardProgram({ + ...rewardProgram, + product_ids: newProductIds, + }); + }; - if (!products || products.length === 0) { - return

No products available

; - } + if (!products || products.length === 0) { + return

No products available

; + } - const getProductText = (productId: string) => { - const product = products.find((p: any) => p.id === productId); - return product?.name || "Unknown Product"; - }; + const getProductText = (productId: string) => { + const product = products.find((p: any) => p.id === productId); + return product?.name || "Unknown Product"; + }; - return ( - - - - - ))} - - )} - - - - - - - - - No products found. - - {products.map((product: any) => ( - handleProductToggle(product.id)} - className="cursor-pointer" - > -
{product.name}
- {rewardProgram.product_ids?.includes(product.id) && ( - - )} -
- ))} -
-
-
-
-
-
- ); + return ( + + + + + ))} + + )} + + + + + + + + + No products found. + + {products.map((product: any) => ( + handleProductToggle(product.id)} + className="cursor-pointer" + > +
{product.name}
+ {rewardProgram.product_ids?.includes(product.id) && ( + + )} +
+ ))} +
+
+
+
+
+
+ ); }; From 43a65ae9a6785444940c59523fcb58da1d508c67 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:42:00 +0100 Subject: [PATCH 13/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20tests=20for=20paid?= =?UTF-8?q?=20referrals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals5.ts | 234 +++ .../advanced/referrals/paid/referrals6.ts | 205 ++ .../advanced/referrals/paid/referrals7.ts | 236 +++ .../advanced/referrals/paid/referrals8.ts | 234 +++ server/tests/global.ts | 1739 +++++++++-------- .../expectUtils/expectProductAttached.ts | 290 +-- server/tests/utils/init.ts | 744 +++---- server/tests/utils/setup.ts | 780 ++++---- 8 files changed, 2763 insertions(+), 1699 deletions(-) create mode 100644 server/tests/advanced/referrals/paid/referrals5.ts create mode 100644 server/tests/advanced/referrals/paid/referrals6.ts create mode 100644 server/tests/advanced/referrals/paid/referrals7.ts create mode 100644 server/tests/advanced/referrals/paid/referrals8.ts diff --git a/server/tests/advanced/referrals/paid/referrals5.ts b/server/tests/advanced/referrals/paid/referrals5.ts new file mode 100644 index 000000000..ab853519b --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals5.ts @@ -0,0 +1,234 @@ +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals5"; + +describe(`${chalk.yellowBright( + "referrals5: Testing referrals (immediate, paid, recurring, both)", +)}`, () => { + const mainCustomerId = "main-referral-5"; + const redeemers = ["referral5-r1", "referral5-r2", "referral5-r3"]; + const redeemerPMs = ["success", undefined, "fail"]; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + const 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; + + [mainCustomerId, ...redeemers].forEach(async (customerId) => { + await autumn.customers.delete(customerId); + }); + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockIds.push(res.testClockId); + + // await autumn.attach({ + // customer_id: mainCustomerId, + // product_id: pro.id, + // }); + + const batchCreate = []; + for (const redeemer of redeemers) { + batchCreate.push( + initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + attachPm: redeemerPMs[redeemers.indexOf(redeemer)] as + | "success" + | "fail" + | undefined, + withTestClock: true, + }), + ); + } + + const batchRes = await Promise.all(batchCreate); + for (const r of batchRes) { + testClockIds.push(r.testClockId); + } + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + assert.equal(referralCode2.code, referralCode.code); + }); + + it("should create redemption for each redeemer and fail if redeemed again", async () => { + for (const redeemer of redeemers) { + const redemption: RewardRedemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); + + redemptions.push(redemption); + } + + // Try redeem for redeemer1 again + try { + 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 have given the paid product to all redeemers", async () => { + for (const redeemer of redeemers) { + const redemption = await autumn.redemptions.get( + redemptions[redeemers.indexOf(redeemer)].id, + ); + assert.equal(redemption.applied, true); + } + + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemers[0])).products; + + assert.equal(mainProds.length, 1); + assert.equal(redeemerProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); + assert.equal(redeemerProds[0].id, products.pro.id); + + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomer.id), + product: products.pro, + status: CusProductStatus.Trialing, + }); + + redeemers.forEach(async (r) => { + expectProductV1Attached({ + customer: await autumn.customers.get(r), + product: products.pro, + status: CusProductStatus.Trialing, + }); + }); + }); + + it("should advance test clock and have pro attached", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, ...redeemers].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer. + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Referrer 1, valid payment method. + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Referrer 2, no valid payment method. + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Expired }, + { name: "Free", status: CusProductStatus.Active }, + ], + [ + // Referrer 3, no valid payment method. + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Expired }, + { 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); \ No newline at end of file diff --git a/server/tests/advanced/referrals/paid/referrals6.ts b/server/tests/advanced/referrals/paid/referrals6.ts new file mode 100644 index 000000000..a24df383c --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals6.ts @@ -0,0 +1,205 @@ +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals6"; + +describe(`${chalk.yellowBright( + "referrals6: Testing referrals (immediate, paid, recurring, referrer only)", +)}`, () => { + const mainCustomerId = "main-referral-6"; + const redeemer = "referral6-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: 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; + + try { + await autumn.customers.delete(mainCustomerId); + await autumn.customers.delete(redeemer); + } catch {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockIds.push(res.testClockId); + + // await autumn.attach({ + // customer_id: mainCustomerId, + // product_id: pro.id, + // }); + + 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); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + 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, + }); + + // 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 given the paid product to referrer only, not redeemer", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true); + + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemer)).products; + + // Main customer (referrer) should have the pro product + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); + + // Redeemer should only have the free product (no pro product given) + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.free.id); + + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomer.id), + product: products.pro, + status: CusProductStatus.Trialing, + }); + + // 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 have pro attached for referrer only", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the pro product + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward) + { 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); \ No newline at end of file diff --git a/server/tests/advanced/referrals/paid/referrals7.ts b/server/tests/advanced/referrals/paid/referrals7.ts new file mode 100644 index 000000000..75ec69854 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals7.ts @@ -0,0 +1,236 @@ +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import type { Customer } from "autumn-js"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { setTimeout } from "timers/promises"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "refferals7"; + +describe(`${chalk.yellowBright( + "referrals7: Testing referrals (immediate, paid, one-off add-on, both)", +)}`, () => { + const mainCustomerId = "main-referral-7"; + const redeemer = "referral7-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: 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; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockIds.push(res.testClockId); + + // await autumn.attach({ + // customer_id: mainCustomerId, + // product_id: pro.id, + // }); + + 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); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnAll.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnAll.id, + }); + + 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, + }); + + // 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 given the paid add-on to both referrer and redeemer", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true); + + await setTimeout(1000 * 10); + + const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ + autumn.customers.get(mainCustomerId), + autumn.customers.get(redeemer), + ])) as (Customer & { add_ons: any[] })[]; + + // Main customer (referrer) should have the proAddOn product + const mainProds = mainCustomerData.products; + const mainAddons = mainCustomerData.add_ons; + + assert.equal(mainProds.length + mainAddons.length, 2); + + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); + assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); + + // Redeemer should have both free and proAddOn products + const redeemerProds = redeemerCustomerData.products; + const redeemerAddons = redeemerCustomerData.add_ons; + + assert.equal(redeemerProds.length + redeemerAddons.length, 2); + + const redeemerHasProAddOn = redeemerAddons.some( + (p) => p.id === products.proAddOn.id, + ); + const redeemerHasFree = redeemerProds.some( + (p) => p.id === products.free.id, + ); + + assert.isTrue(redeemerHasProAddOn, "Redeemer should have proAddOn product"); + assert.isTrue(redeemerHasFree, "Redeemer should have free product"); + + // Verify products are properly attached + expectAddOnAttached({ + customer: await autumn.customers.get(mainCustomerId) as Customer & { add_ons: any[] }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + + expectAddOnAttached({ + customer: await autumn.customers.get(redeemer) as Customer & { add_ons: any[] }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and have proAddOn attached for both", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the proAddOn product + { name: "Free", status: CusProductStatus.Active }, + { name: "proAddOn", status: CusProductStatus.Active }, + ], + [ + // Redeemer - gets both free and proAddOn products + { name: "Free", status: CusProductStatus.Active }, + { name: "proAddOn", 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals8.ts b/server/tests/advanced/referrals/paid/referrals8.ts new file mode 100644 index 000000000..85153e0eb --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals8.ts @@ -0,0 +1,234 @@ +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import type { Customer } from "autumn-js"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { setTimeout } from "timers/promises"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals8"; + +describe(`${chalk.yellowBright( + "referrals8: Testing referrals (immediate, paid, one-off add-on, referrer only)", +)}`, () => { + const mainCustomerId = "main-referral-8"; + const redeemer = "referral8-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: 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; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + attachPm: "success", + }); + + mainCustomer = res.customer; + testClockIds.push(res.testClockId); + + // await autumn.attach({ + // customer_id: mainCustomerId, + // product_id: pro.id, + // }); + + 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); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnReferrer.id, + }); + + 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, + }); + + // 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 given the paid add-on to referrer only, not redeemer", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true); + + await setTimeout(1000 * 10); + + const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ + autumn.customers.get(mainCustomerId), + autumn.customers.get(redeemer), + ])) as (Customer & { add_ons: any[] })[]; + + // Main customer (referrer) should have the proAddOn product + const mainProds = mainCustomerData.products; + const mainAddons = mainCustomerData.add_ons; + + assert.equal(mainProds.length + mainAddons.length, 2); + + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); + assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); + + // Redeemer should only have free product (no proAddOn) + const redeemerProds = redeemerCustomerData.products; + const redeemerAddons = redeemerCustomerData.add_ons; + + assert.equal(redeemerProds.length + redeemerAddons.length, 1); + + const redeemerHasProAddOn = redeemerAddons.some( + (p) => p.id === products.proAddOn.id, + ); + const redeemerHasFree = redeemerProds.some( + (p) => p.id === products.free.id, + ); + + assert.isFalse(redeemerHasProAddOn, "Redeemer should not have proAddOn product"); + assert.isTrue(redeemerHasFree, "Redeemer should have free product"); + + // Verify products are properly attached + expectAddOnAttached({ + customer: await autumn.customers.get(mainCustomerId) as Customer & { add_ons: any[] }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + + // Verify redeemer does not have the add-on + const redeemerCustomer = await autumn.customers.get(redeemer) as Customer & { add_ons: any[] }; + const redeemerAddOn = redeemerCustomer.add_ons.find((a) => a.id === products.proAddOn.id); + assert.isUndefined(redeemerAddOn, "Redeemer should not have proAddOn product"); + }); + + it("should advance test clock and have proAddOn attached for referrer only", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the proAddOn product + { name: "Free", status: CusProductStatus.Active }, + { name: "proAddOn", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has free product (no reward) + { 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); \ No newline at end of file diff --git a/server/tests/global.ts b/server/tests/global.ts index 7f4465065..b8209d82c 100644 --- a/server/tests/global.ts +++ b/server/tests/global.ts @@ -1,900 +1,989 @@ import dotenv from "dotenv"; + dotenv.config(); + import { - AggregateType, - AllowanceType, - AppEnv, - BillingInterval, - CouponDurationType, - EntInterval, - Feature, - FeatureUsageType, - RewardReceivedBy, - RewardTriggerEvent, - RewardType, + AggregateType, + AllowanceType, + AppEnv, + BillingInterval, + CouponDurationType, + EntInterval, + type Feature, + FeatureType, + FeatureUsageType, + RewardReceivedBy, + RewardTriggerEvent, + RewardType, } from "@autumn/shared"; -import { FeatureType } from "@autumn/shared"; -import { - initReward, - initEntitlement, - initFeature, - initFreeTrial, - initPrice, - initProduct, - initRewardProgram, -} from "./utils/init.js"; -import { createSupabaseClient } from "@/external/supabaseUtils.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; 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, +} 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, + }), }; 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, - }), - 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, + }), + paidProduct: initReward({ + id: "paidProduct", + type: RewardType.FreeProduct, + freeProductId: products.pro.id, + }), + 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, + }), }; export const referralPrograms = { - 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, - }), - 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.pro.id], + }), + paidProductCheckoutReferrer: initRewardProgram({ + id: "paidProduct-checkout-referrer", + internalRewardId: rewards.paidProductWithConfig.id, + when: RewardTriggerEvent.Checkout, + receivedBy: RewardReceivedBy.Referrer, + productIds: [products.pro.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.proAddOn.id], + }), + paidAddOnCheckoutReferrer: initRewardProgram({ + id: "paidAddOn-checkout-referrer", + internalRewardId: rewards.paidProductAddOn.id, + when: RewardTriggerEvent.Checkout, + receivedBy: RewardReceivedBy.Referrer, + productIds: [products.proAddOn.id], + }), }; const ORG_SLUG = process.env.TESTS_ORG!; before(async function () { - try { - this.env = AppEnv.Sandbox; - let { 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) { - let feature = features[featureId as keyof typeof features]; - let 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-ignore 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/utils/expectUtils/expectProductAttached.ts b/server/tests/utils/expectUtils/expectProductAttached.ts index ed02da03f..d64da0d09 100644 --- a/server/tests/utils/expectUtils/expectProductAttached.ts +++ b/server/tests/utils/expectUtils/expectProductAttached.ts @@ -1,137 +1,197 @@ -import { notNullish, nullish } from "@/utils/genUtils.js"; -import { CusProductStatus, ProductV2 } from "@autumn/shared"; -import { Customer } from "autumn-js"; +import { + type CreateFreeTrial, + CusProductStatus, + type Entitlement, + type ProductV2, +} from "@autumn/shared"; +import type { Customer, ProductItem } from "autumn-js"; import { expect } from "chai"; -import { Decimal } from "decimal.js"; export const expectProductAttached = ({ - customer, - product, - productId, - status, - entityId, - isCanceled = false, - quantity, + customer, + product, + productId, + status, + entityId, + isCanceled = false, + quantity, }: { - customer: Customer; - product?: ProductV2; - productId?: string; - status?: CusProductStatus; - entityId?: string; - isCanceled?: boolean; - quantity?: number; + customer: Customer; + product?: ProductV2; + productId?: string; + status?: CusProductStatus; + entityId?: string; + isCanceled?: boolean; + quantity?: number; }) => { - const cusProducts = customer.products; - const finalProductId = productId || product?.id; - const productAttached = cusProducts.find( - (p) => - p.id === finalProductId && (entityId ? p.entity_id === entityId : true) - ); + const cusProducts = customer.products; + const finalProductId = productId || product?.id; + const productAttached = cusProducts.find( + (p) => + p.id === finalProductId && (entityId ? p.entity_id === entityId : true), + ); - if (!productAttached) { - console.log(`product ${finalProductId} not attached`); - console.log(cusProducts); - } + if (!productAttached) { + console.log(`product ${finalProductId} not attached`); + console.log(cusProducts); + } - expect(productAttached, `product ${finalProductId} is attached`).to.exist; + expect(productAttached, `product ${finalProductId} is attached`).to.exist; - if (status) { - expect(productAttached?.status).to.equal( - status, - `product ${finalProductId} should have status ${status}` - ); - } else { - expect( - productAttached?.status, - `product ${finalProductId} is not expired` - ).to.not.equal(CusProductStatus.Expired); - } + if (status) { + expect(productAttached?.status).to.equal( + status, + `product ${finalProductId} should have status ${status}`, + ); + } else { + expect( + productAttached?.status, + `product ${finalProductId} is not expired`, + ).to.not.equal(CusProductStatus.Expired); + } - if (quantity) { - // @ts-ignore - expect(productAttached?.quantity).to.equal(quantity); - } + if (quantity) { + // @ts-expect-error + expect(productAttached?.quantity).to.equal(quantity); + } - if (entityId) { - // @ts-ignore - expect(productAttached?.entity_id).to.equal(entityId); - } + if (entityId) { + expect(productAttached?.entity_id).to.equal(entityId); + } - if (isCanceled) { - expect(productAttached?.canceled_at).to.exist; - // @ts-ignore - // expect(productAttached?.canceled).to.be.true; - } + if (isCanceled) { + expect(productAttached?.canceled_at).to.exist; + // expect(productAttached?.canceled).to.be.true; + } }; +export const expectProductV1Attached = ({ + customer, + product, + productId, + status, + entityId, +}: { + customer: Customer; + product: { + id: string; + isDefault?: boolean; + isAddOn?: boolean; + items?: Record; + entitlements: Record; + prices: any[]; + freeTrial?: CreateFreeTrial; + group?: string; + }; + productId?: string; + status?: CusProductStatus; + entityId?: string; +}) => { + const cusProducts = customer.products; + const finalProductId = productId || product?.id; + const productAttached = cusProducts.find( + (p) => + p.id === finalProductId && (entityId ? p.entity_id === entityId : true), + ); + + expect(productAttached, `product ${finalProductId} is attached`).to.exist; + + if (status) { + expect(productAttached?.status).to.equal( + status, + `product ${finalProductId} should have status ${status}`, + ); + } +}; + +export const expectAddOnAttached = ({ + customer, + productId, + status, +}: { + customer: Customer & { add_ons: { + id: string; + status: CusProductStatus; + }[] }; + productId: string; + status?: CusProductStatus; +}) => { + const addOn = customer.add_ons.find((a) => a.id === productId); + expect(addOn, `add on ${productId} is attached`).to.exist; + + if (status) { + expect(addOn?.status).to.equal(status, `add on ${productId} should have status ${status}`); + } +} + export const expectInvoicesCorrect = ({ - customer, - first, - // second, + customer, + first, + // second, }: { - customer: Customer; - first: { - productId: string; - total: number; - }; - // second?: { - // productId: string; - // total: number; - // }; + customer: Customer; + first: { + productId: string; + total: number; + }; + // second?: { + // productId: string; + // total: number; + // }; }) => { - const invoices = customer.invoices; - if (!invoices) { - console.log(`invoices is nullish`); - } + const invoices = customer.invoices; + if (!invoices) { + console.log(`invoices is nullish`); + } - try { - expect(invoices![0].total).to.approximately( - first.total, - 0.01, - `invoice total is correct: ${first.total}` - ); + try { + expect(invoices![0].total).to.approximately( + first.total, + 0.01, + `invoice total is correct: ${first.total}`, + ); - expect(invoices![0].product_ids).to.include( - first.productId, - `invoice includes product ${first.productId}` - ); - } catch (error) { - console.log(`invoice for ${first.productId}, ${first.total} not found`); - throw error; - } + expect(invoices![0].product_ids).to.include( + first.productId, + `invoice includes product ${first.productId}`, + ); + } catch (error) { + console.log(`invoice for ${first.productId}, ${first.total} not found`); + throw error; + } - // if (first) { + // if (first) { - // } + // } - // if (second) { - // const totalAmount = new Decimal(invoices![0].total) - // .plus(invoices![1].total) - // .toDecimalPlaces(2) - // .toNumber(); - // // console.log("First invoice:", invoices![0].total, invoices![0].product_ids); - // // console.log( - // // "Second invoice:", - // // invoices![1].total, - // // invoices![1].product_ids, - // // ); - // try { - // expect(totalAmount).to.approximately( - // second.total, - // 0.01, - // `first & second invoice total should sum to ${second.total}`, - // ); - // expect( - // invoices![0].product_ids.includes(second.productId), - // `invoice 1 includes product ${second.productId}`, - // ).to.be.true; - // expect( - // invoices![1].product_ids.includes(second.productId), - // `invoice 2 includes product ${second.productId}`, - // ).to.be.true; - // } catch (error) { - // console.log(`invoice for ${second.productId}, ${second.total} not found`); - // throw error; - // } - // } + // if (second) { + // const totalAmount = new Decimal(invoices![0].total) + // .plus(invoices![1].total) + // .toDecimalPlaces(2) + // .toNumber(); + // // console.log("First invoice:", invoices![0].total, invoices![0].product_ids); + // // console.log( + // // "Second invoice:", + // // invoices![1].total, + // // invoices![1].product_ids, + // // ); + // try { + // expect(totalAmount).to.approximately( + // second.total, + // 0.01, + // `first & second invoice total should sum to ${second.total}`, + // ); + // expect( + // invoices![0].product_ids.includes(second.productId), + // `invoice 1 includes product ${second.productId}`, + // ).to.be.true; + // expect( + // invoices![1].product_ids.includes(second.productId), + // `invoice 2 includes product ${second.productId}`, + // ).to.be.true; + // } catch (error) { + // console.log(`invoice for ${second.productId}, ${second.total} not found`); + // throw error; + // } + // } }; diff --git a/server/tests/utils/init.ts b/server/tests/utils/init.ts index 917b63358..0d15bf37e 100644 --- a/server/tests/utils/init.ts +++ b/server/tests/utils/init.ts @@ -1,435 +1,441 @@ import { - AggregateType, - AllowanceType, - AppEnv, - BillingInterval, - BillWhen, - CouponDurationType, - CreateFreeTrial, - EntInterval, - Entitlement, - Feature, - FeatureType, - FeatureUsageType, - FreeTrialDuration, - Organization, - PriceType, - ProductItem, - RewardReceivedBy, - RewardTriggerEvent, - RewardType, + AggregateType, + AllowanceType, + type AppEnv, + BillingInterval, + BillWhen, + CouponDurationType, + type CreateFreeTrial, + EntInterval, + type Entitlement, + type Feature, + FeatureType, + FeatureUsageType, FreeTrialDuration, + type Organization, + PriceType, + type ProductItem, + RewardReceivedBy, + RewardTriggerEvent, + RewardType } from "@autumn/shared"; - -import { getAxiosInstance } from "./setup.js"; -import { SupabaseClient } from "@supabase/supabase-js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js"; import { generateId } from "@/utils/genUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { getAxiosInstance } from "./setup.js"; 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 initFeature = ({ - id, - type, - creditSchema = [], - aggregateType = AggregateType.Sum, - groupBy = "", - eventName, - usageType = FeatureUsageType.Single, + id, + type, + creditSchema = [], + aggregateType = AggregateType.Sum, + groupBy = "", + eventName, + usageType = FeatureUsageType.Single, }: { - id: string; - type: FeatureType; - creditSchema?: { - metered_feature_id: string; - feature_amount: number; - credit_amount: number; - }[]; - aggregateType?: AggregateType; - groupBy?: string; - eventName?: string; - usageType?: FeatureUsageType; + id: string; + type: FeatureType; + creditSchema?: { + metered_feature_id: string; + feature_amount: number; + credit_amount: number; + }[]; + aggregateType?: AggregateType; + groupBy?: string; + eventName?: string; + usageType?: FeatureUsageType; }): (Feature & { eventName: string }) | any => { - const name = keyToTitle(id); + const name = keyToTitle(id); - if (type == FeatureType.Boolean) { - return { - id, - name, - type, - } as Feature; - } + if (type == FeatureType.Boolean) { + return { + id, + name, + type, + } as Feature; + } - if (type == FeatureType.Metered) { - return { - id, - name, - type, - config: { - filters: [ - { - value: eventName ? [eventName] : [id], - property: "", - operator: "", - }, - ], - aggregate: { - type: aggregateType, - property: "value", - }, - usage_type: usageType, - // group_by: groupBy - // ? { - // property: groupBy, - // } - // : undefined, - }, - } as Feature; - } + if (type == FeatureType.Metered) { + return { + id, + name, + type, + config: { + filters: [ + { + value: eventName ? [eventName] : [id], + property: "", + operator: "", + }, + ], + aggregate: { + type: aggregateType, + property: "value", + }, + usage_type: usageType, + // group_by: groupBy + // ? { + // property: groupBy, + // } + // : undefined, + }, + } as Feature; + } - if (type == FeatureType.CreditSystem) { - return { - id, - name, - type, - config: { - schema: creditSchema, - usage_type: FeatureUsageType.Single, - }, - } as Feature; - } + if (type == FeatureType.CreditSystem) { + return { + id, + name, + type, + config: { + schema: creditSchema, + usage_type: FeatureUsageType.Single, + }, + } as Feature; + } - throw new Error(`Invalid feature type: ${type}`); + throw new Error(`Invalid feature type: ${type}`); }; export const initEntitlement = ({ - feature, - allowance, - interval = EntInterval.Month, - allowanceType = AllowanceType.Fixed, - entityFeatureId, - carryFromPrevious = false, + feature, + allowance, + interval = EntInterval.Month, + allowanceType = AllowanceType.Fixed, + entityFeatureId, + carryFromPrevious = false, }: { - feature: Feature; - allowance?: number; - interval?: EntInterval; - allowanceType?: AllowanceType; - entityFeatureId?: string; - carryFromPrevious?: boolean; + feature: Feature; + allowance?: number; + interval?: EntInterval; + allowanceType?: AllowanceType; + entityFeatureId?: string; + carryFromPrevious?: boolean; }) => { - if (feature.type == FeatureType.Boolean) { - return { - feature_id: feature.id, - internal_feature_id: feature.internal_id, - } as Entitlement; - } + if (feature.type == FeatureType.Boolean) { + return { + feature_id: feature.id, + internal_feature_id: feature.internal_id, + } as Entitlement; + } - const isUnlimitedOrNone = - allowanceType == AllowanceType.Unlimited || allowance == null; + const isUnlimitedOrNone = + allowanceType == AllowanceType.Unlimited || allowance == null; - return { - feature_id: feature.id, - internal_feature_id: feature.internal_id, - allowance_type: allowanceType, - allowance: isUnlimitedOrNone ? null : allowance, - interval: isUnlimitedOrNone ? null : interval, - entity_feature_id: entityFeatureId, - carry_from_previous: carryFromPrevious, - created_at: Date.now(), - id: generateId("ent"), - } as Entitlement; + return { + feature_id: feature.id, + internal_feature_id: feature.internal_id, + allowance_type: allowanceType, + allowance: isUnlimitedOrNone ? null : allowance, + interval: isUnlimitedOrNone ? null : interval, + entity_feature_id: entityFeatureId, + carry_from_previous: carryFromPrevious, + created_at: Date.now(), + id: generateId("ent"), + } as Entitlement; }; export const initPrice = ({ - type, - feature, - billingInterval = BillingInterval.Month, - amount = 10.0, - oneTier = false, - billingUnits = 10, + type, + feature, + billingInterval = BillingInterval.Month, + amount = 10.0, + oneTier = false, + billingUnits = 10, }: { - type: - | "monthly" - | "in_advance" - | "in_arrears" - | "fixed_cycle" - | "in_arrear_prorated"; - feature?: Feature; - billingInterval?: BillingInterval; - amount?: number; - oneTier?: boolean; - billingUnits?: number; + type: + | "monthly" + | "in_advance" + | "in_arrears" + | "fixed_cycle" + | "in_arrear_prorated"; + feature?: Feature; + billingInterval?: BillingInterval; + amount?: number; + oneTier?: boolean; + billingUnits?: number; }) => { - if (type == "monthly" || type == "fixed_cycle") { - return { - name: type == "monthly" ? "Monthly" : "Fixed Cycle", - config: { - type: PriceType.Fixed, - amount: amount, - interval: billingInterval, - }, - }; - } + if (type == "monthly" || type == "fixed_cycle") { + return { + name: type == "monthly" ? "Monthly" : "Fixed Cycle", + config: { + type: PriceType.Fixed, + amount: amount, + interval: billingInterval, + }, + }; + } - if (!feature) { - throw new Error("Feature is required for in_advance and in_arrears"); - } + if (!feature) { + throw new Error("Feature is required for in_advance and in_arrears"); + } - if (type == "in_advance") { - return { - name: "In Advance", - config: { - type: PriceType.Usage, - bill_when: BillWhen.StartOfPeriod, - feature_id: feature!.id, - interval: billingInterval, - billing_units: billingUnits, - usage_tiers: [ - { - from: 0, - to: -1, - amount: amount || 10.0, - }, - ], - }, - }; - } + if (type == "in_advance") { + return { + name: "In Advance", + config: { + type: PriceType.Usage, + bill_when: BillWhen.StartOfPeriod, + feature_id: feature!.id, + interval: billingInterval, + billing_units: billingUnits, + usage_tiers: [ + { + from: 0, + to: -1, + amount: amount || 10.0, + }, + ], + }, + }; + } - if (type == "in_arrears" || type == "in_arrear_prorated") { - return { - name: "In Arrears", - config: { - type: PriceType.Usage, - bill_when: BillWhen.EndOfPeriod, - feature_id: feature!.id, - interval: billingInterval, - billing_units: billingUnits, - should_prorate: type == "in_arrear_prorated", - usage_tiers: oneTier - ? [ - { - from: 0, - to: -1, - amount: amount || 0.01, - }, - ] - : [ - { - from: 0, - to: 10, - amount: 0.5, - }, - { - from: 11, - to: -1, - amount: 0.25, - }, - ], - }, - }; - } + if (type == "in_arrears" || type == "in_arrear_prorated") { + return { + name: "In Arrears", + config: { + type: PriceType.Usage, + bill_when: BillWhen.EndOfPeriod, + feature_id: feature!.id, + interval: billingInterval, + billing_units: billingUnits, + should_prorate: type == "in_arrear_prorated", + usage_tiers: oneTier + ? [ + { + from: 0, + to: -1, + amount: amount || 0.01, + }, + ] + : [ + { + from: 0, + to: 10, + amount: 0.5, + }, + { + from: 11, + to: -1, + amount: 0.25, + }, + ], + }, + }; + } }; export const initFreeTrial = ({ - length, - uniqueFingerprint = false, - cardRequired = true, + length, + uniqueFingerprint = false, + cardRequired = true, }: { - length: number; - uniqueFingerprint?: boolean; - cardRequired?: boolean; + length: number; + uniqueFingerprint?: boolean; + cardRequired?: boolean; }): CreateFreeTrial => { - return { - length, - unique_fingerprint: uniqueFingerprint, - duration: FreeTrialDuration.Day, - card_required: cardRequired, - }; + return { + length, + unique_fingerprint: uniqueFingerprint, + duration: FreeTrialDuration.Day, + card_required: cardRequired, + }; }; export const initProduct = ({ - id, - isDefault = false, - isAddOn = false, - items, - entitlements, - prices, - freeTrial, - group = "", + id, + isDefault = false, + isAddOn = false, + items, + entitlements, + prices, + freeTrial, + group = "", }: { - id: string; - isDefault?: boolean; - isAddOn?: boolean; - items?: Record; - entitlements: Record; - prices: any[]; - freeTrial: CreateFreeTrial | null; - group?: string; + id: string; + isDefault?: boolean; + isAddOn?: boolean; + items?: Record; + entitlements: Record; + prices: any[]; + freeTrial: CreateFreeTrial | null; + group?: string; }) => { - // if (notNullish(items)) { - // return { - // id, - // name: keyToTitle(id), - // is_default: isDefault, - // is_add_on: isAddOn, - // group: group, - // items: items, - // free_trial: freeTrial, - // }; - // } + // if (notNullish(items)) { + // return { + // id, + // name: keyToTitle(id), + // is_default: isDefault, + // is_add_on: isAddOn, + // group: group, + // items: items, + // free_trial: freeTrial, + // }; + // } - return { - id, - name: keyToTitle(id), - is_default: isDefault, - is_add_on: isAddOn, - entitlements: entitlements, - prices, - free_trial: freeTrial, - group: group, - }; + return { + id, + name: keyToTitle(id), + is_default: isDefault, + is_add_on: isAddOn, + entitlements: entitlements, + prices, + free_trial: freeTrial, + group: group, + }; }; export const initCustomer = async ({ - customer_data, - customerId, - attachPm = false, - db, - org, - env, - testClockId, + customer_data, + customerId, + attachPm = false, + db, + org, + env, + testClockId, }: { - customer_data?: { - id: string; - name?: string; - email?: string; - fingerprint?: string; - }; - customerId?: string; - attachPm?: boolean; - db: DrizzleCli; - org: Organization; - env: AppEnv; - testClockId?: string; + customer_data?: { + id: string; + name?: string; + email?: string; + fingerprint?: string; + }; + customerId?: string; + attachPm?: boolean; + db: DrizzleCli; + org: Organization; + env: AppEnv; + testClockId?: string; }) => { - const axiosInstance = getAxiosInstance(); + const axiosInstance = getAxiosInstance(); - if (!customerId && !customer_data) { - throw new Error("customerId or customer_data is required"); - } + if (!customerId && !customer_data) { + throw new Error("customerId or customer_data is required"); + } - let customerData = customerId - ? { - id: customerId, - name: customerId, - email: `${customerId}@example.com`, - } - : customer_data; + const customerData = customerId + ? { + id: customerId, + name: customerId, + email: `${customerId}@example.com`, + } + : customer_data; - // Delete customer if exists - try { - await axiosInstance.delete(`/v1/customers/${customerData!.id}`); - // console.log(" - Successfully deleted customer"); - } catch (error) { - // console.log("Failed to delete customer"); - } + // Delete customer if exists + try { + await axiosInstance.delete(`/v1/customers/${customerData!.id}`); + // console.log(" - Successfully deleted customer"); + } catch (error) { + // console.log("Failed to delete customer"); + } - try { - const { data } = await axiosInstance.post(`/v1/customers`, customerData); - // Attach stripe card + try { + const { data } = await axiosInstance.post(`/v1/customers`, customerData); + // Attach stripe card - if (attachPm) { - await attachPmToCus({ - customer: data.customer, - org: org, - env: env, - db: db, - testClockId: testClockId, - }); - } + if (attachPm) { + await attachPmToCus({ + customer: data.customer, + org: org, + env: env, + db: db, + testClockId: testClockId, + }); + } - return data.customer; - } catch (error) { - console.log("Failed to create customer", error); - } + return data.customer; + } catch (error) { + console.log("Failed to create customer", error); + } }; // Init Reward export const initReward = ({ - id, - type = RewardType.PercentageDiscount, - discountValue, - durationType = CouponDurationType.OneOff, - durationValue = 0, - onlyUsagePrices = false, - productIds, - applyToAll = false, - freeProductId, + id, + type = RewardType.PercentageDiscount, + discountValue, + durationType = CouponDurationType.OneOff, + durationValue = 0, + onlyUsagePrices = false, + productIds, + applyToAll = false, + freeProductId, + freeProductConfig, }: { - id: string; - type?: RewardType; - discountValue?: number; - durationType?: CouponDurationType; - durationValue?: number; - onlyUsagePrices?: boolean; - productIds?: string[]; - applyToAll?: boolean; - freeProductId?: string; + id: string; + type?: RewardType; + discountValue?: number; + durationType?: CouponDurationType; + durationValue?: number; + onlyUsagePrices?: boolean; + productIds?: string[]; + applyToAll?: boolean; + freeProductId?: string; + freeProductConfig?: { + durationType: CouponDurationType; + durationValue: number; + }; }): any => { - if ( - type == RewardType.PercentageDiscount || - type == RewardType.FixedDiscount || - type == RewardType.InvoiceCredits - ) { - return { - id, - name: keyToTitle(id), - type, + if ( + type === RewardType.PercentageDiscount || + type === RewardType.FixedDiscount || + type === RewardType.InvoiceCredits + ) { + return { + id, + name: keyToTitle(id), + type, - only_usage_prices: onlyUsagePrices, - product_ids: productIds, + only_usage_prices: onlyUsagePrices, + product_ids: productIds, - discount_config: { - discount_value: discountValue, - duration_type: durationType, - duration_value: durationValue, - apply_to_all: applyToAll, - }, - }; - } else if (type == RewardType.FreeProduct) { - return { - id, - name: keyToTitle(id), - type, - free_product_id: freeProductId, - }; - } + discount_config: { + discount_value: discountValue, + duration_type: durationType, + duration_value: durationValue, + apply_to_all: applyToAll, + }, + }; + } else if (type === RewardType.FreeProduct) { + return { + id, + name: keyToTitle(id), + type, + free_product_id: freeProductId, + free_product_config: { + duration_type: freeProductConfig?.durationType, + duration_value: freeProductConfig?.durationValue, + }, + }; + } }; export const initRewardProgram = ({ - id, - when = RewardTriggerEvent.CustomerCreation, - productIds = [], - internalRewardId, - maxRedemptions = 2, - receivedBy = RewardReceivedBy.Referrer, + id, + when = RewardTriggerEvent.CustomerCreation, + productIds = [], + internalRewardId, + maxRedemptions = 2, + receivedBy = RewardReceivedBy.Referrer, }: { - id: string; - productIds?: string[]; - internalRewardId: string; - when?: RewardTriggerEvent; - maxRedemptions?: number; - receivedBy?: RewardReceivedBy; + id: string; + productIds?: string[]; + internalRewardId: string; + when?: RewardTriggerEvent; + maxRedemptions?: number; + receivedBy?: RewardReceivedBy; }): any => { - return { - id, - when, - product_ids: productIds, - internal_reward_id: internalRewardId, - max_redemptions: maxRedemptions, - received_by: receivedBy, - }; + return { + id, + when, + product_ids: productIds, + internal_reward_id: internalRewardId, + max_redemptions: maxRedemptions, + received_by: receivedBy, + }; }; diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index fdcca2442..c4586d9f3 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -1,486 +1,486 @@ -import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { AppEnv, - CreateReward, - Feature, + type CreateReward, + type Feature, FeatureType, - FullProduct, - Organization, - Price, + type FullProduct, + type Organization, + type Price, PriceType, - RewardProgram, + type RewardProgram, RewardType, } from "@autumn/shared"; - import axios from "axios"; - -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { deleteAllStripeTestClocks } from "./stripeUtils.js"; +import type Stripe from "stripe"; import { AutumnCli } from "tests/cli/AutumnCli.js"; -import Stripe from "stripe"; +import { features as v2Features } from "tests/setup/v2Features.js"; +import { initDrizzle } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js"; -import { mapToProductItems } from "@/internal/products/productV2Utils.js"; import { CacheManager } from "@/external/caching/CacheManager.js"; import { CacheType } from "@/external/caching/cacheActions.js"; -import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; -import { initDrizzle } from "@/db/initDrizzle.js"; +import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { ProductService } from "@/internal/products/ProductService.js"; -import { RewardService } from "@/internal/rewards/RewardService.js"; +import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; -import { features as v2Features } from "tests/setup/v2Features.js"; -import { timeout } from "./genUtils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { mapToProductItems } from "@/internal/products/productV2Utils.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { deleteAllStripeTestClocks } from "./stripeUtils.js"; export const getAxiosInstance = ( - apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY! + apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!, ) => { - return axios.create({ - baseURL: "http://localhost:8080", - headers: { - Authorization: `Bearer ${apiKey}`, - }, - }); + return axios.create({ + baseURL: "http://localhost:8080", + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }); }; export const getPublicAxiosInstance = ({ - withBearer, - pkey = process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!, + withBearer, + pkey = process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!, }: { - withBearer: boolean; - pkey?: string; + withBearer: boolean; + pkey?: string; }) => { - let headers = withBearer - ? { - Authorization: `Bearer ${pkey}`, - } - : { - "x-publishable-key": pkey, - }; - return axios.create({ - baseURL: "http://localhost:8080", - headers: headers, - }); + const headers = withBearer + ? { + Authorization: `Bearer ${pkey}`, + } + : { + "x-publishable-key": pkey, + }; + return axios.create({ + baseURL: "http://localhost:8080", + headers: headers, + }); }; export const clearOrg = async ({ - orgSlug, - env, + orgSlug, + env, }: { - orgSlug: string; - env?: AppEnv; + orgSlug: string; + env?: AppEnv; }) => { - if (env !== AppEnv.Sandbox) { - console.error("Cannot clear non-sandbox orgs"); - process.exit(1); - } + if (env !== AppEnv.Sandbox) { + console.error("Cannot clear non-sandbox orgs"); + process.exit(1); + } - const autumn = new AutumnInt(); + const autumn = new AutumnInt(); - if (process.env.STRIPE_TEST_KEY) { - console.log(`Reconnecting stripe...`); + if (process.env.STRIPE_TEST_KEY) { + console.log(`Reconnecting stripe...`); - try { - await autumn.stripe.delete(); - } catch (error) {} - try { - await autumn.stripe.connect({ - secret_key: process.env.STRIPE_TEST_KEY!, - success_url: "https://useautumn.com", - default_currency: "usd", - }); - } catch (error: any) { - console.error("Error reconnecting stripe", error.message); - process.exit(1); - } - } + try { + await autumn.stripe.delete(); + } catch (error) {} + try { + await autumn.stripe.connect({ + secret_key: process.env.STRIPE_TEST_KEY!, + success_url: "https://useautumn.com", + default_currency: "usd", + }); + } catch (error: any) { + console.error("Error reconnecting stripe", error.message); + process.exit(1); + } + } - const { db, client } = initDrizzle(); - const org = await OrgService.getBySlug({ db, slug: orgSlug }); + const { db, client } = initDrizzle(); + const org = await OrgService.getBySlug({ db, slug: orgSlug }); - await Promise.all([ - CacheManager.invalidate({ - action: CacheType.SecretKey, - value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - }), - CacheManager.invalidate({ - action: CacheType.PublicKey, - value: process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!, - }), - ]); - await CacheManager.disconnect(); + await Promise.all([ + CacheManager.invalidate({ + action: CacheType.SecretKey, + value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), + }), + CacheManager.invalidate({ + action: CacheType.PublicKey, + value: process.env.UNIT_TEST_AUTUMN_PUBLIC_KEY!, + }), + ]); + await CacheManager.disconnect(); - if (!org) { - throw new Error(`Org ${orgSlug} not found`); - } + if (!org) { + throw new Error(`Org ${orgSlug} not found`); + } - if (!(org.slug == "unit-test-org" || org.slug == "ci_cd")) { - console.error("Cannot clear non-unit-test-orgs"); - process.exit(1); - } + if (!(org.slug === "unit-test-org" || org.slug === "ci_cd")) { + console.error("Cannot clear non-unit-test-orgs"); + process.exit(1); + } - const orgId = org.id; + const orgId = org.id; - // 1. Delete all customers - await CusService.deleteByOrgId({ db, orgId, env }); - console.log(" ✅ Deleted customers"); + // 1. Delete all customers + await CusService.deleteByOrgId({ db, orgId, env }); + console.log(" ✅ Deleted customers"); - const stripeCli = createStripeCli({ org, env: env! }); - const stripeCustomers = await stripeCli.customers.list({ - limit: 100, - }); - const deleteCustomer = async (customer: Stripe.Customer) => { - try { - await stripeCli.customers.del(customer.id); - } catch (error) { - console.error("Error deleting stripe customer", customer.id); - } - }; + const stripeCli = createStripeCli({ org, env: env! }); + const stripeCustomers = await stripeCli.customers.list({ + limit: 100, + }); + const deleteCustomer = async (customer: Stripe.Customer) => { + try { + await stripeCli.customers.del(customer.id); + } catch (error) { + console.error("Error deleting stripe customer", customer.id); + } + }; - const cusBatchSize = 5; - for (let i = 0; i < stripeCustomers.data.length; i += cusBatchSize) { - const batch = stripeCustomers.data.slice(i, i + cusBatchSize); - const batchDeleteCustomers = []; - for (const customer of batch) { - batchDeleteCustomers.push(deleteCustomer(customer)); - } - await Promise.all(batchDeleteCustomers); - console.log( - ` ✅ Deleted ${i + batch.length}/${ - stripeCustomers.data.length - } Stripe customers` - ); - } + const cusBatchSize = 5; + for (let i = 0; i < stripeCustomers.data.length; i += cusBatchSize) { + const batch = stripeCustomers.data.slice(i, i + cusBatchSize); + const batchDeleteCustomers = []; + for (const customer of batch) { + batchDeleteCustomers.push(deleteCustomer(customer)); + } + await Promise.all(batchDeleteCustomers); + console.log( + ` ✅ Deleted ${i + batch.length}/${ + stripeCustomers.data.length + } Stripe customers`, + ); + } - console.log(" ✅ Deleted Stripe customers"); + console.log(" ✅ Deleted Stripe customers"); - // 2. Delete all products - await ProductService.deleteByOrgId({ db, orgId, env }); + // 2. Delete all products + await ProductService.deleteByOrgId({ db, orgId, env }); - console.log(" ✅ Deleted products"); + console.log(" ✅ Deleted products"); - const stripeProducts = await stripeCli.products.list({ - limit: 100, - active: true, - }); + const stripeProducts = await stripeCli.products.list({ + limit: 100, + active: true, + }); - const batchSize = 5; - const removeStripeProduct = async (product: Stripe.Product) => { - try { - await stripeCli.products.del(product.id); - } catch (error) { - await stripeCli.products.update(product.id, { - active: false, - }); - } - }; + const batchSize = 5; + const removeStripeProduct = async (product: Stripe.Product) => { + try { + await stripeCli.products.del(product.id); + } catch (error) { + await stripeCli.products.update(product.id, { + active: false, + }); + } + }; - for (let i = 0; i < stripeProducts.data.length; i += batchSize) { - const batch = stripeProducts.data.slice(i, i + batchSize); - const batchDeleteProducts = []; - for (const product of batch) { - batchDeleteProducts.push(removeStripeProduct(product)); - } - await Promise.all(batchDeleteProducts); - // await timeout(800); - console.log( - ` ✅ Deleted ${i + batch.length}/${ - stripeProducts.data.length - } Stripe products` - ); - } + for (let i = 0; i < stripeProducts.data.length; i += batchSize) { + const batch = stripeProducts.data.slice(i, i + batchSize); + const batchDeleteProducts = []; + for (const product of batch) { + batchDeleteProducts.push(removeStripeProduct(product)); + } + await Promise.all(batchDeleteProducts); + // await timeout(800); + console.log( + ` ✅ Deleted ${i + batch.length}/${ + stripeProducts.data.length + } Stripe products`, + ); + } - console.log(" ✅ Deleted Stripe products"); + console.log(" ✅ Deleted Stripe products"); - await deleteAllStripeTestClocks({ stripeCli }); - console.log(" ✅ Deleted Stripe test clocks"); + await deleteAllStripeTestClocks({ stripeCli }); + console.log(" ✅ Deleted Stripe test clocks"); - // Delete all stripe meters - await deactivateStripeMeters({ org, env }); - console.log(" ✅ Deactivated Stripe meters"); + // Delete all stripe meters + await deactivateStripeMeters({ org, env }); + console.log(" ✅ Deactivated Stripe meters"); - // Batch delete coupons + // Batch delete coupons - const batchDeleteCoupons = []; - await RewardService.deleteByOrgId({ db, orgId, env }); + const batchDeleteCoupons = []; + await RewardService.deleteByOrgId({ db, orgId, env }); - const stripeCoupons = await stripeCli.coupons.list({ - limit: 100, - }); - for (const coupon of stripeCoupons.data) { - batchDeleteCoupons.push(stripeCli.coupons.del(coupon.id)); - } + const stripeCoupons = await stripeCli.coupons.list({ + limit: 100, + }); + for (const coupon of stripeCoupons.data) { + batchDeleteCoupons.push(stripeCli.coupons.del(coupon.id)); + } - await Promise.all(batchDeleteCoupons); - console.log(" ✅ Deleted Stripe coupons"); + await Promise.all(batchDeleteCoupons); + console.log(" ✅ Deleted Stripe coupons"); - await FeatureService.deleteByOrgId({ db, orgId, env }); + await FeatureService.deleteByOrgId({ db, orgId, env }); - console.log(`✅ Cleared org ${orgSlug} (${env})`); + console.log(`✅ Cleared org ${orgSlug} (${env})`); - await client.end(); - return org; + await client.end(); + return org; }; export const setupOrg = async ({ - orgId, - env, - features, - products, - rewards, - rewardTriggers, + orgId, + env, + features, + products, + rewards, + rewardTriggers, }: { - orgId: string; - env: AppEnv; - features: Record; - products: Record; - rewards: Record; - rewardTriggers: Record; + orgId: string; + env: AppEnv; + features: Record; + products: Record; + rewards: Record; + rewardTriggers: Record; }) => { - const axiosInstance = getAxiosInstance(); - const { client, db } = initDrizzle(); + const axiosInstance = getAxiosInstance(); + const { client, db } = initDrizzle(); - const autumn = new AutumnInt(); + const autumn = new AutumnInt(); - let insertFeatures = []; - for (const feature of Object.values(features)) { - insertFeatures.push(axiosInstance.post("/v1/internal_features", feature)); - } + const insertFeatures = []; + for (const feature of Object.values(features)) { + insertFeatures.push(axiosInstance.post("/v1/internal_features", feature)); + } - await Promise.all(insertFeatures); + await Promise.all(insertFeatures); - await FeatureService.insert({ - db, - data: Object.values(v2Features), - logger: console, - }); + await FeatureService.insert({ + db, + data: Object.values(v2Features), + logger: console, + }); - // const allFeatures = await FeatureService.list({ db, orgId, env }); + // const allFeatures = await FeatureService.list({ db, orgId, env }); - let org: Organization | null = null; - let newFeatures: Feature[] = []; - try { - org = await OrgService.get({ db, orgId }); - await OrgService.update({ - db, - orgId, - updates: { - config: { - ...org.config, - bill_upgrade_immediately: true, - }, - }, - }); + let org: Organization | null = null; + let newFeatures: Feature[] = []; + try { + org = await OrgService.get({ db, orgId }); + await OrgService.update({ + db, + orgId, + updates: { + config: { + ...org.config, + bill_upgrade_immediately: true, + }, + }, + }); - newFeatures = (await FeatureService.list({ db, orgId, env })).filter((f) => - Object.keys(features).includes(f.id) - ); - } catch (error) { - console.error("Error updating org", error); - } + newFeatures = (await FeatureService.list({ db, orgId, env })).filter((f) => + Object.keys(features).includes(f.id), + ); + } catch (error) { + console.error("Error updating org", error); + } - for (const feature of newFeatures!) { - features[feature.id].internal_id = feature.internal_id; + for (const feature of newFeatures!) { + features[feature.id].internal_id = feature.internal_id; - if (feature.type === FeatureType.Metered) { - features[feature.id].eventName = feature.config?.filters[0].value[0]; - } - } + if (feature.type === FeatureType.Metered) { + features[feature.id].eventName = feature.config?.filters[0].value[0]; + } + } - console.log("✅ Inserted features"); + console.log("✅ Inserted features"); - // 2. Create products - let insertProducts = []; + // 2. Create products + const insertProducts = []; - const productValues = Object.values(products); - const batchSize = 5; + const productValues = Object.values(products); + const batchSize = 5; - for ( - let batchStart = 0; - batchStart < productValues.length; - batchStart += batchSize - ) { - const batch = productValues.slice(batchStart, batchStart + batchSize); - const batchPromises = []; + for ( + let batchStart = 0; + batchStart < productValues.length; + batchStart += batchSize + ) { + const batch = productValues.slice(batchStart, batchStart + batchSize); + const batchPromises = []; - for (const product of batch) { - const insertProduct = async () => { - await autumn.products.create({ - id: product.id, - name: product.name, - group: product.group, - is_add_on: product.is_add_on, - is_default: product.is_default, - }); + for (const product of batch) { + const insertProduct = async () => { + await autumn.products.create({ + id: product.id, + name: product.name, + group: product.group, + is_add_on: product.is_add_on, + is_default: product.is_default, + }); - const prices = product.prices.map((p: any) => ({ - ...p, - config: { - ...p.config, - internal_feature_id: newFeatures!.find( - (f) => f.id === (p.config as any)?.feature_id - )?.internal_id, - }, - })); + const prices = product.prices.map((p: any) => ({ + ...p, + config: { + ...p.config, + internal_feature_id: newFeatures!.find( + (f) => f.id === (p.config as any)?.feature_id, + )?.internal_id, + }, + })); - const entitlements = Object.values(product.entitlements).map( - (ent: any) => ({ - ...ent, - internal_feature_id: newFeatures!.find( - (f) => f.id === ent.feature_id - )?.internal_id, - }) - ); + const entitlements = Object.values(product.entitlements).map( + (ent: any) => ({ + ...ent, + internal_feature_id: newFeatures!.find( + (f) => f.id === ent.feature_id, + )?.internal_id, + }), + ); - const entWithFeatures = entitlements.map((ent) => ({ - ...ent, - feature: newFeatures!.find((f) => f.id === ent.feature_id), - })); + const entWithFeatures = entitlements.map((ent) => ({ + ...ent, + feature: newFeatures!.find((f) => f.id === ent.feature_id), + })); - let items = mapToProductItems({ - prices, - entitlements: entWithFeatures, - allowFeatureMatch: true, - features: newFeatures!, - }); + const items = mapToProductItems({ + prices, + entitlements: entWithFeatures, + allowFeatureMatch: true, + features: newFeatures!, + }); - try { - await axiosInstance.post(`/v1/products/${product.id}`, { - // prices: prices, - // entitlements: entitlements, - items, - free_trial: product.free_trial, - }); - } catch (error) { - console.log("Product:", product.name); - console.error("Error creating product prices / ents"); - console.log("Items", items); - } - return; - }; + try { + await axiosInstance.post(`/v1/products/${product.id}`, { + // prices: prices, + // entitlements: entitlements, + items, + free_trial: product.free_trial, + }); + } catch (error) { + console.log("Product:", product.name); + console.error("Error creating product prices / ents"); + console.log("Items", items); + } + return; + }; - batchPromises.push(insertProduct()); - } + batchPromises.push(insertProduct()); + } - // Wait for the current batch to complete before proceeding to the next - await Promise.all(batchPromises); - insertProducts.push(...batchPromises); - } + // Wait for the current batch to complete before proceeding to the next + await Promise.all(batchPromises); + insertProducts.push(...batchPromises); + } - await Promise.all(insertProducts); - console.log("✅ Inserted products"); + await Promise.all(insertProducts); + console.log("✅ Inserted products"); - if (process.env.MOCHA_PARALLEL === "true") { - console.log("MOCHA RUNNING IN PARALLEL"); - await AutumnCli.initStripeProducts(); - console.log("✅ Initialized stripe products / prices"); - } else { - console.log("MOCHA RUNNING IN SERIAL"); - } + if (process.env.MOCHA_PARALLEL === "true") { + console.log("MOCHA RUNNING IN PARALLEL"); + await AutumnCli.initStripeProducts(); + console.log("✅ Initialized stripe products / prices"); + } else { + console.log("MOCHA RUNNING IN SERIAL"); + } - // return; + // return; - // Fetch all products - const { list: allProducts } = await AutumnCli.getProducts(); - const productIds = allProducts.map((p: any) => p.id); + // Fetch all products + const { list: allProducts } = await AutumnCli.getProducts(); + const productIds = allProducts.map((p: any) => p.id); - // Insert coupons - let insertCoupons = []; - for (const reward of Object.values(rewards)) { - const createReward = async () => { - let priceIds = []; + // Insert coupons + const insertCoupons = []; + for (const reward of Object.values(rewards)) { + const createReward = async () => { + let priceIds = []; - let rewardData: any = { - id: reward.id, - name: reward.name, - promo_codes: [ - { - code: reward.id, - }, - ], - type: reward.type, - }; + const rewardData: any = { + id: reward.id, + name: reward.name, + promo_codes: [ + { + code: reward.id, + }, + ], + type: reward.type, + }; - if (reward.type === RewardType.FreeProduct) { - rewardData.free_product_id = reward.free_product_id; - } else { - if (reward.only_usage_prices) { - let filteredProducts = allProducts.filter((product: FullProduct) => { - if (reward.product_ids) { - return reward.product_ids.includes(product.id); - } else return true; - }); + if (reward.type === RewardType.FreeProduct) { + rewardData.free_product_id = reward.free_product_id; + rewardData.free_product_config = reward.free_product_config; + } else { + if (reward.only_usage_prices) { + const filteredProducts = allProducts.filter( + (product: FullProduct) => { + if (reward.product_ids) { + return reward.product_ids.includes(product.id); + } else return true; + }, + ); - priceIds = filteredProducts.flatMap((product: FullProduct) => - product.prices - .filter((price: Price) => price.config!.type === PriceType.Usage) - .map((price) => { - return price.id; - }) - ); - } else if (reward.product_ids) { - priceIds = allProducts - .filter((product: FullProduct) => - reward.product_ids.includes(product.id) - ) - .flatMap((product: FullProduct) => - product.prices.map((price) => price.id) - ); - } + priceIds = filteredProducts.flatMap((product: FullProduct) => + product.prices + .filter((price: Price) => price.config!.type === PriceType.Usage) + .map((price) => { + return price.id; + }), + ); + } else if (reward.product_ids) { + priceIds = allProducts + .filter((product: FullProduct) => + reward.product_ids.includes(product.id), + ) + .flatMap((product: FullProduct) => + product.prices.map((price) => price.id), + ); + } - rewardData.discount_config = { - discount_value: reward.discount_config.discount_value, - duration_type: reward.discount_config.duration_type, - duration_value: reward.discount_config.duration_value, - apply_to_all: reward.discount_config.apply_to_all, - price_ids: priceIds, - }; - } + rewardData.discount_config = { + discount_value: reward.discount_config.discount_value, + duration_type: reward.discount_config.duration_type, + duration_value: reward.discount_config.duration_value, + apply_to_all: reward.discount_config.apply_to_all, + price_ids: priceIds, + }; + } - const newReward: CreateReward & { internal_id: string } = { - internal_id: reward.id, - id: reward.id, - name: reward.name, - promo_codes: [ - { - code: reward.id, - }, - ], - type: reward.type, - discount_config: rewardData.discount_config, - free_product_id: rewardData.free_product_id, - }; + const newReward: CreateReward & { internal_id: string } = { + internal_id: reward.id, + id: reward.id, + name: reward.name, + promo_codes: [ + { + code: reward.id, + }, + ], + 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, + }; - let rewardRes = await autumn.rewards.create(newReward); + const rewardRes = await autumn.rewards.create(newReward); - return { - id: reward.id, - rewardRes, - }; - }; + return { + id: reward.id, + rewardRes, + }; + }; - console.log("Creating reward", reward.id); - insertCoupons.push(createReward()); - } + console.log("Creating reward", reward.id); + insertCoupons.push(createReward()); + } - await Promise.all(insertCoupons); - console.log("✅ Inserted coupons"); + await Promise.all(insertCoupons); + console.log("✅ Inserted coupons"); - // CREATE REWARD TRIGGERS - let insertRewardTriggers = []; - let insertedRewards = await RewardService.list({ db, orgId, env }); - for (const rewardTrigger of Object.values(rewardTriggers)) { - let rt = { - ...rewardTrigger, - internal_reward_id: insertedRewards.find( - (r) => r.id === rewardTrigger.internal_reward_id - )?.internal_id!, - }; - insertRewardTriggers.push(autumn.rewardPrograms.create(rt)); - } - await Promise.all(insertRewardTriggers); - console.log("✅ Inserted reward triggers"); + // CREATE REWARD TRIGGERS + const insertRewardTriggers = []; + const insertedRewards = await RewardService.list({ db, orgId, env }); + for (const rewardTrigger of Object.values(rewardTriggers)) { + const rt = { + ...rewardTrigger, + internal_reward_id: insertedRewards.find( + (r) => r.id === rewardTrigger.internal_reward_id, + )?.internal_id!, + }; + insertRewardTriggers.push(autumn.rewardPrograms.create(rt)); + } + await Promise.all(insertRewardTriggers); + console.log("✅ Inserted reward triggers"); - await client.end(); + await client.end(); }; From 76c1dfa6d5b9d877d75304e45928a983daede4e3 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:42:16 +0100 Subject: [PATCH 14/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20allow=20clearing?= =?UTF-8?q?=20of=20redemptions=20in=20db=20for=20testing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rewards/RewardRedemptionService.ts | 391 +++++++++--------- 1 file changed, 201 insertions(+), 190 deletions(-) diff --git a/server/src/internal/rewards/RewardRedemptionService.ts b/server/src/internal/rewards/RewardRedemptionService.ts index e3fcfc63e..4e8a29d00 100644 --- a/server/src/internal/rewards/RewardRedemptionService.ts +++ b/server/src/internal/rewards/RewardRedemptionService.ts @@ -1,217 +1,228 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { notNullish } from "@/utils/genUtils.js"; import { customers, ErrCode, + type RewardRedemption, + type RewardTriggerEvent, referralCodes, rewardPrograms, - RewardRedemption, - rewardRedemptions, - rewards, - RewardTriggerEvent, + rewardRedemptions } from "@autumn/shared"; -import { and, eq } from "drizzle-orm"; +import { and, eq, inArray } 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); - let 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( - rewardPrograms, - eq( - rewardRedemptions.internal_reward_program_id, - rewardPrograms.internal_id, - ), - ) - .where( - and( - eq(referralCodes.internal_customer_id, internalCustomerId), - eq(rewardRedemptions.triggered, true), - eq(rewardRedemptions.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( + rewardPrograms, + eq( + rewardRedemptions.internal_reward_program_id, + rewardPrograms.internal_id, + ), + ) + .where( + and( + eq(referralCodes.internal_customer_id, internalCustomerId), + eq(rewardRedemptions.triggered, true), + eq(rewardRedemptions.applied, false), + ), + ); - if (data.length == 0) return []; + if (data.length === 0) return []; - let 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)); + } } From 30056aa6f11264e863f580ba14b79d0db59bddcd Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 10 Sep 2025 17:44:33 +0100 Subject: [PATCH 15/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20paid=20products=20?= =?UTF-8?q?for=20N=20months=20in=20reward=20programs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../addProductFlow/createStripeSub2.ts | 295 +++++----- .../addProductFlow/handleAddProduct.ts | 173 +++--- .../addProductFlow/handlePaidProduct.ts | 423 ++++++------- .../upgradeFlow/updateStripeSub2.ts | 248 ++++---- .../attachParams/convertToParams.ts | 299 ++++++---- .../attach/attachUtils/getAttachFunction.ts | 459 ++++++++------- .../customers/cusProducts/AttachParams.ts | 162 ++--- server/src/internal/rewards/RewardService.ts | 292 ++++----- server/src/internal/rewards/referralUtils.ts | 554 +++++++++++------- 9 files changed, 1554 insertions(+), 1351 deletions(-) diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts index 578d877bf..d28fbc5ad 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/createStripeSub2.ts @@ -1,163 +1,186 @@ -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode, Reward, IntervalConfig, AttachConfig } from "@autumn/shared"; -import Stripe from "stripe"; -import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { SubService } from "@/internal/subscriptions/SubService.js"; -import { generateId } from "@/utils/genUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; - import { - getLatestPeriodStart, - getEarliestPeriodEnd, + type AttachConfig, + ErrCode +} from "@autumn/shared"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { + getEarliestPeriodEnd, + getLatestPeriodStart, } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; - -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { ItemSet } from "@/utils/models/ItemSet.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { buildInvoiceMemoFromEntitlements } from "@/internal/invoices/invoiceMemoUtils.js"; +import { + freeTrialToStripeTimestamp, + rewardTrialToStripeTimestamp, +} from "@/internal/products/free-trials/freeTrialUtils.js"; +import { SubService } from "@/internal/subscriptions/SubService.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { generateId } from "@/utils/genUtils.js"; +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 } = attachParams; - const isDefaultTrial = freeTrial && !freeTrial.card_required; + const { customer, invoiceOnly, freeTrial, org, now, rewards, rewardTrial } = + attachParams; + const isDefaultTrial = freeTrial && !freeTrial.card_required; - let shouldErrorIfNoPm = !invoiceOnly; - if (isDefaultTrial) shouldErrorIfNoPm = false; + let shouldErrorIfNoPm = !invoiceOnly; + if (isDefaultTrial) shouldErrorIfNoPm = false; + if (rewardTrial) shouldErrorIfNoPm = false; - let paymentMethod = await getCusPaymentMethod({ - stripeCli, - stripeId: customer.processor.id, - errorIfNone: shouldErrorIfNoPm, - }); + const paymentMethod = await getCusPaymentMethod({ + stripeCli, + stripeId: customer.processor.id, + errorIfNone: shouldErrorIfNoPm, + }); - 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; + console.log("paymentMethod", paymentMethod?.id, "shouldErrorIfNoPm", shouldErrorIfNoPm, "rewardTrial", rewardTrial); - const discounts = rewards - ? rewards.map((reward) => ({ coupon: reward.id })) - : undefined; + const { subItems, invoiceItems, usageFeatures } = itemSet; - try { - const subscription = await stripeCli.subscriptions.create({ - ...paymentMethodData, - customer: customer.processor.id, - items: sanitizeSubItems(subItems), + const discounts = rewards + ? rewards.map((reward) => ({ coupon: reward.id })) + : undefined; - billing_mode: { type: "flexible" }, - trial_end: freeTrialToStripeTimestamp({ freeTrial, now }), - 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, + try { + const subscription = await stripeCli.subscriptions.create({ + ...paymentMethodData, + customer: customer.processor.id, + items: sanitizeSubItems(subItems), - discounts, - expand: ["latest_invoice"], + 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, - trial_settings: - freeTrial && !freeTrial.card_required - ? { - end_behavior: { - missing_payment_method: "cancel", - }, - } - : undefined, - }); + discounts, + expand: ["latest_invoice"], - const latestInvoice = subscription.latest_invoice as Stripe.Invoice; + ...{ + trial_settings: + freeTrial && !freeTrial.card_required + ? { + end_behavior: { + missing_payment_method: "cancel", + }, + } + : undefined, - 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 }); - } - } + trial_end: freeTrialToStripeTimestamp({ freeTrial, now }), + }, - if ( - invoiceOnly && - config.invoiceCheckout && - config.finalizeInvoice && - latestInvoice && - latestInvoice.status === "draft" - ) { - subscription.latest_invoice = await stripeCli.invoices.finalizeInvoice( - (subscription.latest_invoice as Stripe.Invoice).id! - ); - } + ...{ + trial_settings: rewardTrial + ? { + end_behavior: { + missing_payment_method: "cancel", + }, + } + : undefined, - // Store - const earliestPeriodEnd = getEarliestPeriodEnd({ sub: subscription }); - const currentPeriodStart = getLatestPeriodStart({ sub: subscription }); + trial_end: rewardTrialToStripeTimestamp({ rewardTrial, now }), + }, + }); - 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, - }, - }); + const latestInvoice = subscription.latest_invoice as Stripe.Invoice; - 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); + 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 }); + } + } - throw new RecaseError({ - code: ErrCode.CreateStripeSubscriptionFailed, - message: `Create stripe subscription failed ${ - error.code ? `(${error.code})` : "" - }: ${error.message || ""}`, - statusCode: 500, - }); - } + 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 }); + + 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); + + 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/handleAddProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts index 5be2d8dc9..b0063a390 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.ts @@ -1,111 +1,114 @@ -import { createFullCusProduct } from "../../../add-product/createFullCusProduct.js"; - import { - AttachParams, + 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, } from "../../../cusProducts/AttachParams.js"; -import { APIVersion, AttachBranch, AttachConfig } from "@autumn/shared"; -import { attachToInsertParams } from "@/internal/products/productUtils.js"; -import { SuccessCode } from "@autumn/shared"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { handlePaidProduct } from "./handlePaidProduct.js"; import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js"; import { getDefaultAttachConfig } from "../../attachUtils/getAttachConfig.js"; import { getMergeCusProduct } from "./getMergeCusProduct.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { handlePaidProduct } from "./handlePaidProduct.js"; export const handleAddProduct = 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 logger = req.logtail; - const { customer, products, prices } = attachParams; + const logger = req.logtail; + const { customer, products, prices } = attachParams; - const defaultConfig: AttachConfig = getDefaultAttachConfig(); + const defaultConfig: AttachConfig = getDefaultAttachConfig(); - // 1. If paid product + // 1. If paid product - if (prices.length > 0) { - await handlePaidProduct({ - req, - res, - attachParams, - config: config || defaultConfig, - }); + if (prices.length > 0) { + await handlePaidProduct({ + req, + res, + attachParams, + config: config || defaultConfig, + }); - return; - } + return; + } - logger.info("Inserting free product in handleAddProduct"); + logger.info("Inserting free product in handleAddProduct"); - const batchInsert = []; + const batchInsert = []; - const { mergeCusProduct, mergeSub } = await getMergeCusProduct({ - attachParams, - config: config || defaultConfig, - products, - }); + const { mergeSub } = await getMergeCusProduct({ + attachParams, + config: config || defaultConfig, + products, + }); - // console.log("Free trial:", attachParams.freeTrial); - // throw new Error("test"); + // console.log("Free trial:", attachParams.freeTrial); + // throw new Error("test"); - for (const product of products) { - let curCusProduct = attachParamsToCurCusProduct({ attachParams }); - let anchorToUnix = undefined; + for (const product of products) { + const curCusProduct = attachParamsToCurCusProduct({ attachParams }); + let anchorToUnix; - if (curCusProduct && config?.branch == AttachBranch.NewVersion) { - anchorToUnix = curCusProduct.created_at; - } + if (curCusProduct && config?.branch === AttachBranch.NewVersion) { + anchorToUnix = curCusProduct.created_at; + } - if (mergeSub) { - const { end } = subToPeriodStartEnd({ sub: mergeSub }); - anchorToUnix = end * 1000; - } + if (mergeSub) { + const { end } = subToPeriodStartEnd({ sub: mergeSub }); + anchorToUnix = end * 1000; + } - // Expire previous product + // Expire previous product - batchInsert.push( - createFullCusProduct({ - db: req.db, - attachParams: attachToInsertParams(attachParams, product), - billLaterOnly: true, - carryExistingUsages: config?.carryUsage || false, - anchorToUnix, - logger, - }) - ); - } - await Promise.all(batchInsert); + batchInsert.push( + createFullCusProduct({ + db: req.db, + attachParams: attachToInsertParams(attachParams, product), + billLaterOnly: true, + carryExistingUsages: config?.carryUsage || false, + anchorToUnix, + logger, + }), + ); + } + await Promise.all(batchInsert); - logger.info("Successfully created full cus product"); + logger.info("Successfully created full cus product"); - if (res) { - let apiVersion = attachParams.org.api_version || 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({ - success: true, - code: SuccessCode.FreeProductAttached, - message: `Successfully attached ${productNames} to ${customerName}`, - product_ids: products.map((p) => p.id), - customer_id: customer.id || customer.internal_id, - }) - ); - } else { - res.status(200).json({ - success: true, - }); - } - } + if (res) { + const apiVersion = attachParams.org.api_version || 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({ + success: true, + code: SuccessCode.FreeProductAttached, + message: `Successfully attached ${productNames} to ${customerName}`, + product_ids: products.map((p) => p.id), + customer_id: customer.id || customer.internal_id, + }), + ); + } else { + res.status(200).json({ + success: true, + }); + } + } }; diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts index b7af249fc..aea783ccc 100644 --- a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts @@ -1,252 +1,257 @@ -import RecaseError from "@/utils/errorUtils.js"; - +import { + APIVersion, + type AttachConfig, + AttachScenario, + ErrCode, + isTrialing, + SuccessCode, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; +import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js"; import { - AttachParams, + type AttachParams, AttachResultSchema, } from "@/internal/customers/cusProducts/AttachParams.js"; import { attachToInvoiceResponse, insertInvoiceFromAttach, } from "@/internal/invoices/invoiceUtils.js"; -import { attachToInsertParams } from "@/internal/products/productUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - APIVersion, - AttachBranch, - AttachConfig, - AttachScenario, - ErrCode, - SuccessCode, -} from "@autumn/shared"; -import Stripe from "stripe"; -import { - getEarliestPeriodEnd, - subToPeriodStartEnd, -} from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; -import { createStripeSub2 } from "./createStripeSub2.js"; -import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; - -import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js"; -import { getCustomerSub } from "../../attachUtils/convertAttachParams.js"; -import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js"; -import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js"; -import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js"; -import { isTrialing } from "@autumn/shared"; +import { rewardTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; import { addIntervalToAnchor } from "@/internal/products/prices/billingIntervalUtils2.js"; +import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js"; +import { attachToInsertParams } from "@/internal/products/productUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { getCustomerSub } from "../../attachUtils/convertAttachParams.js"; +import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js"; +import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js"; +import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js"; +import { createStripeSub2 } from "./createStripeSub2.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; - let { - 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, + }); - let 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 = null; - let trialEndsAt = undefined; + let sub: Stripe.Subscription | null = null; + let schedule: Stripe.SubscriptionSchedule | null = null; + let trialEndsAt; - // 1. If merge sub - if (mergeSub && !config.disableMerge) { - if (mergeCusProduct?.free_trial) { - trialEndsAt = isTrialing({ - cusProduct: mergeCusProduct, - now: attachParams.now, - }) - ? mergeCusProduct.trial_ends_at - : undefined; - } + // 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; + attachParams.freeTrial = null; - // 1. If merged sub is canceled, also add to current schedule - const newItemSet = await paramsToSubItems({ - req, - sub: mergeSub, - attachParams, - config, - }); + // 1. If merged sub is canceled, also add to current schedule + const newItemSet = await paramsToSubItems({ + req, + sub: mergeSub, + attachParams, + config, + }); - const { updatedSub } = await updateStripeSub2({ - req, - attachParams, - curSub: mergeSub, - itemSet: newItemSet, - config, - fromCreate: true, - }); + const { updatedSub } = await updateStripeSub2({ + req, + attachParams, + curSub: mergeSub, + itemSet: newItemSet, + config, + fromCreate: true, + }); - sub = updatedSub; + sub = updatedSub; - if (mergeSub.cancel_at) { - 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), - }); - } + if (mergeSub.cancel_at) { + 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), + }); + } - // 1. - } else { - let billingCycleAnchorUnix = undefined; - const smallestInterval = getSmallestInterval({ - prices: attachParams.prices, - }); + // 1. + } else { + let billingCycleAnchorUnix; + 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; + } - try { - sub = await createStripeSub2({ - db: req.db, - stripeCli, - attachParams, - itemSet, - billingCycleAnchorUnix, - config, - logger, - }); - } catch (error: any) { - if ( - error instanceof RecaseError && - !invoiceOnly && - error.code == ErrCode.CreateStripeSubscriptionFailed - ) { - return await handleCreateCheckout({ - req, - res, - attachParams, - config, - }); - } + try { + sub = await createStripeSub2({ + db: req.db, + stripeCli, + attachParams, + itemSet, + billingCycleAnchorUnix, + config, + logger, + }); + } catch (error: any) { + if ( + error instanceof RecaseError && + !invoiceOnly && + error.code === ErrCode.CreateStripeSubscriptionFailed + ) { + logger.error("CREATE STRIPE SUB: error creating stripe subscription", { + error, + }); + return await handleCreateCheckout({ + req, + res, + attachParams, + config, + }); + } - throw error; - } - } + throw error; + } + } - subscriptions.push(sub); + subscriptions.push(sub); - let invoice: Stripe.Invoice | undefined; - if (sub?.latest_invoice) { - invoice = await insertInvoiceFromAttach({ - db: req.db, - stripeInvoice: sub.latest_invoice as Stripe.Invoice, - attachParams, - logger, - }); - } + let invoice: Stripe.Invoice | undefined; + if (sub?.latest_invoice) { + invoice = await insertInvoiceFromAttach({ + db: req.db, + stripeInvoice: sub.latest_invoice as Stripe.Invoice, + attachParams, + logger, + }); + } - 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 || 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) { - let 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/upgradeFlow/updateStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts index 614063fbf..04635463f 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts @@ -1,155 +1,153 @@ -import Stripe from "stripe"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { AttachConfig, ProrationBehavior } from "@autumn/shared"; +import { type AttachConfig, ProrationBehavior } from "@autumn/shared"; +import type Stripe from "stripe"; +import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; +import { createProrationInvoice } from "@/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; +import { SubService } from "@/internal/subscriptions/SubService.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { nullish } from "@/utils/genUtils.js"; +import type { ItemSet } from "@/utils/models/ItemSet.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js"; +import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js"; import { createUsageInvoiceItems, resetUsageBalances, } from "../upgradeDiffIntFlow/createUsageInvoiceItems.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { createProrationInvoice } from "@/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.js"; -import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js"; -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { getContUseInvoiceItems } from "../../attachUtils/getContUseItems/getContUseInvoiceItems.js"; -import { ItemSet } from "@/utils/models/ItemSet.js"; -import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { SubService } from "@/internal/subscriptions/SubService.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { nullish } from "@/utils/genUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; export const updateStripeSub2 = async ({ - req, - attachParams, - config, - curSub, - itemSet, - fromCreate = false, + req, + attachParams, + config, + curSub, + itemSet, + fromCreate = false, }: { - req: ExtendedRequest; - attachParams: AttachParams; - config: AttachConfig; - curSub: Stripe.Subscription; - itemSet: ItemSet; - fromCreate?: boolean; + req: ExtendedRequest; + attachParams: AttachParams; + config: AttachConfig; + curSub: Stripe.Subscription; + itemSet: ItemSet; + fromCreate?: boolean; }) => { - const { db, logger } = req; + const { db, logger } = req; - const { stripeCli, customer, org, paymentMethod } = attachParams; - const { invoiceOnly, proration } = config; + const { stripeCli, customer, org, paymentMethod } = attachParams; + const { invoiceOnly, proration } = config; - if (!invoiceOnly && !attachParams.fromCancel && nullish(paymentMethod)) { - throw new RecaseError({ - message: "Payment method is required", - code: "payment_method_required", - }); - } + if (!invoiceOnly && !attachParams.fromCancel && nullish(paymentMethod)) { + throw new RecaseError({ + message: "Payment method is required", + code: "payment_method_required", + }); + } - if (curSub.billing_mode.type !== "flexible") { - curSub = await stripeCli.subscriptions.migrate(curSub.id, { - billing_mode: { type: "flexible" }, - }); - } + if (curSub.billing_mode.type !== "flexible") { + curSub = await stripeCli.subscriptions.migrate(curSub.id, { + billing_mode: { type: "flexible" }, + }); + } - let trialEnd = - config.disableTrial || config.carryTrial - ? undefined - : freeTrialToStripeTimestamp({ - freeTrial: attachParams.freeTrial, - now: attachParams.now, - }); + const trialEnd = + config.disableTrial || config.carryTrial + ? undefined + : freeTrialToStripeTimestamp({ + freeTrial: attachParams.freeTrial, + now: attachParams.now, + }); - // 1. Update subscription + // 1. Update subscription - let updatedSub = await stripeCli.subscriptions.update(curSub.id, { - items: sanitizeSubItems(itemSet.subItems), - proration_behavior: - proration == ProrationBehavior.None - ? "none" - : fromCreate - ? "always_invoice" - : "create_prorations", - // proration_behavior: "create_prorations", - trial_end: trialEnd, - // default_payment_method: paymentMethod?.id, - add_invoice_items: itemSet.invoiceItems, - ...((invoiceOnly && { - collection_method: "send_invoice", - days_until_due: 30, - }) as any), - payment_behavior: "error_if_incomplete", - expand: ["latest_invoice"], - }); + const updatedSub = await stripeCli.subscriptions.update(curSub.id, { + items: sanitizeSubItems(itemSet.subItems), + proration_behavior: + proration === ProrationBehavior.None + ? "none" + : fromCreate + ? "always_invoice" + : "create_prorations", + // proration_behavior: "create_prorations", + trial_end: trialEnd, + // default_payment_method: paymentMethod?.id, + add_invoice_items: itemSet.invoiceItems, + ...((invoiceOnly && { + collection_method: "send_invoice", + days_until_due: 30, + }) as any), + payment_behavior: "error_if_incomplete", + expand: ["latest_invoice"], + }); - let latestInvoice = updatedSub.latest_invoice as Stripe.Invoice | null; + let latestInvoice = updatedSub.latest_invoice as Stripe.Invoice | null; - await SubService.updateFromStripe({ db, stripeSub: updatedSub }); + await SubService.updateFromStripe({ db, stripeSub: updatedSub }); - if (proration == ProrationBehavior.None) { - return { - updatedSub, - latestInvoice: null, - }; - } + if (proration === ProrationBehavior.None) { + return { + updatedSub, + latestInvoice: null, + }; + } - if (fromCreate) { - return { - updatedSub, - latestInvoice: updatedSub.latest_invoice as Stripe.Invoice, - }; - } + if (fromCreate) { + return { + updatedSub, + latestInvoice: updatedSub.latest_invoice as Stripe.Invoice, + }; + } - const { curMainProduct } = attachParamToCusProducts({ attachParams }); + const { curMainProduct } = attachParamToCusProducts({ attachParams }); - // 2. Create prorations for single use items - let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({ - db, - attachParams, - cusProduct: curMainProduct!, - sub: curSub, - logger, - }); + // 2. Create prorations for single use items + const { invoiceItems, cusEntIds } = await createUsageInvoiceItems({ + db, + attachParams, + cusProduct: curMainProduct!, + sub: curSub, + logger, + }); - // // // 3. Create prorations for continuous use items - // let { replaceables, newItems } = await getContUseInvoiceItems({ - // attachParams, - // cusProduct: curMainProduct!, - // sub: curSub, - // logger, - // }); + // // // 3. Create prorations for continuous use items + // let { replaceables, newItems } = await getContUseInvoiceItems({ + // attachParams, + // cusProduct: curMainProduct!, + // sub: curSub, + // logger, + // }); - const { replaceables } = await createAndFilterContUseItems({ - attachParams, - curMainProduct: curMainProduct!, - sub: curSub, - logger, - }); + const { replaceables } = await createAndFilterContUseItems({ + attachParams, + curMainProduct: curMainProduct!, + sub: curSub, + logger, + }); - if (proration === ProrationBehavior.Immediately) { - latestInvoice = await createProrationInvoice({ - attachParams, - invoiceOnly, - curSub, - updatedSub, - logger, - }); + if (proration === ProrationBehavior.Immediately) { + latestInvoice = await createProrationInvoice({ + attachParams, + invoiceOnly, + curSub, + updatedSub, + logger, + }); - console.log(`FINALIZED INVOICE ${latestInvoice?.id}`); - console.log(latestInvoice?.lines.data.map((line) => line.description)); - } + console.log(`FINALIZED INVOICE ${latestInvoice?.id}`); + console.log(latestInvoice?.lines.data.map((line) => line.description)); + } - await resetUsageBalances({ - db, - cusEntIds, - cusProduct: curMainProduct!, - }); + await resetUsageBalances({ + db, + cusEntIds, + cusProduct: curMainProduct!, + }); - return { - updatedSub, - latestInvoice: latestInvoice, - cusEntIds, - replaceables, - }; + return { + updatedSub, + latestInvoice: latestInvoice, + cusEntIds, + replaceables, + }; }; // await SubService.addUsageFeatures({ diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts index 8ff2bf59d..e1d56e16e 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts @@ -1,158 +1,201 @@ import { - AttachParams, - InsertCusProductParams, + 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, } from "@/internal/customers/cusProducts/AttachParams.js"; -import { - cusProductToEnts, - cusProductToPrices, - cusProductToProduct, -} from "@autumn/shared"; import { newCusToFullCus } from "@/internal/customers/cusUtils/cusUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - Customer, - Entity, - FullCusProduct, - FullCustomer, - FullProduct, - FreeTrial, -} from "@autumn/shared"; -import Stripe from "stripe"; +import { isFreeProduct, isOneOff } 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: ExtendedRequest; + rewardProgram: FullRewardProgram; + customer: FullCustomer; + product: FullProduct; + org?: Organization; +}): AttachParams => { + const reward = rewardProgram.reward; + const isPaid = !isFreeProduct(product.prices); + const isRecurring = !isOneOff(product.prices); + console.log("isPaid in convertToParams", isPaid); + console.log("isRecurring in convertToParams", isRecurring); + + 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, + 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/getAttachFunction.ts b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts index 1f06e5ee8..5729d904c 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts @@ -1,23 +1,26 @@ +import { + type AttachBody, + AttachBranch, + type AttachConfig, + AttachFunction, + CusProductStatus, +} from "@autumn/shared"; import chalk from "chalk"; -import { AttachParams } from "../../cusProducts/AttachParams.js"; -import { AttachBranch, AttachFunction, CusProductStatus } from "@autumn/shared"; import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js"; +import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js"; +import type { AttachParams } from "../../cusProducts/AttachParams.js"; +import { CusProductService } from "../../cusProducts/CusProductService.js"; import { handleAddProduct } from "../attachFunctions/addProductFlow/handleAddProduct.js"; -import { AttachBody } from "@autumn/shared"; -import { AttachConfig } from "@autumn/shared"; +import { handleOneOffFunction } from "../attachFunctions/addProductFlow/handleOneOffFunction.js"; +import { handleRenewProduct } from "../attachFunctions/handleRenewProduct.js"; +import { handleMultiAttachFlow } from "../attachFunctions/multiAttach/handleMultiAttachFlow.js"; +import { handleScheduleFunction2 } from "../attachFunctions/scheduleFlow/handleScheduleFlow2.js"; import { handleUpdateQuantityFunction } from "../attachFunctions/updateQuantityFlow/updateQuantityFlow.js"; - +import { handleUpgradeFlow } from "../attachFunctions/upgradeFlow/handleUpgradeFlow.js"; import { attachParamsToCurCusProduct, attachParamToCusProducts, } from "./convertAttachParams.js"; -import { handleOneOffFunction } from "../attachFunctions/addProductFlow/handleOneOffFunction.js"; -import { CusProductService } from "../../cusProducts/CusProductService.js"; -import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js"; -import { handleUpgradeFlow } from "../attachFunctions/upgradeFlow/handleUpgradeFlow.js"; -import { handleScheduleFunction2 } from "../attachFunctions/scheduleFlow/handleScheduleFlow2.js"; -import { handleRenewProduct } from "../attachFunctions/handleRenewProduct.js"; -import { handleMultiAttachFlow } from "../attachFunctions/multiAttach/handleMultiAttachFlow.js"; /* 1. If from new version, free trial should just carry over @@ -27,252 +30,252 @@ import { handleMultiAttachFlow } from "../attachFunctions/multiAttach/handleMult */ export const getAttachFunction = async ({ - branch, - attachParams, - attachBody, - config, + branch, + attachParams, + attachBody, + config, }: { - branch: AttachBranch; - attachParams: AttachParams; - attachBody: AttachBody; - config: AttachConfig; + branch: AttachBranch; + attachParams: AttachParams; + attachBody: AttachBody; + config: AttachConfig; }) => { - const { onlyCheckout } = config; - const { curCusProduct } = attachParamToCusProducts({ - attachParams, - }); + const { onlyCheckout } = config; + const { curCusProduct } = attachParamToCusProducts({ + attachParams, + }); - // 1. Checkout function - const newScenario = [ - AttachBranch.MultiAttach, - AttachBranch.MultiProduct, - AttachBranch.OneOff, - AttachBranch.New, - AttachBranch.AddOn, - AttachBranch.MainIsFree, - AttachBranch.MainIsTrial, - ].includes(branch); + // 1. Checkout function + const newScenario = [ + AttachBranch.MultiAttach, + AttachBranch.MultiProduct, + AttachBranch.OneOff, + AttachBranch.New, + AttachBranch.AddOn, + AttachBranch.MainIsFree, + AttachBranch.MainIsTrial, + ].includes(branch); - if (newScenario && onlyCheckout) { - return AttachFunction.CreateCheckout; - } else if (branch == AttachBranch.OneOff) { - return AttachFunction.OneOff; - } else if ( - branch == AttachBranch.MultiAttach || - branch == AttachBranch.MultiAttachUpdate - ) { - return AttachFunction.MultiAttach; - } else if (newScenario) { - return AttachFunction.AddProduct; - } + if (newScenario && onlyCheckout) { + return AttachFunction.CreateCheckout; + } else if (branch == AttachBranch.OneOff) { + return AttachFunction.OneOff; + } else if ( + branch == AttachBranch.MultiAttach || + branch == AttachBranch.MultiAttachUpdate + ) { + return AttachFunction.MultiAttach; + } else if (newScenario) { + return AttachFunction.AddProduct; + } - // 2. Upgrade scenarios - let updateScenarios = [ - AttachBranch.NewVersion, - AttachBranch.SameCustom, - AttachBranch.SameCustomEnts, - AttachBranch.Upgrade, - ]; + // 2. Upgrade scenarios + const updateScenarios = [ + AttachBranch.NewVersion, + AttachBranch.SameCustom, + AttachBranch.SameCustomEnts, + AttachBranch.Upgrade, + ]; - if (updateScenarios.includes(branch)) { - if (config.sameIntervals) { - return AttachFunction.UpgradeSameInterval; - } else { - return AttachFunction.UpgradeDiffInterval; - } - } + if (updateScenarios.includes(branch)) { + if (config.sameIntervals) { + return AttachFunction.UpgradeSameInterval; + } else { + return AttachFunction.UpgradeDiffInterval; + } + } - // 3. Downgrade scenarios - if (branch == AttachBranch.Downgrade) { - return AttachFunction.ScheduleProduct; - } + // 3. Downgrade scenarios + if (branch == AttachBranch.Downgrade) { + return AttachFunction.ScheduleProduct; + } - // 4. Prepaid scenarios - if (branch == AttachBranch.UpdatePrepaidQuantity) { - let curSameProduct = attachParamsToCurCusProduct({ attachParams }); - if (curSameProduct?.free_trial) { - attachParams.freeTrial = curSameProduct.free_trial; - } - return AttachFunction.UpdatePrepaidQuantity; - } + // 4. Prepaid scenarios + if (branch == AttachBranch.UpdatePrepaidQuantity) { + const curSameProduct = attachParamsToCurCusProduct({ attachParams }); + if (curSameProduct?.free_trial) { + attachParams.freeTrial = curSameProduct.free_trial; + } + return AttachFunction.UpdatePrepaidQuantity; + } - if (branch == AttachBranch.Renew) { - return AttachFunction.Renew; - } + if (branch == AttachBranch.Renew) { + return AttachFunction.Renew; + } - return AttachFunction.AddProduct; + return AttachFunction.AddProduct; }; export const runAttachFunction = async ({ - req, - res, - branch, - attachParams, - attachBody, - config, + req, + res, + branch, + attachParams, + attachBody, + config, }: { - req: any; - res: any; - branch: AttachBranch; - attachParams: AttachParams; - attachBody: AttachBody; - config: AttachConfig; + req: any; + res: any; + branch: AttachBranch; + attachParams: AttachParams; + attachBody: AttachBody; + config: AttachConfig; }) => { - const { logtail: logger, db } = req; - const { stripeCli } = attachParams; + const { logtail: logger, db } = req; + const { stripeCli } = attachParams; - const attachFunction = await getAttachFunction({ - branch, - attachParams, - attachBody, - config, - }); + const attachFunction = await getAttachFunction({ + branch, + attachParams, + attachBody, + config, + }); - // console.log("Attach Function:", attachFunction); - // throw new Error("Attach Function:"); + // console.log("Attach Function:", attachFunction); + // throw new Error("Attach Function:"); - const customer = attachParams.customer; - const org = attachParams.org; + const customer = attachParams.customer; + const org = attachParams.org; - const productIdsStr = attachParams.products.map((p) => p.id).join(", "); - const { curMainProduct, curSameProduct, curScheduledProduct } = - attachParamToCusProducts({ - attachParams, - }); + const productIdsStr = attachParams.products.map((p) => p.id).join(", "); + const { curMainProduct, curSameProduct, curScheduledProduct } = + attachParamToCusProducts({ + attachParams, + }); - const curCusProduct = attachParamsToCurCusProduct({ attachParams }); + const curCusProduct = attachParamsToCurCusProduct({ attachParams }); - logger.info(`--------------------------------`); - logger.info( - `ATTACHING ${productIdsStr} to ${customer.name} (${customer.id || customer.email}), org: ${org.slug}\n` - ); - if (customer.entity) { - logger.info(`Entity: ${customer.entity.name} (${customer.entity.id})`); - } - logger.info( - `Branch: ${chalk.yellow(branch)}, Function: ${chalk.yellow(attachFunction)}` - ); + logger.info(`--------------------------------`); + logger.info( + `ATTACHING ${productIdsStr} to ${customer.name} (${customer.id || customer.email}), org: ${org.slug}\n`, + ); + if (customer.entity) { + logger.info(`Entity: ${customer.entity.name} (${customer.entity.id})`); + } + logger.info( + `Branch: ${chalk.yellow(branch)}, Function: ${chalk.yellow(attachFunction)}`, + ); - if (curMainProduct) { - logger.info(`→ Current Main Product: ${curMainProduct.product.id}`); - } - if (curSameProduct) { - logger.info(`→ Current Same Product: ${curSameProduct.product.id}`); - } - if (curScheduledProduct) { - logger.info( - `→ Current Scheduled Product: ${curScheduledProduct.product.id}` - ); - } + if (curMainProduct) { + logger.info(`→ Current Main Product: ${curMainProduct.product.id}`); + } + if (curSameProduct) { + logger.info(`→ Current Same Product: ${curSameProduct.product.id}`); + } + if (curScheduledProduct) { + logger.info( + `→ Current Scheduled Product: ${curScheduledProduct.product.id}`, + ); + } - if (attachFunction == AttachFunction.OneOff) { - return await handleOneOffFunction({ - req, - res, - attachParams, - config, - }); - } + if (attachFunction === AttachFunction.OneOff) { + return await handleOneOffFunction({ + req, + res, + attachParams, + config, + }); + } - if (attachFunction == AttachFunction.Renew) { - return await handleRenewProduct({ - req, - res, - attachParams, - config, - }); - } + if (attachFunction === AttachFunction.Renew) { + return await handleRenewProduct({ + req, + res, + attachParams, + config, + }); + } - // 2. If main is trial, cancel it... - if (branch == AttachBranch.MainIsTrial) { - await CusProductService.update({ - db, - cusProductId: curMainProduct!.id, - updates: { - ended_at: attachParams.now, - canceled: true, - status: CusProductStatus.Expired, - }, - }); + // 2. If main is trial, cancel it... + if (branch === AttachBranch.MainIsTrial) { + await CusProductService.update({ + db, + cusProductId: curMainProduct!.id, + updates: { + ended_at: attachParams.now, + canceled: true, + status: CusProductStatus.Expired, + }, + }); - const subId = curMainProduct?.subscription_ids?.[0]; - if (subId) { - await stripeCli.subscriptions.cancel(subId, { - cancellation_details: { - comment: "autumn_downgrade,trial_canceled", - }, - }); - } - } + const subId = curMainProduct?.subscription_ids?.[0]; + if (subId) { + await stripeCli.subscriptions.cancel(subId, { + cancellation_details: { + comment: "autumn_downgrade,trial_canceled", + }, + }); + } + } - if (attachFunction == AttachFunction.MultiAttach) { - return await handleMultiAttachFlow({ - req, - res, - attachParams, - attachBody, - branch, - config, - }); - } + if (attachFunction === AttachFunction.MultiAttach) { + return await handleMultiAttachFlow({ + req, + res, + attachParams, + attachBody, + branch, + config, + }); + } - if (attachFunction == AttachFunction.CreateCheckout) { - if (config.invoiceCheckout) { - return await handleCreateInvoiceCheckout({ - req, - res, - attachParams, - attachBody, - config, - branch, - }); - } - return await handleCreateCheckout({ - req, - res, - attachParams, - config, - }); - } + if (attachFunction === AttachFunction.CreateCheckout) { + if (config.invoiceCheckout) { + return await handleCreateInvoiceCheckout({ + req, + res, + attachParams, + attachBody, + config, + branch, + }); + } + return await handleCreateCheckout({ + req, + res, + attachParams, + config, + }); + } - if (attachFunction == AttachFunction.AddProduct) { - return await handleAddProduct({ - req, - res, - attachParams, - config, - branch, - }); - } + if (attachFunction === AttachFunction.AddProduct) { + return await handleAddProduct({ + req, + res, + attachParams, + config, + branch, + }); + } - if (attachFunction == AttachFunction.ScheduleProduct) { - return await handleScheduleFunction2({ - req, - res, - attachParams, - config, - }); - } + if (attachFunction === AttachFunction.ScheduleProduct) { + return await handleScheduleFunction2({ + req, + res, + attachParams, + config, + }); + } - if ( - attachFunction == AttachFunction.UpgradeDiffInterval || - attachFunction == AttachFunction.UpgradeSameInterval - ) { - return await handleUpgradeFlow({ - req, - res, - attachParams, - config, - branch, - }); - } + if ( + attachFunction === AttachFunction.UpgradeDiffInterval || + attachFunction === AttachFunction.UpgradeSameInterval + ) { + return await handleUpgradeFlow({ + req, + res, + attachParams, + config, + branch, + }); + } - if (attachFunction == AttachFunction.UpdatePrepaidQuantity) { - return await handleUpdateQuantityFunction({ - req, - res, - attachParams, - config, - }); - } + if (attachFunction === AttachFunction.UpdatePrepaidQuantity) { + return await handleUpdateQuantityFunction({ + req, + res, + attachParams, + config, + }); + } }; diff --git a/server/src/internal/customers/cusProducts/AttachParams.ts b/server/src/internal/customers/cusProducts/AttachParams.ts index 388f061f0..17a284091 100644 --- a/server/src/internal/customers/cusProducts/AttachParams.ts +++ b/server/src/internal/customers/cusProducts/AttachParams.ts @@ -1,125 +1,127 @@ -import { +import type { + APIVersion, + AttachReplaceable, + AttachScenario, Customer, EntitlementWithFeature, Entity, - FeatureOptions, Feature, + FeatureOptions, + FreeProductConfig, FreeTrial, FullCusProduct, + FullCustomer, FullProduct, Organization, Price, - AttachScenario, - APIVersion, - FullCustomer, - AttachReplaceable, - Reward, ProductOptions, + Reward, } from "@autumn/shared"; -import Stripe from "stripe"; +import type Stripe from "stripe"; 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; - 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; + fromMigration?: boolean; + finalizeInvoice?: boolean; + req?: any; + fromCancel?: 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/rewards/RewardService.ts b/server/src/internal/rewards/RewardService.ts index e5a0f1e1e..25be93f17 100644 --- a/server/src/internal/rewards/RewardService.ts +++ b/server/src/internal/rewards/RewardService.ts @@ -1,164 +1,164 @@ -import { AppEnv, ErrCode, Reward, rewards } from "@autumn/shared"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { type AppEnv, ErrCode, type Reward, rewards } 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"; -import { and, arrayContains, desc, eq, inArray, or, sql } from "drizzle-orm"; export class RewardService { - static async get({ - db, - idOrInternalId, - orgId, - env, - }: { - db: DrizzleCli; - idOrInternalId: string; - orgId: string; - env: AppEnv; - }) { - let 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; - }) { - let 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[]; - }) { - let 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, - }: { - db: DrizzleCli; - orgId: string; - env: AppEnv; - }) { - let results = await db.query.rewards.findMany({ - where: and(eq(rewards.org_id, orgId), eq(rewards.env, env)), - orderBy: [desc(rewards.internal_id)], - }); + static async list({ + db, + orgId, + env, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + }) { + const results = await db.query.rewards.findMany({ + where: and(eq(rewards.org_id, orgId), eq(rewards.env, env)), + 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; - }) { - let 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 98d592889..d536c643f 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -1,260 +1,386 @@ import { - AppEnv, - CusProductStatus, - Customer, - ErrCode, - FullRewardProgram, - ReferralCode, - Reward, - RewardCategory, - RewardReceivedBy, - RewardRedemption, + type AppEnv, + AttachBranch, + type Customer, + ErrCode, + type FullRewardProgram, + type ReferralCode, + type Reward, + RewardReceivedBy, + type RewardRedemption, } from "@autumn/shared"; - -import { CusService } from "../customers/CusService.js"; +import { StatusCodes } from "http-status-codes"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; -import Stripe from "stripe"; -import { RewardRedemptionService } from "./RewardRedemptionService.js"; -import { ProductService } from "../products/ProductService.js"; -import { createFullCusProduct } from "../customers/add-product/createFullCusProduct.js"; -import { InsertCusProductParams } from "../customers/cusProducts/AttachParams.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { CusProductService } from "../customers/cusProducts/CusProductService.js"; import RecaseError from "@/utils/errorUtils.js"; -import { StatusCodes } from "http-status-codes"; -import { getRewardCat } from "./rewardUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { createCusInStripe } from "@/utils/scriptUtils/initCustomer.js"; +import { createFullCusProduct } from "../customers/add-product/createFullCusProduct.js"; +import { handleAddProduct } from "../customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; +import { rewardProgramToAttachParams } from "../customers/attach/attachUtils/attachParams/convertToParams.js"; +import { CusService } from "../customers/CusService.js"; import { deleteCusCache } from "../customers/cusCache/updateCachedCus.js"; +import type { InsertCusProductParams } from "../customers/cusProducts/AttachParams.js"; +import { ProductService } from "../products/ProductService.js"; +import { + isFreeProduct, + isOneOff, + itemsAreOneOff, +} from "../products/productUtils.js"; +import { RewardRedemptionService } from "./RewardRedemptionService.js"; 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: Reward; - redemption: RewardRedemption; + db: DrizzleCli; + org: any; + env: AppEnv; + logger: any; + referralCode: ReferralCode; + 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}`, + ); - let applyToCustomer = await CusService.getByInternalId({ - db, - internalId: referralCode.internal_customer_id, - }); + const applyToCustomer = await CusService.getByInternalId({ + db, + internalId: referralCode.internal_customer_id, + }); - if (!applyToCustomer) { - throw new RecaseError({ - message: `Customer ${referralCode.internal_customer_id} not found`, - code: ErrCode.CustomerNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } + if (!applyToCustomer) { + throw new RecaseError({ + message: `Customer ${referralCode.internal_customer_id} not found`, + code: ErrCode.CustomerNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } - let stripeCli = createStripeCli({ - org, - env, - legacyVersion: true, - }); + const stripeCli = createStripeCli({ + org, + env, + legacyVersion: true, + }); - await createStripeCusIfNotExists({ - db, - customer: applyToCustomer, - org, - env, - logger, - }); + await createStripeCusIfNotExists({ + db, + customer: applyToCustomer, + org, + env, + logger, + }); - let stripeCusId = applyToCustomer.processor.id; - let stripeCus = (await stripeCli.customers.retrieve( - stripeCusId - )) as Stripe.Customer; + const stripeCusId = applyToCustomer.processor.id; + const stripeCus = (await stripeCli.customers.retrieve( + stripeCusId, + )) as Stripe.Customer; - let applied = false; - if (!stripeCus.discount) { - await stripeCli.customers.update(stripeCusId, { - // @ts-ignore - coupon: reward.id, - }); + let applied = false; + if (!stripeCus.discount) { + await stripeCli.customers.update(stripeCusId, { + // @ts-expect-error + coupon: reward.id, + }); - applied = true; - logger.info(`Applied coupon to customer in Stripe`); - } + applied = true; + logger.info(`Applied coupon to customer in Stripe`); + } - let updatedRedemption = await RewardRedemptionService.update({ - db, - id: redemption.id, - updates: { - applied, - triggered: true, - }, - }); + const updatedRedemption = await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + applied, + triggered: true, + }, + }); - logger.info(`Successfully triggered redemption, applied: ${applied}`); + logger.info(`Successfully triggered redemption, applied: ${applied}`); - return updatedRedemption; + return updatedRedemption; }; export const triggerFreeProduct = async ({ - req, - db, - referralCode, - redeemer, - redemption, - rewardProgram, - org, - env, - logger, + req, + db, + referralCode, + redeemer, + redemption, + rewardProgram, + org, + env, + logger, }: { - req?: ExtendedRequest; - db: DrizzleCli; - referralCode: ReferralCode; - redeemer: Customer; - redemption: RewardRedemption; - rewardProgram: FullRewardProgram; - org: any; - env: AppEnv; - logger: any; + req?: ExtendedRequest; + db: DrizzleCli; + referralCode: ReferralCode; + redeemer: Customer; + redemption: RewardRedemption; + rewardProgram: FullRewardProgram & { reward: Reward }; + org: any; + env: AppEnv; + logger: any; }) => { - logger.info(`Triggering free product reward`); - let { received_by } = rewardProgram; + logger.info(`Triggering free product reward`); + const { received_by } = rewardProgram; - let addToRedeemer = received_by === RewardReceivedBy.All; - let addToReferrer = - received_by === RewardReceivedBy.Referrer || - received_by === RewardReceivedBy.All; + let addToRedeemer = received_by === RewardReceivedBy.All; + let addToReferrer = + received_by === RewardReceivedBy.Referrer || + received_by === RewardReceivedBy.All; - let productId = rewardProgram.reward.free_product_id!; + const productId = rewardProgram.reward.free_product_id!; - let fullProduct = await ProductService.getFull({ - db, - idOrInternalId: productId, - orgId: org.id, - env, - }); + const fullProduct = await ProductService.getFull({ + db, + idOrInternalId: productId, + orgId: org.id, + env, + }); - if (!fullProduct) { - throw new RecaseError({ - message: `Product ${productId} not found`, - code: ErrCode.ProductNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } + const isPaidProduct = !isFreeProduct(fullProduct.prices); + const isRecurring = + !isOneOff(fullProduct.prices) && !itemsAreOneOff(fullProduct.entitlements); - let referrer = await CusService.getByInternalId({ - db, - internalId: referralCode.internal_customer_id, - }); + if (!fullProduct) { + throw new RecaseError({ + message: `Product ${productId} not found`, + code: ErrCode.ProductNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } - if (!referrer) { - throw new RecaseError({ - message: `Referrer ${referralCode.internal_customer_id} not found`, - code: ErrCode.CustomerNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } + const referrer = await CusService.getByInternalId({ + db, + internalId: referralCode.internal_customer_id, + }); - let [fullReferrer, fullRedeemer] = await Promise.all([ - CusService.getFull({ - db, - idOrInternalId: referrer.id!, - orgId: org.id, - env, - }), - CusService.getFull({ - db, - idOrInternalId: redeemer.id!, - orgId: org.id, - env, - }), - ]); + if (!referrer) { + throw new RecaseError({ + message: `Referrer ${referralCode.internal_customer_id} not found`, + code: ErrCode.CustomerNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } - let attachParams: InsertCusProductParams = { - req, - org, - product: fullProduct, - prices: fullProduct.prices, - entitlements: fullProduct.entitlements, - optionsList: [], - entities: [], - freeTrial: null, - features: [], - customer: fullReferrer, - cusProducts: fullReferrer.customer_products, - replaceables: [], - }; + const [fullReferrer, fullRedeemer] = await Promise.all([ + CusService.getFull({ + db, + idOrInternalId: referrer.id!, + orgId: org.id, + env, + }), + CusService.getFull({ + db, + idOrInternalId: redeemer.id!, + orgId: org.id, + env, + }), + ]); - if (addToRedeemer) { - let redeemerAttachParams = structuredClone({ - ...attachParams, - customer: fullRedeemer, - cusProducts: fullRedeemer.customer_products, - }); + if (fullReferrer.customer_products.find((cp) => cp.product.id === productId)) + addToReferrer = false; - await createFullCusProduct({ - db, - attachParams: redeemerAttachParams, - logger, - }); - logger.info(`✅ Added ${fullProduct.name} to redeemer`); + if (fullRedeemer.customer_products.find((cp) => cp.product.id === productId)) + addToRedeemer = false; - await deleteCusCache({ - db, - customerId: fullRedeemer.id!, - org, - env, - }); - } + function seedReq(req: ExtendedRequest) { + // Seed in properties that aren't usually present dependent on the trigger type + return { + ...req, + db: req?.db ? undefined : db, + org: req?.org ? undefined : org, + env: req?.env ? undefined : env, + logger: req?.logger ? undefined : logger, + logtail: req?.logtail ? undefined : logger, + } as ExtendedRequest; + } - if (addToReferrer) { - await createFullCusProduct({ - db, - attachParams: { - ...attachParams, - customer: fullReferrer, - cusProducts: fullReferrer.customer_products, - }, - logger, - }); - await deleteCusCache({ - db, - customerId: fullReferrer.id!, - org, - env, - }); - logger.info(`✅ Added ${fullProduct.name} to referrer`); - } + // Branch 1: Free add-on product or non-recurring product + if (!isPaidProduct || !isRecurring) { + logger.info(`Branch 1: Free add-on product`); + const attachParams: InsertCusProductParams = { + req, + org, + product: fullProduct, + prices: fullProduct.prices, + entitlements: fullProduct.entitlements, + optionsList: [], + entities: [], + freeTrial: null, + features: [], + customer: fullReferrer, + cusProducts: fullReferrer.customer_products, + replaceables: [], + }; - await RewardRedemptionService.update({ - db, - id: redemption.id, - updates: { - triggered: true, - applied: true, - }, - }); + if (addToRedeemer) { + const redeemerAttachParams = { + ...structuredClone(attachParams), + customer: fullRedeemer, + cusProducts: fullRedeemer.customer_products, + }; + + await createFullCusProduct({ + db, + attachParams: redeemerAttachParams, + logger, + }); + logger.info(`✅ Added ${fullProduct.name} to redeemer`); + + await deleteCusCache({ + db, + customerId: fullRedeemer.id!, + org, + env, + }); + } + + if (addToReferrer) { + await createFullCusProduct({ + db, + attachParams: { + ...structuredClone(attachParams), + customer: fullReferrer, + cusProducts: fullReferrer.customer_products, + }, + logger, + }); + await deleteCusCache({ + db, + customerId: fullReferrer.id!, + org, + env, + }); + logger.info(`✅ Added ${fullProduct.name} to referrer`); + } + + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + triggered: true, + applied: true, + }, + }); + } + // Branch 2: Paid product from Customer Redemption + else if (req && isPaidProduct) { + logger.info(`Branch 2: Paid product from Customer Redemption`); + req = seedReq(req); + const ensureStripeIDs = [ + !fullRedeemer.processor?.id && + (await createCusInStripe({ + customer: fullRedeemer, + org, + env, + db, + testClockId: req.body.testClockId, + })), + !fullReferrer.processor?.id && + (await createCusInStripe({ + customer: fullReferrer, + org, + env, + db, + testClockId: req.body.testClockId, + })), + ]; + + const updatedIDs = await Promise.all(ensureStripeIDs); + + // Update customers with new Stripe IDs if they were created + const updatedRedeemer = updatedIDs[0] + ? { + ...fullRedeemer, + processor: { + type: "stripe" as const, + id: updatedIDs[0].id, + }, + } + : fullRedeemer; + + const updatedReferrer = updatedIDs[1] + ? { + ...fullReferrer, + processor: { + type: "stripe" as const, + id: updatedIDs[1].id, + }, + } + : fullReferrer; + + const executions = [ + addToRedeemer && + (await handleAddProduct({ + req, + attachParams: rewardProgramToAttachParams({ + req, + rewardProgram: rewardProgram, + customer: updatedRedeemer, + product: fullProduct, + org, + }), + branch: AttachBranch.New, + })), + addToReferrer && + (await handleAddProduct({ + req, + attachParams: rewardProgramToAttachParams({ + req, + rewardProgram: rewardProgram, + customer: updatedReferrer, + product: fullProduct, + org, + }), + branch: AttachBranch.New, + })), + ]; + + await Promise.allSettled(executions) + .then(async (x) => { + if (x.every((y) => y.status === "fulfilled")) { + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + triggered: true, + applied: true, + }, + }); + } else { + logger.error(`Error in executions: ${x}`); + } + }) + .catch((error) => { + logger.error(`Error in executions: ${error}`); + }); + } + // Branch 3: Paid product from Checkout + else if (!req && isPaidProduct) { + logger.info(`Branch 3: Paid product from Checkout`); + } }; From 47af6ce7040057b09792d4dea9799e4a3ce1048b Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:40:09 +0100 Subject: [PATCH 16/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20checkout=20trigger?= =?UTF-8?q?,=20cleaner=20response=20on=20redemption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../referrals/handleRedeemReferral.ts | 58 ++- .../upgradeFlow/updateStripeSub2.ts | 28 +- .../attachParams/convertToParams.ts | 8 +- server/src/internal/rewards/referralUtils.ts | 256 ++++++++--- .../internal/rewards/triggerCheckoutReward.ts | 225 +++++----- server/src/queue/workersInit.ts | 399 +++++++++--------- 6 files changed, 595 insertions(+), 379 deletions(-) diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts index 85e484c93..e47de6c83 100644 --- a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts +++ b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts @@ -157,7 +157,7 @@ export default async (req: any, res: any) => const rewardCat = getRewardCat(reward); if (rewardCat === RewardCategory.FreeProduct) { - await triggerFreeProduct({ + const applications = await triggerFreeProduct({ req: parseReqForAction(req) as ExtendedRequest, db, referralCode, @@ -168,6 +168,23 @@ export default async (req: any, res: any) => logger, redemption, }); + + 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, + code: applications.referrer.cause, + }, + redeemer: { + code: applications.redeemer.cause, + ...applications.redeemer.meta, + }, + }); } else { await triggerRedemption({ db, @@ -178,20 +195,31 @@ export default async (req: any, res: any) => reward, redemption, }); - } - } - // Add coupon to customer? - 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, - }, - }); + 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, + }, + }); + } + } else if (referralCode.reward_program.when === RewardTriggerEvent.Checkout) { + 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, + }, + }) + } }, }); diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts index 04635463f..8381d617c 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/updateStripeSub2.ts @@ -3,7 +3,10 @@ import type Stripe from "stripe"; import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; import { createProrationInvoice } from "@/external/stripe/stripeSubUtils/updateStripeSub/createProrationinvoice.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; +import { + freeTrialToStripeTimestamp, + rewardTrialToStripeTimestamp, +} from "@/internal/products/free-trials/freeTrialUtils.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; import RecaseError from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; @@ -12,8 +15,8 @@ import type { ExtendedRequest } from "@/utils/models/Request.js"; import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js"; import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js"; import { - createUsageInvoiceItems, - resetUsageBalances, + createUsageInvoiceItems, + resetUsageBalances, } from "../upgradeDiffIntFlow/createUsageInvoiceItems.js"; export const updateStripeSub2 = async ({ @@ -33,10 +36,15 @@ export const updateStripeSub2 = async ({ }) => { const { db, logger } = req; - const { stripeCli, customer, org, paymentMethod } = attachParams; + const { stripeCli, customer, org, paymentMethod, rewardTrial } = attachParams; const { invoiceOnly, proration } = config; - if (!invoiceOnly && !attachParams.fromCancel && nullish(paymentMethod)) { + if ( + !invoiceOnly && + !attachParams.fromCancel && + !rewardTrial && + nullish(paymentMethod) + ) { throw new RecaseError({ message: "Payment method is required", code: "payment_method_required", @@ -52,10 +60,12 @@ export const updateStripeSub2 = async ({ const trialEnd = config.disableTrial || config.carryTrial ? undefined - : freeTrialToStripeTimestamp({ - freeTrial: attachParams.freeTrial, - now: attachParams.now, - }); + : rewardTrial?.duration_value + ? rewardTrialToStripeTimestamp({ rewardTrial, now: attachParams.now }) + : freeTrialToStripeTimestamp({ + freeTrial: attachParams.freeTrial, + now: attachParams.now, + }); // 1. Update subscription diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts index e1d56e16e..e1be271b7 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts @@ -18,7 +18,7 @@ import type { InsertCusProductParams, } from "@/internal/customers/cusProducts/AttachParams.js"; import { newCusToFullCus } from "@/internal/customers/cusUtils/cusUtils.js"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { isFreeProduct, isOneOff, itemsAreOneOff } from "@/internal/products/productUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; export const webhookToAttachParams = ({ @@ -174,9 +174,7 @@ export const rewardProgramToAttachParams = ({ }): AttachParams => { const reward = rewardProgram.reward; const isPaid = !isFreeProduct(product.prices); - const isRecurring = !isOneOff(product.prices); - console.log("isPaid in convertToParams", isPaid); - console.log("isRecurring in convertToParams", isRecurring); + const isRecurring = !isOneOff(product.prices) && !itemsAreOneOff(product.entitlements);; return { req, @@ -187,7 +185,7 @@ export const rewardProgramToAttachParams = ({ entitlements: product.entitlements, freeTrial: null, rewardTrial: - isPaid && isRecurring && reward.free_product_config + (isPaid && isRecurring && reward.free_product_config) ? reward.free_product_config : null, optionsList: [], diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index d536c643f..498a60a31 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -31,6 +31,14 @@ import { } from "../products/productUtils.js"; import { RewardRedemptionService } from "./RewardRedemptionService.js"; +export const ReferralResponseCodes = { + OwnsProduct: "has_product_already", + Success: "success", + Unknown: "unknown", + NotConfigured: "not_configured", + InternalError: "internal_error", +}; + export const generateReferralCode = () => { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const codeLength = 6; @@ -211,17 +219,17 @@ export const triggerFreeProduct = async ({ // Seed in properties that aren't usually present dependent on the trigger type return { ...req, - db: req?.db ? undefined : db, - org: req?.org ? undefined : org, - env: req?.env ? undefined : env, - logger: req?.logger ? undefined : logger, - logtail: req?.logtail ? undefined : logger, + db: req?.db ? req.db : db, + org: req?.org ? req.org : org, + env: req?.env ? req.env : env, + logger: req?.logger ? req.logger : logger, + logtail: req?.logtail ? req.logtail : logger, } as ExtendedRequest; } // Branch 1: Free add-on product or non-recurring product if (!isPaidProduct || !isRecurring) { - logger.info(`Branch 1: Free add-on product`); + logger.info(`Branch 1: ${isPaidProduct ? "Paid" : "Free"} add-on product`); const attachParams: InsertCusProductParams = { req, org, @@ -286,11 +294,42 @@ export const triggerFreeProduct = async ({ applied: true, }, }); + + return { + redeemer: { + applied: addToRedeemer, + cause: addToRedeemer + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + meta: { + id: fullRedeemer.id, + name: fullRedeemer.name, + email: fullRedeemer.email, + created_at: fullRedeemer.created_at, + }, + }, + referrer: { + applied: addToReferrer, + cause: addToReferrer + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + }, + }; } - // Branch 2: Paid product from Customer Redemption - else if (req && isPaidProduct) { + // Branch 2: Paid product from Customer Redemption Or Checkout + else if (isPaidProduct) { logger.info(`Branch 2: Paid product from Customer Redemption`); + if (!req) { + req = { + db, + org, + env, + logger, + logtail: logger, + } as ExtendedRequest; + } req = seedReq(req); + const ensureStripeIDs = [ !fullRedeemer.processor?.id && (await createCusInStripe({ @@ -298,7 +337,7 @@ export const triggerFreeProduct = async ({ org, env, db, - testClockId: req.body.testClockId, + testClockId: req?.body?.testClockId || undefined, })), !fullReferrer.processor?.id && (await createCusInStripe({ @@ -306,32 +345,14 @@ export const triggerFreeProduct = async ({ org, env, db, - testClockId: req.body.testClockId, + testClockId: req?.body?.testClockId || undefined, })), ]; - const updatedIDs = await Promise.all(ensureStripeIDs); - // Update customers with new Stripe IDs if they were created - const updatedRedeemer = updatedIDs[0] - ? { - ...fullRedeemer, - processor: { - type: "stripe" as const, - id: updatedIDs[0].id, - }, - } - : fullRedeemer; - - const updatedReferrer = updatedIDs[1] - ? { - ...fullReferrer, - processor: { - type: "stripe" as const, - id: updatedIDs[1].id, - }, - } - : fullReferrer; + const updatedIDs = await Promise.all(ensureStripeIDs); + if (updatedIDs[0]) fullRedeemer.processor.id = updatedIDs[0].id; + if (updatedIDs[1]) fullReferrer.processor.id = updatedIDs[1].id; const executions = [ addToRedeemer && @@ -340,7 +361,7 @@ export const triggerFreeProduct = async ({ attachParams: rewardProgramToAttachParams({ req, rewardProgram: rewardProgram, - customer: updatedRedeemer, + customer: fullRedeemer, product: fullProduct, org, }), @@ -352,7 +373,7 @@ export const triggerFreeProduct = async ({ attachParams: rewardProgramToAttachParams({ req, rewardProgram: rewardProgram, - customer: updatedReferrer, + customer: fullReferrer, product: fullProduct, org, }), @@ -360,27 +381,158 @@ export const triggerFreeProduct = async ({ })), ]; - await Promise.allSettled(executions) - .then(async (x) => { - if (x.every((y) => y.status === "fulfilled")) { - await RewardRedemptionService.update({ - db, - id: redemption.id, - updates: { - triggered: true, - applied: true, - }, - }); - } else { - logger.error(`Error in executions: ${x}`); - } - }) - .catch((error) => { - logger.error(`Error in executions: ${error}`); + const results = await Promise.allSettled(executions); + const redeemerCause = !addToRedeemer + ? ReferralResponseCodes.OwnsProduct + : results[0]?.status === "fulfilled" + ? ReferralResponseCodes.Success + : ReferralResponseCodes.InternalError; + const referrerCause = !addToReferrer + ? ReferralResponseCodes.OwnsProduct + : results[1]?.status === "fulfilled" + ? ReferralResponseCodes.Success + : ReferralResponseCodes.InternalError; + const appliedToRedeemer = + addToRedeemer && results[0]?.status === "fulfilled"; + const appliedToReferrer = + addToReferrer && results[1]?.status === "fulfilled"; + + if (results.every((result) => result.status === "fulfilled")) { + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + triggered: true, + applied: true, + }, }); + } else { + logger.error(`Error in executions: ${results}`); + } + + return { + redeemer: { + applied: appliedToRedeemer, + cause: redeemerCause, + meta: { + id: fullRedeemer.id, + name: fullRedeemer.name, + email: fullRedeemer.email, + created_at: fullRedeemer.created_at, + }, + }, + referrer: { applied: appliedToReferrer, cause: referrerCause }, + }; } // Branch 3: Paid product from Checkout - else if (!req && isPaidProduct) { - logger.info(`Branch 3: Paid product from Checkout`); + else { + return { + redeemer: { applied: false, cause: ReferralResponseCodes.Unknown }, + referrer: { applied: false, cause: ReferralResponseCodes.Unknown }, + }; } }; + +// else if (!req && isPaidProduct) { +// logger.info(`Branch 3: Paid product from Checkout`); +// logger.info(`Branch 2: Paid product from Customer Redemption`); + +// const ensureStripeIDs = [ +// !fullRedeemer.processor?.id && +// (await createCusInStripe({ +// customer: fullRedeemer, +// org, +// env, +// db, +// })), +// !fullReferrer.processor?.id && +// (await createCusInStripe({ +// customer: fullReferrer, +// org, +// env, +// db, +// })), +// ]; + +// // Update customers with new Stripe IDs if they were created +// const updatedIDs = await Promise.all(ensureStripeIDs); +// if (updatedIDs[0]) fullRedeemer.processor.id = updatedIDs[0].id; +// if (updatedIDs[1]) fullReferrer.processor.id = updatedIDs[1].id; + +// const executions = [ +// addToRedeemer && +// (await handleAddProduct({ +// req: { +// db, +// logtail: logger, +// logger, +// } as ExtendedRequest, +// attachParams: rewardProgramToAttachParams({ +// req: { +// db, +// logtail: logger, +// logger, +// } as ExtendedRequest, +// rewardProgram: rewardProgram, +// customer: fullRedeemer, +// product: fullProduct, +// org, +// }), +// branch: AttachBranch.New, +// })), +// addToReferrer && +// (await handleAddProduct({ +// req: { +// db, +// logtail: logger, +// logger, +// } as ExtendedRequest, +// attachParams: rewardProgramToAttachParams({ +// req: { +// db, +// logtail: logger, +// logger, +// } as ExtendedRequest, +// rewardProgram: rewardProgram, +// customer: fullReferrer, +// product: fullProduct, +// org, +// }), +// branch: AttachBranch.New, +// })), +// ]; + +// await Promise.allSettled(executions) +// .then(async (x) => { +// if (x.every((y) => y.status === "fulfilled")) { +// await RewardRedemptionService.update({ +// db, +// id: redemption.id, +// updates: { +// triggered: true, +// applied: true, +// }, +// }); +// } else { +// logger.error(`Error in executions: ${x}`); +// } +// }) +// .catch((error) => { +// logger.error(`Error in executions: ${error}`); +// return ``; +// }); + +// return { +// redeemer: { +// applied: addToRedeemer, +// cause: "redeemerCause", +// meta: { +// id: fullRedeemer.id, +// name: fullRedeemer.name, +// email: fullRedeemer.email, +// created_at: fullRedeemer.created_at, +// }, +// }, +// referrer: { applied: addToReferrer, cause: "referrerCause" }, +// }; +// } diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index f2ee85000..d236c2eb8 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -1,115 +1,140 @@ -import { RewardRedemptionService } from "./RewardRedemptionService.js"; -import { RewardCategory, RewardTriggerEvent } from "@autumn/shared"; -import { triggerFreeProduct, triggerRedemption } from "./referralUtils.js"; -import { RewardProgramService } from "../rewards/RewardProgramService.js"; -import { getRewardCat } from "./rewardUtils.js"; +import { + 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"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { RewardProgramService } from "../rewards/RewardProgramService.js"; +import { RewardRedemptionService } from "./RewardRedemptionService.js"; +import { triggerFreeProduct, 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 - let { customer, product, org, env, subId } = payload; + try { + console.log("Triggering checkout reward in worker"); + console.info("Triggering checkout reward in worker"); + // Customer redeeming code, product they're buying + const { customer, product, org, env, subId } = payload; + console.info(`Customer: ${customer.name} (${customer.id})`); + const stripeCli = createStripeCli({ + org, + env, + }); - let 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 - let redemptions = await RewardRedemptionService.getByCustomer({ - db, - internalCustomerId: customer.internal_id, // customer that redeemed code - withRewardProgram: true, - triggered: false, - withReferralCode: true, - triggerWhen: RewardTriggerEvent.Checkout, - }); + console.info(`Found ${redemptions.length} redemptions`); - for (let redemption of redemptions) { - if ( - !redemption || - redemption.reward_program.when !== RewardTriggerEvent.Checkout - ) { - 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; + } - let { reward_program, referral_code: referralCode } = redemption; - let { reward } = reward_program; + const { reward_program, referral_code: referralCode } = + redemption as RewardRedemption & { + reward_program: RewardProgram & { reward: Reward }; + referral_code: ReferralCode; + }; + const { reward } = reward_program; - logger.info(`--------------------------------`); - logger.info(`CHECKING FOR CHECKOUT REWARD, ORG: ${org.slug}`); - logger.info( - `Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_program.id}` - ); - logger.info(`Referral code: ${referralCode.code} (${referralCode.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)) { - logger.info( - `Product ${product.name} (${product.id}) not included in referral program, skipping` - ); - 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) { - let 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) { - logger.info(`Subscription is on trial, not triggering reward`); - return; - } + if (hasTrial) { + console.info(`Subscription is on trial, not triggering reward`); + return; + } - // Get redemption count - let 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) { - logger.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; + } - let rewardCat = getRewardCat(reward); - if (rewardCat === RewardCategory.FreeProduct) { - await triggerFreeProduct({ - db, - referralCode, - redeemer: customer, - rewardProgram: reward_program, - org, - env, - logger, - redemption, - }); - } else { - await triggerRedemption({ - db, - referralCode, - org, - env, - logger, - reward, - redemption, - }); - } - } - } catch (error) { - logger.error("Failed to trigger checkout reward"); - logger.error(error); - } -}; + const rewardCat = getRewardCat(reward); + if (rewardCat === RewardCategory.FreeProduct) { + await triggerFreeProduct({ + db, + referralCode, + redeemer: customer, + rewardProgram: reward_program, + org, + env, + logger, + redemption, + }); + } else { + await triggerRedemption({ + db, + referralCode, + org, + env, + logger, + reward, + redemption, + }); + } + } + } catch (error) { + console.error("Failed to trigger checkout reward"); + console.error(error); + } +}; \ No newline at end of file diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index e7537d50d..e4f49b80d 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -1,236 +1,239 @@ -import { Job, Queue, Worker } from "bullmq"; -import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js"; -import { QueueManager } from "./QueueManager.js"; - -import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; -import { JobName } from "./JobName.js"; - -import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js"; -import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; -import { runSaveFeatureDisplayTask } from "@/internal/features/featureUtils.js"; -import { CacheManager } from "@/external/caching/CacheManager.js"; +import { type Job, type Queue, Worker } from "bullmq"; +import type { Logger } from "pino"; import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js"; -import { acquireLock, getRedisConnection, releaseLock } from "./lockUtils.js"; -import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask.js"; +import { CacheManager } from "@/external/caching/CacheManager.js"; import { logger } from "@/external/logtail/logtailUtils.js"; +import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask.js"; +import { runSaveFeatureDisplayTask } from "@/internal/features/featureUtils.js"; +import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js"; import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js"; -import { Logger } from "pino"; +import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; +import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js"; +import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; import { generateId } from "@/utils/genUtils.js"; +import { JobName } from "./JobName.js"; +import { acquireLock, getRedisConnection, releaseLock } from "./lockUtils.js"; +import { QueueManager } from "./QueueManager.js"; const NUM_WORKERS = 10; const actionHandlers = [ - JobName.HandleProductsUpdated, - JobName.HandleCustomerCreated, + JobName.HandleProductsUpdated, + JobName.HandleCustomerCreated, ]; const { db, client } = 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; }) => { - let 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; - } - } catch (error: any) { - logtail.error(`Failed to process bullmq job: ${job.name}`, { - jobName: job.name, - error: { - message: error.message, - stack: error.stack, - }, - }); - } + if (actionHandlers.includes(job.name as JobName)) { + await runActionHandlerTask({ + queue, + job, + logger: logtail, + db, + useBackup, + }); + return; + } + } 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) { - let 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, + }); + logger.info("Lock not acquired for checkout reward, adding task to queue"); + return; + } - try { - await runTriggerCheckoutReward({ - db, - payload: job.data, - logger: logtail, - }); - } catch (error) { - console.error("Error processing job:", error); - } finally { - await releaseLock({ lockKey, useBackup }); - } + try { + logger.info("Running checkout reward"); + await runTriggerCheckoutReward({ + db, + payload: job.data, + logger: logtail, + }); + logger.info("Checkout reward triggered"); + } catch (error) { + logger.error("Error processing job:", error); + } finally { + logger.info("Releasing lock for checkout reward"); + await releaseLock({ lockKey, useBackup }); + logger.info("Lock released for checkout reward"); + } - 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", (job, error) => { - console.log("WORKER FAILED:", error.message); - }); + worker.on("failed", (job, 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; }; From 35b23876d8b01c66ccee5d86f0835d7487602554 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:40:28 +0100 Subject: [PATCH 17/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20checkout=20trigger?= =?UTF-8?q?=20stuff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handleCheckoutCompleted.ts | 429 +++++++++--------- .../addProductFlow/handlePaidProduct.ts | 20 +- 2 files changed, 225 insertions(+), 224 deletions(-) diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index e9a67f6be..241bba910 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -1,253 +1,254 @@ -import { Stripe } from "stripe"; -import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js"; import { - AppEnv, - AttachScenario, - CusProductStatus, - Organization, + type AppEnv, + AttachScenario, + CusProductStatus, + type Organization, } from "@autumn/shared"; -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { createStripeCli } from "../utils.js"; - +import type { Stripe } from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; +import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtils.js"; import { attachToInsertParams } from "@/internal/products/productUtils.js"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; +import { notNullish } from "@/utils/genUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js"; +import { createStripeCli } from "../utils.js"; +import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js"; import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js"; -import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; -import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js"; -import { notNullish } from "@/utils/genUtils.js"; -import { CusService } from "@/internal/customers/CusService.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, + ); - 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) { - 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, + }); + } - // if ( - // !attachParams.customer.name && - // notNullish(checkoutSession.customer_details?.name) - // ) { - // updates.push( - // CusService.update({ - // db, - // internalCusId: attachParams.customer.internal_id, - // update: { - // name: checkoutSession.customer_details?.name, - // }, - // }) - // ); - // } + // if ( + // !attachParams.customer.name && + // notNullish(checkoutSession.customer_details?.name) + // ) { + // updates.push( + // CusService.update({ + // db, + // internalCusId: attachParams.customer.internal_id, + // update: { + // name: checkoutSession.customer_details?.name, + // }, + // }) + // ); + // } - // if ( - // !attachParams.customer.email && - // notNullish(checkoutSession.customer_details?.email) - // ) { - // updates.push( - // CusService.update({ - // db, - // internalCusId: attachParams.customer.internal_id, - // update: { - // email: checkoutSession.customer_details?.email, - // }, - // }) - // ); - // } + // if ( + // !attachParams.customer.email && + // notNullish(checkoutSession.customer_details?.email) + // ) { + // updates.push( + // CusService.update({ + // db, + // internalCusId: attachParams.customer.internal_id, + // update: { + // email: checkoutSession.customer_details?.email, + // }, + // }) + // ); + // } - // // Let it fail silently if any of the updates fail. - // if (updates.length > 0) await Promise.allSettled(updates); + // // Let it fail silently if any of the updates fail. + // if (updates.length > 0) await Promise.allSettled(updates); - return; + return; }; // for (const invoiceId of invoiceIds) { diff --git a/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts b/server/src/internal/customers/attach/attachFunctions/addProductFlow/handlePaidProduct.ts index aea783ccc..89281db81 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 { rewardTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; From 93b8c64b69117dc1d64dc212316120b452e0510b Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:34:15 +0100 Subject: [PATCH 18/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20cleanup=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/internal/rewards/triggerCheckoutReward.ts | 5 ----- server/src/queue/workersInit.ts | 12 +++++++----- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index d236c2eb8..70b5736ce 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -22,11 +22,8 @@ export const runTriggerCheckoutReward = async ({ logger: any; }) => { try { - console.log("Triggering checkout reward in worker"); - console.info("Triggering checkout reward in worker"); // Customer redeeming code, product they're buying const { customer, product, org, env, subId } = payload; - console.info(`Customer: ${customer.name} (${customer.id})`); const stripeCli = createStripeCli({ org, env, @@ -42,8 +39,6 @@ export const runTriggerCheckoutReward = async ({ triggerWhen: RewardTriggerEvent.Checkout, }); - console.info(`Found ${redemptions.length} redemptions`); - for (const redemption of redemptions) { if ( !redemption || diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index e4f49b80d..4fb00311b 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -110,24 +110,26 @@ const initWorker = ({ await queue.add(job.name, job.data, { delay: 1000, }); - logger.info("Lock not acquired for checkout reward, adding task to queue"); + logger.info( + "Lock not acquired for checkout reward, adding task to queue", + ); return; } try { - logger.info("Running checkout reward"); + logger.info("Running checkout reward"); await runTriggerCheckoutReward({ db, payload: job.data, logger: logtail, }); - logger.info("Checkout reward triggered"); + logger.info("Checkout reward triggered"); } catch (error) { logger.error("Error processing job:", error); } finally { - logger.info("Releasing lock for checkout reward"); + logger.info("Releasing lock for checkout reward"); await releaseLock({ lockKey, useBackup }); - logger.info("Lock released for checkout reward"); + logger.info("Lock released for checkout reward"); } return; From 5edd0ec6e628470b0492b933a5cb8fedaecb23fe Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:34:33 +0100 Subject: [PATCH 19/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20use=20premium=20in?= =?UTF-8?q?stead=20of=20pro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tests/global.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/tests/global.ts b/server/tests/global.ts index b8209d82c..0ae12da8a 100644 --- a/server/tests/global.ts +++ b/server/tests/global.ts @@ -897,14 +897,14 @@ export const referralPrograms = { internalRewardId: rewards.paidProductWithConfig.id, when: RewardTriggerEvent.Checkout, receivedBy: RewardReceivedBy.All, - productIds: [products.pro.id], + productIds: [products.premium.id], }), paidProductCheckoutReferrer: initRewardProgram({ id: "paidProduct-checkout-referrer", internalRewardId: rewards.paidProductWithConfig.id, when: RewardTriggerEvent.Checkout, receivedBy: RewardReceivedBy.Referrer, - productIds: [products.pro.id], + productIds: [products.premium.id], }), paidAddOnAll: initRewardProgram({ @@ -927,14 +927,14 @@ export const referralPrograms = { internalRewardId: rewards.paidProductAddOn.id, when: RewardTriggerEvent.Checkout, receivedBy: RewardReceivedBy.All, - productIds: [products.proAddOn.id], + productIds: [products.premium.id], }), paidAddOnCheckoutReferrer: initRewardProgram({ id: "paidAddOn-checkout-referrer", internalRewardId: rewards.paidProductAddOn.id, when: RewardTriggerEvent.Checkout, receivedBy: RewardReceivedBy.Referrer, - productIds: [products.proAddOn.id], + productIds: [products.premium.id], }), }; From f3c3646db504aaf56a52f7407ea51195cfc40dfd Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:35:15 +0100 Subject: [PATCH 20/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20referrals9:=20(che?= =?UTF-8?q?ckout,=20paid,=20add-on,=20both)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals9.ts | 257 ++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 server/tests/advanced/referrals/paid/referrals9.ts diff --git a/server/tests/advanced/referrals/paid/referrals9.ts b/server/tests/advanced/referrals/paid/referrals9.ts new file mode 100644 index 000000000..788a36181 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals9.ts @@ -0,0 +1,257 @@ +import { setTimeout } from "node:timers/promises"; +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import type { Customer } from "autumn-js"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { + advanceTestClock, + completeCheckoutForm, +} from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals9"; + +describe(`${chalk.yellowBright( + "referrals9: Testing referrals (checkout, paid, add-on, both)", +)}`, () => { + const mainCustomerId = "main-referral-9"; + const redeemer = "referral9-r1"; + const redeemerPM = "success"; + const autumn: AutumnInt = new AutumnInt(); + let stripeCli: Stripe; + const testClockIds: string[] = []; + let referralCode: ReferralCode; + + let redemption: 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; + + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + // attachPm: "success", + }); + + mainCustomer = res.customer; + testClockIds.push(res.testClockId); + + // await autumn.attach({ + // customer_id: mainCustomerId, + // product_id: pro.id, + // }); + + 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); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnCheckoutAll.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnCheckoutAll.id, + }); + + 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, + }); + + // 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 succesfully checkout and trigger the checkout reward", async () => { + const res = await autumn.attach({ + customer_id: redeemer, + product_id: products.premium.id, + }); + + await completeCheckoutForm(res.checkout_url); + await setTimeout(1000 * 5); + }); + + it("should have given the paid add-on to both referrer and redeemer", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true, `Redemption ${redemption.id} should be applied`); + + await setTimeout(1000 * 10); + + const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ + autumn.customers.get(mainCustomerId), + autumn.customers.get(redeemer), + ])) as (Customer & { add_ons: any[] })[]; + + // Main customer (referrer) should have the proAddOn product + const mainProds = mainCustomerData.products; + const mainAddons = mainCustomerData.add_ons; + + assert.equal(mainProds.length + mainAddons.length, 2); + + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); + assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); + + // Redeemer should have both free and proAddOn products (both get reward) + const redeemerProds = redeemerCustomerData.products; + const redeemerAddons = redeemerCustomerData.add_ons; + + assert.equal(redeemerProds.length + redeemerAddons.length, 2); + + const redeemerHasProAddOn = redeemerAddons.some( + (p) => p.id === products.proAddOn.id, + ); + const redeemerHasPremium = redeemerProds.some( + (p) => p.id === products.premium.id, + ); + + assert.isTrue( + redeemerHasProAddOn, + "Redeemer should have proAddOn product", + ); + assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); + + // Verify products are properly attached + expectAddOnAttached({ + customer: (await autumn.customers.get(mainCustomerId)) as Customer & { + add_ons: any[]; + }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + + // Verify redeemer also has the add-on + expectAddOnAttached({ + customer: (await autumn.customers.get(redeemer)) as Customer & { + add_ons: any[]; + }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and have proAddOn attached for both", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the proAddOn product + { name: "Free", status: CusProductStatus.Active }, + { name: "proAddOn", status: CusProductStatus.Active }, + ], + [ + // Redeemer - gets both free and proAddOn products (both get reward) + { name: "Premium", status: CusProductStatus.Active }, + { name: "proAddOn", 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); From 47c027b682e2a5360d7f54f203841ef8bfc73806 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:36:51 +0100 Subject: [PATCH 21/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20block=20customers?= =?UTF-8?q?=20on=20a=20plan=20already?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/internal/rewards/referralUtils.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index 498a60a31..ee79685ba 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -209,12 +209,18 @@ export const triggerFreeProduct = async ({ }), ]); + // If they already have the product, don't add to them if (fullReferrer.customer_products.find((cp) => cp.product.id === productId)) addToReferrer = false; - if (fullRedeemer.customer_products.find((cp) => cp.product.id === productId)) addToRedeemer = false; + // If they are on any paid plan that isn't an add-on, don't add to them + if(fullReferrer.customer_products.some(x => !x.product.is_add_on && !isFreeProduct(x.customer_prices.map(y => y.price)))) + addToReferrer = false; + if(fullRedeemer.customer_products.some(x => !x.product.is_add_on && !isFreeProduct(x.customer_prices.map(y => y.price)))) + addToRedeemer = false; + function seedReq(req: ExtendedRequest) { // Seed in properties that aren't usually present dependent on the trigger type return { From 4eef2ee37d08b943b0a6317220cb795c3644c48c Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:37:48 +0100 Subject: [PATCH 22/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20tests=20:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals10.ts | 249 ++++++++++++++++ .../advanced/referrals/paid/referrals11.ts | 268 ++++++++++++++++++ .../advanced/referrals/paid/referrals12.ts | 218 ++++++++++++++ .../advanced/referrals/paid/referrals5.ts | 24 +- .../advanced/referrals/paid/referrals6.ts | 24 +- .../advanced/referrals/paid/referrals7.ts | 39 ++- .../advanced/referrals/paid/referrals8.ts | 41 +-- .../advanced/referrals/paid/referrals9.ts | 10 - 8 files changed, 801 insertions(+), 72 deletions(-) create mode 100644 server/tests/advanced/referrals/paid/referrals10.ts create mode 100644 server/tests/advanced/referrals/paid/referrals11.ts create mode 100644 server/tests/advanced/referrals/paid/referrals12.ts diff --git a/server/tests/advanced/referrals/paid/referrals10.ts b/server/tests/advanced/referrals/paid/referrals10.ts new file mode 100644 index 000000000..f5773637b --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals10.ts @@ -0,0 +1,249 @@ +import { setTimeout } from "node:timers/promises"; +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import type { Customer } from "autumn-js"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { + advanceTestClock, + completeCheckoutForm, +} from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals10"; + +describe(`${chalk.yellowBright( + "referrals10: Testing referrals (checkout, paid, add-on, referrer only)", +)}`, () => { + const mainCustomerId = "main-referral-10"; + const redeemer = "referral10-r1"; + 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; + + 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 {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + }); + + testClockIds.push(res.testClockId); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnCheckoutReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidAddOnCheckoutReferrer.id, + }); + + 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, + }); + + // 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 succesfully checkout and trigger the checkout reward", async () => { + const res = await autumn.attach({ + customer_id: redeemer, + product_id: products.premium.id, + }); + + await completeCheckoutForm(res.checkout_url); + await setTimeout(1000 * 5); + }); + + it("should have given the paid add-on to referrer only, not redeemer", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true); + + await setTimeout(1000 * 10); + + const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ + autumn.customers.get(mainCustomerId), + autumn.customers.get(redeemer), + ])) as (Customer & { add_ons: any[] })[]; + + // Main customer (referrer) should have the proAddOn product + const mainProds = mainCustomerData.products; + const mainAddons = mainCustomerData.add_ons; + + assert.equal(mainProds.length + mainAddons.length, 2); + + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); + assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); + + // Redeemer should only have Premium product (no proAddOn) + const redeemerProds = redeemerCustomerData.products; + const redeemerAddons = redeemerCustomerData.add_ons; + + assert.equal(redeemerProds.length + redeemerAddons.length, 1); + + const redeemerHasProAddOn = redeemerAddons.some( + (p) => p.id === products.proAddOn.id, + ); + const redeemerHasPremium = redeemerProds.some( + (p) => p.id === products.premium.id, + ); + + assert.isFalse( + redeemerHasProAddOn, + "Redeemer should not have proAddOn product", + ); + assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); + + // Verify products are properly attached + expectAddOnAttached({ + customer: (await autumn.customers.get(mainCustomerId)) as Customer & { + add_ons: any[]; + }, + productId: products.proAddOn.id, + status: CusProductStatus.Active, + }); + + // Verify redeemer does not have the add-on + const redeemerCustomer = (await autumn.customers.get( + redeemer, + )) as Customer & { add_ons: any[] }; + const redeemerAddOn = redeemerCustomer.add_ons.find( + (a) => a.id === products.proAddOn.id, + ); + assert.isUndefined( + redeemerAddOn, + "Redeemer should not have proAddOn product", + ); + }); + + it("should advance test clock and have proAddOn attached for referrer only", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the proAddOn product + { name: "Free", status: CusProductStatus.Active }, + { name: "proAddOn", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has Premium product (no reward) + { name: "Premium", 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals11.ts b/server/tests/advanced/referrals/paid/referrals11.ts new file mode 100644 index 000000000..8dcc60e33 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals11.ts @@ -0,0 +1,268 @@ +import { setTimeout } from "node:timers/promises"; +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { + advanceTestClock, + completeCheckoutForm, +} from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals11"; + +describe(`${chalk.yellowBright( + "referrals11: Testing referrals (checkout, paid, recurring, both)", +)}`, () => { + const mainCustomerId = "main-referral-11"; + const redeemer = "referral11-r1"; + 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; + + 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 {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + }); + + testClockIds.push(res.testClockId); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductCheckoutAll.id, + }); + + assert.exists(referralCode.code, "Referral code should be generated"); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductCheckoutAll.id, + }); + + assert.equal( + referralCode2.code, + referralCode.code, + "Should return same referral code when called multiple times", + ); + }); + + 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, + "Should throw AutumnError when trying to redeem same code twice", + ); + assert.equal( + error.code, + ErrCode.CustomerAlreadyRedeemedReferralCode, + "Should return correct error code for duplicate redemption", + ); + } + }); + + it("should succesfully checkout and trigger the checkout reward", async () => { + const res = await autumn.attach({ + customer_id: redeemer, + product_id: products.premium.id, + }); + + await completeCheckoutForm(res.checkout_url); + await setTimeout(1000 * 5); + }); + + it("should have given the paid product to referrer only, not redeemer (on paid plan)", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal( + redemptionResult.applied, + true, + `Redemption ${redemption.id} should be applied`, + ); + + await setTimeout(1000 * 10); + + const [mainCustomerData, redeemerCustomerData] = await Promise.all([ + autumn.customers.get(mainCustomerId), + autumn.customers.get(redeemer), + ]); + + // Main customer (referrer) should have the pro product + const mainProds = mainCustomerData.products; + + assert.equal( + mainProds.length, + 1, + "Main customer should have exactly 1 product (Pro)", + ); + + const hasProProduct = mainProds.some((p) => p.id === products.pro.id); + assert.isTrue(hasProProduct, "Main customer should have pro product"); + + // Redeemer should only have Premium product (no pro product because they're on paid plan) + const redeemerProds = redeemerCustomerData.products; + + assert.equal( + redeemerProds.length, + 1, + "Redeemer should have exactly 1 product (Premium only)", + ); + + const redeemerHasProProduct = redeemerProds.some( + (p) => p.id === products.pro.id, + ); + const redeemerHasPremium = redeemerProds.some( + (p) => p.id === products.premium.id, + ); + + console.log( + redeemerProds.map((x) => `${x.id} - ${x.name} - ${x.status}`), + mainProds.map((x) => `${x.id} - ${x.name} - ${x.status}`), + ); + + assert.isFalse( + redeemerHasProProduct, + "Redeemer should not have pro product (already on paid plan)", + ); + assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); + + // Verify products are properly attached + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomerId), + product: products.pro, + status: CusProductStatus.Trialing, + }); + + // Verify redeemer only has premium product + expectProductV1Attached({ + customer: await autumn.customers.get(redeemer), + product: products.premium, + status: CusProductStatus.Trialing, + }); + }); + + it("should advance test clock and have pro attached for referrer only", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the pro product + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has Premium product (no Pro reward due to being on paid plan) + { name: "Premium", 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals12.ts b/server/tests/advanced/referrals/paid/referrals12.ts new file mode 100644 index 000000000..384c93693 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals12.ts @@ -0,0 +1,218 @@ +import { setTimeout } from "node:timers/promises"; +import { + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { + advanceTestClock, + completeCheckoutForm, +} from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms } from "../../../global.js"; + +export const group = "referrals12"; + +describe(`${chalk.yellowBright( + "referrals12: Testing referrals (checkout, paid, recurring, referrer only)", +)}`, () => { + const mainCustomerId = "main-referral-12"; + const redeemer = "referral12-r1"; + 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; + + 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 {} + + const res = await initCustomer({ + autumn: this.autumnJs, + customerId: mainCustomerId, + db, + org, + env, + }); + + testClockIds.push(res.testClockId); + + const redeemerRes = await initCustomer({ + autumn: this.autumnJs, + customerId: redeemer, + db: this.db, + org: this.org, + env: this.env, + withTestClock: true, + }); + + testClockIds.push(redeemerRes.testClockId); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductCheckoutReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductCheckoutReferrer.id, + }); + + 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, + }); + + // 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 successfully checkout and trigger the checkout reward", async () => { + const res = await autumn.attach({ + customer_id: redeemer, + product_id: products.premium.id, + }); + + await completeCheckoutForm(res.checkout_url); + await setTimeout(1000 * 5); + }); + + it("should have given the paid product to referrer only, not redeemer (referrer-only reward)", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.applied, true); + + await setTimeout(1000 * 10); + + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemer)).products; + + // Main customer (referrer) should have the pro product + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); + + // Redeemer should only have the premium product (no pro product given) + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.premium.id); + + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomerId), + product: products.pro, + status: CusProductStatus.Trialing, + }); + + // Verify redeemer only has premium product + expectProductV1Attached({ + customer: await autumn.customers.get(redeemer), + product: products.premium, + status: CusProductStatus.Trialing, + }); + }); + + it("should advance test clock and have pro attached for referrer only", async () => { + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 35, + waitForSeconds: 15, + stripeCli, + }), + ), + ); + + const dbCustomers = await Promise.all( + [mainCustomerId, redeemer].map((x) => + CusService.getFull({ + db, + idOrInternalId: x, + orgId: org.id, + env, + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Trialing, + CusProductStatus.Expired, + ], + }), + ), + ); + + const expectedProducts = [ + [ + // Main referrer - gets the pro product + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - only has premium product (no reward) + { name: "Premium", 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, + ); + + assert.isNotNull( + matchingProduct, + `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals5.ts b/server/tests/advanced/referrals/paid/referrals5.ts index ab853519b..a45e9ecfb 100644 --- a/server/tests/advanced/referrals/paid/referrals5.ts +++ b/server/tests/advanced/referrals/paid/referrals5.ts @@ -15,6 +15,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { products, referralPrograms } from "../../../global.js"; @@ -32,7 +33,6 @@ describe(`${chalk.yellowBright( let referralCode: ReferralCode; const redemptions: RewardRedemption[] = []; - let mainCustomer: any; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -44,9 +44,17 @@ describe(`${chalk.yellowBright( org = this.org; env = this.env; - [mainCustomerId, ...redeemers].forEach(async (customerId) => { - await autumn.customers.delete(customerId); - }); + try { + await Promise.all([ + ...([mainCustomerId, ...redeemers].map(customerId => + autumn.customers.delete(customerId) + )), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, ...redeemers], + }), + ]); + } catch {} const res = await initCustomer({ autumn: this.autumnJs, @@ -57,14 +65,8 @@ describe(`${chalk.yellowBright( attachPm: "success", }); - mainCustomer = res.customer; testClockIds.push(res.testClockId); - // await autumn.attach({ - // customer_id: mainCustomerId, - // product_id: pro.id, - // }); - const batchCreate = []; for (const redeemer of redeemers) { batchCreate.push( @@ -146,7 +148,7 @@ describe(`${chalk.yellowBright( assert.equal(redeemerProds[0].id, products.pro.id); expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomer.id), + customer: await autumn.customers.get(mainCustomerId), product: products.pro, status: CusProductStatus.Trialing, }); diff --git a/server/tests/advanced/referrals/paid/referrals6.ts b/server/tests/advanced/referrals/paid/referrals6.ts index a24df383c..7ee2ef7ff 100644 --- a/server/tests/advanced/referrals/paid/referrals6.ts +++ b/server/tests/advanced/referrals/paid/referrals6.ts @@ -15,6 +15,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { products, referralPrograms } from "../../../global.js"; @@ -32,7 +33,6 @@ describe(`${chalk.yellowBright( let referralCode: ReferralCode; let redemption: RewardRedemption; - let mainCustomer: any; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -44,10 +44,16 @@ describe(`${chalk.yellowBright( org = this.org; env = this.env; - try { - await autumn.customers.delete(mainCustomerId); - await autumn.customers.delete(redeemer); - } catch {} + try { + await Promise.all([ + autumn.customers.delete(mainCustomerId), + autumn.customers.delete(redeemer), + RewardRedemptionService._resetCustomerRedemptions({ + db, + internalCustomerId: [mainCustomerId, redeemer], + }), + ]); + } catch {} const res = await initCustomer({ autumn: this.autumnJs, @@ -58,14 +64,8 @@ describe(`${chalk.yellowBright( attachPm: "success", }); - mainCustomer = res.customer; testClockIds.push(res.testClockId); - // await autumn.attach({ - // customer_id: mainCustomerId, - // product_id: pro.id, - // }); - const redeemerRes = await initCustomer({ autumn: this.autumnJs, customerId: redeemer, @@ -131,7 +131,7 @@ describe(`${chalk.yellowBright( assert.equal(redeemerProds[0].id, products.free.id); expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomer.id), + customer: await autumn.customers.get(mainCustomerId), product: products.pro, status: CusProductStatus.Trialing, }); diff --git a/server/tests/advanced/referrals/paid/referrals7.ts b/server/tests/advanced/referrals/paid/referrals7.ts index 75ec69854..6d2c5d654 100644 --- a/server/tests/advanced/referrals/paid/referrals7.ts +++ b/server/tests/advanced/referrals/paid/referrals7.ts @@ -1,10 +1,11 @@ +import { setTimeout } from "node:timers/promises"; import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, + type AppEnv, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, } from "@autumn/shared"; import type { Customer } from "autumn-js"; import { assert } from "chai"; @@ -13,7 +14,6 @@ import type { Stripe } from "stripe"; import { setupBefore } from "tests/before.js"; import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { setTimeout } from "timers/promises"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -21,7 +21,7 @@ import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionServ import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; import { products, referralPrograms } from "../../../global.js"; -export const group = "refferals7"; +export const group = "referrals7"; describe(`${chalk.yellowBright( "referrals7: Testing referrals (immediate, paid, one-off add-on, both)", @@ -35,7 +35,6 @@ describe(`${chalk.yellowBright( let referralCode: ReferralCode; let redemption: RewardRedemption; - let mainCustomer: any; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -67,14 +66,8 @@ describe(`${chalk.yellowBright( attachPm: "success", }); - mainCustomer = res.customer; testClockIds.push(res.testClockId); - // await autumn.attach({ - // customer_id: mainCustomerId, - // product_id: pro.id, - // }); - const redeemerRes = await initCustomer({ autumn: this.autumnJs, customerId: redeemer, @@ -138,18 +131,18 @@ describe(`${chalk.yellowBright( // Main customer (referrer) should have the proAddOn product const mainProds = mainCustomerData.products; const mainAddons = mainCustomerData.add_ons; - + assert.equal(mainProds.length + mainAddons.length, 2); - + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); // Redeemer should have both free and proAddOn products const redeemerProds = redeemerCustomerData.products; const redeemerAddons = redeemerCustomerData.add_ons; - + assert.equal(redeemerProds.length + redeemerAddons.length, 2); - + const redeemerHasProAddOn = redeemerAddons.some( (p) => p.id === products.proAddOn.id, ); @@ -162,13 +155,17 @@ describe(`${chalk.yellowBright( // Verify products are properly attached expectAddOnAttached({ - customer: await autumn.customers.get(mainCustomerId) as Customer & { add_ons: any[] }, + customer: (await autumn.customers.get(mainCustomerId)) as Customer & { + add_ons: any[]; + }, productId: products.proAddOn.id, status: CusProductStatus.Active, }); expectAddOnAttached({ - customer: await autumn.customers.get(redeemer) as Customer & { add_ons: any[] }, + customer: (await autumn.customers.get(redeemer)) as Customer & { + add_ons: any[]; + }, productId: products.proAddOn.id, status: CusProductStatus.Active, }); diff --git a/server/tests/advanced/referrals/paid/referrals8.ts b/server/tests/advanced/referrals/paid/referrals8.ts index 85153e0eb..bb84a0b14 100644 --- a/server/tests/advanced/referrals/paid/referrals8.ts +++ b/server/tests/advanced/referrals/paid/referrals8.ts @@ -1,3 +1,4 @@ +import { setTimeout } from "node:timers/promises"; import { type AppEnv, CusProductStatus, @@ -13,7 +14,6 @@ import type { Stripe } from "stripe"; import { setupBefore } from "tests/before.js"; import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { setTimeout } from "timers/promises"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -35,7 +35,6 @@ describe(`${chalk.yellowBright( let referralCode: ReferralCode; let redemption: RewardRedemption; - let mainCustomer: any; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -67,14 +66,8 @@ describe(`${chalk.yellowBright( attachPm: "success", }); - mainCustomer = res.customer; testClockIds.push(res.testClockId); - // await autumn.attach({ - // customer_id: mainCustomerId, - // product_id: pro.id, - // }); - const redeemerRes = await initCustomer({ autumn: this.autumnJs, customerId: redeemer, @@ -138,18 +131,18 @@ describe(`${chalk.yellowBright( // Main customer (referrer) should have the proAddOn product const mainProds = mainCustomerData.products; const mainAddons = mainCustomerData.add_ons; - + assert.equal(mainProds.length + mainAddons.length, 2); - + const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); // Redeemer should only have free product (no proAddOn) const redeemerProds = redeemerCustomerData.products; const redeemerAddons = redeemerCustomerData.add_ons; - + assert.equal(redeemerProds.length + redeemerAddons.length, 1); - + const redeemerHasProAddOn = redeemerAddons.some( (p) => p.id === products.proAddOn.id, ); @@ -157,20 +150,32 @@ describe(`${chalk.yellowBright( (p) => p.id === products.free.id, ); - assert.isFalse(redeemerHasProAddOn, "Redeemer should not have proAddOn product"); + assert.isFalse( + redeemerHasProAddOn, + "Redeemer should not have proAddOn product", + ); assert.isTrue(redeemerHasFree, "Redeemer should have free product"); // Verify products are properly attached expectAddOnAttached({ - customer: await autumn.customers.get(mainCustomerId) as Customer & { add_ons: any[] }, + customer: (await autumn.customers.get(mainCustomerId)) as Customer & { + add_ons: any[]; + }, productId: products.proAddOn.id, status: CusProductStatus.Active, }); // Verify redeemer does not have the add-on - const redeemerCustomer = await autumn.customers.get(redeemer) as Customer & { add_ons: any[] }; - const redeemerAddOn = redeemerCustomer.add_ons.find((a) => a.id === products.proAddOn.id); - assert.isUndefined(redeemerAddOn, "Redeemer should not have proAddOn product"); + const redeemerCustomer = (await autumn.customers.get( + redeemer, + )) as Customer & { add_ons: any[] }; + const redeemerAddOn = redeemerCustomer.add_ons.find( + (a) => a.id === products.proAddOn.id, + ); + assert.isUndefined( + redeemerAddOn, + "Redeemer should not have proAddOn product", + ); }); it("should advance test clock and have proAddOn attached for referrer only", async () => { @@ -231,4 +236,4 @@ describe(`${chalk.yellowBright( }); }); }); -}); \ No newline at end of file +}); diff --git a/server/tests/advanced/referrals/paid/referrals9.ts b/server/tests/advanced/referrals/paid/referrals9.ts index 788a36181..2f1429432 100644 --- a/server/tests/advanced/referrals/paid/referrals9.ts +++ b/server/tests/advanced/referrals/paid/referrals9.ts @@ -31,14 +31,12 @@ describe(`${chalk.yellowBright( )}`, () => { const mainCustomerId = "main-referral-9"; const redeemer = "referral9-r1"; - const redeemerPM = "success"; const autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; const testClockIds: string[] = []; let referralCode: ReferralCode; let redemption: RewardRedemption; - let mainCustomer: any; let db: DrizzleCli; let org: Organization; let env: AppEnv; @@ -67,24 +65,16 @@ describe(`${chalk.yellowBright( db, org, env, - // attachPm: "success", }); - mainCustomer = res.customer; testClockIds.push(res.testClockId); - // await autumn.attach({ - // customer_id: mainCustomerId, - // product_id: pro.id, - // }); - const redeemerRes = await initCustomer({ autumn: this.autumnJs, customerId: redeemer, db: this.db, org: this.org, env: this.env, - // attachPm: redeemerPM, withTestClock: true, }); From 25c2dea3326ce9f822340f3357a010a11912e200 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:37:54 +0100 Subject: [PATCH 23/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20test=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test.sh | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/server/test.sh b/server/test.sh index d297cf094..96b3e32f1 100755 --- a/server/test.sh +++ b/server/test.sh @@ -40,6 +40,12 @@ if [ "$1" == "g4" ]; then 'tests/advanced/coupons/*.ts' fi +# Group 5 - Paid referrals +if [ "$1" == "paid" ]; then + $MOCHA_SETUP && $MOCHA_CMD \ + 'tests/advanced/referrals/paid/*.ts' +fi + # Group 4 if [ "$1" == "g4" ]; then $MOCHA_SETUP && $MOCHA_CMD \ @@ -77,14 +83,6 @@ else --ignore 'tests/alex/**/*.ts' fi - - - - - - - - # # TEST PARALLEL # if [ "$1" == "basic-parallel" ]; then # MOCHA_PARALLEL=true $MOCHA_SETUP \ @@ -116,5 +114,3 @@ fi # && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\ # # && $MOCHA_CMD 'tests/advanced/usage/*.ts' \ # # && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\ - - From d797b0e8a8c7929d6c79c86f82caf9a8eaa0d00b Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:52:03 +0100 Subject: [PATCH 24/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../biome.yaml | 0 .../test.yml | 0 server/shell/g5.sh | 1 + .../external/autumn/autumnWebhookRouter.ts | 44 ++- server/src/external/resend/resendUtils.ts | 23 +- .../handleCheckoutCompleted.ts | 30 +- .../handleCheckoutSub.ts | 4 +- .../handleRemainingSets.ts | 4 +- .../handleSetupCheckout.ts | 34 ++ .../internal/api/entities/getEntityUtils.ts | 166 +++++----- .../entitled/checkUtils/getV2CheckResponse.ts | 13 - server/src/internal/api/events/eventRouter.ts | 1 + server/src/internal/api/events/usageRouter.ts | 4 +- .../add-product/handleCreateCheckout.ts | 16 +- .../upgradeFlow/handleUpgradeFlow.ts | 39 ++- .../attachParams/getAttachParams.ts | 2 +- .../handleCheckoutErrors.ts | 14 + .../attach/checkout/handleCheckout.ts | 2 +- .../customers/cusProducts/AttachParams.ts | 35 ++- .../cusEnts/cusEntUtils/findCusEntUtils.ts | 4 +- .../cusEnts/cusEntUtils/getExistingUsage.ts | 43 +-- .../handlers/handleCreateCustomer.ts | 1 + .../internal/customers/internalCusRouter.ts | 8 +- server/src/internal/emails/constants.ts | 2 + .../internal/emails/sendInvitationEmail.ts | 8 +- .../src/internal/emails/sendMigrationEmail.ts | 4 +- server/src/internal/emails/sendOTPEmail.ts | 21 +- .../internal/emails/sendOnboardingEmail.ts | 3 +- .../features/handlers/handleUpdateFeature.ts | 2 +- .../features/internalFeatureRouter.ts | 2 +- .../orgs/handlers/handleGetInvites.ts | 5 +- .../products/handlers/handleGetProduct.ts | 3 +- server/src/trigger/handleThresholdReached.ts | 291 ++++++++++++++++++ server/src/trigger/updateBalanceTask.ts | 80 +++-- server/src/trigger/updateUsageTask.ts | 60 ++-- server/src/utils/auth.ts | 15 + .../advanced/multiFeature/multiFeature2.ts | 13 +- shared/enums/WebhookEventType.ts | 4 + shared/index.ts | 1 + shared/models/attachModels/attachBody.ts | 1 + shared/models/cusModels/cusModels.ts | 33 +- vite/src/app/layout.tsx | 3 - vite/src/hooks/common/useOrg.tsx | 48 ++- .../customers-table/CustomerRow.tsx | 2 +- ...eCustomer.tsx => DeleteCustomerDialog.tsx} | 2 +- .../multi-attach/MultiAttachDialog.tsx | 8 +- .../multi-attach/MultiAttachRewards.tsx | 5 +- .../configure-stripe/ConfigureStripe.tsx | 23 ++ .../notifications/InviteNotifications.tsx | 6 +- .../main-sidebar/components/OrgDropdown.tsx | 75 ++--- .../org-dropdown/manage-org/InvitePopover.tsx | 27 +- .../manage-org/OrgInvitesList.tsx | 49 +-- .../features/components/CreateFeature.tsx | 2 +- .../views/products/product/ProductView.tsx | 6 +- .../components/CusomiseIntervalPopover.tsx | 9 +- .../feature-price/SelectBillingCycle.tsx | 11 +- 56 files changed, 917 insertions(+), 395 deletions(-) rename .github/{workflows => archived_workflows}/biome.yaml (100%) rename .github/{workflows => archived_workflows}/test.yml (100%) create mode 100644 server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts create mode 100644 server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts create mode 100644 server/src/internal/emails/constants.ts create mode 100644 server/src/trigger/handleThresholdReached.ts create mode 100644 shared/enums/WebhookEventType.ts rename vite/src/views/customers/customer/components/{DeleteCustomer.tsx => DeleteCustomerDialog.tsx} (96%) diff --git a/.github/workflows/biome.yaml b/.github/archived_workflows/biome.yaml similarity index 100% rename from .github/workflows/biome.yaml rename to .github/archived_workflows/biome.yaml diff --git a/.github/workflows/test.yml b/.github/archived_workflows/test.yml similarity index 100% rename from .github/workflows/test.yml rename to .github/archived_workflows/test.yml diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 979c837ae..1ca91d668 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -7,6 +7,7 @@ source "$(dirname "$0")/config.sh" if [[ "$1" == *"setup"* ]]; then MOCHA_PARALLEL=true $MOCHA_SETUP fi +# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/coupons/*.ts' \ diff --git a/server/src/external/autumn/autumnWebhookRouter.ts b/server/src/external/autumn/autumnWebhookRouter.ts index 7a7d612b8..5bb0cd24c 100644 --- a/server/src/external/autumn/autumnWebhookRouter.ts +++ b/server/src/external/autumn/autumnWebhookRouter.ts @@ -1,3 +1,5 @@ +import RecaseError from "@/utils/errorUtils.js"; +import { ErrCode, WebhookEventType } from "@autumn/shared"; import express, { Router } from "express"; import { Webhook } from "svix"; @@ -14,11 +16,15 @@ const verifyAutumnWebhook = async (req: any, res: any) => { const svix_signature = headers["svix-signature"]; if (!svix_id || !svix_timestamp || !svix_signature) { - res.status(400).json({ - success: false, + throw new RecaseError({ message: "Error: Missing svix headers", + code: ErrCode.InvalidInputs, }); - return; + // res.status(400).json({ + // success: false, + // message: "Error: Missing svix headers", + // }); + // return; } let evt: any; @@ -29,12 +35,16 @@ const verifyAutumnWebhook = async (req: any, res: any) => { "svix-signature": svix_signature as string, }); } catch (err) { - console.log("Error: Could not verify webhook"); - res.status(400).json({ - success: false, + throw new RecaseError({ message: "Error: Could not verify webhook", + code: ErrCode.InvalidInputs, }); - return; + // console.log("Error: Could not verify webhook"); + // res.status(400).json({ + // success: false, + // message: "Error: Could not verify webhook", + // }); + // return; } return evt; @@ -48,14 +58,18 @@ autumnWebhookRouter.post( const evt = await verifyAutumnWebhook(req, res); console.log("Received webhook from autumn"); const { type, data } = evt; - console.log( - "Type", - type, - "Scenario:", - data?.scenario, - "Product:", - data?.updated_product?.id - ); + + 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, diff --git a/server/src/external/resend/resendUtils.ts b/server/src/external/resend/resendUtils.ts index 84efc6a80..28d74ccab 100644 --- a/server/src/external/resend/resendUtils.ts +++ b/server/src/external/resend/resendUtils.ts @@ -1,10 +1,6 @@ import { Resend } from "resend"; import { logger } from "../logtail/logtailUtils.js"; -export const createResendCli = () => { - return new Resend(process.env.RESEND_API_KEY); -}; - export interface ResendEmailProps { to: string; subject: string; @@ -14,26 +10,22 @@ export interface ResendEmailProps { replyTo?: string; } -export const nameToEmail = (name: string) => { - return `${name.toLowerCase().replace(/\s+/g, ".")}@hey.${process.env.RESEND_DOMAIN}`; +export const createResendCli = () => { + return new Resend(process.env.RESEND_API_KEY); }; export const sendTextEmail = async ({ from, - fromEmail, to, subject, body, }: ResendEmailProps) => { const resend = createResendCli(); - fromEmail = fromEmail - ? `${fromEmail}@${process.env.RESEND_DOMAIN}` - : nameToEmail(from); try { logger.info(`Sending email to ${to} with subject ${subject}`); const { data, error } = await resend.emails.send({ - from: `${from} <${fromEmail}>`, + from: from, to: to, subject: subject, text: body, @@ -44,7 +36,6 @@ export const sendTextEmail = async ({ error, data: { from, - fromEmail, to, subject, body, @@ -56,7 +47,6 @@ export const sendTextEmail = async ({ error, data: { from, - fromEmail, to, subject, body, @@ -71,17 +61,12 @@ export const sendHtmlEmail = async ({ to, subject, body, - fromEmail, replyTo, }: ResendEmailProps) => { const resend = createResendCli(); - fromEmail = fromEmail - ? `${fromEmail}@${process.env.RESEND_DOMAIN}` - : nameToEmail(from); - await resend.emails.send({ - from: `${from} <${fromEmail}>`, + from: from, to: to, subject: subject, html: body, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index 241bba910..c2d3cc0ac 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -7,7 +7,7 @@ import { import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { CusService } from "@/internal/customers/CusService.js"; +import { CusService, CusService } from "@/internal/customers/CusService.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; @@ -15,13 +15,20 @@ import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtil import { attachToInsertParams } from "@/internal/products/productUtils.js"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { notNullish } from "@/utils/genUtils.js"; +import { notNullish, notNullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js"; +import { + getEarliestPeriodEnd, + getEarliestPeriodEnd, +} from "../stripeSubUtils/convertSubUtils.js"; import { createStripeCli } from "../utils.js"; -import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; +import { + getOptionsFromCheckoutSession, + getOptionsFromCheckoutSession, +} from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js"; import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js"; +import { handleSetupCheckout } from "./handleCheckoutCompleted/handleSetupCheckout.js"; export const handleCheckoutSessionCompleted = async ({ req, @@ -74,8 +81,22 @@ export const handleCheckoutSessionCompleted = async ({ checkoutSession.metadata?.autumn_metadata_id, ); + <<<<<<< HEAD const checkoutSub = checkoutSession.subscription as Stripe.Subscription | null; + ======= + if (attachParams.setupPayment) { + await handleSetupCheckout({ + req, + db, + attachParams, + }); + return; + } + + const checkoutSub = + checkoutSession.subscription as Stripe.Subscription | null; + >>>>>>> staging if (checkoutSub) { const activeCusProducts = await CusProductService.getByStripeSubId({ @@ -284,3 +305,4 @@ export const handleCheckoutSessionCompleted = async ({ // subscriptionId: !isOneOff // ? (checkoutSession.subscription as string) // : undefined, +// : undefined, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts index e3e4ce412..137b58c3d 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts @@ -27,9 +27,7 @@ export const handleCheckoutSub = async ({ }) => { const { org, customer } = attachParams; - if (!subscription) { - return; - } + if (!subscription) return; const { start, end } = subToPeriodStartEnd({ sub: subscription }); diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts index aa75d4c8c..de792b439 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts @@ -26,7 +26,9 @@ export const handleRemainingSets = async ({ let remainingSets = itemSets ? itemSets.slice(1) : []; const remainingItems = remainingSets.flatMap((set) => set.items); - let invoiceIds: string[] = [checkoutSession.invoice as string]; + let invoiceIds: string[] = checkoutSession.invoice + ? [checkoutSession.invoice as string] + : []; // Replace items with empty price if needed... for (const price of attachParams.prices) { diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts new file mode 100644 index 000000000..e0ec4379f --- /dev/null +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts @@ -0,0 +1,34 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; +import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; +import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { ExtendedRequest } from "@/utils/models/Request.js"; +import { AttachBranch } from "@autumn/shared"; +import Stripe from "stripe"; +import { createStripeCli } from "../../utils.js"; + +export const handleSetupCheckout = async ({ + req, + db, + attachParams, +}: { + req: ExtendedRequest; + db: DrizzleCli; + attachParams: AttachParams; +}) => { + const logger = req.logger; + const { org, customer } = attachParams; + + 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(), + }); +}; diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index d97486b52..be129258f 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -23,8 +23,92 @@ import { type Subscription, CusProductResponse, notNullish, + FullCustomer, + APIVersion, } from "@autumn/shared"; +export const getSingleEntityResponse = async ({ + entityId, + org, + env, + fullCus, + features, + withAutumnId = false, +}: { + entityId: string; + org: Organization; + env: AppEnv; + fullCus: FullCustomer; + features: Feature[]; + withAutumnId?: boolean; +}) => { + let entity = fullCus.entities.find( + (e: Entity) => e.id == entityId || e.internal_id == entityId + ); + + const apiVersion = APIVersion.v1_2; + + 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 + ); + } + + 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 { main, addOns } = await processFullCusProducts({ + fullCusProducts: entityCusProducts, + entities: fullCus.entities, + subs: entitySubs, + org, + apiVersion: APIVersion.v1_2, + features, + }); + + let products: CusProductResponse[] = [...main, ...addOns]; + + 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, + }; +}; + export const getEntityResponse = async ({ db, entityIds, @@ -52,16 +136,6 @@ export const getEntityResponse = async ({ logger: any; skipCache?: boolean; }) => { - // let customer = await CusService.getFull({ - // db, - // idOrInternalId: customerId, - // orgId: org.id, - // env, - // withEntities: true, - // withSubs: true, - // expand, - // entityId, - // }); let customer = await getCusWithCache({ db, idOrInternalId: customerId, @@ -81,76 +155,18 @@ export const getEntityResponse = async ({ }); } - let entities = customer.entities.filter((e: Entity) => - entityIds.includes(e.id) - ); - - let entityCusProducts = customer.customer_products.filter( - (p: FullCusProduct) => { - if (org.config.entity_product) { - return entities.some( - (e: Entity) => - e.internal_id == p.internal_entity_id && - notNullish(p.internal_entity_id) - ); - } - return ( - entities.some((e: Entity) => e.internal_id == p.internal_entity_id) || - nullish(p.internal_entity_id) - ); - } - ); - - let subs = customer.subscriptions || []; - const entityResponses: EntityResponse[] = []; for (const entityId of entityIds) { - let entity = customer.entities.find( - (e: Entity) => e.id == entityId || e.internal_id == entityId - ); - if (!entity) { - throw new RecaseError({ - message: `Entity ${entityId} not found for customer ${customerId}`, - code: ErrCode.EntityNotFound, - statusCode: 400, - }); - } - - let entitySubs = subs.filter((s: Subscription) => - entityCusProducts.some((p: FullCusProduct) => - p.subscription_ids?.includes(s.stripe_id || "") - ) - ); - - let { main, addOns } = await processFullCusProducts({ - fullCusProducts: entityCusProducts, - entities: customer.entities, - subs: entitySubs, + let entityResponse = await getSingleEntityResponse({ + entityId, org, - apiVersion, - features, - }); - - let products: CusProductResponse[] = [...main, ...addOns]; - - let cusFeatures = await getCusFeaturesResponse({ - cusProducts: entityCusProducts, - org, - entity, - apiVersion, - }); - - entityResponses.push({ - ...(withAutumnId ? { autumn_id: entity.internal_id } : {}), - id: entity.id, - name: entity.name, - created_at: entity.created_at, - // feature_id: entity.feature_id, - customer_id: customerId, env, - products, - features: cusFeatures, + fullCus: customer, + features, + withAutumnId, }); + + entityResponses.push(entityResponse); } return { diff --git a/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts b/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts index f9c548de8..5b9d713fe 100644 --- a/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts +++ b/server/src/internal/api/entitled/checkUtils/getV2CheckResponse.ts @@ -121,19 +121,6 @@ export const getV2CheckResponse = async ({ allowed = true; } - // let totalUsageLimit = 0; - - // for (const ent of featureCusEnts) { - // totalUsageLimit += ent.entitlement.usage_limit || 0; - // } - // if (totalUsageLimit > 0) { - // if ( - // cusFeature.balance - (requiredBalance || 1) < - // -(totalUsageLimit - (cusFeature.included_usage || 0)) - // ) { - // allowed = false; - // } - // } let finalRequired = notNullish(requiredBalance) ? requiredBalance : 1; if (featureToUse.type == FeatureType.CreditSystem) { finalRequired = featureToCreditSystem({ diff --git a/server/src/internal/api/events/eventRouter.ts b/server/src/internal/api/events/eventRouter.ts index 279c27f5a..846f690f3 100644 --- a/server/src/internal/api/events/eventRouter.ts +++ b/server/src/internal/api/events/eventRouter.ts @@ -195,6 +195,7 @@ export const handleEventSent = async ({ customerId: customer.id, entityId: event_data.entity_id, features: affectedFeatures, + allFeatures: req.features, event, org, env, diff --git a/server/src/internal/api/events/usageRouter.ts b/server/src/internal/api/events/usageRouter.ts index 2770c4cb3..c029efb8a 100644 --- a/server/src/internal/api/events/usageRouter.ts +++ b/server/src/internal/api/events/usageRouter.ts @@ -1,11 +1,9 @@ import { Router } from "express"; import { CusProductStatus, - type Customer, ErrCode, EventInsert, FeatureType, - FeatureUsageType, FullCustomer, } from "@autumn/shared"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; @@ -20,7 +18,6 @@ import { addTaskToQueue } from "@/queue/queueUtils.js"; import { getEventTimestamp } from "./eventUtils.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; -import { logger } from "@/external/logtail/logtailUtils.js"; import { isPaidContinuousUse } from "@/internal/features/featureUtils.js"; export const eventsRouter: Router = Router(); @@ -195,6 +192,7 @@ export const handleUsageEvent = async ({ internalCustomerId: customer.internal_id, eventId: newEvent.id, features, + allFeatures: req.features, org, env: req.env, properties, diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index add1c6f58..2b1b40d9a 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -119,7 +119,7 @@ export const handleCreateCheckout = async ({ notNullish(checkoutParams.payment_method_types) || notNullish(checkoutParams.payment_method_configuration); - const sessionParams = { + let sessionParams = { customer: customer.processor.id, line_items: items, subscription_data: subscriptionData, @@ -145,6 +145,20 @@ export const handleCreateCheckout = async ({ : 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, + }, + }; + } + try { checkout = await stripeCli.checkout.sessions.create(sessionParams); logger.info( diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index cb8061ebe..756f63a6d 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -14,6 +14,7 @@ import { APIVersion, AttachBranch, AttachConfig, + AttachScenario, CusProductStatus, ProrationBehavior, } from "@autumn/shared"; @@ -30,7 +31,7 @@ import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js"; import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; import { shouldCancelSub } from "./upgradeFlowUtils.js"; import { handleUpgradeFlowSchedule } from "./handleUpgradeFlowSchedule.js"; -import { logPhaseItems } from "../../mergeUtils/phaseUtils/phaseUtils.js"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; export const handleUpgradeFlow = async ({ req, @@ -164,15 +165,33 @@ export const handleUpgradeFlow = async ({ latestInvoice = res.latestInvoice; } - 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 }); + } + } if (attachParams.products.length > 0) { logger.info(`UPGRADE FLOW: creating new cus product`); diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts index 0e3abd259..a63a7c9ce 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/getAttachParams.ts @@ -5,7 +5,6 @@ import { orgToVersion } from "@/utils/versionUtils.js"; import { APIVersion } from "@autumn/shared"; import { AttachParams } from "../../../cusProducts/AttachParams.js"; import { nullish } from "@/utils/genUtils.js"; -import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; export const getAttachParams = async ({ req, @@ -81,6 +80,7 @@ export const getAttachParams = async ({ disableFreeTrial: attachBody.free_trial === false || false, checkoutSessionParams: attachBody.checkout_session_params, isCustom: attachBody.is_custom, + setupPayment: attachBody.setup_payment, }; return { diff --git a/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts new file mode 100644 index 000000000..4f2cbffdd --- /dev/null +++ b/server/src/internal/customers/attach/attachUtils/handleAttachErrors/handleCheckoutErrors.ts @@ -0,0 +1,14 @@ +import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; +import { AttachBranch } from "@autumn/shared"; + +export const handleCheckoutErrors = ({ + attachParams, + branch, +}: { + attachParams: AttachParams; + branch: AttachBranch; +}) => { + if (attachParams.setupPayment) { + // Make sure only usage prices are added? + } +}; diff --git a/server/src/internal/customers/attach/checkout/handleCheckout.ts b/server/src/internal/customers/attach/checkout/handleCheckout.ts index 1744894b1..c5a5c0243 100644 --- a/server/src/internal/customers/attach/checkout/handleCheckout.ts +++ b/server/src/internal/customers/attach/checkout/handleCheckout.ts @@ -23,7 +23,7 @@ import { getPriceOptions } from "@/internal/products/prices/priceUtils.js"; import { getHasProrations } from "./getHasProrations.js"; import { handleCreateInvoiceCheckout } from "../../add-product/handleCreateInvoiceCheckout.js"; import { z } from "zod"; -import { formatUnixToDate } from "@/utils/genUtils.js"; +import { formatUnixToDate, notNullish } from "@/utils/genUtils.js"; const getAttachVars = async ({ req, diff --git a/server/src/internal/customers/cusProducts/AttachParams.ts b/server/src/internal/customers/cusProducts/AttachParams.ts index 17a284091..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"; @@ -78,6 +78,7 @@ export type AttachParams = { finalizeInvoice?: boolean; req?: any; fromCancel?: boolean; + setupPayment?: boolean; }; export type InsertCusProductParams = { diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts index b84ce2657..720813bb9 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.ts @@ -56,7 +56,7 @@ export const findMainCusEntForFeature = ({ feature: Feature; }) => { let mainCusEnt = cusEnts.find( - (e: any) => e.entitlement.feature.internal_id === feature.internal_id, + (e: any) => e.entitlement.feature.internal_id === feature.internal_id ); return mainCusEnt; @@ -70,7 +70,7 @@ export const findLinkedCusEnts = ({ feature: Feature; }) => { return cusEnts.filter( - (e: any) => e.entitlement.entity_feature_id === feature.id, + (e: any) => e.entitlement.entity_feature_id === feature.id ); }; diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts index 82363d236..975f0f3c8 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils/getExistingUsage.ts @@ -67,6 +67,9 @@ export const getExistingUsages = ({ let usages: Record< string, { + feature_id: string; + interval: EntInterval; + interval_count: number; usage: number; entityUsages: Record | null; fromEntities: boolean; @@ -84,6 +87,9 @@ export const getExistingUsages = ({ if (!usages[key]) { usages[key] = { + feature_id: feature?.id || "", + interval: EntInterval.Lifetime, + interval_count: 1, usage: 0, entityUsages: null, fromEntities: true, @@ -97,21 +103,20 @@ export const getExistingUsages = ({ 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; - } + if (feature.type == FeatureType.Boolean) continue; 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, @@ -191,7 +196,7 @@ export const addExistingUsagesToCusEnts = ({ // Sort cusEnts sortCusEntsForDeduction(fullCusEnts); - // printLogs = true; + printLogs = false; if (printLogs) { console.log("DEDUCTING EXISTING USAGE FROM CUS ENTS"); console.log("Existing usages:", existingUsages); @@ -204,30 +209,32 @@ export const addExistingUsagesToCusEnts = ({ ); } - // Perform deductions... - for (const key in existingUsages) { let usage = existingUsages[key].usage; let entityUsages = existingUsages[key].entityUsages; + const { + feature_id = "", + interval = "", + interval_count = 1, + } = existingUsages[key] || {}; + for (const cusEnt of fullCusEnts) { let ent = cusEnt.entitlement; - let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; + // let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`; let fromEntities = existingUsages[key].fromEntities; - if (cusEntKey !== key) { - continue; - } + // if (cusEntKey !== key) continue; + const isSameFeature = cusEnt.feature_id == feature_id; + + if (!isSameFeature) continue; let shouldCarry = ent.carry_from_previous || carryExistingUsages || fromEntities; - if (!shouldCarry) { - continue; - } + + if (!shouldCarry) continue; if (notNullish(entityUsages)) { - // TODO: Check if this works... - for (const entityId in entityUsages) { let { toDeduct, newEntities } = performDeductionOnCusEnt({ cusEnt, diff --git a/server/src/internal/customers/handlers/handleCreateCustomer.ts b/server/src/internal/customers/handlers/handleCreateCustomer.ts index e31051df2..d8c313361 100644 --- a/server/src/internal/customers/handlers/handleCreateCustomer.ts +++ b/server/src/internal/customers/handlers/handleCreateCustomer.ts @@ -188,6 +188,7 @@ export const handleCreateCustomer = async ({ // 1. If no ID and email is not NULL let createdCustomer; + if (newCus.id === null) { createdCustomer = await handleIdIsNull({ req, diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index 540268a84..0591d0ccf 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -352,13 +352,7 @@ cusRouter.get("/:customer_id/sub", async (req: any, res: any) => { (cp: FullCusProduct) => cp.subscription_ids || [] )?.[0]; - if (!subId) { - throw new RecaseError({ - message: "Customer has no active subscription", - code: "CUSTOMER_NO_ACTIVE_SUBSCRIPTION", - statusCode: StatusCodes.NOT_FOUND, - }); - } + if (!subId) return res.status(200).json({ sub: undefined }); const stripeCli = createStripeCli({ org, env }); const sub = await stripeCli.subscriptions.retrieve(subId, { diff --git a/server/src/internal/emails/constants.ts b/server/src/internal/emails/constants.ts new file mode 100644 index 000000000..6263b410f --- /dev/null +++ b/server/src/internal/emails/constants.ts @@ -0,0 +1,2 @@ +export const FROM_AUTUMN = `Autumn `; +export const FROM_AYUSH = `Ayush `; diff --git a/server/src/internal/emails/sendInvitationEmail.ts b/server/src/internal/emails/sendInvitationEmail.ts index 35b92f3a3..2f56134f1 100644 --- a/server/src/internal/emails/sendInvitationEmail.ts +++ b/server/src/internal/emails/sendInvitationEmail.ts @@ -1,13 +1,12 @@ import { logger } from "@/external/logtail/logtailUtils.js"; import { sendTextEmail } from "@/external/resend/resendUtils.js"; 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. -Click the link below to create an account / sign in to Autumn and you'll be automatically added to the organization. - -If the org isn't immediately active, you can click "Switch Organization" in the dropdown at the top left corner. +Click the link below to create an account / sign in to Autumn and accept the invitation. ${process.env.CLIENT_URL}/sign-in `; @@ -25,8 +24,7 @@ export const sendInvitationEmail = safeResend({ }) => { logger.info(`Sending invitation email to ${email}`); await sendTextEmail({ - from: `Autumn`, - fromEmail: "hey", + from: FROM_AUTUMN, to: email, subject: `Join ${orgName} on Autumn`, body: getInvitationEmailBody({ orgName }), diff --git a/server/src/internal/emails/sendMigrationEmail.ts b/server/src/internal/emails/sendMigrationEmail.ts index e542d8905..08a7b4d15 100644 --- a/server/src/internal/emails/sendMigrationEmail.ts +++ b/server/src/internal/emails/sendMigrationEmail.ts @@ -3,6 +3,7 @@ import { sendTextEmail } from "@/external/resend/resendUtils.js"; import { MigrationJobStep, Organization } from "@autumn/shared"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { safeResend } from "@/external/resend/safeResend.js"; +import { FROM_AUTUMN } from "./constants.js"; export const sendMigrationEmail = safeResend({ fn: async ({ @@ -27,8 +28,7 @@ export const sendMigrationEmail = safeResend({ console.log("Sending migration email"); await sendTextEmail({ - from: `Autumn`, - fromEmail: "hey", + from: FROM_AUTUMN, to: "johnyeocx@gmail.com", subject: `Migration Job Finished -- ${migrationJob.id}`, body: ` diff --git a/server/src/internal/emails/sendOTPEmail.ts b/server/src/internal/emails/sendOTPEmail.ts index 2b767a4cc..1f4d2ad35 100644 --- a/server/src/internal/emails/sendOTPEmail.ts +++ b/server/src/internal/emails/sendOTPEmail.ts @@ -1,6 +1,7 @@ import { logger } from "@/external/logtail/logtailUtils.js"; import { createResendCli } from "@/external/resend/resendUtils.js"; 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) { @@ -8,13 +9,19 @@ const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => { return; } - const resend = createResendCli(); - await resend.emails.send({ - from: `Autumn `, - to: email, - subject: "Your verification code for Autumn", - react: OTPEmail({ otpCode: otp }), - }); + 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 955990fa0..22a6da83c 100644 --- a/server/src/internal/emails/sendOnboardingEmail.ts +++ b/server/src/internal/emails/sendOnboardingEmail.ts @@ -1,5 +1,6 @@ import { sendHtmlEmail } from "@/external/resend/resendUtils.js"; import { safeResend } from "@/external/resend/safeResend.js"; +import { FROM_AYUSH } from "./constants.js"; const getWelcomeEmailBody = (userFirstName: string) => { return ` @@ -23,7 +24,7 @@ export const sendOnboardingEmail = safeResend({ const firstName = name.split(" ")[0]; await sendHtmlEmail({ - from: "Ayush", + from: FROM_AYUSH, to: email, subject: "Anything I can help with?", body: getWelcomeEmailBody(firstName), diff --git a/server/src/internal/features/handlers/handleUpdateFeature.ts b/server/src/internal/features/handlers/handleUpdateFeature.ts index fce0ae499..e4ff03ece 100644 --- a/server/src/internal/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/features/handlers/handleUpdateFeature.ts @@ -258,7 +258,7 @@ export const handleUpdateFeature = async ( req, res, action: "Update feature", - handler: async (req: ExtendedRequest, res: ExtendedResponse) => { + handler: async (req: any, res: any) => { let featureId = req.params.feature_id; let data = req.body; let { db, orgId, env, logtail: logger } = req; diff --git a/server/src/internal/features/internalFeatureRouter.ts b/server/src/internal/features/internalFeatureRouter.ts index cc9e7eaf8..e0eb31be5 100644 --- a/server/src/internal/features/internalFeatureRouter.ts +++ b/server/src/internal/features/internalFeatureRouter.ts @@ -88,5 +88,5 @@ internalFeatureRouter.get( handleGetFeatureDeletionInfo ); -internalFeatureRouter.post("/:feature_id", handleUpdateFeature); +internalFeatureRouter.post("/:feature_id", handleUpdateFeature as any); internalFeatureRouter.delete("/:featureId", handleDeleteFeature); diff --git a/server/src/internal/orgs/handlers/handleGetInvites.ts b/server/src/internal/orgs/handlers/handleGetInvites.ts index c0d03e808..5a89b2f86 100644 --- a/server/src/internal/orgs/handlers/handleGetInvites.ts +++ b/server/src/internal/orgs/handlers/handleGetInvites.ts @@ -1,6 +1,6 @@ import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { invitation, user as userTable } from "@autumn/shared"; -import { and, eq } from "drizzle-orm"; +import { and, eq, gt } from "drizzle-orm"; export const handleGetInvites = async ( req: ExtendedRequest, @@ -16,7 +16,8 @@ export const handleGetInvites = async ( const invites = await db.query.invitation.findMany({ where: and( eq(invitation.status, "pending"), - eq(invitation.email, user?.email ?? "") + eq(invitation.email, user?.email ?? ""), + gt(invitation.expiresAt, new Date()) ), with: { inviter: true, diff --git a/server/src/internal/products/handlers/handleGetProduct.ts b/server/src/internal/products/handlers/handleGetProduct.ts index d67acf3f3..48764dba7 100644 --- a/server/src/internal/products/handlers/handleGetProduct.ts +++ b/server/src/internal/products/handlers/handleGetProduct.ts @@ -52,7 +52,8 @@ export const handleGetProduct = async (req: any, res: any) => await getProductResponse({ product, features, - }), + currency: req.org.default_currency, + }) ); } }, diff --git a/server/src/trigger/handleThresholdReached.ts b/server/src/trigger/handleThresholdReached.ts new file mode 100644 index 000000000..b156063fc --- /dev/null +++ b/server/src/trigger/handleThresholdReached.ts @@ -0,0 +1,291 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { sendSvixEvent } from "@/external/svix/svixHelpers.js"; +import { EntityService } from "@/internal/api/entities/EntityService.js"; +import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js"; +import { getV2CheckResponse } from "@/internal/api/entitled/checkUtils/getV2CheckResponse.js"; +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, +} from "@autumn/shared"; + +export const mergeNewCusEntsIntoCusProducts = ({ + cusProducts, + newCusEnts, +}: { + 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 + ); + + if (correspondingCusEnt) { + const { customer_product, ...rest } = correspondingCusEnt; + cusProduct.customer_entitlements[i] = rest; + } + } + } + + return cusProducts; +}; + +export const sendSvixThresholdReachedEvent = async ({ + 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"; +}) => { + const cusDetails = await getCustomerDetails({ + db, + customer: fullCus, + org, + env, + features, + logger, + cusProducts: fullCus.customer_products, + expand: [], + }); + + if (fullCus.entity) { + const entities = await EntityService.list({ + db, + internalCustomerId: fullCus.internal_id, + }); + fullCus.entities = entities; + await getSingleEntityResponse({ + org, + env, + features, + fullCus, + entityId: fullCus.entity.id, + }); + } + + 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; +}; + +export const handleAllowanceUsed = async ({ + 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; +}) => { + // 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 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, + }); + + // 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", + }); + } +}; + +export const handleThresholdReached = async ({ + db, + feature, + cusEnts, + newCusEnts, + fullCus, + org, + env, + features, + logger, +}: { + db: DrizzleCli; + feature: Feature; + cusEnts: FullCusEntWithFullCusProduct[]; + newCusEnts: FullCusEntWithFullCusProduct[]; + + fullCus: FullCustomer; + org: Organization; + env: AppEnv; + features: Feature[]; + logger: any; +}) => { + try { + const newCusProducts = mergeNewCusEntsIntoCusProducts({ + cusProducts: fullCus.customer_products, + newCusEnts: newCusEnts, + }); + + fullCus.customer_products = newCusProducts; + + 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, + }); + + if ( + prevCheckResponse.allowed === true && + v2CheckResponse.allowed === false + ) { + const cusDetails = await getCustomerDetails({ + db, + customer: fullCus, + org, + env, + features, + logger, + cusProducts: newCusProducts, + expand: [], + }); + + if (fullCus.entity) { + const entities = await EntityService.list({ + db, + internalCustomerId: fullCus.internal_id, + }); + fullCus.entities = entities; + await getSingleEntityResponse({ + org, + env, + features, + fullCus, + entityId: fullCus.entity.id, + }); + } + + 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, + }); + } +}; diff --git a/server/src/trigger/updateBalanceTask.ts b/server/src/trigger/updateBalanceTask.ts index 256ee068d..e1d677878 100644 --- a/server/src/trigger/updateBalanceTask.ts +++ b/server/src/trigger/updateBalanceTask.ts @@ -44,6 +44,7 @@ import { } from "@/internal/products/prices/priceUtils.js"; import { deductFromCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js"; import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import { handleThresholdReached } from "./handleThresholdReached.js"; // Decimal.set({ precision: 12 }); // 12 DP precision @@ -348,7 +349,6 @@ export const deductAllowanceFromCusEnt = async ({ const { db, feature, env, org, cusPrices, customer, entity } = deductParams; if (toDeduct == 0) { - return 0; } if ( @@ -445,6 +445,7 @@ export const deductAllowanceFromCusEnt = async ({ cusEnt.balance = newBalance; cusEnt.entities = newEntities; + return newToDeduct; }; @@ -469,6 +470,14 @@ export const deductFromUsageBasedCusEnt = async ({ onlyUsageAllowed: true, }) as FullCusEntWithFullCusProduct; + 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 @@ -516,6 +525,13 @@ export const deductFromUsageBasedCusEnt = async ({ entities: newEntities!, }); + // 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, @@ -538,9 +554,13 @@ export const deductFromUsageBasedCusEnt = async ({ }); if (newReplaceables && newReplaceables.length > 0) { - updates.balance = newBalance! - newReplaceables.length; + const finalBalance = newBalance! - newReplaceables.length; + updates.balance = finalBalance; + usageBasedEnt.balance = finalBalance; } else if (deletedReplaceables && deletedReplaceables.length > 0) { - updates.balance = newBalance! + deletedReplaceables.length; + const finalBalance = newBalance! + deletedReplaceables.length; + updates.balance = finalBalance; + usageBasedEnt.balance = finalBalance; } await CusEntService.update({ @@ -548,6 +568,8 @@ export const deductFromUsageBasedCusEnt = async ({ id: usageBasedEnt!.id, updates, }); + + console.log("Usage based cus ent balance", usageBasedEnt.balance); }; // Main function to update customer balance @@ -560,6 +582,7 @@ export const updateCustomerBalance = async ({ org, env, logger, + allFeatures, }: { db: DrizzleCli; customerId: string; @@ -569,6 +592,7 @@ export const updateCustomerBalance = async ({ org: Organization; env: AppEnv; logger: any; + allFeatures: Feature[]; }) => { const startTime = performance.now(); console.log("REVERSE DEDUCTION ORDER", org.config.reverse_deduction_order); @@ -618,6 +642,8 @@ export const updateCustomerBalance = async ({ for (const obj of featureDeductions) { let { feature, deduction: toDeduct } = obj; + const originalCusEnts = structuredClone(cusEnts); + for (const cusEnt of cusEnts) { if (cusEnt.entitlement.internal_feature_id != feature.internal_id) { continue; @@ -634,9 +660,7 @@ export const updateCustomerBalance = async ({ }, }); - if (toDeduct == 0) { - continue; - } + if (toDeduct == 0) continue; toDeduct = await deductAllowanceFromCusEnt({ toDeduct, @@ -656,23 +680,33 @@ export const updateCustomerBalance = async ({ }); } - if (toDeduct == 0) { - continue; + if (toDeduct !== 0) { + await deductFromUsageBasedCusEnt({ + toDeduct, + cusEnts, + deductParams: { + db, + feature, + env, + org, + cusPrices: cusPrices as any[], + customer, + properties: event.properties, + entity: customer.entity, + }, + }); } - 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, }); } @@ -691,7 +725,8 @@ export const runUpdateBalanceTask = async ({ }) => { try { // 1. Update customer balance - const { customerId, features, event, org, env, entityId } = payload; + const { customerId, features, event, org, env, entityId, allFeatures } = + payload; console.log("--------------------------------"); console.log( @@ -707,6 +742,7 @@ export const runUpdateBalanceTask = async ({ env, logger, entityId, + allFeatures, }); // console.time("refreshCusCache"); diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts index bff3bbd7b..afccf7d92 100644 --- a/server/src/trigger/updateUsageTask.ts +++ b/server/src/trigger/updateUsageTask.ts @@ -11,10 +11,7 @@ import { import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js"; import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js"; -import { - getFeatureBalance, - getPaidFeatureBalance, -} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { Decimal } from "decimal.js"; import { @@ -25,6 +22,7 @@ import { CusService } from "@/internal/customers/CusService.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { deductFromCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js"; import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import { handleThresholdReached } from "./handleThresholdReached.js"; // 2. Get deductions for each feature const getFeatureDeductions = ({ @@ -174,6 +172,7 @@ export const updateUsage = async ({ setUsage, logger, entityId, + allFeatures, }: { db: DrizzleCli; customerId: string; @@ -185,6 +184,7 @@ export const updateUsage = async ({ setUsage: boolean; logger: any; entityId?: string; + allFeatures: Feature[]; }) => { const customer = await CusService.getFull({ db, @@ -193,6 +193,7 @@ export const updateUsage = async ({ env, inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue], entityId, + withSubs: true, }); const { cusEnts, cusPrices } = await getCusEntsInFeatures({ @@ -226,6 +227,7 @@ export const updateUsage = async ({ return; } + const originalCusEnts = structuredClone(cusEnts); for (const obj of featureDeductions) { let { feature, deduction: toDeduct } = obj; @@ -245,9 +247,8 @@ export const updateUsage = async ({ }, }); - if (toDeduct == 0) { - continue; - } + if (toDeduct == 0) continue; + toDeduct = await deductAllowanceFromCusEnt({ toDeduct, cusEnt, @@ -267,24 +268,35 @@ export const updateUsage = async ({ }); } - if (toDeduct == 0) { - continue; + if (toDeduct !== 0) { + await deductFromUsageBasedCusEnt({ + toDeduct, + cusEnts, + deductParams: { + db, + feature, + env, + org, + cusPrices: cusPrices as any[], + customer, + properties, + entity: customer.entity, + }, + setZeroAdjustment: true, + }); } - 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, + + feature, + cusEnts: originalCusEnts, + newCusEnts: cusEnts, + fullCus: customer, + logger, }); } @@ -316,6 +328,7 @@ export const runUpdateUsageTask = async ({ org, env, entityId, + allFeatures, } = payload; console.log("--------------------------------"); @@ -334,6 +347,7 @@ export const runUpdateUsageTask = async ({ setUsage: set_usage, logger, entityId, + allFeatures, }); await refreshCusCache({ diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 60d10b595..6b033f666 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -12,6 +12,9 @@ import { sendOnboardingEmail } from "@/internal/emails/sendOnboardingEmail.js"; import { ADMIN_USER_IDs } from "./constants.js"; import { afterOrgCreated } from "./authUtils/afterOrgCreated.js"; import { createLoopsContact } from "@/external/resend/loopsUtils.js"; +import { invitation } from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import { logger } from "@/external/logtail/logtailUtils.js"; export const auth = betterAuth({ telemetry: { @@ -96,6 +99,18 @@ export const auth = betterAuth({ 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: { diff --git a/server/tests/advanced/multiFeature/multiFeature2.ts b/server/tests/advanced/multiFeature/multiFeature2.ts index 60f9ad4fe..52581d4c3 100644 --- a/server/tests/advanced/multiFeature/multiFeature2.ts +++ b/server/tests/advanced/multiFeature/multiFeature2.ts @@ -99,7 +99,7 @@ export const getLifetimeAndUsageCusEnts = async ({ 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 }); @@ -179,7 +179,7 @@ describe(`${chalk.yellowBright( }); expect(lifetimeCusEnt?.balance).to.equal( - (pro.items.lifetime.included_usage as number) - value, + (pro.items.lifetime.included_usage as number) - value ); expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage); }); @@ -193,13 +193,14 @@ describe(`${chalk.yellowBright( feature_id: features.metered1.id, }); - await timeout(4000); + await timeout(3000); await autumn.attach({ customer_id: customerId, product_id: premium.id, }); + // return; let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, @@ -210,9 +211,7 @@ describe(`${chalk.yellowBright( }); expect(lifetimeCusEnt).to.not.exist; - expect(newUsageCusEnt?.balance).to.equal( - premium.items.payPerUse.included_usage, - ); + expect(newUsageCusEnt?.balance).to.equal(-50); // Check invoice too let res = await autumn2.customers.get(customerId); @@ -221,7 +220,7 @@ describe(`${chalk.yellowBright( let invoice0Amount = value * (pro.items.payPerUse.price ?? 0); expect(invoices![0].total).to.equal( invoice0Amount, - "Invoice 0 should be 0", + "Invoice 0 should be 0" ); }); }); diff --git a/shared/enums/WebhookEventType.ts b/shared/enums/WebhookEventType.ts new file mode 100644 index 000000000..1312f8dd9 --- /dev/null +++ b/shared/enums/WebhookEventType.ts @@ -0,0 +1,4 @@ +export enum WebhookEventType { + CustomerProductsUpdated = "customer.products.updated", + CustomerThresholdReached = "customer.threshold_reached", +} diff --git a/shared/index.ts b/shared/index.ts index 813d13b72..8c0e6e508 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -153,3 +153,4 @@ export * from "./enums/ErrCode.js"; export * from "./enums/LoggerAction.js"; export * from "./enums/AttachErrCode.js"; export * from "./enums/APIVersion.js"; +export * from "./enums/WebhookEventType.js"; diff --git a/shared/models/attachModels/attachBody.ts b/shared/models/attachModels/attachBody.ts index e102f941d..c50249dbb 100644 --- a/shared/models/attachModels/attachBody.ts +++ b/shared/models/attachModels/attachBody.ts @@ -56,6 +56,7 @@ export const AttachBodySchema = z // Checkout params skip_checkout: z.boolean().optional(), + setup_payment: z.boolean().optional(), }) .refine( (data) => { diff --git a/shared/models/cusModels/cusModels.ts b/shared/models/cusModels/cusModels.ts index d48c403f5..4dfe956f4 100644 --- a/shared/models/cusModels/cusModels.ts +++ b/shared/models/cusModels/cusModels.ts @@ -21,28 +21,43 @@ export const CreateCustomerSchema = z.object({ .string() .refine( (val) => { - if (!val) return true; // Allow null/undefined + if (val === null) return true; // Allow null/undefined + return /^[a-zA-Z0-9_-]+$/.test(val); }, (val) => { - if (!val) return { message: "ID is required" }; - + 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." }; + 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." }; + 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." }; + 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 cannot contain '${invalidChar}'. Use only letters, numbers, underscores, and hyphens.`, + }; } - - return { message: "ID must contain only letters, numbers, underscores, and hyphens." }; + + return { + message: + "ID must contain only letters, numbers, underscores, and hyphens.", + }; } ) .nullish(), diff --git a/vite/src/app/layout.tsx b/vite/src/app/layout.tsx index 5c6e12d0d..c25af9d8e 100644 --- a/vite/src/app/layout.tsx +++ b/vite/src/app/layout.tsx @@ -29,9 +29,6 @@ import { ChatWidget } from "@/components/general/ChatWidget"; export function MainLayout() { const env = useEnv(); const { data, isPending } = useSession(); - const [sidebarState, setSidebarState] = useState<"expanded" | "collapsed">( - "expanded" - ); const { handleApiError } = useGlobalErrorHandler(); const navigate = useNavigate(); diff --git a/vite/src/hooks/common/useOrg.tsx b/vite/src/hooks/common/useOrg.tsx index 0f417b02c..326ff3724 100644 --- a/vite/src/hooks/common/useOrg.tsx +++ b/vite/src/hooks/common/useOrg.tsx @@ -1,29 +1,51 @@ -import { - authClient, - useListOrganizations, - useSession, -} from "@/lib/auth-client"; +import { authClient, useListOrganizations } from "@/lib/auth-client"; import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { useAxiosSWR } from "@/services/useAxiosSwr"; import { FrontendOrg } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; import { useEffect } from "react"; export const useOrg = () => { - // const { data, isLoading, error, mutate } = useAxiosSWR({ - // url: "/organization", - // }); const axiosInstance = useAxiosInstance(); + const { data: orgList } = useListOrganizations(); const fetcher = async () => { - const { data } = await axiosInstance.get("/organization"); - return data; + try { + const { data } = await axiosInstance.get("/organization"); + return data; + } catch (error) { + return null; + } }; - const { data, isLoading, error, refetch } = useQuery({ + const { + data: org, + isLoading, + error, + refetch, + } = useQuery({ queryKey: ["org"], queryFn: fetcher, }); - return { org: data as FrontendOrg, isLoading, error, mutate: refetch }; + const handleNoActiveOrg = async () => { + // 1. If there's existing org, set as active + if (orgList && orgList.length > 0) { + await authClient.organization.setActive({ + organizationId: orgList[0].id, + }); + window.location.reload(); + } else { + console.log("No org to set active, signing out"); + await authClient.signOut(); + } + }; + + useEffect(() => { + // 1. If no org... + if (!org && !isLoading) { + handleNoActiveOrg(); + } + }, [org, orgList]); + + return { org: org as FrontendOrg, isLoading, error, mutate: refetch }; }; diff --git a/vite/src/views/customers/components/customers-table/CustomerRow.tsx b/vite/src/views/customers/components/customers-table/CustomerRow.tsx index 4b1e6c70c..2f450dcbf 100644 --- a/vite/src/views/customers/components/customers-table/CustomerRow.tsx +++ b/vite/src/views/customers/components/customers-table/CustomerRow.tsx @@ -32,7 +32,7 @@ import { ContextMenuTrigger, } from "@/components/ui/context-menu"; import { useContextMenu } from "@/components/general/table-components/ContextMenuWrapper"; -import { DeleteCustomerDialog } from "../../customer/components/DeleteCustomer"; +import { DeleteCustomerDialog } from "../../customer/components/DeleteCustomerDialog"; import { useCusSearchQuery } from "../../hooks/useCusSearchQuery"; const CustomerWithProductsSchema = CustomerSchema.extend({ diff --git a/vite/src/views/customers/customer/components/DeleteCustomer.tsx b/vite/src/views/customers/customer/components/DeleteCustomerDialog.tsx similarity index 96% rename from vite/src/views/customers/customer/components/DeleteCustomer.tsx rename to vite/src/views/customers/customer/components/DeleteCustomerDialog.tsx index 6e6c288ee..6ad7e40a2 100644 --- a/vite/src/views/customers/customer/components/DeleteCustomer.tsx +++ b/vite/src/views/customers/customer/components/DeleteCustomerDialog.tsx @@ -42,7 +42,7 @@ export const DeleteCustomerDialog = ({ try { await axiosInstance.delete( - `/v1/customers/${customer.id}?delete_in_stripe=${deleteStripe}` + `/v1/customers/${customer.id || customer.internal_id}?delete_in_stripe=${deleteStripe}` ); await refetch(); diff --git a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx index 8352818e4..8bf14ee70 100644 --- a/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx +++ b/vite/src/views/customers/customer/product/multi-attach/MultiAttachDialog.tsx @@ -9,7 +9,6 @@ import { InvoiceCustomerButton } from "../components/InvoiceCustomerButton"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; import { ArrowUpRightFromSquare, Loader2, Plus, X } from "lucide-react"; -import { useCustomerContext } from "../../CustomerContext"; import { Select, SelectContent, @@ -18,14 +17,13 @@ import { SelectValue, } from "@/components/ui/select"; import { useEffect, useState } from "react"; -import { FullProduct, ProductV2 } from "@autumn/shared"; +import { ProductV2 } from "@autumn/shared"; import { Input } from "@/components/ui/input"; import { toast } from "sonner"; import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { getBackendErr, navigateTo } from "@/utils/genUtils"; +import { getBackendErr } from "@/utils/genUtils"; import { MultiAtttachLines } from "./MultiAttachLines"; import { CheckoutResult } from "autumn-js"; -import { Separator } from "@/components/ui/separator"; import { getStripeInvoiceLink } from "@/utils/linkUtils"; import { formatAmount } from "@/utils/product/productItemUtils"; import { formatUnixToDate } from "@/utils/formatUtils/formatDateUtils"; @@ -62,7 +60,7 @@ export const MultiAttachDialog = ({ url: `/customers/${customer.id}/sub`, }); - const subDiscounts = subData?.sub.discounts || []; + const subDiscounts = subData?.sub?.discounts || []; const getDefaultProductOptions = () => { return [{ product_id: null, quantity: 1 }]; diff --git a/vite/src/views/customers/customer/product/multi-attach/MultiAttachRewards.tsx b/vite/src/views/customers/customer/product/multi-attach/MultiAttachRewards.tsx index 5ad86dffb..641e8fd2b 100644 --- a/vite/src/views/customers/customer/product/multi-attach/MultiAttachRewards.tsx +++ b/vite/src/views/customers/customer/product/multi-attach/MultiAttachRewards.tsx @@ -11,6 +11,8 @@ import { } from "@/components/ui/select"; import { toast } from "sonner"; import { Reward, RewardType } from "@autumn/shared"; +import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { useOrg } from "@/hooks/common/useOrg"; export const AddRewardButton = ({ setAttachRewards, @@ -44,7 +46,8 @@ export const MultiAttachRewards = ({ setAttachRewards: (rewards: any) => void; sub: any; }) => { - const { rewards, org } = useCustomerContext(); + const { org } = useOrg(); + const { rewards } = useRewardsQuery(); const subDiscounts = sub?.discounts || []; const noRewards = attachRewards.length === 0 && subDiscounts.length === 0; diff --git a/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx b/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx index c4e976dd5..9bc497d00 100644 --- a/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx +++ b/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx @@ -1,4 +1,5 @@ import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; import { PageSectionHeader } from "@/components/general/PageSectionHeader"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -12,8 +13,11 @@ import { Check } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { DisconnectStripePopover } from "./DisconnectStripePopover"; +import { AppEnv } from "@autumn/shared"; +import { useEnv } from "@/utils/envUtils"; export const ConfigureStripe = () => { + const env = useEnv(); const { org, isLoading, mutate } = useOrg(); const axiosInstance = useAxiosInstance(); @@ -121,6 +125,25 @@ export const ConfigureStripe = () => { .

+ {env == AppEnv.Live && ( +
+ + If you want to use a restricted key + + +
+

The following scopes are needed:

+
    +
  • Core
  • +
  • Checkout
  • +
  • Billing
  • +
  • All webhook
  • +
  • Accounts
  • +
+
+
+
+ )} {org.stripe_connected ? ( { setLoading(true); if (action === "accept") { - await authClient.organization.acceptInvitation({ + const { data, error } = await authClient.organization.acceptInvitation({ invitationId: invite.id, }); + if (error) throw error; + // Switch to that org await authClient.organization.setActive({ organizationId: invite.organization.id, }); - toast.success(`Invitation accepted successfully`); + // toast.success(`Invitation accepted successfully`); window.location.reload(); } else { await authClient.organization.rejectInvitation({ diff --git a/vite/src/views/main-sidebar/components/OrgDropdown.tsx b/vite/src/views/main-sidebar/components/OrgDropdown.tsx index 3d81e64a0..d07b3e092 100644 --- a/vite/src/views/main-sidebar/components/OrgDropdown.tsx +++ b/vite/src/views/main-sidebar/components/OrgDropdown.tsx @@ -23,19 +23,10 @@ import { useListOrganizations, useSession, } from "@/lib/auth-client"; -import { FrontendOrg, user } from "@autumn/shared"; import { DropdownMenuGroup } from "@radix-ui/react-dropdown-menu"; -import { - ChevronDown, - LogOut, - PanelRight, - Plus, - Settings, - Shield, - Trash, -} from "lucide-react"; -import React from "react"; -import { useState } from "react"; +import { ChevronDown, PanelRight, Plus, Settings } from "lucide-react"; + +import { useState, useMemo } from "react"; import { CreateNewOrg } from "./CreateNewOrg"; import { toast } from "sonner"; import { LogOutItem } from "./LogOutItem"; @@ -54,19 +45,30 @@ export const OrgDropdown = () => { const { org, isLoading, error } = useOrg(); const { expanded, setExpanded } = useSidebarContext(); - const { data: orgs, isPending } = useListOrganizations(); + let { data: orgs, isPending } = useListOrganizations(); + const { data: activeOrganization } = authClient.useActiveOrganization(); + + // Exclude the active organization from the orgs list (this makes it easier for users to understand which org is active) + if (activeOrganization && orgs) { + orgs = orgs.filter((o) => o.id !== activeOrganization.id); + } + const [dialogType, setDialogType] = useState<"create" | "manage" | null>( null ); const { data: session } = useSession(); + // //remove the currect active org from the orgs data + // const inactiveOrgs = useMemo(() => { + // if (!orgs || !org) return []; + // return orgs.filter((orgItem: any) => orgItem.id !== org.id); + // }, [org, orgs]); + // To pre-fetch data useMemberships(); const [dropdownOpen, setDropdownOpen] = useState(false); const [manageOpen, setManageOpen] = useState(false); - const [stopImpersonatingLoading, setStopImpersonatingLoading] = - useState(false); if (isLoading) return ( @@ -124,7 +126,7 @@ export const OrgDropdown = () => {
{session?.user?.name} - + {session?.user?.email}
@@ -166,24 +168,27 @@ export const OrgDropdown = () => {
)} - - - - - Switch Organization - - - - {orgs?.map((org) => ( - - ))} - - - + {orgs && orgs.length > 0 && ( + <> + + + + Switch Organization + + + + {orgs.map((org) => ( + + ))} + + + + + )} @@ -197,8 +202,6 @@ const SwitchOrgItem = ({ org, setDropdownOpen }: any) => { const [loading, setLoading] = useState(false); const [_, setSearchParams] = useSearchParams(); - const { mutate } = useOrg(); - const handleSwitchOrg = async (orgId: string) => { setLoading(true); diff --git a/vite/src/views/main-sidebar/org-dropdown/manage-org/InvitePopover.tsx b/vite/src/views/main-sidebar/org-dropdown/manage-org/InvitePopover.tsx index 93f00a49b..ab838d3f7 100644 --- a/vite/src/views/main-sidebar/org-dropdown/manage-org/InvitePopover.tsx +++ b/vite/src/views/main-sidebar/org-dropdown/manage-org/InvitePopover.tsx @@ -16,38 +16,21 @@ import { useMemberships } from "../hooks/useMemberships"; export const InvitePopover = () => { const [email, setEmail] = useState(""); const [loading, setLoading] = useState(false); - const axiosInstance = useAxiosInstance(); - const { mutate } = useMemberships(); const [open, setOpen] = useState(false); const handleInvite = async () => { try { setLoading(true); - await authClient.organization.inviteMember({ + const { data, error } = await authClient.organization.inviteMember({ email: email, role: "admin", resend: true, }); - toast.success(`Successfully sent invitation to ${email}`); - // const { data, status } = await axiosInstance.post( - // "/organization/invite", - // { - // email: email, - // role: "admin", - // } - // ); - // if (status === 202) { - // await authClient.organization.inviteMember({ - // email: email, - // role: "admin", - // resend: true, - // }); - // toast.success(`Successfully sent invitation to ${email}`); - // } else { - // toast.success(data.message); - // } - // await mutate(); + if (error) throw error; + + toast.success(`Successfully sent invitation to ${email}`); + setOpen(false); } catch (error) { console.error(error); diff --git a/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgInvitesList.tsx b/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgInvitesList.tsx index 9cc9639d7..87f8a5d38 100644 --- a/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgInvitesList.tsx +++ b/vite/src/views/main-sidebar/org-dropdown/manage-org/OrgInvitesList.tsx @@ -17,7 +17,7 @@ export const OrgInvitesList = () => { if (isMembersLoading) return null; const membership = memberships.find( - (membership: Membership) => membership.user.id === data?.session?.userId, + (membership: Membership) => membership.user.id === data?.session?.userId ); const isAdmin = membership?.member.role === "admin" || membership?.member.role === "owner"; @@ -32,27 +32,32 @@ export const OrgInvitesList = () => { Expires At - {invites.map((invite: Invite) => { - return ( - - {invite.email} - {invite.status} - - {invite.role} - - - - {formatDateStr(invite.expiresAt)} - - - {isAdmin && } - - - ); - })} + {invites + .filter((invite: Invite) => { + if (!invite.expiresAt) return false; + return new Date(invite.expiresAt).getTime() > Date.now(); + }) + .map((invite: Invite) => { + return ( + + {invite.email} + {invite.status} + + {invite.role} + + + + {formatDateStr(invite.expiresAt)} + + + {isAdmin && } + + + ); + })} ); }; diff --git a/vite/src/views/products/features/components/CreateFeature.tsx b/vite/src/views/products/features/components/CreateFeature.tsx index 2f59444d5..14f32850b 100644 --- a/vite/src/views/products/features/components/CreateFeature.tsx +++ b/vite/src/views/products/features/components/CreateFeature.tsx @@ -90,7 +90,7 @@ export const CreateFeature = ({ return ( <> - + Create Feature diff --git a/vite/src/views/products/product/ProductView.tsx b/vite/src/views/products/product/ProductView.tsx index 11d22852a..9415fc35d 100644 --- a/vite/src/views/products/product/ProductView.tsx +++ b/vite/src/views/products/product/ProductView.tsx @@ -7,10 +7,8 @@ import ProductViewBreadcrumbs from "./components/ProductViewBreadcrumbs"; import { useState } from "react"; import { ProductContext } from "./ProductContext"; -import { useParams, useSearchParams } from "react-router"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useParams } from "react-router"; import { ManageProduct } from "./ManageProduct"; -import { AppEnv } from "@autumn/shared"; import { useProductChangedAlert } from "./hooks/useProductChangedAlert"; import { useProductData } from "./hooks/useProductData"; import { UpdateProductButton } from "./components/UpdateProductButton"; @@ -130,7 +128,7 @@ function ProductView() { -
+
diff --git a/vite/src/views/products/product/product-item/product-item-config/components/CusomiseIntervalPopover.tsx b/vite/src/views/products/product/product-item/product-item-config/components/CusomiseIntervalPopover.tsx index a4e719149..ee2fb797f 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/CusomiseIntervalPopover.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/CusomiseIntervalPopover.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef } from "react"; import { useProductItemContext } from "../../ProductItemContext"; import FieldLabel from "@/components/general/modal-components/FieldLabel"; import { Input } from "@/components/ui/input"; @@ -14,6 +14,7 @@ export const CustomiseIntervalPopover = () => { const [open, setOpen] = useState(false); const { item, setItem } = useProductItemContext(); const [intervalCount, setIntervalCount] = useState(item.interval_count || 1); + const triggerRef = useRef(null); const handleSave = () => { setItem({ @@ -27,20 +28,22 @@ export const CustomiseIntervalPopover = () => { e.preventDefault()} onCloseAutoFocus={(e) => e.preventDefault()} + // avoidCollisions={false} + // sticky="always" >
Interval Count diff --git a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx index 171bae1cb..7f2fcc821 100644 --- a/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/components/feature-price/SelectBillingCycle.tsx @@ -1,7 +1,6 @@ import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { Select } from "@/components/ui/select"; - import { + Select, SelectContent, SelectItem, SelectTrigger, @@ -29,14 +28,6 @@ export const SelectCycle = () => { }); }; - // const intervalText = (interval: BillingInterval) => { - // return interval === BillingInterval.SemiAnnual - // ? "per half year" - // : interval === BillingInterval.OneOff - // ? "one off" - // : `per ${interval}`; - // }; - return (
From fec50c7d14bad6b787bda6bcd50872c8b8b7290a Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 12:52:50 +0100 Subject: [PATCH 25/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20merge=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../webhookHandlers/handleCheckoutCompleted.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index c2d3cc0ac..3220a0111 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -7,7 +7,7 @@ import { import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { CusService, CusService } from "@/internal/customers/CusService.js"; +import { CusService } from "@/internal/customers/CusService.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js"; @@ -15,16 +15,14 @@ import { getMetadataFromCheckoutSession } from "@/internal/metadata/metadataUtil import { attachToInsertParams } from "@/internal/products/productUtils.js"; import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { notNullish, notNullish } from "@/utils/genUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { - getEarliestPeriodEnd, - getEarliestPeriodEnd, + getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js"; import { createStripeCli } from "../utils.js"; import { - getOptionsFromCheckoutSession, - getOptionsFromCheckoutSession, + getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js"; import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js"; import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js"; @@ -81,10 +79,6 @@ export const handleCheckoutSessionCompleted = async ({ checkoutSession.metadata?.autumn_metadata_id, ); - <<<<<<< HEAD - const checkoutSub = - checkoutSession.subscription as Stripe.Subscription | null; - ======= if (attachParams.setupPayment) { await handleSetupCheckout({ req, @@ -96,7 +90,6 @@ export const handleCheckoutSessionCompleted = async ({ const checkoutSub = checkoutSession.subscription as Stripe.Subscription | null; - >>>>>>> staging if (checkoutSub) { const activeCusProducts = await CusProductService.getByStripeSubId({ From fe96c87a7a53f95c48ce588fdb85986b3009c5f8 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:47:16 +0100 Subject: [PATCH 26/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20timezones=20not=20l?= =?UTF-8?q?ocalised?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customer/analytics/AnalyticsGraph.tsx | 338 +++++++++--------- 1 file changed, 178 insertions(+), 160 deletions(-) diff --git a/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx b/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx index 8a12c2c84..f7d14afe8 100644 --- a/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx +++ b/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx @@ -1,195 +1,213 @@ +import type { AgChartOptions, FormatterParams } from "ag-charts-community"; +import { AgCharts } from "ag-charts-react"; import { AllCommunityModule, - ColDef, + type ColDef, ModuleRegistry, + type PaginationChangedEvent, + type RowDataUpdatedEvent, ValidationModule, - ValueFormatterParams, - RowDataUpdatedEvent, - PaginationChangedEvent, + type ValueFormatterParams, } from "ag-grid-community"; -import { AgChartOptions, FormatterParams } from "ag-charts-community"; -import { AgCharts } from "ag-charts-react"; // Register all Community features import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "react"; -import { IRow, Row, autumnTheme, paginationOptions } from "./components/AGGrid"; import { useAnalyticsContext } from "./AnalyticsContext"; +import { + autumnTheme, + type IRow, + paginationOptions, + type Row, +} from "./components/AGGrid"; import { RowClickDialog } from "./components/RowClickDialog"; +const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + +// Helper function to parse UTC timestamps from the backend +const parseUTCTimestamp = (timestamp: string): Date => { + // If the timestamp doesn't end with 'Z' or have timezone info, assume it's UTC + if (!timestamp.includes('Z') && !timestamp.includes('+') && !timestamp.includes('-', 10)) { + // Add 'Z' to indicate UTC if it's missing + return new Date(timestamp + (timestamp.includes('T') ? 'Z' : ' UTC')); + } + return new Date(timestamp); +}; + const dateFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", { - month: "short", - day: "numeric", + month: "short", + day: "numeric", + timeZone: userTimeZone, }); const hourFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", { - hour: "numeric", - minute: "numeric", + hour: "numeric", + minute: "numeric", + timeZone: userTimeZone, }); const timestampFormatter = new Intl.DateTimeFormat( - navigator.language || "en-US", - { - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - } + navigator.language || "en-US", + { + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZone: userTimeZone, + }, ); export function EventsBarChart({ - data, - chartConfig, + data, + chartConfig, }: { - data: { - meta: any[]; - rows: number; - data: Row[]; - }; - chartConfig: any; + data: { + meta: any[]; + rows: number; + data: Row[]; + }; + chartConfig: any; }) { - const { selectedInterval } = useAnalyticsContext(); - const [options, setOptions] = useState({ - data: data.data, - series: chartConfig, - theme: { - params: { - fontFamily: { - googleFont: "Inter", - }, - }, - palette: { - fills: [ - "#9c5aff", - "#a97eff", - "#8268ff", - "#7571ff", - "#687aff", - "#5b83ff", - "#4e8cff", - "#4195ff", - "#349eff", - "#27a7ff", - ], - }, - }, - background: { - fill: "#fafaf9", - }, - axes: [ - { - type: "category", - position: "bottom", - label: { - color: "#52525b", - }, - line: { - enabled: false, - }, - }, - { - type: "number", - position: "left", - label: { - color: "#52525b", - }, - }, - ], - formatter: { - x: (params: FormatterParams) => { - if (params.type !== "category") return; - return selectedInterval === "24h" - ? hourFormatter.format(new Date(params.value as string)) - : dateFormatter.format(new Date(params.value as string)); - }, - }, - legend: { - enabled: true, - }, - }); + const { selectedInterval } = useAnalyticsContext(); + const [options, setOptions] = useState({ + data: data.data, + series: chartConfig, + theme: { + params: { + fontFamily: { + googleFont: "Inter", + }, + }, + palette: { + fills: [ + "#9c5aff", + "#a97eff", + "#8268ff", + "#7571ff", + "#687aff", + "#5b83ff", + "#4e8cff", + "#4195ff", + "#349eff", + "#27a7ff", + ], + }, + }, + background: { + fill: "#fafaf9", + }, + axes: [ + { + type: "category", + position: "bottom", + label: { + color: "#52525b", + }, + line: { + enabled: false, + }, + }, + { + type: "number", + position: "left", + label: { + color: "#52525b", + }, + }, + ], + formatter: { + x: (params: FormatterParams) => { + if (params.type !== "category") return; + return selectedInterval === "24h" + ? hourFormatter.format(parseUTCTimestamp(params.value as string)) + : dateFormatter.format(parseUTCTimestamp(params.value as string)); + }, + }, + legend: { + enabled: true, + }, + }); - const chartData = data.data; + useEffect(() => { + setOptions((prevOptions) => ({ + ...prevOptions, + data: data.data, + series: chartConfig, + })); + }, [chartConfig, data]); - useEffect(() => { - setOptions({ - ...options, - data: data.data, - series: chartConfig, - }); - }, [chartConfig, data]); - - return ; + return ; } export function EventsAGGrid({ data }: { data: any }) { - const [rowData, setRowData] = useState([]); - const [isOpen, setIsOpen] = useState(false); - const [event, setEvent] = useState(null); - const [colDefs] = useState[]>([ - { - field: "timestamp", - flex: 1, - valueFormatter: (params: ValueFormatterParams) => { - return timestampFormatter.format(new Date(params.value as string)); - }, - cellStyle: { - paddingLeft: "2.5rem", - fontWeight: "normal", - }, - headerStyle: { - paddingLeft: "2.5rem", - }, - }, - { field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } }, - { field: "value", flex: 0, cellStyle: { fontWeight: "normal" } }, - { field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } }, - ]); + const [rowData, setRowData] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [event, setEvent] = useState(null); + const [colDefs] = useState[]>([ + { + field: "timestamp", + flex: 1, + valueFormatter: (params: ValueFormatterParams) => { + return timestampFormatter.format(parseUTCTimestamp(params.value as string)); + }, + cellStyle: { + paddingLeft: "2.5rem", + fontWeight: "normal", + }, + headerStyle: { + paddingLeft: "2.5rem", + }, + }, + { field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } }, + { field: "value", flex: 0, cellStyle: { fontWeight: "normal" } }, + { field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } }, + ]); - ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]); + ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]); - const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } = - useAnalyticsContext(); + const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } = + useAnalyticsContext(); - useEffect(() => { - setRowData(data.data); - }, [data]); + useEffect(() => { + setRowData(data.data); + }, [data]); - return ( -
- { - setEvent(event.data as IRow); - setIsOpen(true); - }} - onRowDataUpdated={(event: RowDataUpdatedEvent) => { - setTotalRows(event.api.paginationGetRowCount()); - }} - onPaginationChanged={(event: PaginationChangedEvent) => { - setTotalPages(event.api.paginationGetTotalPages()); - setCurrentPage(event.api.paginationGetCurrentPage() + 1); - }} - /> - {event && ( - - )} -
- ); + return ( +
+ { + setEvent(event.data as IRow); + setIsOpen(true); + }} + onRowDataUpdated={(event: RowDataUpdatedEvent) => { + setTotalRows(event.api.paginationGetRowCount()); + }} + onPaginationChanged={(event: PaginationChangedEvent) => { + setTotalPages(event.api.paginationGetTotalPages()); + setCurrentPage(event.api.paginationGetCurrentPage() + 1); + }} + /> + {event && ( + + )} +
+ ); } From 77790cb1762f9fa77c736ef3b044ef5d8f2084e0 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:11:05 +0100 Subject: [PATCH 27/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20applying=20to=20all?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rewards/reward-config/DiscountConfig.tsx | 496 +++++++++--------- .../rewards/reward-config/RewardConfig.tsx | 279 +++++----- .../rewards/utils/defaultRewardModels.ts | 34 +- 3 files changed, 408 insertions(+), 401 deletions(-) diff --git a/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx index 7100175b2..db556f7fb 100644 --- a/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx @@ -1,20 +1,13 @@ -import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { Input } from "@/components/ui/input"; -import { useState } from "react"; -import { Select, SelectContent, SelectItem } from "@/components/ui/select"; -import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { - Reward, CouponDurationType, + type ProductItem, + type Reward, RewardType, - ProductItem, } from "@autumn/shared"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { Check, ChevronsUpDown, X } from "lucide-react"; +import { useState } from "react"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -23,104 +16,115 @@ import { CommandItem, CommandList, } from "@/components/ui/command"; -import { Check, ChevronsUpDown, Trash2, X } from "lucide-react"; - -import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { isFeatureItem } from "@/utils/product/getItemType"; import { formatProductItemText } from "@/utils/product/product-item/formatProductItem"; -import { useOrg } from "@/hooks/common/useOrg"; -import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; -import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; export const DiscountConfig = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const { org } = useOrg(); + const { org } = useOrg(); - const config = reward.discount_config!; - const setConfig = (key: any, value: any) => { - setReward({ - ...reward, - discount_config: { ...config, [key]: value }, - }); - }; + const config = reward.discount_config!; + const setConfig = (key: any, value: any) => { + setReward({ + ...reward, + discount_config: { ...config, [key]: value }, + }); + }; - return ( -
-
-
- Amount - - setConfig("discount_value", Number(e.target.value)) - } - endContent={ -

- {reward.type === RewardType.PercentageDiscount - ? "%" - : org?.default_currency || "USD"} -

- } - /> -
-
- Duration -
- {config.duration_type === CouponDurationType.Months && ( - { - setConfig("duration_value", Number(e.target.value)); - }} - type="number" - /> - )} - -
-
-
+ return ( +
+
+
+ Amount + + setConfig("discount_value", Number(e.target.value)) + } + endContent={ +

+ {reward.type === RewardType.PercentageDiscount + ? "%" + : org?.default_currency || "USD"} +

+ } + /> +
+
+ Duration +
+ {config.duration_type === CouponDurationType.Months && ( + { + setConfig("duration_value", Number(e.target.value)); + }} + type="number" + /> + )} + +
+
+
- {/* {config.duration_type !== CouponDurationType.OneOff && + {/* {config.duration_type !== CouponDurationType.OneOff && reward.type === RewardType.FixedDiscount && (
)} */} -
- {/*

Products

*/} - Products +
+ {/*

Products

*/} + Products - -
-
- ); + +
+
+ ); }; const ProductPriceSelector = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const { org } = useOrg(); - const { products } = useProductsQuery(); - const { features } = useFeaturesQuery(); + const { org } = useOrg(); + const { products } = useProductsQuery(); + const { features } = useFeaturesQuery(); - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); - const config = reward.discount_config!; - const setConfig = (key: any, value: any) => { - setReward({ - ...reward, - discount_config: { ...config, [key]: value }, - }); - }; + const config = reward.discount_config!; + const setConfig = (key: any, value: any) => { + setReward({ + ...reward, + discount_config: { ...config, [key]: value }, + }); + }; - // Handle selection/deselection of a price - const handlePriceToggle = (priceId: string) => { - let newPriceIds = [...(config.price_ids || [])]; - if (config.price_ids?.includes(priceId)) { - newPriceIds = config.price_ids?.filter((id) => id !== priceId) || []; - } else { - newPriceIds = [...(config.price_ids || []), priceId]; - } - setConfig("price_ids", newPriceIds); - }; + // Handle selection/deselection of a price + const handlePriceToggle = (priceId: string) => { + let newPriceIds = [...(config.price_ids || [])]; + if (config.price_ids?.includes(priceId)) { + newPriceIds = config.price_ids?.filter((id) => id !== priceId) || []; + } else { + newPriceIds = [...(config.price_ids || []), priceId]; + } + setConfig("price_ids", newPriceIds); + }; - if (!products || products.length === 0) { - return

No products available

; - } + if (!products || products.length === 0) { + return

No products available

; + } - return ( - - - -
- ); - })} - - )} - - - - - - - - - No prices found. - - { - setConfig("apply_to_all", !config.apply_to_all); - }} - className="cursor-pointer" - > -

Apply to all products

- {config.apply_to_all && ( - - )} -
-
- {!config.apply_to_all && - products.map((product: any) => ( - - {product.items.length > 0 ? ( - product.items - ?.filter((item: ProductItem) => { - return !isFeatureItem(item); - }) - .map((item: any) => ( - handlePriceToggle(item.price_id)} - className="cursor-pointer overflow-x-hidden max-w-[380px]" - > - - {formatProductItemText({ - item, - org, - features, - })} - + const text = item + ? formatProductItemText({ + item, + org, + features, + }) + : "Deleted price"; + return ( +
+

{text}

+ +
+ ); + })} + + + + + + + + + No prices found. + + { + setConfig("apply_to_all", !config.apply_to_all); + }} + className="cursor-pointer" + > +

Apply to all products

+ {config.apply_to_all && ( + + )} +
+
+ {!config.apply_to_all && + products.map((product: any) => ( + + {product.items.length > 0 ? ( + product.items + ?.filter((item: ProductItem) => { + return !isFeatureItem(item); + }) + .map((item: any) => ( + handlePriceToggle(item.price_id)} + className="cursor-pointer overflow-x-hidden max-w-[380px]" + > + + {formatProductItemText({ + item, + org, + features, + })} + - {config.price_ids?.includes(item.price_id) && ( - - )} - - )) - ) : ( - -

No prices available

-
- )} -
- ))} -
-
-
-
- - ); + {config.price_ids?.includes(item.price_id) && ( + + )} +
+ )) + ) : ( + +

No prices available

+
+ )} +
+ ))} +
+
+
+
+ + ); }; diff --git a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx index c0feeb552..32caff09f 100644 --- a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx @@ -1,146 +1,153 @@ -import FieldLabel from "@/components/general/modal-components/FieldLabel"; - -import { Input } from "@/components/ui/input"; +import { + type ProductV2, + type Reward, + RewardType +} from "@autumn/shared"; import { useEffect, useState } from "react"; -import { Select, SelectContent, SelectItem } from "@/components/ui/select"; -import { SelectTrigger, SelectValue } from "@/components/ui/select"; -import { keyToTitle, slugify } from "@/utils/formatUtils/formatTextUtils"; -import { Reward, RewardType, Product, ProductV2 } from "@autumn/shared"; -import { useProductsContext } from "../../ProductsContext"; -import { DiscountConfig } from "./DiscountConfig"; -import { notNullish } from "@/utils/genUtils"; -import { defaultDiscountConfig } from "../utils/defaultRewardModels"; -import { isFreeProduct } from "@/utils/product/priceUtils"; +import FieldLabel from "@/components/general/modal-components/FieldLabel"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; +import { keyToTitle, slugify } from "@/utils/formatUtils/formatTextUtils"; +import { notNullish } from "@/utils/genUtils"; +import { isFreeProduct } from "@/utils/product/priceUtils"; +import { defaultDiscountConfig } from "../utils/defaultRewardModels"; +import { DiscountConfig } from "./DiscountConfig"; export const RewardConfig = ({ - reward, - setReward, + reward, + setReward, }: { - reward: Reward; - setReward: (reward: Reward) => void; + reward: Reward; + setReward: (reward: Reward) => void; }) => { - const [idChanged, setIdChanged] = useState(false); - const { products } = useProductsQuery(); + const [idChanged, setIdChanged] = useState(false); + const { products } = useProductsQuery(); - useEffect(() => { - if (!idChanged) { - setReward({ - ...reward, - id: slugify(reward.name || ""), - }); - } - }, [reward.name]); + useEffect(() => { + if (!idChanged) { + setReward({ + ...reward, + id: slugify(reward.name || ""), + }); + } + }, [idChanged, reward, setReward]); - return ( -
-
-
- Name - setReward({ ...reward, name: e.target.value })} - /> -
-
- - ID - - { - setReward({ ...reward, id: e.target.value }); - setIdChanged(true); - }} - /> -
-
-
-
- Promotional Code - 0 ? reward.promo_codes[0].code : "" - } - onChange={(e) => - setReward({ - ...reward, - promo_codes: [{ code: e.target.value }], - }) - } - /> -
-
- Type - -
-
- {reward.type === RewardType.FreeProduct ? ( -
- - Product - - setReward({ ...reward, name: e.target.value })} + /> +
+
+ + ID + + { + setReward({ ...reward, id: e.target.value }); + setIdChanged(true); + }} + /> +
+
+
+
+ Promotional Code + 0 ? reward.promo_codes[0].code : "" + } + onChange={(e) => + setReward({ + ...reward, + promo_codes: [{ code: e.target.value }], + }) + } + /> +
+
+ Type + +
+
+ {reward.type === RewardType.FreeProduct ? ( +
+ + Product + + -
- ) : notNullish(reward.type) ? ( - - ) : null} -
- ); + const empty = freeAddOns.length === 0; + return ( + <> + + + + + {freeAddOns.map((product: ProductV2) => ( + + {product.name} + + ))} + + + ); + })()} + +
+ ) : notNullish(reward.type) ? ( + + ) : null} + + ); }; diff --git a/vite/src/views/products/rewards/utils/defaultRewardModels.ts b/vite/src/views/products/rewards/utils/defaultRewardModels.ts index 50b876616..0c913ea35 100644 --- a/vite/src/views/products/rewards/utils/defaultRewardModels.ts +++ b/vite/src/views/products/rewards/utils/defaultRewardModels.ts @@ -1,29 +1,29 @@ import { - CreateReward, - RewardType, CouponDurationType, - DiscountConfig, + type CreateReward, + type DiscountConfig, + RewardType, } from "@autumn/shared"; export const defaultDiscountConfig: DiscountConfig = { - discount_value: 0, - duration_type: CouponDurationType.Months, - duration_value: 0, - should_rollover: true, - apply_to_all: true, - price_ids: [], + discount_value: 0, + duration_type: CouponDurationType.Months, + duration_value: 0, + should_rollover: true, + apply_to_all: false, + price_ids: [], }; export const defaultReward: CreateReward = { - name: "", - id: "", - promo_codes: [{ code: "" }], + name: "", + id: "", + promo_codes: [{ code: "" }], - type: RewardType.PercentageDiscount, + type: RewardType.PercentageDiscount, - // For free product coupons - free_product_id: null, + // For free product coupons + free_product_id: null, - // For discount type coupons - discount_config: defaultDiscountConfig, + // For discount type coupons + discount_config: defaultDiscountConfig, }; From c26bd73627fa390ab69f81d5cb479b567be30f61 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:25:47 +0100 Subject: [PATCH 28/60] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20type=20sugar?= =?UTF-8?q?=20:)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/queue/JobName.ts | 1 + server/src/queue/queueUtils.ts | 57 ++++++++++++++++++++-------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/server/src/queue/JobName.ts b/server/src/queue/JobName.ts index 9eafe1ef7..213dad18c 100644 --- a/server/src/queue/JobName.ts +++ b/server/src/queue/JobName.ts @@ -3,6 +3,7 @@ export enum JobName { UpdateUsage = "update-usage", Migration = "migration", + RewardMigration = "reward-migration", TriggerCheckoutReward = "trigger-checkout-reward", GenerateFeatureDisplay = "generate-feature-display", diff --git a/server/src/queue/queueUtils.ts b/server/src/queue/queueUtils.ts index 71b9bf434..325554c7e 100644 --- a/server/src/queue/queueUtils.ts +++ b/server/src/queue/queueUtils.ts @@ -1,30 +1,39 @@ +import type { Price } from "@autumn/shared"; import RecaseError from "@/utils/errorUtils.js"; +import { JobName } from "./JobName.js"; import { QueueManager } from "./QueueManager.js"; -export const addTaskToQueue = async ({ - jobName, - payload, +export interface JobNameToPayloadType { + [JobName.RewardMigration]: { + oldPrices: Price[]; + newPrices: Price[]; + }; +} + +export const addTaskToQueue = async ({ + jobName, + payload, }: { - jobName: string; - payload: any; + jobName: T; + payload: JobNameToPayloadType[T]; }) => { - try { - const queue = await QueueManager.getQueue({ useBackup: false }); - await queue.add(jobName, payload); - } catch (error: any) { - try { - console.log(`Adding ${jobName} to backup queue`); - const backupQueue = await QueueManager.getQueue({ useBackup: true }); - await backupQueue.add(jobName, 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, payload); + } catch (error: any) { + try { + console.log(`Adding ${jobName} to backup queue`); + const backupQueue = await QueueManager.getQueue({ useBackup: true }); + await backupQueue.add(jobName, 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, + }, + }); + } + } }; From d13505621c03a9f0a167755d9365edfe7f6d825e Mon Sep 17 00:00:00 2001 From: shreyash1231 Date: Tue, 16 Sep 2025 16:41:45 +0530 Subject: [PATCH 29/60] Disabled-Create-Customer-Button-Until-Name-Id-Or-Email-isfilled-to-Prevent-API-CALL --- .../customers/components/CreateCustomer.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/vite/src/views/customers/components/CreateCustomer.tsx b/vite/src/views/customers/components/CreateCustomer.tsx index 394064392..c58a653fa 100644 --- a/vite/src/views/customers/components/CreateCustomer.tsx +++ b/vite/src/views/customers/components/CreateCustomer.tsx @@ -75,14 +75,18 @@ function CreateCustomer() {
- Name + + Name + setFields({ ...fields, name: e.target.value })} />
- ID + + ID + setFields({ ...fields, id: e.target.value })} @@ -107,12 +111,11 @@ function CreateCustomer() { />
*/} - From 609bd24b60e8d1cca81d069bf1e3c82d67a2c7b4 Mon Sep 17 00:00:00 2001 From: shreyash1231 Date: Tue, 16 Sep 2025 17:08:33 +0530 Subject: [PATCH 30/60] fix/Disabled-Create-Customer-Button-Until-Id-Or-Email-is-Filled-To-Prevent-API-CALL --- .../views/customers/components/CreateCustomer.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/vite/src/views/customers/components/CreateCustomer.tsx b/vite/src/views/customers/components/CreateCustomer.tsx index c58a653fa..0653de7b2 100644 --- a/vite/src/views/customers/components/CreateCustomer.tsx +++ b/vite/src/views/customers/components/CreateCustomer.tsx @@ -111,11 +111,13 @@ function CreateCustomer() { />
*/} - From d6e9802cdce2f6f4184ed05fc980186686f4ef95 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:52:28 +0100 Subject: [PATCH 31/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20reward=20migration?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/runRewardMigrationTask.ts | 242 +++++++++++ .../handleUpdateProduct.ts | 370 ++++++++-------- .../products/handlers/handleVersionProduct.ts | 242 ++++++----- server/src/internal/rewards/RewardService.ts | 304 ++++++------- server/src/queue/queueUtils.ts | 16 +- server/src/queue/workersInit.ts | 405 +++++++++--------- .../rewards/reward-config/DiscountConfig.tsx | 2 +- 7 files changed, 936 insertions(+), 645 deletions(-) create mode 100644 server/src/internal/migrations/runRewardMigrationTask.ts diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts new file mode 100644 index 000000000..858e70bd7 --- /dev/null +++ b/server/src/internal/migrations/runRewardMigrationTask.ts @@ -0,0 +1,242 @@ +import { + type AppEnv, + type FixedPriceConfig, + type FullProduct, + type Price, + RewardType, + type UsagePriceConfig, +} from "@autumn/shared"; +import { + isFixedPrice, + isUsagePrice, +} from "@shared/utils/productUtils/priceUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { logger as loggerType } from "@/external/logtail/logtailUtils.js"; +import type { JobName } from "@/queue/JobName.js"; +import type { Payloads } from "@/queue/queueUtils.js"; +import { RewardService } from "../rewards/RewardService.js"; + +// Helper function to check if tier structures match +const tiersMatch = (oldTiers: any[], newTiers: any[]): boolean => { + 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; + }); +}; + +// Match fixed prices by amount +const findMatchingFixedPrice = ( + oldPrice: Price, + candidates: Price[], +): Price | null => { + const oldConfig = oldPrice.config as FixedPriceConfig; + + return ( + candidates.find((candidate) => { + const newConfig = candidate.config as FixedPriceConfig; + return newConfig.amount === oldConfig.amount; + }) || null + ); +}; + +// Match usage prices by feature and billing characteristics +const findMatchingUsagePrice = ( + oldPrice: Price, + candidates: Price[], +): Price | null => { + const oldConfig = oldPrice.config as UsagePriceConfig; + + return ( + candidates.find((candidate) => { + const newConfig = candidate.config as UsagePriceConfig; + + // Match by feature + if (newConfig.feature_id !== oldConfig.feature_id) return false; + if (newConfig.internal_feature_id !== oldConfig.internal_feature_id) + return false; + + // Match by billing behavior + if (newConfig.bill_when !== oldConfig.bill_when) return false; + if (newConfig.should_prorate !== oldConfig.should_prorate) return false; + + // Optionally match by tier structure (you can disable this if too strict) + if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers)) + return false; + + return true; + }) || null + ); +}; + +// Main matching function with type-specific logic +const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => { + // First, filter by basic characteristics + const candidates = newPrices.filter( + (newPrice) => + newPrice.config.type === oldPrice.config.type && + newPrice.config.interval === oldPrice.config.interval && + newPrice.config.interval_count === oldPrice.config.interval_count, + ); + + 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); + } + + // Fallback to first candidate + return candidates[0]; +}; + +export async function runRewardMigrationTask({ + db, + payload, + logger +}: { + db: DrizzleCli; + payload: Payloads[JobName.RewardMigration]; + logger: ReturnType; +}) { + try { + const { + oldPrices, + newPrices, + orgId, + env, + }: { + oldPrices: Price[]; + newPrices: Price[]; + product: FullProduct; + orgId: string; + env: AppEnv; + } = payload; + + const rewards = await RewardService.list({ + db, + orgId, + env, + inTypes: [ + RewardType.PercentageDiscount, + RewardType.FixedDiscount, + RewardType.InvoiceCredits, + ], + }); + + logger.info("--------------------------------"); + logger.info("REWARDS: ", rewards.length); + logger.info("--------------------------------"); + logger.info(rewards.map((r) => `${r.name} - ${r.id}`)); + logger.info("--------------------------------"); + + 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), + ), + ); + + logger.info("--------------------------------"); + logger.info("FILTERED REWARDS: ", filteredRewards.length); + logger.info("--------------------------------"); + logger.info(filteredRewards); + logger.info("--------------------------------"); + + 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 (!oldPrice) { + logger.warn(`Old price ${priceId} not found in oldPrices array`); + continue; + } + + const matchingNewPrice = findBestMatch(oldPrice, newPrices); + + if (matchingNewPrice) { + newPriceIds.push(matchingNewPrice.id); + logger.info( + `Matched ${oldPrice.id} -> ${matchingNewPrice.id} (${oldPrice.config.type})`, + ); + } else { + unmatchedPrices.push(oldPrice.id); + logger.warn( + `No match found for price ${oldPrice.id} (${oldPrice.config.type})`, + ); + } + } + } + + // Update the reward with new price IDs + if (newPriceIds.length > 0) { + try { + await RewardService.update({ + db, + internalId: reward.internal_id!, + env, + orgId, + update: { + discount_config: { + discount_value: reward.discount_config!.discount_value, + duration_type: reward.discount_config!.duration_type, + duration_value: reward.discount_config!.duration_value, + should_rollover: reward.discount_config!.should_rollover, + apply_to_all: reward.discount_config!.apply_to_all, + price_ids: [...unmatchedPrices, ...newPriceIds], + }, + }, + }); + logger.info( + `Updated reward "${reward.name}" (${reward.id}) with ${newPriceIds.length} prices`, + ); + } catch (error) { + logger.error(`Failed to update reward ${reward.id}:`, error); + } + } else { + logger.warn( + `Reward "${reward.name}" (${reward.id}) has no matching prices - skipping update`, + ); + } + + if (unmatchedPrices.length > 0) { + logger.warn( + `Unmatched prices for reward ${reward.id}:`, + unmatchedPrices, + ); + } + } + + // Migration summary + const totalRewards = filteredRewards.length; + const updatedRewards = filteredRewards.filter((r) => + r.discount_config?.price_ids?.some( + (priceId) => + oldPrices.find((p) => p.id === priceId) && + findBestMatch(oldPrices.find((p) => p.id === priceId)!, newPrices), + ), + ).length; + + logger.info("================================"); + logger.info("REWARD MIGRATION SUMMARY"); + logger.info("================================"); + logger.info(`Total rewards processed: ${totalRewards}`); + logger.info(`Rewards with successful matches: ${updatedRewards}`); + logger.info(`Rewards with no matches: ${totalRewards - updatedRewards}`); + logger.info("================================"); + } catch (error) { + logger.error("Error running reward migration task", { error }); + throw error; + } +} diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index 0c177f7fa..233a2024e 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -1,211 +1,223 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode, FullProduct, UpdateProductSchema } from "@autumn/shared"; - -import { ProductService } from "../../ProductService.js"; -import { notNullish } from "@/utils/genUtils.js"; +import { ErrCode, type FullProduct, UpdateProductSchema } from "@autumn/shared"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; - import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { handleVersionProductV2 } from "../handleVersionProduct.js"; -import { routeHandler } from "@/utils/routerUtils.js"; import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; -import { handleUpdateProductDetails } from "./updateProductDetails.js"; -import { addTaskToQueue } from "@/queue/queueUtils.js"; import { JobName } from "@/queue/JobName.js"; +import { addTaskToQueue } from "@/queue/queueUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { getEntsWithFeature } from "../../entitlements/entitlementUtils.js"; +import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js"; +import { ProductService } from "../../ProductService.js"; import { productsAreSame } from "../../productUtils/compareProductUtils.js"; import { initProductInStripe } from "../../productUtils.js"; +import { mapToProductItems } from "../../productV2Utils.js"; import { disableCurrentDefault, handleCreateProduct, } from "../handleCreateProduct.js"; -import { mapToProductItems } from "../../productV2Utils.js"; -import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js"; +import { handleVersionProductV2 } from "../handleVersionProduct.js"; +import { handleUpdateProductDetails } from "./updateProductDetails.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, + }); - let 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, + }); - let 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, - }); - return; - } + 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, + }); + } - const { prices, entitlements } = await handleNewProductItems({ - db, - curPrices: fullProduct.prices, - curEnts: fullProduct.entitlements, - newItems: items, - features, - product: fullProduct, - logger, - isCustom: false, - }); + const { prices, entitlements } = await handleNewProductItems({ + db, + curPrices: fullProduct.prices, + curEnts: fullProduct.entitlements, + newItems: items, + features, + product: fullProduct, + logger, + isCustom: false, + }); - if (free_trial !== undefined) { - await validateOneOffTrial({ - prices, - freeTrial: free_trial, - }); + if (free_trial !== undefined) { + await validateOneOffTrial({ + 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, + }); + } - await initProductInStripe({ - db, - product: { - ...fullProduct, - prices, - entitlements, - } as FullProduct, - org, - env, - logger, - }); + await initProductInStripe({ + db, + product: { + ...fullProduct, + prices, + entitlements, + } as FullProduct, + org, + env, + logger, + }); - logger.info("Adding task to queue to detect base variant"); - await addTaskToQueue({ - jobName: JobName.DetectBaseVariant, - payload: { - curProduct: { - ...fullProduct, - prices: prices.length > 0 ? prices : fullProduct.prices, - entitlements, - }, - }, - }); + logger.info("Adding task to queue to detect base variant"); + await addTaskToQueue({ + jobName: JobName.DetectBaseVariant, + payload: { + curProduct: { + ...fullProduct, + prices: prices.length > 0 ? prices : fullProduct.prices, + entitlements, + }, + }, + }); - res.status(200).send({ message: "Product updated" }); - return; - }, - }); + await addTaskToQueue({ + jobName: JobName.RewardMigration, + payload: { + oldPrices: fullProduct.prices, + newPrices: prices, + product: { + ...fullProduct, + prices, + entitlements: getEntsWithFeature({ ents: entitlements, features }), + }, + orgId: org.id, + env, + }, + }); + res.status(200).send({ message: "Product updated" }); + return; + }, + }); diff --git a/server/src/internal/products/handlers/handleVersionProduct.ts b/server/src/internal/products/handlers/handleVersionProduct.ts index 1747eded1..7cb6142c3 100644 --- a/server/src/internal/products/handlers/handleVersionProduct.ts +++ b/server/src/internal/products/handlers/handleVersionProduct.ts @@ -1,142 +1,156 @@ +import { + 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"; import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; +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, } from "@/internal/products/productUtils.js"; -import { - AppEnv, - CreateProductSchema, - FreeTrial, - Organization, - ProductItem, -} from "@autumn/shared"; - -import { FullProduct } from "@autumn/shared"; -import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js"; -import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js"; -import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; -import { PriceService } from "@/internal/products/prices/PriceService.js"; -import { addTaskToQueue } from "@/queue/queueUtils.js"; import { JobName } from "@/queue/JobName.js"; +import { addTaskToQueue } from "@/queue/queueUtils.js"; import { getEntsWithFeature } from "../entitlements/entitlementUtils.js"; export const handleVersionProductV2 = async ({ - req, - res, - latestProduct, - org, - env, - items, - freeTrial, + req, + res, + latestProduct, + org, + env, + items, + freeTrial, }: { - req: any; - res: any; - latestProduct: FullProduct; - org: Organization; - env: AppEnv; - items: ProductItem[]; - freeTrial: FreeTrial; + req: any; + res: any; + latestProduct: FullProduct; + org: Organization; + env: AppEnv; + items: ProductItem[]; + freeTrial: FreeTrial; }) => { - const { db } = req; + const { db } = req; - let curVersion = latestProduct.version; - let newVersion = curVersion + 1; + const curVersion = latestProduct.version; + const newVersion = curVersion + 1; - let features = await FeatureService.getFromReq(req); + const features = await FeatureService.getFromReq(req); - console.log( - `Updating product ${latestProduct.id} version from ${curVersion} to ${newVersion}` - ); + console.log( + `Updating product ${latestProduct.id} version from ${curVersion} to ${newVersion}`, + ); - const newProduct = constructProduct({ - productData: CreateProductSchema.parse({ - ...latestProduct, - ...req.body, - version: newVersion, - }), - orgId: org.id, - env: latestProduct.env as AppEnv, - processor: latestProduct.processor, - baseVariantId: latestProduct.base_variant_id, - }); + const newProduct = constructProduct({ + productData: CreateProductSchema.parse({ + ...latestProduct, + ...req.body, + version: newVersion, + }), + orgId: org.id, + env: latestProduct.env as AppEnv, + processor: latestProduct.processor, + baseVariantId: latestProduct.base_variant_id, + }); - // Validate product items... - validateProductItems({ - newItems: items, - features, - orgId: org.id, - env, - }); + // Validate product items... + validateProductItems({ + newItems: items, + features, + orgId: org.id, + env, + }); - if (latestProduct.is_default) { - await ProductService.updateByInternalId({ - db, - internalId: latestProduct.internal_id, - update: { - is_default: false, - }, - }); - } + if (latestProduct.is_default) { + await ProductService.updateByInternalId({ + db, + internalId: latestProduct.internal_id, + update: { + is_default: false, + }, + }); + } - await ProductService.insert({ db, product: newProduct }); + await ProductService.insert({ db, product: newProduct }); - const { customPrices, customEnts } = await handleNewProductItems({ - db, - curPrices: latestProduct.prices, - curEnts: latestProduct.entitlements, - newItems: items, - features, - product: newProduct, - logger: console, - isCustom: false, - newVersion: true, - }); + const { customPrices, customEnts } = await handleNewProductItems({ + db, + curPrices: latestProduct.prices, + curEnts: latestProduct.entitlements, + newItems: items, + features, + product: newProduct, + logger: console, + isCustom: false, + newVersion: true, + }); - await EntitlementService.insert({ - db, - data: customEnts, - }); + await EntitlementService.insert({ + db, + data: customEnts, + }); - await PriceService.insert({ - db, - data: customPrices, - }); + await PriceService.insert({ + db, + data: customPrices, + }); - // Handle new free trial - if (freeTrial) { - await handleNewFreeTrial({ - db, - newFreeTrial: freeTrial, - curFreeTrial: null, - internalProductId: newProduct.internal_id, - isCustom: false, - }); - } + // Handle new free trial + if (freeTrial) { + await handleNewFreeTrial({ + db, + newFreeTrial: freeTrial, + curFreeTrial: null, + internalProductId: newProduct.internal_id, + isCustom: false, + }); + } - // await addTaskToQueue({ - // jobName: JobName.DetectBaseVariant, - // payload: { - // curProduct: { - // ...newProduct, - // // prices: customPrices, - // // entitlements: getEntsWithFeature({ ents: customEnts, features }), - // }, - // }, - // }); + // await addTaskToQueue({ + // jobName: JobName.DetectBaseVariant, + // payload: { + // curProduct: { + // ...newProduct, + // // prices: customPrices, + // // entitlements: getEntsWithFeature({ ents: customEnts, features }), + // }, + // }, + // }); - await initProductInStripe({ - db, - product: { - ...newProduct, - prices: customPrices, - entitlements: getEntsWithFeature({ ents: customEnts, features }), - } as FullProduct, - org, - env, - logger: console, - }); + await initProductInStripe({ + db, + product: { + ...newProduct, + prices: customPrices, + entitlements: getEntsWithFeature({ ents: customEnts, features }), + } as FullProduct, + org, + env, + logger: console, + }); - res.status(200).send(newProduct); + await addTaskToQueue({ + jobName: JobName.RewardMigration, + payload: { + oldPrices: latestProduct.prices, + newPrices: customPrices, + product: { + ...newProduct, + prices: customPrices, + entitlements: getEntsWithFeature({ ents: customEnts, features }), + }, + orgId: org.id, + env, + }, + }); + + res.status(200).send(newProduct); }; diff --git a/server/src/internal/rewards/RewardService.ts b/server/src/internal/rewards/RewardService.ts index e5a0f1e1e..6ebb63b1a 100644 --- a/server/src/internal/rewards/RewardService.ts +++ b/server/src/internal/rewards/RewardService.ts @@ -1,164 +1,176 @@ -import { AppEnv, ErrCode, Reward, rewards } from "@autumn/shared"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { + type AppEnv, + ErrCode, + type Reward, + type RewardType, + rewards, +} 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"; -import { and, arrayContains, desc, eq, inArray, or, sql } from "drizzle-orm"; export class RewardService { - static async get({ - db, - idOrInternalId, - orgId, - env, - }: { - db: DrizzleCli; - idOrInternalId: string; - orgId: string; - env: AppEnv; - }) { - let 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; - }) { - let 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[]; - }) { - let 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, - }: { - db: DrizzleCli; - orgId: string; - env: AppEnv; - }) { - let results = await db.query.rewards.findMany({ - where: and(eq(rewards.org_id, orgId), eq(rewards.env, env)), - 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; - }) { - let 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/queue/queueUtils.ts b/server/src/queue/queueUtils.ts index 325554c7e..62aeb7073 100644 --- a/server/src/queue/queueUtils.ts +++ b/server/src/queue/queueUtils.ts @@ -1,30 +1,34 @@ -import type { Price } from "@autumn/shared"; +import type { AppEnv, FullProduct, Price } from "@autumn/shared"; import RecaseError from "@/utils/errorUtils.js"; import { JobName } from "./JobName.js"; import { QueueManager } from "./QueueManager.js"; -export interface JobNameToPayloadType { +export interface Payloads { [JobName.RewardMigration]: { oldPrices: Price[]; newPrices: Price[]; + product: FullProduct; + orgId: string; + env: AppEnv; }; + [key: string]: any; } -export const addTaskToQueue = async ({ +export const addTaskToQueue = async ({ jobName, payload, }: { jobName: T; - payload: JobNameToPayloadType[T]; + payload: Payloads[T]; }) => { try { const queue = await QueueManager.getQueue({ useBackup: false }); - await queue.add(jobName, payload); + 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, payload); + await backupQueue.add(jobName as string, payload); } catch (error: any) { throw new RecaseError({ message: `Failed to add ${jobName} to queue (backup)`, diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index e7537d50d..13c7ac6a8 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -1,236 +1,243 @@ -import { Job, Queue, Worker } from "bullmq"; -import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js"; -import { QueueManager } from "./QueueManager.js"; - -import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; -import { JobName } from "./JobName.js"; - -import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js"; -import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; -import { runSaveFeatureDisplayTask } from "@/internal/features/featureUtils.js"; -import { CacheManager } from "@/external/caching/CacheManager.js"; +import { type Job, type Queue, Worker } from "bullmq"; +import type { Logger } from "pino"; import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js"; -import { acquireLock, getRedisConnection, releaseLock } from "./lockUtils.js"; -import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask.js"; +import { CacheManager } from "@/external/caching/CacheManager.js"; import { logger } from "@/external/logtail/logtailUtils.js"; +import { runActionHandlerTask } from "@/internal/analytics/runActionHandlerTask.js"; +import { runSaveFeatureDisplayTask } from "@/internal/features/featureUtils.js"; +import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js"; +import { runRewardMigrationTask } from "@/internal/migrations/runRewardMigrationTask.js"; import { detectBaseVariant } from "@/internal/products/productUtils/detectProductVariant.js"; -import { Logger } from "pino"; +import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; +import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js"; +import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; import { generateId } from "@/utils/genUtils.js"; +import { JobName } from "./JobName.js"; +import { acquireLock, getRedisConnection, releaseLock } from "./lockUtils.js"; +import { QueueManager } from "./QueueManager.js"; const NUM_WORKERS = 10; const actionHandlers = [ - JobName.HandleProductsUpdated, - JobName.HandleCustomerCreated, + JobName.HandleProductsUpdated, + JobName.HandleCustomerCreated, ]; -const { db, client } = initDrizzle({ maxConnections: 10 }); +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; }) => { - let 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; - } - } catch (error: any) { - logtail.error(`Failed to process bullmq job: ${job.name}`, { - jobName: job.name, - error: { - message: error.message, - stack: error.stack, - }, - }); - } + if (actionHandlers.includes(job.name as JobName)) { + await runActionHandlerTask({ + queue, + job, + logger: logtail, + db, + useBackup, + }); + return; + } - // TRIGGER CHECKOUT REWARD - if (job.name == JobName.TriggerCheckoutReward) { - let 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; - } + 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, + }, + }); + } - try { - await runTriggerCheckoutReward({ - db, - payload: job.data, - logger: logtail, - }); - } catch (error) { - console.error("Error processing job:", error); - } finally { - await releaseLock({ lockKey, useBackup }); - } + // 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; + } - return; - } + try { + await runTriggerCheckoutReward({ + db, + payload: job.data, + logger: logtail, + }); + } catch (error) { + console.error("Error processing job:", error); + } finally { + await releaseLock({ lockKey, useBackup }); + } - // EVENT HANDLERS - const { internalCustomerId } = job.data; // customerId is internal customer id + return; + } - while ( - !(await acquireLock({ - lockKey: `event:${internalCustomerId}`, - timeout: 10000, - useBackup, - })) - ) { - await queue.add(job.name, job.data, { - delay: 200, - }); - return; - } + // EVENT HANDLERS + const { internalCustomerId } = job.data; // customerId is internal customer id - 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, - } - ); + while ( + !(await acquireLock({ + lockKey: `event:${internalCustomerId}`, + timeout: 10000, + useBackup, + })) + ) { + await queue.add(job.name, job.data, { + delay: 200, + }); + return; + } - worker.on("ready", () => { - console.log(`Worker ${id} ready (${useBackup ? "BACKUP" : "MAIN"})`); - }); + 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("stalled", (jobId: string) => { - console.log(`Worker ${id} stalled (${useBackup ? "BACKUP" : "MAIN"})`); - console.log("JOB ID:", jobId); - }); + worker.on("ready", () => { + console.log(`Worker ${id} ready (${useBackup ? "BACKUP" : "MAIN"})`); + }); - worker.on("error", async (error: any) => { - if (error.code !== "ECONNREFUSED") { - console.log("WORKER ERROR:", error.message); - } - }); + worker.on("stalled", (jobId: string) => { + console.log(`Worker ${id} stalled (${useBackup ? "BACKUP" : "MAIN"})`); + console.log("JOB ID:", jobId); + }); - worker.on("failed", (job, error) => { - console.log("WORKER FAILED:", 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); + }); }; 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/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx index db556f7fb..277a27e60 100644 --- a/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/DiscountConfig.tsx @@ -209,7 +209,7 @@ const ProductPriceSelector = ({ org, features, }) - : "Deleted price"; + : "Unmigrated price"; return (
Date: Tue, 16 Sep 2025 13:52:35 +0100 Subject: [PATCH 32/60] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vite/src/hooks/queries/useFeaturesQuery.tsx | 28 ++++---- vite/src/hooks/queries/useProductsQuery.tsx | 74 ++++++++++----------- vite/src/hooks/queries/useRewardsQuery.tsx | 37 ++++++----- 3 files changed, 70 insertions(+), 69 deletions(-) diff --git a/vite/src/hooks/queries/useFeaturesQuery.tsx b/vite/src/hooks/queries/useFeaturesQuery.tsx index 29005c71d..56fdf1c36 100644 --- a/vite/src/hooks/queries/useFeaturesQuery.tsx +++ b/vite/src/hooks/queries/useFeaturesQuery.tsx @@ -1,21 +1,21 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { Feature } from "@autumn/shared"; +import type { Feature } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const useFeaturesQuery = () => { - const axiosInstance = useAxiosInstance(); + const axiosInstance = useAxiosInstance(); - const fetchFeatures = async () => { - const { data } = await axiosInstance.get("/products/features"); - return data; - }; + const fetchFeatures = async () => { + const { data } = await axiosInstance.get("/products/features"); + return data; + }; - const { data, isLoading, error, refetch } = useQuery<{ - features: Feature[]; - }>({ - queryKey: ["features"], - queryFn: fetchFeatures, - }); + const { data, isLoading, error, refetch } = useQuery<{ + features: Feature[]; + }>({ + queryKey: ["features"], + queryFn: fetchFeatures, + }); - return { features: data?.features || [], isLoading, error, refetch }; + return { features: (data?.features || []) as Feature[], isLoading, error, refetch }; }; diff --git a/vite/src/hooks/queries/useProductsQuery.tsx b/vite/src/hooks/queries/useProductsQuery.tsx index 64693a32d..0c010aef1 100644 --- a/vite/src/hooks/queries/useProductsQuery.tsx +++ b/vite/src/hooks/queries/useProductsQuery.tsx @@ -1,46 +1,46 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { FullProduct, ProductCounts, ProductV2 } from "@autumn/shared"; +import type { FullProduct, ProductCounts, ProductV2 } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const useProductsQuery = () => { - const axiosInstance = useAxiosInstance(); + const axiosInstance = useAxiosInstance(); - const fetchProducts = async () => { - const { data } = await axiosInstance.get("/products/products"); - return data; - }; + const fetchProducts = async () => { + const { data } = await axiosInstance.get("/products/products"); + return data; + }; - const fetchProductCounts = async () => { - const { data } = await axiosInstance.get("/products/product_counts"); - return data; - }; + const fetchProductCounts = async () => { + const { data } = await axiosInstance.get("/products/product_counts"); + return data; + }; - const { data, isLoading, error, refetch } = useQuery<{ - products: ProductV2[]; - groupToDefaults: Record>; - }>({ - queryKey: ["products"], - queryFn: fetchProducts, - }); + const { data, isLoading, error, refetch } = useQuery<{ + products: ProductV2[]; + groupToDefaults: Record>; + }>({ + queryKey: ["products"], + queryFn: fetchProducts, + }); - const { data: countsData, refetch: countsRefetch } = useQuery< - Record - >({ - queryKey: ["product_counts"], - queryFn: fetchProductCounts, - }); + const { data: countsData, refetch: countsRefetch } = useQuery< + Record + >({ + queryKey: ["product_counts"], + queryFn: fetchProductCounts, + }); - return { - products: data?.products || [], - counts: countsData || {}, - groupToDefaults: data?.groupToDefaults || {}, - isLoading, - error, - refetch: async () => { - await Promise.all([countsRefetch(), refetch()]); - }, - // mutate: async () => { - // await Promise.all([countsRefetch(), refetch()]); - // }, - }; + return { + products: (data?.products || []) as ProductV2[], + counts: countsData || {}, + groupToDefaults: data?.groupToDefaults || {}, + isLoading, + error, + refetch: async () => { + await Promise.all([countsRefetch(), refetch()]); + }, + // mutate: async () => { + // await Promise.all([countsRefetch(), refetch()]); + // }, + }; }; diff --git a/vite/src/hooks/queries/useRewardsQuery.tsx b/vite/src/hooks/queries/useRewardsQuery.tsx index e29eeb293..4f9e4acac 100644 --- a/vite/src/hooks/queries/useRewardsQuery.tsx +++ b/vite/src/hooks/queries/useRewardsQuery.tsx @@ -1,24 +1,25 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; +import type { Reward, RewardProgram } from "@autumn/shared"; import { useQuery } from "@tanstack/react-query"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const useRewardsQuery = () => { - const axiosInstance = useAxiosInstance(); + const axiosInstance = useAxiosInstance(); - const fetchRewards = async () => { - const { data } = await axiosInstance.get("/products/rewards"); - return data; - }; + const fetchRewards = async () => { + const { data } = await axiosInstance.get("/products/rewards"); + return data; + }; - const { data, isLoading, error, refetch } = useQuery({ - queryKey: ["rewards"], - queryFn: fetchRewards, - }); + const { data, isLoading, error, refetch } = useQuery({ + queryKey: ["rewards"], + queryFn: fetchRewards, + }); - return { - rewards: data?.rewards || [], - rewardPrograms: data?.rewardPrograms || [], - isLoading, - error, - refetch, - }; -}; + return { + rewards: (data?.rewards || []) as Reward[], + rewardPrograms: (data?.rewardPrograms || []) as RewardProgram[], + isLoading, + error, + refetch, + }; +}; \ No newline at end of file From 590a2ae86f9eeb63bb3d3bd417f67bc8a29d61d4 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:23:26 +0100 Subject: [PATCH 33/60] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20clean=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migrations/runRewardMigrationTask.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts index 858e70bd7..67b7d0cd1 100644 --- a/server/src/internal/migrations/runRewardMigrationTask.ts +++ b/server/src/internal/migrations/runRewardMigrationTask.ts @@ -61,7 +61,7 @@ const findMatchingUsagePrice = ( if (newConfig.bill_when !== oldConfig.bill_when) return false; if (newConfig.should_prorate !== oldConfig.should_prorate) return false; - // Optionally match by tier structure (you can disable this if too strict) + // Optionally match by tier structure if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers)) return false; @@ -128,12 +128,6 @@ export async function runRewardMigrationTask({ ], }); - logger.info("--------------------------------"); - logger.info("REWARDS: ", rewards.length); - logger.info("--------------------------------"); - logger.info(rewards.map((r) => `${r.name} - ${r.id}`)); - logger.info("--------------------------------"); - const filteredRewards = rewards.filter( (x) => x.org_id === orgId && @@ -145,12 +139,6 @@ export async function runRewardMigrationTask({ ), ); - logger.info("--------------------------------"); - logger.info("FILTERED REWARDS: ", filteredRewards.length); - logger.info("--------------------------------"); - logger.info(filteredRewards); - logger.info("--------------------------------"); - for (const reward of filteredRewards) { const newPriceIds: string[] = []; const unmatchedPrices: string[] = []; @@ -229,7 +217,7 @@ export async function runRewardMigrationTask({ ).length; logger.info("================================"); - logger.info("REWARD MIGRATION SUMMARY"); + logger.info("REWARD MIGRATION SUMMARY FOR ORG: ", orgId); logger.info("================================"); logger.info(`Total rewards processed: ${totalRewards}`); logger.info(`Rewards with successful matches: ${updatedRewards}`); From 7e201dae41e64c439be7bd2d07b1f0a658bbe15e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 14:47:04 +0100 Subject: [PATCH 34/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20only=20counted=20ro?= =?UTF-8?q?w=20instead=20of=20value?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/analytics/AnalyticsService.ts | 564 +++++++++--------- 1 file changed, 284 insertions(+), 280 deletions(-) diff --git a/server/src/internal/analytics/AnalyticsService.ts b/server/src/internal/analytics/AnalyticsService.ts index a266a0c5c..5e1525cae 100644 --- a/server/src/internal/analytics/AnalyticsService.ts +++ b/server/src/internal/analytics/AnalyticsService.ts @@ -1,50 +1,52 @@ -import { ClickHouseClient } from "@clickhouse/client"; -import { ErrCode, FullCustomer } from "@autumn/shared"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import RecaseError from "@/utils/errorUtils.js"; +/** biome-ignore-all lint/complexity/noStaticOnlyClass: wrap it up buddy */ + +import { ErrCode, type FullCustomer } from "@autumn/shared"; +import type { ClickHouseClient } from "@clickhouse/client"; import { StatusCodes } from "http-status-codes"; +import RecaseError from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { generateEventCountExpressions, getBillingCycleStartDate, } from "./analyticsUtils.js"; export class AnalyticsService { - static clickhouseAvailable = - process.env.CLICKHOUSE_URL && - process.env.CLICKHOUSE_USERNAME && - process.env.CLICKHOUSE_PASSWORD; + static clickhouseAvailable = + process.env.CLICKHOUSE_URL && + process.env.CLICKHOUSE_USERNAME && + process.env.CLICKHOUSE_PASSWORD; - static handleEarlyExit = () => { - if (!AnalyticsService.clickhouseAvailable) { - throw new RecaseError({ - message: "ClickHouse is disabled, cannot fetch events", - code: ErrCode.ClickHouseDisabled, - statusCode: StatusCodes.SERVICE_UNAVAILABLE, - }); - } - }; + static handleEarlyExit = () => { + if (!AnalyticsService.clickhouseAvailable) { + throw new RecaseError({ + message: "ClickHouse is disabled, cannot fetch events", + code: ErrCode.ClickHouseDisabled, + statusCode: StatusCodes.SERVICE_UNAVAILABLE, + }); + } + }; - static formatJsDateToClickHouseDateTime(date: Date) { - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes() - 1).padStart(2, "0"); - const seconds = String(date.getSeconds() - 1).padStart(2, "0"); + static formatJsDateToClickHouseDateTime(date: Date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hours = String(date.getHours()).padStart(2, "0"); + const minutes = String(date.getMinutes() - 1).padStart(2, "0"); + const seconds = String(date.getSeconds() - 1).padStart(2, "0"); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - } + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + } - static async getTopEventNames({ - req, - limit = 3, - }: { - req: ExtendedRequest; - limit?: number; - }) { - const { clickhouseClient, org, env } = req; + static async getTopEventNames({ + req, + limit = 3, + }: { + req: ExtendedRequest; + limit?: number; + }) { + const { clickhouseClient, org, env } = req; - const query = ` + const query = ` select count(*) as count, event_name from org_events_view(org_id={org_id:String}, org_slug='', env={env:String}) where timestamp >= NOW() - INTERVAL '1 month' @@ -52,27 +54,27 @@ export class AnalyticsService { order by count(*) desc limit {limit:UInt32} `; - const result = await clickhouseClient.query({ - query, - query_params: { - org_id: org?.id, - env: env, - limit, - }, - }); + const result = await clickhouseClient.query({ + query, + query_params: { + org_id: org?.id, + env: env, + limit, + }, + }); - const resultJson = await result.json(); + const resultJson = await result.json(); - return { - eventNames: resultJson.data.map((row: any) => row.event_name), - result: resultJson, - }; - } + return { + eventNames: resultJson.data.map((row: any) => row.event_name), + result: resultJson, + }; + } - static async getTopUser({ req }: { req: ExtendedRequest }) { - const { clickhouseClient, org, env, db } = req; + static async getTopUser({ req }: { req: ExtendedRequest }) { + const { clickhouseClient, org, env, db } = req; - const query = ` + const query = ` SELECT c.name FROM @@ -113,116 +115,118 @@ WHERE ) `; - 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 { name: string; count: number }[])[0]; - } + return (resultJson.data as { name: string; count: number }[])[0]; + } - static async getTotalEvents({ - req, - eventName, - }: { - req: ExtendedRequest; - eventName?: string; - }) { - const { clickhouseClient, org, env, db } = req; + static async getTotalEvents({ + req, + eventName, + }: { + req: ExtendedRequest; + eventName?: string; + }) { + const { clickhouseClient, org, env, db } = req; - const query = ` -SELECT org_id, env, COUNT(*) AS total_events -FROM events -WHERE org_id = {org_id: String} - AND env = {env: String} - ${eventName ? `AND event_name = {eventName: String}` : ""} -GROUP BY org_id, env -LIMIT 1; + const query = ` +SELECT SUM( + CASE + WHEN JSONHas(properties, 'value') THEN toInt64(JSONExtractFloat(properties, 'value')) + WHEN value IS NOT NULL THEN toInt64(value) + ELSE 1 + END +) AS total_events +FROM org_events_view(org_id={org_id:String}, org_slug='', env={env:String}) +WHERE event_name = {eventName:String} `; - const result = await clickhouseClient.query({ - query, - query_params: { - org_id: org?.id, - env: env, - eventName: eventName ?? undefined, - }, - }); + const result = await clickhouseClient.query({ + query, + query_params: { + org_id: org?.id, + env: env, + eventName: eventName ?? undefined, + }, + }); - const resultJson = await result.json(); + const resultJson = await result.json(); - return (resultJson.data as { total_events: number }[])[0].total_events; - } + return (resultJson.data as { total_events: number }[])[0].total_events; + } - static async getTotalCustomers({ req }: { req: ExtendedRequest }) { - const { clickhouseClient, org, env, db } = req; - const query = `SELECT COUNT(DISTINCT id) AS total_customers + static async getTotalCustomers({ req }: { req: ExtendedRequest }) { + const { clickhouseClient, org, env, db } = req; + const query = `SELECT COUNT(DISTINCT id) AS total_customers FROM customers WHERE org_id = {org_id:String} AND 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_customers: number }[])[0] - .total_customers; - } + return (resultJson.data as { total_customers: number }[])[0] + .total_customers; + } - static async getTimeseriesEvents({ - req, - params, - customer, - aggregateAll = false, - }: { - req: ExtendedRequest; - params: { - event_names: string[]; - interval: "24h" | "7d" | "30d" | "90d" | "1bc" | "3bc"; - customer_id?: string; - no_count?: boolean; - }; - customer?: FullCustomer; - aggregateAll?: boolean; - }) { - const { clickhouseClient, org, env, db } = req; + static async getTimeseriesEvents({ + req, + params, + customer, + aggregateAll = false, + }: { + req: ExtendedRequest; + params: { + event_names: string[]; + interval: "24h" | "7d" | "30d" | "90d" | "1bc" | "3bc"; + customer_id?: string; + no_count?: boolean; + }; + customer?: FullCustomer; + aggregateAll?: boolean; + }) { + const { clickhouseClient, org, env, db } = req; - const intervalType: "24h" | "7d" | "30d" | "90d" | "1bc" | "3bc" = - params.interval || "24h"; + const intervalType: "24h" | "7d" | "30d" | "90d" | "1bc" | "3bc" = + params.interval || "24h"; - const isBillingCycle = intervalType === "1bc" || intervalType === "3bc"; - AnalyticsService.handleEarlyExit(); + const isBillingCycle = intervalType === "1bc" || intervalType === "3bc"; + AnalyticsService.handleEarlyExit(); - // Skip billing cycle calculation if aggregating all customers - let getBCResults = - isBillingCycle && !aggregateAll && customer - ? ((await getBillingCycleStartDate( - env, - org?.id, - customer, - db, - intervalType as "1bc" | "3bc" - )) as { startDate: string; endDate: string; gap: number } | null) - : null; + // Skip billing cycle calculation if aggregating all customers + const getBCResults = + isBillingCycle && !aggregateAll && customer + ? ((await getBillingCycleStartDate( + env, + org?.id, + customer, + db, + intervalType as "1bc" | "3bc", + )) as { startDate: string; endDate: string; gap: number } | null) + : null; - const countExpressions = generateEventCountExpressions( - params.event_names, - params.no_count - ); + const countExpressions = generateEventCountExpressions( + params.event_names, + params.no_count, + ); - if (AnalyticsService.clickhouseAvailable) { - const query = ` + if (AnalyticsService.clickhouseAvailable) { + const query = ` with customer_events as ( select * from org_events_view(org_id={org_id:String}, org_slug='', env={env:String}) @@ -238,7 +242,7 @@ group by dr.period order by dr.period; `; - const queryBillingCycle = ` + const queryBillingCycle = ` with customer_events as ( select * from org_events_view(org_id={org_id:String}, org_slug='', env={env:String}) @@ -254,118 +258,118 @@ group by dr.period order by dr.period; `; - const queryParams = { - org_id: org?.id, - env: env, - customer_id: params.customer_id, - days: - intervalType === "24h" - ? 1 - : intervalType === "7d" - ? 7 - : intervalType === "30d" - ? 30 - : intervalType === "90d" - ? 90 - : intervalType === "1bc" - ? (getBCResults?.gap ?? 0) + 1 - : intervalType === "3bc" - ? (getBCResults?.gap ?? 0) - : 0, - bin_size: intervalType === "24h" ? "hour" : "day", - end_date: isBillingCycle ? getBCResults?.endDate : undefined, - }; + const queryParams = { + org_id: org?.id, + env: env, + customer_id: params.customer_id, + days: + intervalType === "24h" + ? 1 + : intervalType === "7d" + ? 7 + : intervalType === "30d" + ? 30 + : intervalType === "90d" + ? 90 + : intervalType === "1bc" + ? (getBCResults?.gap ?? 0) + 1 + : intervalType === "3bc" + ? (getBCResults?.gap ?? 0) + : 0, + bin_size: intervalType === "24h" ? "hour" : "day", + end_date: isBillingCycle ? getBCResults?.endDate : undefined, + }; - // Use regular query for aggregateAll or when no billing cycle data is available - const queryToUse = - isBillingCycle && !aggregateAll && getBCResults?.startDate - ? queryBillingCycle - : query; + // Use regular query for aggregateAll or when no billing cycle data is available + const queryToUse = + isBillingCycle && !aggregateAll && getBCResults?.startDate + ? queryBillingCycle + : query; - const result = await (clickhouseClient as ClickHouseClient).query({ - query: queryToUse, - query_params: queryParams, - format: "JSON", - clickhouse_settings: { - output_format_json_quote_decimals: 0, - output_format_json_quote_64bit_integers: 1, - output_format_json_quote_64bit_floats: 1, - }, - }); + const result = await (clickhouseClient as ClickHouseClient).query({ + query: queryToUse, + query_params: queryParams, + format: "JSON", + clickhouse_settings: { + output_format_json_quote_decimals: 0, + output_format_json_quote_64bit_integers: 1, + output_format_json_quote_64bit_floats: 1, + }, + }); - let resultJson = await result.json(); + const resultJson = await result.json(); - resultJson.data.forEach((row: any) => { - Object.keys(row).forEach((key: string) => { - if (key !== "period") { - row[key] = parseInt(row[key]); - } - }); - }); + resultJson.data.forEach((row: any) => { + Object.keys(row).forEach((key: string) => { + if (key !== "period") { + row[key] = parseInt(row[key]); + } + }); + }); - return resultJson; - } - } + return resultJson; + } + } - static async getRawEvents({ - req, - params, - customer, - aggregateAll = false, - }: { - req: ExtendedRequest; - params: any; - customer?: FullCustomer; - aggregateAll?: boolean; - }) { - const { clickhouseClient, org, db, env } = req; + static async getRawEvents({ + req, + params, + customer, + aggregateAll = false, + }: { + req: ExtendedRequest; + params: any; + customer?: FullCustomer; + aggregateAll?: boolean; + }) { + const { clickhouseClient, org, db, env } = req; - AnalyticsService.handleEarlyExit(); + AnalyticsService.handleEarlyExit(); - let startDate = new Date(); - const intervalType = params.interval || "day"; - const isBillingCycle = intervalType === "1bc" || intervalType === "3bc"; + const startDate = new Date(); + const intervalType = params.interval || "day"; + const isBillingCycle = intervalType === "1bc" || intervalType === "3bc"; - // Skip billing cycle calculation if aggregating all customers - let getBCResults = - isBillingCycle && !aggregateAll && customer - ? ((await getBillingCycleStartDate( - env, - org?.id, - customer, - db, - intervalType as "1bc" | "3bc" - )) as { startDate: string; endDate: string; gap: number } | null) - : null; + // Skip billing cycle calculation if aggregating all customers + const getBCResults = + isBillingCycle && !aggregateAll && customer + ? ((await getBillingCycleStartDate( + env, + org?.id, + customer, + db, + intervalType as "1bc" | "3bc", + )) as { startDate: string; endDate: string; gap: number } | null) + : null; - switch (intervalType) { - case "24h": - startDate.setHours(startDate.getHours() - 24); - break; - case "7d": - startDate.setDate(startDate.getDate() - 7); - break; - case "30d": - startDate.setDate(startDate.getDate() - 30); - break; - case "90d": - startDate.setDate(startDate.getDate() - 90); - break; - default: - startDate.setDate(startDate.getDate() - 24); - break; - } + switch (intervalType) { + case "24h": + startDate.setHours(startDate.getHours() - 24); + break; + case "7d": + startDate.setDate(startDate.getDate() - 7); + break; + case "30d": + startDate.setDate(startDate.getDate() - 30); + break; + case "90d": + startDate.setDate(startDate.getDate() - 90); + break; + default: + startDate.setDate(startDate.getDate() - 24); + break; + } - const finalStartDate = - isBillingCycle && getBCResults?.startDate - ? getBCResults.startDate - : AnalyticsService.formatJsDateToClickHouseDateTime(startDate); - const finalEndDate = - isBillingCycle && getBCResults?.endDate - ? getBCResults.endDate - : AnalyticsService.formatJsDateToClickHouseDateTime(new Date()); + const finalStartDate = + isBillingCycle && getBCResults?.startDate + ? getBCResults.startDate + : AnalyticsService.formatJsDateToClickHouseDateTime(startDate); + const finalEndDate = + isBillingCycle && getBCResults?.endDate + ? getBCResults.endDate + : AnalyticsService.formatJsDateToClickHouseDateTime(new Date()); - const query = ` + const query = ` SELECT * FROM org_events_view(org_id={organizationId:String}, org_slug='', env={env:String}) WHERE timestamp >= toDateTime({startDate:String}) @@ -375,49 +379,49 @@ order by dr.period; limit 10000 `; - const filledQuery = query - .replace("{organizationId:String}", org?.id ?? "") - .replace("{customerId:String}", params.customer_id ?? "") - .replace("{startDate:String}", finalStartDate) - .replace("{endDate:String}", finalEndDate) - .replace("{env:String}", env); + const filledQuery = query + .replace("{organizationId:String}", org?.id ?? "") + .replace("{customerId:String}", params.customer_id ?? "") + .replace("{startDate:String}", finalStartDate) + .replace("{endDate:String}", finalEndDate) + .replace("{env:String}", env); - // console.log("filledQuery", filledQuery); + // console.log("filledQuery", filledQuery); - const result = await clickhouseClient.query({ - query: query, - query_params: { - organizationId: org?.id, - customerId: params.customer_id, - startDate: finalStartDate, - endDate: finalEndDate, - env: env, - }, - }); + const result = await clickhouseClient.query({ + query: query, + query_params: { + organizationId: org?.id, + customerId: params.customer_id, + startDate: finalStartDate, + endDate: finalEndDate, + env: env, + }, + }); - // log the actual query... with params filled in...? - // console.log("query", query); + // log the actual query... with params filled in...? + // console.log("query", query); - const resultJson = await result.json(); + const resultJson = await result.json(); - return resultJson; - } + return resultJson; + } - // private static async getSubscriptionsIfNeeded( - // customer: FullCustomer, - // customerHasSubscriptions: boolean, - // db: DrizzleCli - // ): Promise { - // if (customerHasSubscriptions) { - // return []; - // } + // private static async getSubscriptionsIfNeeded( + // customer: FullCustomer, + // customerHasSubscriptions: boolean, + // db: DrizzleCli + // ): Promise { + // if (customerHasSubscriptions) { + // return []; + // } - // return await SubService.getInStripeIds({ - // db, - // ids: - // customer.customer_products?.flatMap( - // (product: FullCusProduct) => product.subscription_ids ?? [] - // ) ?? [], - // }); - // } + // return await SubService.getInStripeIds({ + // db, + // ids: + // customer.customer_products?.flatMap( + // (product: FullCusProduct) => product.subscription_ids ?? [] + // ) ?? [], + // }); + // } } From 652ab88ed0eda4a1963590aef8d20ac95c0ac115 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:03:27 +0100 Subject: [PATCH 35/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20unify=20response=20?= =?UTF-8?q?output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handlers/referrals/handleRedeemReferral.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts index e47de6c83..d13e5624f 100644 --- a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts +++ b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts @@ -206,6 +206,12 @@ export default async (req: any, res: any) => email: codeCustomer.email, created_at: codeCustomer.created_at, }, + redeemer: { + id: customer.id, + name: customer.name, + email: customer.email, + created_at: customer.created_at, + } }); } } else if (referralCode.reward_program.when === RewardTriggerEvent.Checkout) { @@ -219,7 +225,13 @@ export default async (req: any, res: any) => email: codeCustomer.email, created_at: codeCustomer.created_at, }, - }) + redeemer: { + id: customer.id, + name: customer.name, + email: customer.email, + created_at: customer.created_at, + }, + }); } }, }); From 2ed62b4a8d8baa02ebbbad0bf78c2ce9aa6b2aff Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:03:51 +0100 Subject: [PATCH 36/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20fixing=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/tests/advanced/referrals/paid/referrals10.ts | 8 +++++--- server/tests/advanced/referrals/paid/referrals11.ts | 11 +++-------- server/tests/advanced/referrals/paid/referrals12.ts | 4 ++-- server/tests/advanced/referrals/paid/referrals9.ts | 4 ++-- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/server/tests/advanced/referrals/paid/referrals10.ts b/server/tests/advanced/referrals/paid/referrals10.ts index f5773637b..91c39a132 100644 --- a/server/tests/advanced/referrals/paid/referrals10.ts +++ b/server/tests/advanced/referrals/paid/referrals10.ts @@ -129,7 +129,11 @@ describe(`${chalk.yellowBright( it("should have given the paid add-on to referrer only, not redeemer", async () => { const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true); + assert.equal( + redemptionResult.applied, + true, + "Redemption should be applied", + ); await setTimeout(1000 * 10); @@ -141,7 +145,6 @@ describe(`${chalk.yellowBright( // Main customer (referrer) should have the proAddOn product const mainProds = mainCustomerData.products; const mainAddons = mainCustomerData.add_ons; - assert.equal(mainProds.length + mainAddons.length, 2); const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); @@ -150,7 +153,6 @@ describe(`${chalk.yellowBright( // Redeemer should only have Premium product (no proAddOn) const redeemerProds = redeemerCustomerData.products; const redeemerAddons = redeemerCustomerData.add_ons; - assert.equal(redeemerProds.length + redeemerAddons.length, 1); const redeemerHasProAddOn = redeemerAddons.some( diff --git a/server/tests/advanced/referrals/paid/referrals11.ts b/server/tests/advanced/referrals/paid/referrals11.ts index 8dcc60e33..476f01656 100644 --- a/server/tests/advanced/referrals/paid/referrals11.ts +++ b/server/tests/advanced/referrals/paid/referrals11.ts @@ -181,11 +181,6 @@ describe(`${chalk.yellowBright( (p) => p.id === products.premium.id, ); - console.log( - redeemerProds.map((x) => `${x.id} - ${x.name} - ${x.status}`), - mainProds.map((x) => `${x.id} - ${x.name} - ${x.status}`), - ); - assert.isFalse( redeemerHasProProduct, "Redeemer should not have pro product (already on paid plan)", @@ -194,16 +189,16 @@ describe(`${chalk.yellowBright( // Verify products are properly attached expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomerId), + customer: mainCustomerData, product: products.pro, status: CusProductStatus.Trialing, }); // Verify redeemer only has premium product expectProductV1Attached({ - customer: await autumn.customers.get(redeemer), + customer: redeemerCustomerData, product: products.premium, - status: CusProductStatus.Trialing, + status: CusProductStatus.Active, }); }); diff --git a/server/tests/advanced/referrals/paid/referrals12.ts b/server/tests/advanced/referrals/paid/referrals12.ts index 384c93693..7b7928575 100644 --- a/server/tests/advanced/referrals/paid/referrals12.ts +++ b/server/tests/advanced/referrals/paid/referrals12.ts @@ -128,7 +128,7 @@ describe(`${chalk.yellowBright( it("should have given the paid product to referrer only, not redeemer (referrer-only reward)", async () => { const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true); + assert.equal(redemptionResult.applied, true, `Redemption should be applied: ${JSON.stringify(redemptionResult, null, 4)}`); await setTimeout(1000 * 10); @@ -153,7 +153,7 @@ describe(`${chalk.yellowBright( expectProductV1Attached({ customer: await autumn.customers.get(redeemer), product: products.premium, - status: CusProductStatus.Trialing, + status: CusProductStatus.Active, }); }); diff --git a/server/tests/advanced/referrals/paid/referrals9.ts b/server/tests/advanced/referrals/paid/referrals9.ts index 2f1429432..72fd00ee9 100644 --- a/server/tests/advanced/referrals/paid/referrals9.ts +++ b/server/tests/advanced/referrals/paid/referrals9.ts @@ -142,7 +142,7 @@ describe(`${chalk.yellowBright( const mainProds = mainCustomerData.products; const mainAddons = mainCustomerData.add_ons; - assert.equal(mainProds.length + mainAddons.length, 2); + assert.equal(mainProds.length + mainAddons.length, 2, `Main customer should have 2 products: ${JSON.stringify(mainProds, null, 4)} ${JSON.stringify(mainAddons, null, 4)}`); const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); @@ -151,7 +151,7 @@ describe(`${chalk.yellowBright( const redeemerProds = redeemerCustomerData.products; const redeemerAddons = redeemerCustomerData.add_ons; - assert.equal(redeemerProds.length + redeemerAddons.length, 2); + assert.equal(redeemerProds.length + redeemerAddons.length, 2, `Redeemer should have 2 products: ${JSON.stringify(redeemerProds, null, 4)} ${JSON.stringify(redeemerAddons, null, 4)}`); const redeemerHasProAddOn = redeemerAddons.some( (p) => p.id === products.proAddOn.id, From 181b803e59723ef472903f4a73e1d52e069e7948 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:04:11 +0100 Subject: [PATCH 37/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20allow=20parallisat?= =?UTF-8?q?ion=20of=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/test.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/test.sh b/server/test.sh index 96b3e32f1..24893f49c 100755 --- a/server/test.sh +++ b/server/test.sh @@ -41,7 +41,7 @@ if [ "$1" == "g4" ]; then fi # Group 5 - Paid referrals -if [ "$1" == "paid" ]; then +if [ "$1" == "paid-referrals" ]; then $MOCHA_SETUP && $MOCHA_CMD \ 'tests/advanced/referrals/paid/*.ts' fi @@ -74,8 +74,11 @@ elif [ "$1" == "custom" ]; then ARG3="$3" if [ "$ARG3" == "setup" ]; then npx mocha --bail --timeout 10000000 'tests/00_setup.ts' + elif [ "$ARG3" == "parallel" ]; then + npx mocha --parallel --bail --timeout 10000000 "tests/$FILE_TO_TEST.ts" + else + npx mocha --bail --timeout 10000000 "tests/$FILE_TO_TEST.ts" fi - npx mocha --bail --timeout 10000000 "tests/$FILE_TO_TEST.ts" else npx mocha --timeout 10000000 'tests/00_setup.ts' && npx mocha --timeout 10000000 \ 'tests/**/*.ts' \ From f7bdab57f7cd21a5e7fbd5c063562cddf0d75416 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:04:20 +0100 Subject: [PATCH 38/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20allow=20paid=20addo?= =?UTF-8?q?ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/internal/rewards/referralUtils.ts | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index ee79685ba..0097bb740 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -16,7 +16,6 @@ import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js" import { createStripeCli } from "@/external/stripe/utils.js"; import RecaseError from "@/utils/errorUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { createCusInStripe } from "@/utils/scriptUtils/initCustomer.js"; import { createFullCusProduct } from "../customers/add-product/createFullCusProduct.js"; import { handleAddProduct } from "../customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; import { rewardProgramToAttachParams } from "../customers/attach/attachUtils/attachParams/convertToParams.js"; @@ -215,10 +214,24 @@ export const triggerFreeProduct = async ({ if (fullRedeemer.customer_products.find((cp) => cp.product.id === productId)) addToRedeemer = false; - // If they are on any paid plan that isn't an add-on, don't add to them - if(fullReferrer.customer_products.some(x => !x.product.is_add_on && !isFreeProduct(x.customer_prices.map(y => y.price)))) + // If they are on any paid plan that isn't an add-on, and the reward isn't an, don't add to them + if ( + fullReferrer.customer_products.some( + (x) => + !x.product.is_add_on && + !isFreeProduct(x.customer_prices.map((y) => y.price)), + ) && + !fullProduct.is_add_on + ) addToReferrer = false; - if(fullRedeemer.customer_products.some(x => !x.product.is_add_on && !isFreeProduct(x.customer_prices.map(y => y.price)))) + if ( + fullRedeemer.customer_products.some( + (x) => + !x.product.is_add_on && + !isFreeProduct(x.customer_prices.map((y) => y.price)), + ) && + !fullProduct.is_add_on + ) addToRedeemer = false; function seedReq(req: ExtendedRequest) { @@ -338,20 +351,20 @@ export const triggerFreeProduct = async ({ const ensureStripeIDs = [ !fullRedeemer.processor?.id && - (await createCusInStripe({ + (await createStripeCusIfNotExists({ + db, customer: fullRedeemer, org, env, - db, - testClockId: req?.body?.testClockId || undefined, + logger, })), !fullReferrer.processor?.id && - (await createCusInStripe({ + (await createStripeCusIfNotExists({ + db, customer: fullReferrer, org, env, - db, - testClockId: req?.body?.testClockId || undefined, + logger, })), ]; From f3482ae981bf2e6c71d94ff27468b859a947b9d4 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:11:22 +0100 Subject: [PATCH 39/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20rollback=20useTrans?= =?UTF-8?q?ition=20to=20useState?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rewards/reward-config/CreateReward.tsx | 23 ++++++++------- .../reward-programs/CreateRewardProgram.tsx | 28 +++++++++---------- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/vite/src/views/products/rewards/reward-config/CreateReward.tsx b/vite/src/views/products/rewards/reward-config/CreateReward.tsx index 0c870349d..0b31f7aa7 100644 --- a/vite/src/views/products/rewards/reward-config/CreateReward.tsx +++ b/vite/src/views/products/rewards/reward-config/CreateReward.tsx @@ -1,13 +1,13 @@ -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, } from "@/components/ui/dialog"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; import { RewardService } from "@/services/products/RewardService"; @@ -20,7 +20,7 @@ function CreateReward() { const axiosInstance = useAxiosInstance(); const { refetch } = useRewardsQuery(); - const [isLoading, startTransition] = useTransition(); + const [isLoading, setIsLoading] = useState(false); const [open, setOpen] = useState(false); const [reward, setReward] = useState(defaultReward); @@ -31,7 +31,8 @@ function CreateReward() { } }, [open]); - const handleCreate = () => startTransition(() => { + const handleCreate = () => { + setIsLoading(true); (async () => { if (!reward?.id && !reward?.name) { toast.error("ID and name are required"); @@ -49,9 +50,11 @@ function CreateReward() { } catch (error) { console.log("Error:", error); toast.error(getBackendErr(error, "Failed to create coupon")); + } finally { + setIsLoading(false); } })(); - }); + }; return ( diff --git a/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx b/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx index 59e952dad..0b6918357 100644 --- a/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx +++ b/vite/src/views/products/rewards/reward-programs/CreateRewardProgram.tsx @@ -1,6 +1,6 @@ import type { CreateRewardProgram } from "@autumn/shared"; import { RewardReceivedBy, RewardTriggerEvent } from "@autumn/shared"; -import { useEffect, useState, useTransition } from "react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { @@ -35,7 +35,7 @@ function CreateRewardProgramModal() { const { refetch } = useRewardsQuery(); const axiosInstance = useAxiosInstance(); - const [isLoading, startTransition] = useTransition(); + const [isLoading, setIsLoading] = useState(false); const [open, setOpen] = useState(false); const [rewardProgram, setRewardProgram] = useState(defaultRewardProgram); @@ -47,19 +47,19 @@ function CreateRewardProgramModal() { }, [open]); const handleCreate = () => { - startTransition(() => { - (async () => { - try { - await axiosInstance.post("/v1/reward_programs", rewardProgram); + setIsLoading(true); + (async () => { + try { + await axiosInstance.post("/v1/reward_programs", rewardProgram); - await refetch(); - setOpen(false); - } catch (error) { - toast.error(getBackendErr(error, "Failed to create referral program")); - } finally { - } - })(); - }); + await refetch(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to create referral program")); + } finally { + setIsLoading(false); + } + })(); }; return ( From 5f3efa8f3832a5424821de320e1df25f68234b65 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Wed, 17 Sep 2025 11:36:07 +0100 Subject: [PATCH 40/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../referralModels/referralModels.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/shared/models/rewardModels/referralModels/referralModels.ts b/shared/models/rewardModels/referralModels/referralModels.ts index ede3c25ff..4913107a0 100644 --- a/shared/models/rewardModels/referralModels/referralModels.ts +++ b/shared/models/rewardModels/referralModels/referralModels.ts @@ -1,37 +1,37 @@ import { z } from "zod"; -let ReferralCodeSchema = z.object({ - // Code, org id, env should be unique - code: z.string(), - org_id: z.string(), - env: z.string(), +const ReferralCodeSchema = z.object({ + // Code, org id, env should be unique + code: z.string(), + org_id: z.string(), + env: z.string(), - created_at: z.number(), - internal_customer_id: z.string(), - internal_reward_program_id: z.string(), + created_at: z.number(), + internal_customer_id: z.string(), + internal_reward_program_id: z.string(), - // ID of the referral code - id: z.string(), + // ID of the referral code + id: z.string(), }); -let RewardRedemptionSchema = z.object({ - id: z.string(), - created_at: z.number(), - updated_at: z.number(), +const RewardRedemptionSchema = z.object({ + id: z.string(), + created_at: z.number(), + updated_at: z.number(), - // Customer who signed up / paid - internal_customer_id: z.string(), // customer who redeemed the code... - internal_reward_program_id: z.string(), // reward scheme that was redeemed + // Customer who signed up / paid + internal_customer_id: z.string(), // customer who redeemed the code... + internal_reward_program_id: z.string(), // reward scheme that was redeemed - // Referral code used - // code: z.string(), - referral_code_id: z.string(), + // Referral code used + // code: z.string(), + referral_code_id: z.string(), - // Whether the reward was triggered - triggered: z.boolean(), + // Whether the reward was triggered + triggered: z.boolean(), - // Whether the (coupon) was applied - applied: z.boolean(), + // Whether the (coupon) was applied + applied: z.boolean(), }); export type ReferralCode = z.infer; From 7b427cefe7f646708cb4885135bb3be64eb02913 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Wed, 17 Sep 2025 10:50:41 -0700 Subject: [PATCH 41/60] latest --- .../stripe/stripeCouponUtils/stripeCouponUtils.ts | 12 +++++++----- .../rewards/handlers/rewards/handleCreateCoupon.ts | 8 -------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 427669fd7..792ba501f 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -157,11 +157,13 @@ export const createStripeCoupon = async ({ autumn_internal_id: reward.internal_id, }, applies_to: - reward.type === RewardType.FreeProduct || !discountConfig!.apply_to_all - ? { - products: stripeProdIds, - } - : undefined, + reward.type === RewardType.FreeProduct + ? undefined + : !discountConfig!.apply_to_all + ? { + products: stripeProdIds, + } + : undefined, }); // Create promo codes diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts index 6e63b8bec..ac4fe144d 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts @@ -75,14 +75,6 @@ export default async (req: any, res: any) => env, }); - await initProductInStripe({ - db, - product: fullProduct, - org, - env, - logger, - }); - if (!isFreeProduct(fullProduct.prices)) { await createStripeCoupon({ reward: newReward, From d731c21f774ba6571dddef70a96f6cd37b5aee0e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:23:54 +0100 Subject: [PATCH 42/60] =?UTF-8?q?chore:=20=F0=9F=A4=96=20lint=20and=20smal?= =?UTF-8?q?l=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/external/autumn/autumnCli.ts | 35 ++++++++++--------- server/src/queue/workersInit.ts | 6 ++-- server/tests/global.ts | 5 --- .../referralModels/referralModels.ts | 1 + 4 files changed, 23 insertions(+), 24 deletions(-) diff --git a/server/src/external/autumn/autumnCli.ts b/server/src/external/autumn/autumnCli.ts index dd6b0a55c..ea6dd3300 100644 --- a/server/src/external/autumn/autumnCli.ts +++ b/server/src/external/autumn/autumnCli.ts @@ -3,22 +3,25 @@ import dotenv from "dotenv"; dotenv.config(); import { - type APIVersion, - type AttachBody, - type CreateEntity, - type CreateRewardProgram, - CusExpand, - EntityExpand, - ErrCode, type OrgConfig + type APIVersion, + type AttachBody, + type CreateEntity, + type CreateRewardProgram, + CusExpand, + EntityExpand, + ErrCode, + type OrgConfig, + type RewardRedemption, } from "@autumn/shared"; -import { - type CancelParams, - type CheckoutParams, - type CheckoutResult, - type CheckParams, - type CheckResult, - type Customer, - type TrackParams, type UsageParams +import type { + CancelParams, + CheckoutParams, + CheckoutResult, + CheckParams, + CheckResult, + Customer, + TrackParams, + UsageParams, } from "autumn-js"; export default class AutumnError extends Error { @@ -411,7 +414,7 @@ export class AutumnInt { redemptions = { get: async (redemptionId: string) => { const data = await this.get(`/redemptions/${redemptionId}`); - return data; + return data as RewardRedemption; }, }; diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index 4fb00311b..1ae6259eb 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -50,7 +50,7 @@ const initWorker = ({ }); try { - if (job.name == JobName.DetectBaseVariant) { + if (job.name === JobName.DetectBaseVariant) { await detectBaseVariant({ db, curProduct: job.data.curProduct, @@ -59,7 +59,7 @@ const initWorker = ({ return; } - if (job.name == JobName.GenerateFeatureDisplay) { + if (job.name === JobName.GenerateFeatureDisplay) { await runSaveFeatureDisplayTask({ db, feature: job.data.feature, @@ -68,7 +68,7 @@ const initWorker = ({ return; } - if (job.name == JobName.Migration) { + if (job.name === JobName.Migration) { await runMigrationTask({ db, payload: job.data, diff --git a/server/tests/global.ts b/server/tests/global.ts index 0ae12da8a..22596efe6 100644 --- a/server/tests/global.ts +++ b/server/tests/global.ts @@ -842,11 +842,6 @@ export const rewards = { durationType: CouponDurationType.Months, durationValue: 1, }), - paidProduct: initReward({ - id: "paidProduct", - type: RewardType.FreeProduct, - freeProductId: products.pro.id, - }), paidProductWithConfig: initReward({ id: "paidProductWithConfig", type: RewardType.FreeProduct, diff --git a/shared/models/rewardModels/referralModels/referralModels.ts b/shared/models/rewardModels/referralModels/referralModels.ts index 4913107a0..bcf143639 100644 --- a/shared/models/rewardModels/referralModels/referralModels.ts +++ b/shared/models/rewardModels/referralModels/referralModels.ts @@ -32,6 +32,7 @@ const RewardRedemptionSchema = z.object({ // Whether the (coupon) was applied applied: z.boolean(), + redeemer_applied: z.boolean(), }); export type ReferralCode = z.infer; From 1d136d2e9eab63126e7d9faa37894589bd3ace50 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:39:29 +0100 Subject: [PATCH 43/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20add=20checkout=20t?= =?UTF-8?q?rigger=20and=20others?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handlers/rewards/handleCreateCoupon.ts | 158 +++--- .../handlers/rewards/handleUpdateCoupon.ts | 49 +- .../referralUtils/triggerFreePaidProduct.ts | 325 ++++++----- .../referralUtils/triggerFreeProduct.ts | 503 +++++++----------- .../internal/rewards/triggerCheckoutReward.ts | 6 +- 5 files changed, 461 insertions(+), 580 deletions(-) diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts index 6e63b8bec..fa47aa989 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts @@ -5,7 +5,12 @@ import { } from "@autumn/shared"; import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; +import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; +import { + isFreeProduct +} from "@/internal/products/productUtils.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { constructReward, @@ -13,97 +18,88 @@ import { initRewardStripePrices, } from "@/internal/rewards/rewardUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { ProductService } from "@/internal/products/ProductService.js"; -import { - initProductInStripe, - isFreeProduct, -} from "@/internal/products/productUtils.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, + }); - await initProductInStripe({ - db, - product: fullProduct, - org, - env, - logger, - }); + 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)) { - await createStripeCoupon({ - reward: newReward, - org, - env, - prices: fullProduct.prices - .filter((price) => isFixedPrice({ price })) - .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/handleUpdateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts index 76ab77ad8..a664f8497 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts @@ -1,7 +1,8 @@ -import { ErrCode, notNullish, RewardCategory } from "@autumn/shared"; +import { ErrCode, PriceType, RewardCategory } from "@autumn/shared"; import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { getRewardCat } from "@/internal/rewards/rewardUtils.js"; @@ -38,12 +39,40 @@ export default async (req: any, res: any) => }); } - const prices = await PriceService.getInIds({ - db, - ids: notNullish(rewardBody.price_ids) - ? rewardBody.price_ids - : reward.discount_config?.price_ids, - }); + // Determine prices depending on reward category + const rewardCat = getRewardCat(rewardBody); + + let prices: any[] = []; + if (rewardCat === RewardCategory.Discount) { + const stripePriceIds = + rewardBody.discount_config?.price_ids ?? + reward.discount_config?.price_ids ?? + []; + const byStripeId = await PriceService.getByStripeIds({ + db, + stripePriceIds, + }); + prices = stripePriceIds + .map((id: string) => byStripeId[id]) + .filter(Boolean); + } else if (rewardCat === RewardCategory.FreeProduct) { + const freeProductId = + rewardBody.free_product_id ?? reward.free_product_id; + if (freeProductId) { + const fullProduct = await ProductService.getFull({ + db, + idOrInternalId: freeProductId, + orgId: org.id, + env, + }); + prices = fullProduct.prices + .map((price) => ({ + ...price, + product: fullProduct, + })) + .filter((x) => x.config?.type === PriceType.Fixed); + } + } // 1. Delete old prices from stripe try { @@ -53,8 +82,10 @@ export default async (req: any, res: any) => // console.log(`Failed to delete coupon from stripe: ${error.message}`); } - const rewardCat = getRewardCat(rewardBody); - if (rewardCat === RewardCategory.Discount) { + if ( + rewardCat === RewardCategory.Discount || + (rewardCat === RewardCategory.FreeProduct && prices.length > 0) + ) { await createStripeCoupon({ reward: rewardBody, org, diff --git a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts index 37e34558e..1e2350a89 100644 --- a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts +++ b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts @@ -1,196 +1,191 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; -import { rewardProgramToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js"; -import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; -import { InsertCusProductParams } from "@/internal/customers/cusProducts/AttachParams.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { ProductService } from "@/internal/products/ProductService.js"; import { - isFreeProduct, - isOneOff, - itemsAreOneOff, -} from "@/internal/products/productUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - ReferralCode, - RewardRedemption, - FullRewardProgram, - Reward, - RewardReceivedBy, - ErrCode, AttachBranch, - FullProduct, - Customer, + type Customer, + ErrCode, + type FullProduct, + type FullRewardProgram, + type ReferralCode, + type Reward, + RewardReceivedBy, + type RewardRedemption, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; -import { ReferralResponseCodes } from "../referralUtils.js"; -import { RewardRedemptionService } from "../RewardRedemptionService.js"; +import type Stripe from "stripe"; +import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; +import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; +import { rewardProgramToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js"; import { getCustomerSub } from "@/internal/customers/attach/attachUtils/convertAttachParams.js"; import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; +import { CusService } from "@/internal/customers/CusService.js"; import { isStripeConnected } from "@/internal/orgs/orgUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import Stripe from "stripe"; +import RecaseError from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { RewardRedemptionService } from "../RewardRedemptionService.js"; +import { ReferralResponseCodes } from "../referralUtils.js"; export const receivedByReferrer = (received_by: RewardReceivedBy) => { - return ( - received_by === RewardReceivedBy.Referrer || - received_by === RewardReceivedBy.All - ); + return ( + received_by === RewardReceivedBy.Referrer || + received_by === RewardReceivedBy.All + ); }; export const receivedByRedeemer = (received_by: RewardReceivedBy) => { - return received_by === RewardReceivedBy.All; + return received_by === RewardReceivedBy.All; }; export const triggerFreePaidProduct = async ({ - req, - referralCode, - redeemer, - rewardProgram, - fullProduct, - redemption, + req, + referralCode, + redeemer, + rewardProgram, + fullProduct, + redemption, }: { - req: ExtendedRequest; - referralCode: ReferralCode; - redeemer: Customer; - rewardProgram: FullRewardProgram & { reward: Reward }; - fullProduct: FullProduct; - redemption: RewardRedemption; + req: ExtendedRequest; + referralCode: ReferralCode; + redeemer: Customer; + rewardProgram: FullRewardProgram & { reward: Reward }; + fullProduct: FullProduct; + redemption: RewardRedemption; }) => { - const { db, org, env, logger } = req; - const { received_by } = rewardProgram; + const { db, org, env, logger } = req; + const { received_by } = rewardProgram; - logger.info( - `Triggering free paid product reward for referral code ${referralCode.code}` - ); + logger.info( + `Triggering free paid product reward for referral code ${referralCode.code}`, + ); - const [fullReferrer, fullRedeemer] = await Promise.all([ - CusService.getFull({ - db, - idOrInternalId: referralCode.internal_customer_id, - orgId: org.id, - env, - withEntities: true, - withSubs: true, - }), - CusService.getFull({ - db, - idOrInternalId: redeemer.id!, - orgId: org.id, - env, - withEntities: true, - withSubs: true, - }), - ]); + const [fullReferrer, fullRedeemer] = await Promise.all([ + CusService.getFull({ + db, + idOrInternalId: referralCode.internal_customer_id, + orgId: org.id, + env, + withEntities: true, + withSubs: true, + }), + CusService.getFull({ + db, + idOrInternalId: redeemer.id!, + orgId: org.id, + env, + withEntities: true, + withSubs: true, + }), + ]); - if (!isStripeConnected({ org, env })) { - throw new RecaseError({ - message: "Stripe is not connected", - code: ErrCode.StripeConfigNotFound, - statusCode: StatusCodes.BAD_REQUEST, - }); - } + if (!isStripeConnected({ org, env })) { + throw new RecaseError({ + message: "Stripe is not connected", + code: ErrCode.StripeConfigNotFound, + statusCode: StatusCodes.BAD_REQUEST, + }); + } - // Add to referrer / redeemer - const stripeCli = createStripeCli({ org, env }); + // Add to referrer / redeemer + const stripeCli = createStripeCli({ org, env }); - let applied = [false, false]; // [referrerApplied, redeemerApplied] - for (let i = 0; i < 2; i++) { - if (i == 0 && !receivedByReferrer(received_by)) { - applied[i] = true; - continue; - } - if (i == 1 && !receivedByRedeemer(received_by)) { - applied[i] = true; - continue; - } + const applied = [false, false]; // [referrerApplied, redeemerApplied] + for (let i = 0; i < 2; i++) { + if (i === 0 && !receivedByReferrer(received_by)) { + applied[i] = true; + continue; + } + if (i === 1 && !receivedByRedeemer(received_by)) { + applied[i] = true; + continue; + } - const fullCus = [fullReferrer, fullRedeemer][i]; - const attachParams = rewardProgramToAttachParams({ - req, - rewardProgram, - customer: fullCus, - product: fullProduct, - }); + const fullCus = [fullReferrer, fullRedeemer][i]; + const attachParams = rewardProgramToAttachParams({ + req, + rewardProgram, + customer: fullCus, + product: fullProduct, + }); - const { sub } = await getCustomerSub({ attachParams }); + const { sub } = await getCustomerSub({ attachParams }); - if (sub) { - const curDiscounts = (sub.discounts as Stripe.Discount[]) || []; + if (sub) { + console.log("Detected sub", !!sub); + const curDiscounts = (sub.discounts as Stripe.Discount[]) || []; - // If coupon already applied, don't add it again - if ( - curDiscounts.some((d: any) => d.coupon?.id === rewardProgram.reward.id) - ) { - } else { - await stripeCli.subscriptions.update(sub.id, { - discounts: [ - ...curDiscounts.map((d: Stripe.Discount) => ({ - discount: d.id, - })), - { - coupon: rewardProgram.reward.id, - }, - ], - }); - applied[i] = true; - } - } else { - // Create stripe customer if not exists - await createStripeCusIfNotExists({ - db, - customer: fullCus, - org, - env, - logger, - }); + // If coupon already applied, don't add it again + if ( + !curDiscounts.some((d: any) => d.coupon?.id === rewardProgram.reward.id) + ) { + console.log("Detected no discount, adding it"); + try { + await stripeCli.subscriptions.update(sub.id, { + discounts: [ + ...curDiscounts.map((d: Stripe.Discount) => ({ + discount: d.id, + })), + { + coupon: rewardProgram.reward.id, + }, + ], + }); + } catch (error) { + console.log("Error adding discount", error); + } + applied[i] = true; + } + } else { + // Create stripe customer if not exists + await createStripeCusIfNotExists({ + db, + customer: fullCus, + org, + env, + logger, + }); - await handleAddProduct({ - req, - attachParams, - branch: AttachBranch.New, - config: { - ...getDefaultAttachConfig(), - requirePaymentMethod: false, - }, - }); - applied[i] = true; - } - } + await handleAddProduct({ + req, + attachParams, + branch: AttachBranch.New, + config: { + ...getDefaultAttachConfig(), + requirePaymentMethod: false, + }, + }); + applied[i] = true; + } + } - const updates = { - triggered: true, - applied: applied?.[0] || false, // referrer applied - redeemer_applied: applied?.[1] || false, // redeemer applied - }; + const updates = { + triggered: true, + applied: applied?.[0] || false, // referrer applied + redeemer_applied: applied?.[1] || false, // redeemer applied + }; - await RewardRedemptionService.update({ - db, - id: redemption.id, - updates, - }); + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates, + }); - return { - redeemer: { - applied: true, - cause: true - ? ReferralResponseCodes.Success - : ReferralResponseCodes.OwnsProduct, - meta: { - id: fullRedeemer.id, - name: fullRedeemer.name, - email: fullRedeemer.email, - created_at: fullRedeemer.created_at, - }, - }, - referrer: { - applied: true, - cause: true - ? ReferralResponseCodes.Success - : ReferralResponseCodes.OwnsProduct, - }, - }; + return { + redeemer: { + applied: true, + cause: applied?.[0] + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + meta: { + id: fullRedeemer.id, + name: fullRedeemer.name, + email: fullRedeemer.email, + created_at: fullRedeemer.created_at, + }, + }, + referrer: { + applied: true, + cause: applied?.[1] + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + }, + }; }; diff --git a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts index 709838c90..fc10fc6b6 100644 --- a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts +++ b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts @@ -1,351 +1,208 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { + 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"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; -import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; -import { InsertCusProductParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import type { InsertCusProductParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - ReferralCode, - RewardRedemption, - FullRewardProgram, - Reward, - RewardReceivedBy, - ErrCode, -} from "@autumn/shared"; -import { Customer, AppEnv } from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; -import { ReferralResponseCodes } from "../referralUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { RewardRedemptionService } from "../RewardRedemptionService.js"; +import { ReferralResponseCodes } from "../referralUtils.js"; import { triggerFreePaidProduct } from "./triggerFreePaidProduct.js"; export const triggerFreeProduct = async ({ - req, - db, - referralCode, - redeemer, - redemption, - rewardProgram, - org, - env, - logger, + req, + db, + referralCode, + redeemer, + redemption, + rewardProgram, + org, + env, + logger, }: { - req?: ExtendedRequest; - db: DrizzleCli; - referralCode: ReferralCode; - redeemer: Customer; - redemption: RewardRedemption; - rewardProgram: FullRewardProgram & { reward: Reward }; - org: any; - env: AppEnv; - logger: any; + req?: ExtendedRequest; + db: DrizzleCli; + referralCode: ReferralCode; + redeemer: Customer; + redemption: RewardRedemption; + rewardProgram: FullRewardProgram & { reward: Reward }; + org: any; + env: AppEnv; + logger: any; }) => { - logger.info(`Triggering free product reward`); - const { received_by } = rewardProgram; + logger.info(`Triggering free product reward`); + const { received_by } = rewardProgram; - let addToRedeemer = received_by === RewardReceivedBy.All; - let addToReferrer = - received_by === RewardReceivedBy.Referrer || - received_by === RewardReceivedBy.All; + const addToRedeemer = received_by === RewardReceivedBy.All; + const addToReferrer = + received_by === RewardReceivedBy.Referrer || + received_by === RewardReceivedBy.All; - const productId = rewardProgram.reward.free_product_id!; + const productId = rewardProgram.reward.free_product_id!; - const fullProduct = await ProductService.getFull({ - db, - idOrInternalId: productId, - orgId: org.id, - env, - }); + const fullProduct = await ProductService.getFull({ + db, + idOrInternalId: productId, + orgId: org.id, + env, + }); - function seedReq(req?: ExtendedRequest) { - // Seed in properties that aren't usually present dependent on the trigger type - return { - ...(req || {}), - db: req?.db ? req.db : db, - org: req?.org ? req.org : org, - env: req?.env ? req.env : env, - logger: req?.logger ? req.logger : logger, - logtail: req?.logtail ? req.logtail : logger, - } as ExtendedRequest; - } + function seedReq(req?: ExtendedRequest) { + // Seed in properties that aren't usually present dependent on the trigger type + return { + ...(req || {}), + db: req?.db ? req.db : db, + org: req?.org ? req.org : org, + env: req?.env ? req.env : env, + logger: req?.logger ? req.logger : logger, + logtail: req?.logtail ? req.logtail : logger, + } as ExtendedRequest; + } - if (!isFreeProduct(fullProduct.prices) && !isOneOff(fullProduct.prices)) { - req = seedReq(req); - return await triggerFreePaidProduct({ - req, - referralCode, - redeemer, - rewardProgram, - fullProduct, - redemption, - }); - } + if (!isFreeProduct(fullProduct.prices) && !isOneOff(fullProduct.prices)) { + req = seedReq(req); + return await triggerFreePaidProduct({ + req, + referralCode, + redeemer, + rewardProgram, + fullProduct, + redemption, + }); + } - // const isPaidProduct = !isFreeProduct(fullProduct.prices); - // const isRecurring = - // !isOneOff(fullProduct.prices) && !itemsAreOneOff(fullProduct.entitlements); + // const isPaidProduct = !isFreeProduct(fullProduct.prices); + // const isRecurring = + // !isOneOff(fullProduct.prices) && !itemsAreOneOff(fullProduct.entitlements); - if (!fullProduct) { - throw new RecaseError({ - message: `Product ${productId} not found`, - code: ErrCode.ProductNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } + if (!fullProduct) { + throw new RecaseError({ + message: `Product ${productId} not found`, + code: ErrCode.ProductNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } - const [fullReferrer, fullRedeemer] = await Promise.all([ - CusService.getFull({ - db, - idOrInternalId: referralCode.internal_customer_id, - orgId: org.id, - env, - allowNotFound: true, - }), - CusService.getFull({ - db, - idOrInternalId: redeemer.id!, - orgId: org.id, - env, - }), - ]); + const [fullReferrer, fullRedeemer] = await Promise.all([ + CusService.getFull({ + db, + idOrInternalId: referralCode.internal_customer_id, + orgId: org.id, + env, + allowNotFound: true, + }), + CusService.getFull({ + db, + idOrInternalId: redeemer.id!, + orgId: org.id, + env, + }), + ]); - if (!fullReferrer) { - throw new RecaseError({ - message: `Referrer (internal ID: ${referralCode.internal_customer_id}) not found`, - code: ErrCode.CustomerNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } + if (!fullReferrer) { + throw new RecaseError({ + message: `Referrer (internal ID: ${referralCode.internal_customer_id}) not found`, + code: ErrCode.CustomerNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } - // // If they already have the product, don't add to them - // if (fullReferrer.customer_products.find((cp) => cp.product.id === productId)) - // addToReferrer = false; - // if (fullRedeemer.customer_products.find((cp) => cp.product.id === productId)) - // addToRedeemer = false; + const attachParams: InsertCusProductParams = { + req, + org, + product: fullProduct, + prices: fullProduct.prices, + entitlements: fullProduct.entitlements, + optionsList: [], + entities: [], + freeTrial: null, + features: [], + customer: fullReferrer, + cusProducts: fullReferrer.customer_products, + replaceables: [], + }; - // // If they are on any paid plan that isn't an add-on, and the reward isn't an, don't add to them - // if ( - // fullReferrer.customer_products.some( - // (x) => - // !x.product.is_add_on && - // !isFreeProduct(x.customer_prices.map((y) => y.price)) - // ) && - // !fullProduct.is_add_on - // ) - // addToReferrer = false; - // if ( - // fullRedeemer.customer_products.some( - // (x) => - // !x.product.is_add_on && - // !isFreeProduct(x.customer_prices.map((y) => y.price)) - // ) && - // !fullProduct.is_add_on - // ) - // addToRedeemer = false; + if (addToRedeemer) { + const redeemerAttachParams = { + ...structuredClone(attachParams), + customer: fullRedeemer, + cusProducts: fullRedeemer.customer_products, + }; - // Branch 1: Free add-on product or non-recurring product - // logger.info(`Branch 1: ${isPaidProduct ? "Paid" : "Free"} add-on product`); + await createFullCusProduct({ + db, + attachParams: redeemerAttachParams, + logger, + }); + logger.info(`✅ Added ${fullProduct.name} to redeemer`); - const attachParams: InsertCusProductParams = { - req, - org, - product: fullProduct, - prices: fullProduct.prices, - entitlements: fullProduct.entitlements, - optionsList: [], - entities: [], - freeTrial: null, - features: [], - customer: fullReferrer, - cusProducts: fullReferrer.customer_products, - replaceables: [], - }; + await deleteCusCache({ + db, + customerId: fullRedeemer.id!, + org, + env, + }); + } - if (addToRedeemer) { - const redeemerAttachParams = { - ...structuredClone(attachParams), - customer: fullRedeemer, - cusProducts: fullRedeemer.customer_products, - }; + if (addToReferrer) { + await createFullCusProduct({ + db, + attachParams: { + ...structuredClone(attachParams), + customer: fullReferrer, + cusProducts: fullReferrer.customer_products, + }, + logger, + }); + await deleteCusCache({ + db, + customerId: fullReferrer.id!, + org, + env, + }); + logger.info(`✅ Added ${fullProduct.name} to referrer`); + } - await createFullCusProduct({ - db, - attachParams: redeemerAttachParams, - logger, - }); - logger.info(`✅ Added ${fullProduct.name} to redeemer`); + await RewardRedemptionService.update({ + db, + id: redemption.id, + updates: { + triggered: true, + applied: true, + }, + }); - await deleteCusCache({ - db, - customerId: fullRedeemer.id!, - org, - env, - }); - } - - if (addToReferrer) { - await createFullCusProduct({ - db, - attachParams: { - ...structuredClone(attachParams), - customer: fullReferrer, - cusProducts: fullReferrer.customer_products, - }, - logger, - }); - await deleteCusCache({ - db, - customerId: fullReferrer.id!, - org, - env, - }); - logger.info(`✅ Added ${fullProduct.name} to referrer`); - } - - await RewardRedemptionService.update({ - db, - id: redemption.id, - updates: { - triggered: true, - applied: true, - }, - }); - - return { - redeemer: { - applied: addToRedeemer, - cause: addToRedeemer - ? ReferralResponseCodes.Success - : ReferralResponseCodes.OwnsProduct, - meta: { - id: fullRedeemer.id, - name: fullRedeemer.name, - email: fullRedeemer.email, - created_at: fullRedeemer.created_at, - }, - }, - referrer: { - applied: addToReferrer, - cause: addToReferrer - ? ReferralResponseCodes.Success - : ReferralResponseCodes.OwnsProduct, - }, - }; - - // if (isPaidProduct) { - // logger.info(`Branch 2: Paid product from Customer Redemption`); - // if (!req) { - // req = { - // db, - // org, - // env, - // logger, - // logtail: logger, - // } as ExtendedRequest; - // } - // req = seedReq(req); - - // const ensureStripeIDs = [ - // !fullRedeemer.processor?.id && - // (await createStripeCusIfNotExists({ - // db, - // customer: fullRedeemer, - // org, - // env, - // logger, - // })), - // !fullReferrer.processor?.id && - // (await createStripeCusIfNotExists({ - // db, - // customer: fullReferrer, - // org, - // env, - // logger, - // })), - // ]; - - // // Update customers with new Stripe IDs if they were created - // const updatedIDs = await Promise.all(ensureStripeIDs); - // if (updatedIDs[0]) fullRedeemer.processor.id = updatedIDs[0].id; - // if (updatedIDs[1]) fullReferrer.processor.id = updatedIDs[1].id; - - // const executions = [ - // addToRedeemer && - // (await handleAddProduct({ - // req, - // attachParams: rewardProgramToAttachParams({ - // req, - // rewardProgram: rewardProgram, - // customer: fullRedeemer, - // product: fullProduct, - // org, - // }), - // branch: AttachBranch.New, - // })), - // addToReferrer && - // (await handleAddProduct({ - // req, - // attachParams: rewardProgramToAttachParams({ - // req, - // rewardProgram: rewardProgram, - // customer: fullReferrer, - // product: fullProduct, - // org, - // }), - // branch: AttachBranch.New, - // })), - // ]; - - // const results = await Promise.allSettled(executions); - // const redeemerCause = !addToRedeemer - // ? ReferralResponseCodes.OwnsProduct - // : results[0]?.status === "fulfilled" - // ? ReferralResponseCodes.Success - // : ReferralResponseCodes.InternalError; - // const referrerCause = !addToReferrer - // ? ReferralResponseCodes.OwnsProduct - // : results[1]?.status === "fulfilled" - // ? ReferralResponseCodes.Success - // : ReferralResponseCodes.InternalError; - // const appliedToRedeemer = - // addToRedeemer && results[0]?.status === "fulfilled"; - // const appliedToReferrer = - // addToReferrer && results[1]?.status === "fulfilled"; - - // if (results.every((result) => result.status === "fulfilled")) { - // await RewardRedemptionService.update({ - // db, - // id: redemption.id, - // updates: { - // triggered: true, - // applied: true, - // }, - // }); - // } else { - // logger.error(`Error in executions: ${results}`); - // } - - // return { - // redeemer: { - // applied: appliedToRedeemer, - // cause: redeemerCause, - // meta: { - // id: fullRedeemer.id, - // name: fullRedeemer.name, - // email: fullRedeemer.email, - // created_at: fullRedeemer.created_at, - // }, - // }, - // referrer: { applied: appliedToReferrer, cause: referrerCause }, - // }; - // } - // // Branch 3: Paid product from Checkout - // else { - // return { - // redeemer: { applied: false, cause: ReferralResponseCodes.Unknown }, - // referrer: { applied: false, cause: ReferralResponseCodes.Unknown }, - // }; - // } + return { + redeemer: { + applied: addToRedeemer, + cause: addToRedeemer + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + meta: { + id: fullRedeemer.id, + name: fullRedeemer.name, + email: fullRedeemer.email, + created_at: fullRedeemer.created_at, + }, + }, + referrer: { + applied: addToReferrer, + cause: addToReferrer + ? ReferralResponseCodes.Success + : ReferralResponseCodes.OwnsProduct, + }, + }; }; diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index 70b5736ce..cfb93d981 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -10,7 +10,8 @@ import type { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { RewardProgramService } from "../rewards/RewardProgramService.js"; import { RewardRedemptionService } from "./RewardRedemptionService.js"; -import { triggerFreeProduct, triggerRedemption } from "./referralUtils.js"; +import { triggerFreeProduct } from "./referralUtils/triggerFreeProduct.js"; +import { triggerRedemption } from "./referralUtils.js"; import { getRewardCat } from "./rewardUtils.js"; export const runTriggerCheckoutReward = async ({ db, @@ -107,6 +108,7 @@ export const runTriggerCheckoutReward = async ({ const rewardCat = getRewardCat(reward); if (rewardCat === RewardCategory.FreeProduct) { await triggerFreeProduct({ + req: undefined, db, referralCode, redeemer: customer, @@ -132,4 +134,4 @@ export const runTriggerCheckoutReward = async ({ console.error("Failed to trigger checkout reward"); console.error(error); } -}; \ No newline at end of file +}; From 4fbf82b1feb9ee977297a65cec526c11663959ed Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:39:43 +0100 Subject: [PATCH 44/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20allow=20one=20off?= =?UTF-8?q?=20coupons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../stripeCouponUtils/stripeCouponUtils.ts | 317 ++++++++++-------- 1 file changed, 173 insertions(+), 144 deletions(-) diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 427669fd7..5097df7c4 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -1,174 +1,203 @@ -import RecaseError from "@/utils/errorUtils.js"; import { - Reward, + type AppEnv, CouponDurationType, ErrCode, - Organization, - Price, + type FixedPriceConfig, + type Organization, + type Price, PriceType, - Product, - UsagePriceConfig, + type Product, + type Reward, RewardType, - AppEnv, - FullProduct, - FixedPriceConfig, + type UsagePriceConfig, } from "@autumn/shared"; -import { Stripe } from "stripe"; +import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { createStripeCli } from "../utils.js"; -const couponToStripeDuration = (coupon: Reward) => { - if (coupon.type === RewardType.FreeProduct) { - return { - duration: "repeating", - duration_in_months: coupon.free_product_config?.duration_value, - }; - } +const couponToStripeDuration = ({ + coupon, + isOneOffProduct = false, +}: { + 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, + }; + } - let 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", + }; + } - 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, - }; - } + // 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, + }; + } }; 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, + ); - 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", + }; + } - let 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; }) => { - let 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 (error) {} + try { + await stripeCli.coupons.del(reward.id); + } catch (_) {} - let 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} already exists in Stripe`, - code: ErrCode.PromoCodeAlreadyExistsInStripe, - }); - } catch (error) {} - } + 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(reward) as any), - ...(couponToStripeValue({ reward, org, prices }) as any), - name: reward.name, - metadata: { - autumn_internal_id: reward.internal_id, - }, - applies_to: - reward.type === RewardType.FreeProduct || !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, + }); + } }; From 0aa4256c369d4db597a7317f8630f1be33cc88a6 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:52:31 +0100 Subject: [PATCH 45/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20rollover=20free=20p?= =?UTF-8?q?roduct=20coupons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handleCusDiscountDeleted.ts | 272 +++++----- .../webhookHandlers/handleInvoicePaid.ts | 479 +++++++++--------- .../handleInvoicePaidDiscount.ts | 280 +++++----- 3 files changed, 517 insertions(+), 514 deletions(-) diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts index 9e95c8ac7..9b292e000 100644 --- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts @@ -1,168 +1,170 @@ +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusService } from "@/internal/customers/CusService.js"; import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { createStripeCli } from "../utils.js"; -import Stripe from "stripe"; -import { notNullish, timeout } from "@/utils/genUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; +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; }) { - let 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`); - 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 - let 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; - let 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 - let 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 + // Mark reward redemption as applied / redeemer applied to true - const sub = await stripeCli.subscriptions.retrieve(discount.subscription); + 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; + } - let redemption = redemptions[0]; + const redemption = redemptions[0]; - // Apply redemption to customer - let stripeCli = createStripeCli({ - org, - env, - }); + // Apply redemption to customer + const stripeCli = createStripeCli({ + org, + env, + }); - let 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-ignore - 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/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts index 9abaaab75..6c727f283 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts @@ -1,14 +1,18 @@ -import Stripe from "stripe"; -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -import { +import type { AppEnv, FullCusProduct, FullCustomerPrice, InvoiceStatus, Organization, } from "@autumn/shared"; -import { createStripeCli } from "../utils.js"; - +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; +import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; +import { JobName } from "@/queue/JobName.js"; +import { addTaskToQueue } from "@/queue/queueUtils.js"; import { nullish } from "@/utils/genUtils.js"; import { getFullStripeInvoice, @@ -16,282 +20,277 @@ import { invoiceToSubId, updateInvoiceIfExists, } from "../stripeInvoiceUtils.js"; -import { getStripeSubs } from "../stripeSubUtils.js"; -import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { JobName } from "@/queue/JobName.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; -import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; -import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js"; -import { handleInvoiceCheckoutPaid } from "@/internal/customers/attach/attachFunctions/invoiceCheckoutPaid/handleInvoiceCheckoutPaid.js"; import { lineItemInCusProduct } from "../stripeSubUtils/stripeSubItemUtils.js"; +import { getStripeSubs } from "../stripeSubUtils.js"; +import { createStripeCli } from "../utils.js"; +import { handleInvoicePaidDiscount } from "./handleInvoicePaidDiscount.js"; const handleOneOffInvoicePaid = async ({ - db, - stripeInvoice, - logger, + db, + stripeInvoice, + logger, }: { - db: DrizzleCli; - stripeInvoice: Stripe.Invoice; - event: Stripe.Event; - logger: any; + db: DrizzleCli; + stripeInvoice: Stripe.Invoice; + event: Stripe.Event; + logger: any; }) => { - // Search for invoice - const invoice = await InvoiceService.getByStripeId({ - db, - stripeId: stripeInvoice.id!, - }); + // Search for invoice + const invoice = await InvoiceService.getByStripeId({ + db, + stripeId: stripeInvoice.id!, + }); - if (!invoice) { - console.log(`Invoice not found`); - return; - } + if (!invoice) { + console.log(`Invoice not found`); + return; + } - // Update invoice status - await InvoiceService.updateByStripeId({ - db, - stripeId: stripeInvoice.id!, - updates: { - status: stripeInvoice.status as InvoiceStatus, - hosted_invoice_url: stripeInvoice.hosted_invoice_url, - discounts: getInvoiceDiscounts({ - expandedInvoice: stripeInvoice, - }), - }, - }); + // Update invoice status + await InvoiceService.updateByStripeId({ + db, + stripeId: stripeInvoice.id!, + updates: { + status: stripeInvoice.status as InvoiceStatus, + hosted_invoice_url: stripeInvoice.hosted_invoice_url, + discounts: getInvoiceDiscounts({ + expandedInvoice: stripeInvoice, + }), + }, + }); - console.log(`Updated one off invoice status to ${stripeInvoice.status}`); + console.log(`Updated one off invoice status to ${stripeInvoice.status}`); }; const convertToChargeAutomatically = async ({ - org, - env, - invoice, - activeCusProducts, - logger, + org, + env, + invoice, + activeCusProducts, + logger, }: { - org: Organization; - env: AppEnv; - invoice: Stripe.Invoice; - activeCusProducts: FullCusProduct[]; - logger: any; + org: Organization; + env: AppEnv; + invoice: Stripe.Invoice; + activeCusProducts: FullCusProduct[]; + logger: any; }) => { - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env }); - const subs = await getStripeSubs({ - stripeCli, - subIds: activeCusProducts.flatMap((p) => p.subscription_ids || []), - }); + const subs = await getStripeSubs({ + stripeCli, + subIds: activeCusProducts.flatMap((p) => p.subscription_ids || []), + }); - const payments = invoice.payments; - const firstPayment = payments?.data?.[0]; - const paymentIntentId = firstPayment?.payment?.payment_intent as string; + const payments = invoice.payments; + const firstPayment = payments?.data?.[0]; + const paymentIntentId = firstPayment?.payment?.payment_intent as string; - if ( - subs.every((s) => s.collection_method === "charge_automatically") || - nullish(paymentIntentId) - ) { - return; - } + if ( + subs.every((s) => s.collection_method === "charge_automatically") || + nullish(paymentIntentId) + ) { + return; + } - // Get payment intent... + // Get payment intent... - // Try to attach payment method to subscription - try { - logger.info(`Converting to charge automatically`); - // 1. Get payment intent - const paymentIntent = - await stripeCli.paymentIntents.retrieve(paymentIntentId); + // Try to attach payment method to subscription + try { + logger.info(`Converting to charge automatically`); + // 1. Get payment intent + const paymentIntent = + await stripeCli.paymentIntents.retrieve(paymentIntentId); - // 2. Get payment method - const paymentMethod = await stripeCli.paymentMethods.retrieve( - paymentIntent.payment_method as string - ); + // 2. Get payment method + const paymentMethod = await stripeCli.paymentMethods.retrieve( + paymentIntent.payment_method as string, + ); - await stripeCli.paymentMethods.attach(paymentMethod.id, { - customer: invoice.customer as string, - }); + await stripeCli.paymentMethods.attach(paymentMethod.id, { + customer: invoice.customer as string, + }); - const batchUpdateSubs = []; - const updateSub = async (sub: Stripe.Subscription) => { - try { - await stripeCli.subscriptions.update(sub.id, { - collection_method: "charge_automatically", - default_payment_method: paymentMethod.id, - }); - } catch (error) { - logger.warn( - `Convert to charge automatically: error updating subscription ${sub.id}` - ); - logger.warn(error); - } - }; + const batchUpdateSubs = []; + const updateSub = async (sub: Stripe.Subscription) => { + try { + await stripeCli.subscriptions.update(sub.id, { + collection_method: "charge_automatically", + default_payment_method: paymentMethod.id, + }); + } catch (error) { + logger.warn( + `Convert to charge automatically: error updating subscription ${sub.id}`, + ); + logger.warn(error); + } + }; - for (const sub of subs) { - batchUpdateSubs.push(updateSub(sub)); - } + for (const sub of subs) { + batchUpdateSubs.push(updateSub(sub)); + } - await Promise.all(batchUpdateSubs); + await Promise.all(batchUpdateSubs); - logger.info("Convert to charge automatically successful!"); - } catch (error) { - logger.warn(`Convert to charge automatically failed: ${error}`); - } + logger.info("Convert to charge automatically successful!"); + } catch (error) { + logger.warn(`Convert to charge automatically failed: ${error}`); + } }; export const handleInvoicePaid = async ({ - db, - req, - org, - invoiceData, - env, - event, + db, + req, + org, + invoiceData, + env, + event, }: { - db: DrizzleCli; - req: any; - org: Organization; - invoiceData: Stripe.Invoice; - env: AppEnv; - event: Stripe.Event; + db: DrizzleCli; + req: any; + org: Organization; + invoiceData: Stripe.Invoice; + env: AppEnv; + event: Stripe.Event; }) => { - const logger = req.logtail; - const stripeCli = createStripeCli({ org, env }); - const invoice = await getFullStripeInvoice({ - stripeCli, - stripeId: invoiceData.id!, - expand: ["payments"], - }); + const logger = req.logtail; + const stripeCli = createStripeCli({ org, env }); + const invoice = await getFullStripeInvoice({ + stripeCli, + stripeId: invoiceData.id!, + expand: ["payments"], + }); - if (invoice.metadata?.autumn_metadata_id) { - await handleInvoiceCheckoutPaid({ - req, - org, - env, - db, - stripeCli, - invoice, - }); - } + if (invoice.metadata?.autumn_metadata_id) { + await handleInvoiceCheckoutPaid({ + req, + org, + env, + db, + stripeCli, + invoice, + }); + } - await handleInvoicePaidDiscount({ - db, - expandedInvoice: invoice, - org, - env, - logger, - }); + await handleInvoicePaidDiscount({ + db, + expandedInvoice: invoice, + org, + env, + logger, + }); - const subId = invoiceToSubId({ invoice }); - if (subId) { - // Get customer product - const activeCusProducts = await CusProductService.getByStripeSubId({ - db, - stripeSubId: subId, - orgId: org.id, - env, - }); + const subId = invoiceToSubId({ invoice }); + if (subId) { + // Get customer product + const activeCusProducts = await CusProductService.getByStripeSubId({ + db, + stripeSubId: subId, + orgId: org.id, + env, + }); - if (!activeCusProducts || activeCusProducts.length === 0) { - // TODO: Send alert - if (invoice.livemode) { - logger.warn( - `invoice.paid: customer product not found for invoice ${invoice.id}` - ); - } - return; - } + if (!activeCusProducts || activeCusProducts.length === 0) { + // TODO: Send alert + if (invoice.livemode) { + logger.warn( + `invoice.paid: customer product not found for invoice ${invoice.id}`, + ); + } + return; + } - if (org.config.convert_to_charge_automatically) { - await convertToChargeAutomatically({ - org, - env, - invoice, - activeCusProducts, - logger, - }); - } + if (org.config.convert_to_charge_automatically) { + await convertToChargeAutomatically({ + org, + env, + invoice, + activeCusProducts, + logger, + }); + } - const updated = await updateInvoiceIfExists({ - db, - invoice, - }); + const updated = await updateInvoiceIfExists({ + db, + invoice, + }); - if (!updated) { - let invoiceItems = await getInvoiceItems({ - stripeInvoice: invoice, - prices: activeCusProducts.flatMap((p) => - p.customer_prices.map((cpr: FullCustomerPrice) => cpr.price) - ), - logger, - }); + if (!updated) { + const invoiceItems = await getInvoiceItems({ + stripeInvoice: invoice, + prices: activeCusProducts.flatMap((p) => + p.customer_prices.map((cpr: FullCustomerPrice) => cpr.price), + ), + logger, + }); - const invoiceLines = invoice.lines.data; - let cusProducts: FullCusProduct[] = activeCusProducts; - try { - cusProducts = activeCusProducts.filter((cp) => - invoiceLines.some((l) => - lineItemInCusProduct({ cusProduct: cp, lineItem: l }) - ) - ); + const invoiceLines = invoice.lines.data; + let cusProducts: FullCusProduct[] = activeCusProducts; + try { + cusProducts = activeCusProducts.filter((cp) => + invoiceLines.some((l) => + lineItemInCusProduct({ cusProduct: cp, lineItem: l }), + ), + ); - console.log( - "Invoice paid, filtered cus products:", - cusProducts.map((cp) => `${cp.product.name} - ${cp.product.id}`) - ); + console.log( + "Invoice paid, filtered cus products:", + cusProducts.map((cp) => `${cp.product.name} - ${cp.product.id}`), + ); - if (cusProducts.length == 0) { - cusProducts = activeCusProducts; - } - } catch (error) { - logger.error("Failed to filter cus products for invoice"); - logger.error({ error }); - } + if (cusProducts.length == 0) { + cusProducts = activeCusProducts; + } + } catch (error) { + logger.error("Failed to filter cus products for invoice"); + logger.error({ error }); + } - const internalEntityId = new Set( - cusProducts.map((cp) => cp.internal_entity_id) - ); + const internalEntityId = new Set( + cusProducts.map((cp) => cp.internal_entity_id), + ); - await InvoiceService.createInvoiceFromStripe({ - db, - stripeInvoice: invoice, - internalCustomerId: activeCusProducts[0].internal_customer_id, - internalEntityId: - internalEntityId.size > 1 - ? undefined - : internalEntityId.values().next().value, + await InvoiceService.createInvoiceFromStripe({ + db, + stripeInvoice: invoice, + internalCustomerId: activeCusProducts[0].internal_customer_id, + internalEntityId: + internalEntityId.size > 1 + ? undefined + : internalEntityId.values().next().value, - productIds: [...new Set(cusProducts.map((p) => p.product_id))], - internalProductIds: [ - ...new Set(cusProducts.map((p) => p.internal_product_id)), - ], - org: org, - items: invoiceItems, - }); - } + productIds: [...new Set(cusProducts.map((p) => p.product_id))], + internalProductIds: [ + ...new Set(cusProducts.map((p) => p.internal_product_id)), + ], + org: org, + items: invoiceItems, + }); + } - for (const cusProd of activeCusProducts) { - try { - await addTaskToQueue({ - jobName: JobName.TriggerCheckoutReward, - payload: { - customer: cusProd.customer, - product: cusProd.product, - org, - env: cusProd.customer!.env, - subId: cusProd.subscription_ids?.[0], - }, - }); - } catch (error) { - logger.error(`invoice.paid: failed to trigger checkout reward check`); - logger.error(error); - } - } - } else { - await handleOneOffInvoicePaid({ - db, - stripeInvoice: invoice, - event, - logger, - }); - } + for (const cusProd of activeCusProducts) { + try { + await addTaskToQueue({ + jobName: JobName.TriggerCheckoutReward, + payload: { + customer: cusProd.customer, + product: cusProd.product, + org, + env: cusProd.customer!.env, + subId: cusProd.subscription_ids?.[0], + }, + }); + } catch (error) { + logger.error(`invoice.paid: failed to trigger checkout reward check`); + logger.error(error); + } + } + } else { + await handleOneOffInvoicePaid({ + db, + stripeInvoice: invoice, + event, + logger, + }); + } }; diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts index 9a37eb8e7..bf781198a 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts @@ -1,181 +1,183 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { RewardService } from "@/internal/rewards/RewardService.js"; import { + type AppEnv, CouponDurationType, - Organization, - Reward, + type Organization, + type Reward, RewardType, } from "@autumn/shared"; -import { AppEnv } from "@autumn/shared"; +import { addMonths } from "date-fns"; +import { Decimal } from "decimal.js"; -import Stripe from "stripe"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { generateId } from "@/utils/genUtils.js"; +import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; import { deleteCouponFromCus, deleteCouponFromSub, } from "../stripeCouponUtils/deleteCouponFromCus.js"; -import { createStripeCli } from "../utils.js"; -import { Decimal } from "decimal.js"; -import { generateId } from "@/utils/genUtils.js"; -import { addMonths } from "date-fns"; -import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; import { invoiceToSubId } from "../stripeInvoiceUtils.js"; +import { createStripeCli } from "../utils.js"; export const handleInvoicePaidDiscount = async ({ - db, - expandedInvoice, - org, - env, - logger, + db, + expandedInvoice, + org, + env, + logger, }: { - db: DrizzleCli; - expandedInvoice: Stripe.Invoice; - org: Organization; - env: AppEnv; - logger: any; + db: DrizzleCli; + expandedInvoice: Stripe.Invoice; + org: Organization; + env: AppEnv; + logger: any; }) => { - // Handle coupon - const stripeCli = createStripeCli({ org, env }); - if (expandedInvoice.discounts.length === 0) { - return; - } + // Handle coupon + const stripeCli = createStripeCli({ org, env }); + if (expandedInvoice.discounts.length === 0) { + return; + } - let stripeCus = await stripeCli.customers.retrieve( - expandedInvoice.customer as string - ); + const stripeCus = await stripeCli.customers.retrieve( + expandedInvoice.customer as string, + ); - try { - const totalDiscountAmounts = expandedInvoice.total_discount_amounts; + try { + const totalDiscountAmounts = expandedInvoice.total_discount_amounts; - // Log coupon information for debugging - for (const discount of expandedInvoice.discounts) { - if (typeof discount === "string") { - continue; - } + // Log coupon information for debugging + for (const discount of expandedInvoice.discounts) { + if (typeof discount === "string") { + continue; + } - const curCoupon = discount.coupon; + const curCoupon = discount.coupon; - if (!curCoupon) { - continue; - } + if (!curCoupon) { + continue; + } - const rollSuffixIndex = curCoupon.id.indexOf("_roll_"); - const couponId = - rollSuffixIndex !== -1 - ? curCoupon.id.substring(0, rollSuffixIndex) - : curCoupon.id; + const rollSuffixIndex = curCoupon.id.indexOf("_roll_"); + const couponId = + rollSuffixIndex !== -1 + ? curCoupon.id.substring(0, rollSuffixIndex) + : curCoupon.id; - const autumnReward: Reward | null = await RewardService.get({ - db, - idOrInternalId: couponId, - orgId: org.id, - env, - }); + const autumnReward: Reward | null = await RewardService.get({ + db, + idOrInternalId: couponId, + orgId: org.id, + env, + }); - let shouldRollover = - autumnReward && autumnReward.type == RewardType.InvoiceCredits; + const shouldRollover = + autumnReward && + (autumnReward.type === RewardType.InvoiceCredits || + autumnReward.type === RewardType.FreeProduct); - if (!shouldRollover) { - continue; - } + if (!shouldRollover) { + continue; + } - // Get ID of coupon - const originalCoupon = await stripeCli.coupons.retrieve(couponId, { - expand: ["applies_to"], - }); + // Get ID of coupon + const originalCoupon = await stripeCli.coupons.retrieve(couponId, { + expand: ["applies_to"], + }); - // 1. New amount: - const curAmount = discount.coupon.amount_off; + // 1. New amount: + const curAmount = discount.coupon.amount_off; - const amountUsed = totalDiscountAmounts?.find( - (item) => item.discount === discount.id - )?.amount; + const amountUsed = totalDiscountAmounts?.find( + (item) => item.discount === discount.id, + )?.amount; - const newAmount = new Decimal(curAmount!).sub(amountUsed!).toNumber(); + const newAmount = new Decimal(curAmount!).sub(amountUsed!).toNumber(); - let curExpiresAt = curCoupon.metadata?.expires_at - ? Number(curCoupon.metadata.expires_at) - : null; + const curExpiresAt = curCoupon.metadata?.expires_at + ? Number(curCoupon.metadata.expires_at) + : null; - let discountFinished = newAmount <= 0; + const discountFinished = newAmount <= 0; - let now = await getStripeNow({ - stripeCli, - stripeCus: stripeCus as Stripe.Customer, - }); + const now = await getStripeNow({ + stripeCli, + stripeCus: stripeCus as Stripe.Customer, + }); - let expired = curExpiresAt && curExpiresAt < now; - const subId = invoiceToSubId({ invoice: expandedInvoice }); + const expired = curExpiresAt && curExpiresAt < now; + const subId = invoiceToSubId({ invoice: expandedInvoice }); - if (discountFinished || expired) { - logger.info( - `Coupon ${couponId}, stripeCus: ${stripeCus.id}: credits used up or expired. discountFinished: ${discountFinished}, expired: ${expired}` - ); + if (discountFinished || expired) { + logger.info( + `Coupon ${couponId}, stripeCus: ${stripeCus.id}: credits used up or expired. discountFinished: ${discountFinished}, expired: ${expired}`, + ); - if (subId) { - await deleteCouponFromCus({ - stripeCli, - stripeSubId: subId, - stripeCusId: expandedInvoice.customer as string, - discountId: discount.id, - logger, - }); - } + if (subId) { + await deleteCouponFromCus({ + stripeCli, + stripeSubId: subId, + stripeCusId: expandedInvoice.customer as string, + discountId: discount.id, + logger, + }); + } - continue; - } + continue; + } - logger.info( - `Coupon ${couponId}, stripeCus: ${stripeCus.id}, updating amount from ${curAmount} to ${newAmount}` - ); + logger.info( + `Coupon ${couponId}, stripeCus: ${stripeCus.id}, updating amount from ${curAmount} to ${newAmount}`, + ); - // Set expiry date - let expiresAt = curCoupon.metadata?.expires_at || null; - let discountConfig = autumnReward?.discount_config; - if (discountConfig?.duration_type == CouponDurationType.Months) { - expiresAt = addMonths(new Date(), discountConfig.duration_value) - .getTime() - .toString(); - } + // Set expiry date + let expiresAt = curCoupon.metadata?.expires_at || null; + const discountConfig = autumnReward?.discount_config; + if (discountConfig?.duration_type === CouponDurationType.Months) { + expiresAt = addMonths(new Date(), discountConfig.duration_value) + .getTime() + .toString(); + } - const newCoupon = await stripeCli.coupons.create({ - id: `${couponId}_${generateId("roll")}`, - name: discount.coupon.name as string, - amount_off: newAmount, - currency: expandedInvoice.currency, - duration: "once", - applies_to: originalCoupon.applies_to, - metadata: { - expires_at: expiresAt, - }, - }); + const newCoupon = await stripeCli.coupons.create({ + id: `${couponId}_${generateId("roll")}`, + name: discount.coupon.name as string, + amount_off: newAmount, + currency: expandedInvoice.currency, + duration: "once", + applies_to: originalCoupon.applies_to, + metadata: { + expires_at: expiresAt, + }, + }); - const legacyStripeCli = createStripeCli({ - org, - env, - legacyVersion: true, - }); + const legacyStripeCli = createStripeCli({ + org, + env, + legacyVersion: true, + }); - await legacyStripeCli.rawRequest( - "POST", - `/v1/customers/${expandedInvoice.customer}`, - { - coupon: newCoupon.id, - } - ); + await legacyStripeCli.rawRequest( + "POST", + `/v1/customers/${expandedInvoice.customer}`, + { + coupon: newCoupon.id, + }, + ); - await stripeCli.coupons.del(newCoupon.id); + await stripeCli.coupons.del(newCoupon.id); - if (subId) { - await deleteCouponFromSub({ - stripeCli, - stripeSubId: subId, - discountId: discount.id, - logger, - }); - } - } - } catch (error) { - logger.error("invoice.paid: error updating coupon"); - logger.error(error); - } + if (subId) { + await deleteCouponFromSub({ + stripeCli, + stripeSubId: subId, + discountId: discount.id, + logger, + }); + } + } + } catch (error) { + logger.error("invoice.paid: error updating coupon"); + logger.error(error); + } }; From ad9d3822d7dad456052641b98e11e09ac8af9a1b Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:12:18 +0100 Subject: [PATCH 46/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20ui=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../reward-config/FreeDurationSelect.tsx | 15 +- .../rewards/reward-config/RewardConfig.tsx | 365 ++++++++++-------- 2 files changed, 217 insertions(+), 163 deletions(-) diff --git a/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx b/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx index 174518978..1d4831a13 100644 --- a/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx +++ b/vite/src/views/products/rewards/reward-config/FreeDurationSelect.tsx @@ -21,17 +21,16 @@ export const FreeDurationSelect = ({
Duration
- {reward.free_product_config?.duration_type === - CouponDurationType.Months && ( + { { - setReward({ + setReward({ ...reward, - // @ts-expect-error + // @ts-expect-error free_product_config: { - ...(reward.free_product_config ?? {}), + ...(reward.free_product_config ?? {}), duration_value: Number(e.target.value), }, }); @@ -40,13 +39,13 @@ export const FreeDurationSelect = ({ min={1} max={12} /> - )} + } setReward({ ...reward, name: e.target.value })} - /> -
-
- - ID - - { - setReward({ ...reward, id: e.target.value }); - setIdChanged(true); - }} - /> -
-
-
-
- Promotional Code - 0 ? reward.promo_codes[0].code : "" - } - onChange={(e) => - setReward({ - ...reward, - promo_codes: [{ code: e.target.value }], - }) - } - /> -
-
- Type - -
-
- {reward.type === RewardType.FreeProduct ? ( -
-
- - Product - -
- setReward({ ...reward, name: e.target.value })} + /> +
+
+ + ID + + { + setReward({ ...reward, id: e.target.value }); + setIdChanged(true); + }} + /> +
+
+
+
+ Promotional Code + 0 ? reward.promo_codes[0].code : "" + } + onChange={(e) => + setReward({ + ...reward, + promo_codes: [{ code: e.target.value }], + }) + } + /> +
+
+ Type + +
+
+ {reward.type === RewardType.FreeProduct ? ( +
+
+ + Product + +
+ -
- ) : notNullish(reward.type) ? ( - - ) : null} + const empty = filteredProducts.length === 0; + return ( + <> + + + + + {filteredProducts.map((product: ProductV2) => ( + + {product.name} + + ))} + + + ); + })()} + - {reward.type === RewardType.FreeProduct && - notNullish(reward.free_product_id) && - reward.free_product_id && - !isOneOffProduct( - products.find( - (product: ProductV2) => product.id === reward.free_product_id - )?.items || [] - ) ? ( - - ) : null} - - ); + {(() => { + const selectedProduct = products.find( + (p: ProductV2) => p.id === reward.free_product_id, + ); + + if (!selectedProduct) return null; + + const isPaidSelected = !isFreeProduct(selectedProduct.items); + if (!isPaidSelected) return null; + + const isRecurringSelected = !isOneOffProduct(selectedProduct.items); + const hasUsagePrices = selectedProduct.items.some( + (x) => + isFeaturePriceItem(x) && x.usage_model === UsageModel.PayPerUse, + ); + + const priceItem = selectedProduct.items.find((x) => isPriceItem(x)); + const currency = org?.default_currency || "USD"; + const fixedAmountStr = priceItem?.price + ? formatCurrency({ amount: priceItem.price, currency }) + : undefined; + + if (isRecurringSelected) { + return ( +
+ + Users will receive a coupon equal to this product's fixed + price amount.{" "} + {fixedAmountStr + ? `If they're on a different tier, they will receive ${fixedAmountStr} off.` + : "If they're on a different tier, they will receive the fixed amount off."}{" "} + {hasUsagePrices + ? "Charges due to usage prices will not be included in the coupon." + : ""} + +
+ ); + } + + return ( +
+ + Users will simply receive this product for free. + +
+ ); + })()} + + ) : notNullish(reward.type) ? ( + + ) : null} + + {reward.type === RewardType.FreeProduct && + notNullish(reward.free_product_id) && + reward.free_product_id && + !isOneOffProduct( + products.find( + (product: ProductV2) => product.id === reward.free_product_id, + )?.items || [], + ) ? ( + + ) : null} + + ); }; From 5225ca37b83d6e9c62ae1bbbaca16f54d81ff78c Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:12:37 +0100 Subject: [PATCH 47/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20working=20test=20c?= =?UTF-8?q?ases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals13.ts | 273 +++++++++++++++++ .../advanced/referrals/paid/referrals14.ts | 276 ++++++++++++++++++ 2 files changed, 549 insertions(+) create mode 100644 server/tests/advanced/referrals/paid/referrals13.ts create mode 100644 server/tests/advanced/referrals/paid/referrals14.ts diff --git a/server/tests/advanced/referrals/paid/referrals13.ts b/server/tests/advanced/referrals/paid/referrals13.ts new file mode 100644 index 000000000..a6b35e3d5 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals13.ts @@ -0,0 +1,273 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +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", +)}`, () => { + 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; + + 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 {} + + // 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); + + // 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, + }); + + 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 create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + 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, + }); + + // 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); + + 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); + + // 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, + }); + + // 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, + }), + ), + ); + + // Test that main customer's Pro invoice has discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices, CusExpand.Rewards], + }, + ); + + console.log( + "mainCustomerWithInvoices", + mainCustomerWithInvoices.invoices + .map( + (x) => + `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, + ) + .join("\n"), + ); + const proInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id), + ); + console.log("proInvoice", proInvoice); + + const expectedTotal = products.pro.prices[0].config.amount; + console.log("expectedTotal", expectedTotal); + const actualTotal = proInvoice?.total; + console.log("actualTotal", actualTotal); + + 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", + ); + } + + 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 }, + ], + ]; + + 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` : ""}`, + ); + }); + }); + }); +}); diff --git a/server/tests/advanced/referrals/paid/referrals14.ts b/server/tests/advanced/referrals/paid/referrals14.ts new file mode 100644 index 000000000..a0f1e1bc6 --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals14.ts @@ -0,0 +1,276 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +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", +)}`, () => { + 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; + + 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 {} + + // 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); + + // 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, + }); + + 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, + }), + ), + ); + }); + + it("should create code once", async () => { + referralCode = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateReferrer.id, + }); + + 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, + }); + + // 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); + + 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); + + // 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, + }); + + // 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, + }), + ), + ); + + // Test that main customer's Premium invoice has pro_amount discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices], + }, + ); + + console.log( + "Invoices:\n", + mainCustomerWithInvoices.invoices + .map( + (x) => + `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, + ) + .join("\n"), + ); + + 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 + console.log("Expected total:", expectedTotal); + console.log("Premium invoice total:", 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", + ); + } + + 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 }, + ], + ]; + + 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` : ""}`, + ); + }); + }); + }); +}); From 4de94b7dd3c657fa3c225bc2eb21f3a9f4d9568e Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:15:42 +0100 Subject: [PATCH 48/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20remove=20redundant?= =?UTF-8?q?=20add=20on=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/products/rewards/reward-config/RewardConfig.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx index fa422bc43..092dc6cff 100644 --- a/vite/src/views/products/rewards/reward-config/RewardConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/RewardConfig.tsx @@ -214,13 +214,7 @@ export const RewardConfig = ({ ); } - return ( -
- - Users will simply receive this product for free. - -
- ); + return; })()} ) : notNullish(reward.type) ? ( From bfa1f62958dfa8017e4c69a15803af2b4daea252 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Thu, 18 Sep 2025 13:45:25 -0700 Subject: [PATCH 49/60] fix: removed mobile blocker --- vite/src/app/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vite/src/app/layout.tsx b/vite/src/app/layout.tsx index c25af9d8e..55c7e460f 100644 --- a/vite/src/app/layout.tsx +++ b/vite/src/app/layout.tsx @@ -162,10 +162,10 @@ const MainContent = () => { "w-full h-full overflow-auto flex justify-center bg-stone-50" )} > -
+
-
+ {/*

Autumn is coming to mobile soon @@ -175,7 +175,7 @@ const MainContent = () => { your desktop?

-
+
*/}
From 64706449e8eac4959f8d45eb19621a24a3ab5664 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:48:26 +0100 Subject: [PATCH 50/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20delete=20redundant?= =?UTF-8?q?=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals10.ts | 251 ----------------- .../advanced/referrals/paid/referrals11.ts | 263 ------------------ .../advanced/referrals/paid/referrals12.ts | 218 --------------- .../advanced/referrals/paid/referrals5.ts | 236 ---------------- .../advanced/referrals/paid/referrals6.ts | 205 -------------- .../advanced/referrals/paid/referrals7.ts | 233 ---------------- .../advanced/referrals/paid/referrals8.ts | 239 ---------------- .../advanced/referrals/paid/referrals9.ts | 247 ---------------- 8 files changed, 1892 deletions(-) delete mode 100644 server/tests/advanced/referrals/paid/referrals10.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals11.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals12.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals5.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals6.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals7.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals8.ts delete mode 100644 server/tests/advanced/referrals/paid/referrals9.ts diff --git a/server/tests/advanced/referrals/paid/referrals10.ts b/server/tests/advanced/referrals/paid/referrals10.ts deleted file mode 100644 index 91c39a132..000000000 --- a/server/tests/advanced/referrals/paid/referrals10.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import type { Customer } from "autumn-js"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { - advanceTestClock, - completeCheckoutForm, -} from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals10"; - -describe(`${chalk.yellowBright( - "referrals10: Testing referrals (checkout, paid, add-on, referrer only)", -)}`, () => { - const mainCustomerId = "main-referral-10"; - const redeemer = "referral10-r1"; - 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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - }); - - testClockIds.push(res.testClockId); - - const redeemerRes = await initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - withTestClock: true, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnCheckoutReferrer.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnCheckoutReferrer.id, - }); - - 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, - }); - - // 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 succesfully checkout and trigger the checkout reward", async () => { - const res = await autumn.attach({ - customer_id: redeemer, - product_id: products.premium.id, - }); - - await completeCheckoutForm(res.checkout_url); - await setTimeout(1000 * 5); - }); - - it("should have given the paid add-on to referrer only, not redeemer", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal( - redemptionResult.applied, - true, - "Redemption should be applied", - ); - - await setTimeout(1000 * 10); - - const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ - autumn.customers.get(mainCustomerId), - autumn.customers.get(redeemer), - ])) as (Customer & { add_ons: any[] })[]; - - // Main customer (referrer) should have the proAddOn product - const mainProds = mainCustomerData.products; - const mainAddons = mainCustomerData.add_ons; - assert.equal(mainProds.length + mainAddons.length, 2); - - const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); - assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); - - // Redeemer should only have Premium product (no proAddOn) - const redeemerProds = redeemerCustomerData.products; - const redeemerAddons = redeemerCustomerData.add_ons; - assert.equal(redeemerProds.length + redeemerAddons.length, 1); - - const redeemerHasProAddOn = redeemerAddons.some( - (p) => p.id === products.proAddOn.id, - ); - const redeemerHasPremium = redeemerProds.some( - (p) => p.id === products.premium.id, - ); - - assert.isFalse( - redeemerHasProAddOn, - "Redeemer should not have proAddOn product", - ); - assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); - - // Verify products are properly attached - expectAddOnAttached({ - customer: (await autumn.customers.get(mainCustomerId)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - - // Verify redeemer does not have the add-on - const redeemerCustomer = (await autumn.customers.get( - redeemer, - )) as Customer & { add_ons: any[] }; - const redeemerAddOn = redeemerCustomer.add_ons.find( - (a) => a.id === products.proAddOn.id, - ); - assert.isUndefined( - redeemerAddOn, - "Redeemer should not have proAddOn product", - ); - }); - - it("should advance test clock and have proAddOn attached for referrer only", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the proAddOn product - { name: "Free", status: CusProductStatus.Active }, - { name: "proAddOn", status: CusProductStatus.Active }, - ], - [ - // Redeemer - only has Premium product (no reward) - { name: "Premium", 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); diff --git a/server/tests/advanced/referrals/paid/referrals11.ts b/server/tests/advanced/referrals/paid/referrals11.ts deleted file mode 100644 index 476f01656..000000000 --- a/server/tests/advanced/referrals/paid/referrals11.ts +++ /dev/null @@ -1,263 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { - advanceTestClock, - completeCheckoutForm, -} from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals11"; - -describe(`${chalk.yellowBright( - "referrals11: Testing referrals (checkout, paid, recurring, both)", -)}`, () => { - const mainCustomerId = "main-referral-11"; - const redeemer = "referral11-r1"; - 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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - }); - - testClockIds.push(res.testClockId); - - const redeemerRes = await initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - withTestClock: true, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductCheckoutAll.id, - }); - - assert.exists(referralCode.code, "Referral code should be generated"); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductCheckoutAll.id, - }); - - assert.equal( - referralCode2.code, - referralCode.code, - "Should return same referral code when called multiple times", - ); - }); - - 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, - "Should throw AutumnError when trying to redeem same code twice", - ); - assert.equal( - error.code, - ErrCode.CustomerAlreadyRedeemedReferralCode, - "Should return correct error code for duplicate redemption", - ); - } - }); - - it("should succesfully checkout and trigger the checkout reward", async () => { - const res = await autumn.attach({ - customer_id: redeemer, - product_id: products.premium.id, - }); - - await completeCheckoutForm(res.checkout_url); - await setTimeout(1000 * 5); - }); - - it("should have given the paid product to referrer only, not redeemer (on paid plan)", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal( - redemptionResult.applied, - true, - `Redemption ${redemption.id} should be applied`, - ); - - await setTimeout(1000 * 10); - - const [mainCustomerData, redeemerCustomerData] = await Promise.all([ - autumn.customers.get(mainCustomerId), - autumn.customers.get(redeemer), - ]); - - // Main customer (referrer) should have the pro product - const mainProds = mainCustomerData.products; - - assert.equal( - mainProds.length, - 1, - "Main customer should have exactly 1 product (Pro)", - ); - - const hasProProduct = mainProds.some((p) => p.id === products.pro.id); - assert.isTrue(hasProProduct, "Main customer should have pro product"); - - // Redeemer should only have Premium product (no pro product because they're on paid plan) - const redeemerProds = redeemerCustomerData.products; - - assert.equal( - redeemerProds.length, - 1, - "Redeemer should have exactly 1 product (Premium only)", - ); - - const redeemerHasProProduct = redeemerProds.some( - (p) => p.id === products.pro.id, - ); - const redeemerHasPremium = redeemerProds.some( - (p) => p.id === products.premium.id, - ); - - assert.isFalse( - redeemerHasProProduct, - "Redeemer should not have pro product (already on paid plan)", - ); - assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); - - // Verify products are properly attached - expectProductV1Attached({ - customer: mainCustomerData, - product: products.pro, - status: CusProductStatus.Trialing, - }); - - // Verify redeemer only has premium product - expectProductV1Attached({ - customer: redeemerCustomerData, - product: products.premium, - status: CusProductStatus.Active, - }); - }); - - it("should advance test clock and have pro attached for referrer only", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the pro product - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Redeemer - only has Premium product (no Pro reward due to being on paid plan) - { name: "Premium", 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); diff --git a/server/tests/advanced/referrals/paid/referrals12.ts b/server/tests/advanced/referrals/paid/referrals12.ts deleted file mode 100644 index 7b7928575..000000000 --- a/server/tests/advanced/referrals/paid/referrals12.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { - advanceTestClock, - completeCheckoutForm, -} from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals12"; - -describe(`${chalk.yellowBright( - "referrals12: Testing referrals (checkout, paid, recurring, referrer only)", -)}`, () => { - const mainCustomerId = "main-referral-12"; - const redeemer = "referral12-r1"; - 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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - }); - - testClockIds.push(res.testClockId); - - const redeemerRes = await initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - withTestClock: true, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductCheckoutReferrer.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductCheckoutReferrer.id, - }); - - 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, - }); - - // 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 successfully checkout and trigger the checkout reward", async () => { - const res = await autumn.attach({ - customer_id: redeemer, - product_id: products.premium.id, - }); - - await completeCheckoutForm(res.checkout_url); - await setTimeout(1000 * 5); - }); - - it("should have given the paid product to referrer only, not redeemer (referrer-only reward)", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true, `Redemption should be applied: ${JSON.stringify(redemptionResult, null, 4)}`); - - await setTimeout(1000 * 10); - - const mainProds = (await autumn.customers.get(mainCustomerId)).products; - const redeemerProds = (await autumn.customers.get(redeemer)).products; - - // Main customer (referrer) should have the pro product - assert.equal(mainProds.length, 1); - assert.equal(mainProds[0].id, products.pro.id); - - // Redeemer should only have the premium product (no pro product given) - assert.equal(redeemerProds.length, 1); - assert.equal(redeemerProds[0].id, products.premium.id); - - expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomerId), - product: products.pro, - status: CusProductStatus.Trialing, - }); - - // Verify redeemer only has premium product - expectProductV1Attached({ - customer: await autumn.customers.get(redeemer), - product: products.premium, - status: CusProductStatus.Active, - }); - }); - - it("should advance test clock and have pro attached for referrer only", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the pro product - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Redeemer - only has premium product (no reward) - { name: "Premium", 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); diff --git a/server/tests/advanced/referrals/paid/referrals5.ts b/server/tests/advanced/referrals/paid/referrals5.ts deleted file mode 100644 index a45e9ecfb..000000000 --- a/server/tests/advanced/referrals/paid/referrals5.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals5"; - -describe(`${chalk.yellowBright( - "referrals5: Testing referrals (immediate, paid, recurring, both)", -)}`, () => { - const mainCustomerId = "main-referral-5"; - const redeemers = ["referral5-r1", "referral5-r2", "referral5-r3"]; - const redeemerPMs = ["success", undefined, "fail"]; - const autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - const testClockIds: string[] = []; - let referralCode: ReferralCode; - - const redemptions: 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; - - try { - await Promise.all([ - ...([mainCustomerId, ...redeemers].map(customerId => - autumn.customers.delete(customerId) - )), - RewardRedemptionService._resetCustomerRedemptions({ - db, - internalCustomerId: [mainCustomerId, ...redeemers], - }), - ]); - } catch {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - attachPm: "success", - }); - - testClockIds.push(res.testClockId); - - const batchCreate = []; - for (const redeemer of redeemers) { - batchCreate.push( - initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - attachPm: redeemerPMs[redeemers.indexOf(redeemer)] as - | "success" - | "fail" - | undefined, - withTestClock: true, - }), - ); - } - - const batchRes = await Promise.all(batchCreate); - for (const r of batchRes) { - testClockIds.push(r.testClockId); - } - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductImmediateAll.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductImmediateAll.id, - }); - - assert.equal(referralCode2.code, referralCode.code); - }); - - it("should create redemption for each redeemer and fail if redeemed again", async () => { - for (const redeemer of redeemers) { - const redemption: RewardRedemption = await autumn.referrals.redeem({ - customerId: redeemer, - code: referralCode.code, - }); - - redemptions.push(redemption); - } - - // Try redeem for redeemer1 again - try { - 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 have given the paid product to all redeemers", async () => { - for (const redeemer of redeemers) { - const redemption = await autumn.redemptions.get( - redemptions[redeemers.indexOf(redeemer)].id, - ); - assert.equal(redemption.applied, true); - } - - const mainProds = (await autumn.customers.get(mainCustomerId)).products; - const redeemerProds = (await autumn.customers.get(redeemers[0])).products; - - assert.equal(mainProds.length, 1); - assert.equal(redeemerProds.length, 1); - assert.equal(mainProds[0].id, products.pro.id); - assert.equal(redeemerProds[0].id, products.pro.id); - - expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomerId), - product: products.pro, - status: CusProductStatus.Trialing, - }); - - redeemers.forEach(async (r) => { - expectProductV1Attached({ - customer: await autumn.customers.get(r), - product: products.pro, - status: CusProductStatus.Trialing, - }); - }); - }); - - it("should advance test clock and have pro attached", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, ...redeemers].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer. - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Referrer 1, valid payment method. - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Referrer 2, no valid payment method. - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Expired }, - { name: "Free", status: CusProductStatus.Active }, - ], - [ - // Referrer 3, no valid payment method. - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Expired }, - { 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); \ No newline at end of file diff --git a/server/tests/advanced/referrals/paid/referrals6.ts b/server/tests/advanced/referrals/paid/referrals6.ts deleted file mode 100644 index 7ee2ef7ff..000000000 --- a/server/tests/advanced/referrals/paid/referrals6.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals6"; - -describe(`${chalk.yellowBright( - "referrals6: Testing referrals (immediate, paid, recurring, referrer only)", -)}`, () => { - const mainCustomerId = "main-referral-6"; - const redeemer = "referral6-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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - attachPm: "success", - }); - - 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, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductImmediateReferrer.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductImmediateReferrer.id, - }); - - 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, - }); - - // 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 given the paid product to referrer only, not redeemer", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true); - - const mainProds = (await autumn.customers.get(mainCustomerId)).products; - const redeemerProds = (await autumn.customers.get(redeemer)).products; - - // Main customer (referrer) should have the pro product - assert.equal(mainProds.length, 1); - assert.equal(mainProds[0].id, products.pro.id); - - // Redeemer should only have the free product (no pro product given) - 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.Trialing, - }); - - // 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 have pro attached for referrer only", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the pro product - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Redeemer - only has free product (no reward) - { 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); \ No newline at end of file diff --git a/server/tests/advanced/referrals/paid/referrals7.ts b/server/tests/advanced/referrals/paid/referrals7.ts deleted file mode 100644 index 6d2c5d654..000000000 --- a/server/tests/advanced/referrals/paid/referrals7.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import type { Customer } from "autumn-js"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals7"; - -describe(`${chalk.yellowBright( - "referrals7: Testing referrals (immediate, paid, one-off add-on, both)", -)}`, () => { - const mainCustomerId = "main-referral-7"; - const redeemer = "referral7-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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - attachPm: "success", - }); - - 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, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnAll.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnAll.id, - }); - - 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, - }); - - // 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 given the paid add-on to both referrer and redeemer", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true); - - await setTimeout(1000 * 10); - - const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ - autumn.customers.get(mainCustomerId), - autumn.customers.get(redeemer), - ])) as (Customer & { add_ons: any[] })[]; - - // Main customer (referrer) should have the proAddOn product - const mainProds = mainCustomerData.products; - const mainAddons = mainCustomerData.add_ons; - - assert.equal(mainProds.length + mainAddons.length, 2); - - const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); - assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); - - // Redeemer should have both free and proAddOn products - const redeemerProds = redeemerCustomerData.products; - const redeemerAddons = redeemerCustomerData.add_ons; - - assert.equal(redeemerProds.length + redeemerAddons.length, 2); - - const redeemerHasProAddOn = redeemerAddons.some( - (p) => p.id === products.proAddOn.id, - ); - const redeemerHasFree = redeemerProds.some( - (p) => p.id === products.free.id, - ); - - assert.isTrue(redeemerHasProAddOn, "Redeemer should have proAddOn product"); - assert.isTrue(redeemerHasFree, "Redeemer should have free product"); - - // Verify products are properly attached - expectAddOnAttached({ - customer: (await autumn.customers.get(mainCustomerId)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - - expectAddOnAttached({ - customer: (await autumn.customers.get(redeemer)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - }); - - it("should advance test clock and have proAddOn attached for both", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the proAddOn product - { name: "Free", status: CusProductStatus.Active }, - { name: "proAddOn", status: CusProductStatus.Active }, - ], - [ - // Redeemer - gets both free and proAddOn products - { name: "Free", status: CusProductStatus.Active }, - { name: "proAddOn", 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); diff --git a/server/tests/advanced/referrals/paid/referrals8.ts b/server/tests/advanced/referrals/paid/referrals8.ts deleted file mode 100644 index bb84a0b14..000000000 --- a/server/tests/advanced/referrals/paid/referrals8.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import type { Customer } from "autumn-js"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals8"; - -describe(`${chalk.yellowBright( - "referrals8: Testing referrals (immediate, paid, one-off add-on, referrer only)", -)}`, () => { - const mainCustomerId = "main-referral-8"; - const redeemer = "referral8-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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - attachPm: "success", - }); - - 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, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnReferrer.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnReferrer.id, - }); - - 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, - }); - - // 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 given the paid add-on to referrer only, not redeemer", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true); - - await setTimeout(1000 * 10); - - const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ - autumn.customers.get(mainCustomerId), - autumn.customers.get(redeemer), - ])) as (Customer & { add_ons: any[] })[]; - - // Main customer (referrer) should have the proAddOn product - const mainProds = mainCustomerData.products; - const mainAddons = mainCustomerData.add_ons; - - assert.equal(mainProds.length + mainAddons.length, 2); - - const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); - assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); - - // Redeemer should only have free product (no proAddOn) - const redeemerProds = redeemerCustomerData.products; - const redeemerAddons = redeemerCustomerData.add_ons; - - assert.equal(redeemerProds.length + redeemerAddons.length, 1); - - const redeemerHasProAddOn = redeemerAddons.some( - (p) => p.id === products.proAddOn.id, - ); - const redeemerHasFree = redeemerProds.some( - (p) => p.id === products.free.id, - ); - - assert.isFalse( - redeemerHasProAddOn, - "Redeemer should not have proAddOn product", - ); - assert.isTrue(redeemerHasFree, "Redeemer should have free product"); - - // Verify products are properly attached - expectAddOnAttached({ - customer: (await autumn.customers.get(mainCustomerId)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - - // Verify redeemer does not have the add-on - const redeemerCustomer = (await autumn.customers.get( - redeemer, - )) as Customer & { add_ons: any[] }; - const redeemerAddOn = redeemerCustomer.add_ons.find( - (a) => a.id === products.proAddOn.id, - ); - assert.isUndefined( - redeemerAddOn, - "Redeemer should not have proAddOn product", - ); - }); - - it("should advance test clock and have proAddOn attached for referrer only", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the proAddOn product - { name: "Free", status: CusProductStatus.Active }, - { name: "proAddOn", status: CusProductStatus.Active }, - ], - [ - // Redeemer - only has free product (no reward) - { 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); diff --git a/server/tests/advanced/referrals/paid/referrals9.ts b/server/tests/advanced/referrals/paid/referrals9.ts deleted file mode 100644 index 72fd00ee9..000000000 --- a/server/tests/advanced/referrals/paid/referrals9.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { setTimeout } from "node:timers/promises"; -import { - type AppEnv, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import type { Customer } from "autumn-js"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectAddOnAttached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { - advanceTestClock, - completeCheckoutForm, -} from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms } from "../../../global.js"; - -export const group = "referrals9"; - -describe(`${chalk.yellowBright( - "referrals9: Testing referrals (checkout, paid, add-on, both)", -)}`, () => { - const mainCustomerId = "main-referral-9"; - const redeemer = "referral9-r1"; - 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; - - 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 {} - - const res = await initCustomer({ - autumn: this.autumnJs, - customerId: mainCustomerId, - db, - org, - env, - }); - - testClockIds.push(res.testClockId); - - const redeemerRes = await initCustomer({ - autumn: this.autumnJs, - customerId: redeemer, - db: this.db, - org: this.org, - env: this.env, - withTestClock: true, - }); - - testClockIds.push(redeemerRes.testClockId); - }); - - it("should create code once", async () => { - referralCode = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnCheckoutAll.id, - }); - - assert.exists(referralCode.code); - - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidAddOnCheckoutAll.id, - }); - - 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, - }); - - // 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 succesfully checkout and trigger the checkout reward", async () => { - const res = await autumn.attach({ - customer_id: redeemer, - product_id: products.premium.id, - }); - - await completeCheckoutForm(res.checkout_url); - await setTimeout(1000 * 5); - }); - - it("should have given the paid add-on to both referrer and redeemer", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.applied, true, `Redemption ${redemption.id} should be applied`); - - await setTimeout(1000 * 10); - - const [mainCustomerData, redeemerCustomerData] = (await Promise.all([ - autumn.customers.get(mainCustomerId), - autumn.customers.get(redeemer), - ])) as (Customer & { add_ons: any[] })[]; - - // Main customer (referrer) should have the proAddOn product - const mainProds = mainCustomerData.products; - const mainAddons = mainCustomerData.add_ons; - - assert.equal(mainProds.length + mainAddons.length, 2, `Main customer should have 2 products: ${JSON.stringify(mainProds, null, 4)} ${JSON.stringify(mainAddons, null, 4)}`); - - const hasProAddOn = mainAddons.some((p) => p.id === products.proAddOn.id); - assert.isTrue(hasProAddOn, "Main customer should have proAddOn product"); - - // Redeemer should have both free and proAddOn products (both get reward) - const redeemerProds = redeemerCustomerData.products; - const redeemerAddons = redeemerCustomerData.add_ons; - - assert.equal(redeemerProds.length + redeemerAddons.length, 2, `Redeemer should have 2 products: ${JSON.stringify(redeemerProds, null, 4)} ${JSON.stringify(redeemerAddons, null, 4)}`); - - const redeemerHasProAddOn = redeemerAddons.some( - (p) => p.id === products.proAddOn.id, - ); - const redeemerHasPremium = redeemerProds.some( - (p) => p.id === products.premium.id, - ); - - assert.isTrue( - redeemerHasProAddOn, - "Redeemer should have proAddOn product", - ); - assert.isTrue(redeemerHasPremium, "Redeemer should have premium product"); - - // Verify products are properly attached - expectAddOnAttached({ - customer: (await autumn.customers.get(mainCustomerId)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - - // Verify redeemer also has the add-on - expectAddOnAttached({ - customer: (await autumn.customers.get(redeemer)) as Customer & { - add_ons: any[]; - }, - productId: products.proAddOn.id, - status: CusProductStatus.Active, - }); - }); - - it("should advance test clock and have proAddOn attached for both", async () => { - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 35, - waitForSeconds: 15, - stripeCli, - }), - ), - ); - - const dbCustomers = await Promise.all( - [mainCustomerId, redeemer].map((x) => - CusService.getFull({ - db, - idOrInternalId: x, - orgId: org.id, - env, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Trialing, - CusProductStatus.Expired, - ], - }), - ), - ); - - const expectedProducts = [ - [ - // Main referrer - gets the proAddOn product - { name: "Free", status: CusProductStatus.Active }, - { name: "proAddOn", status: CusProductStatus.Active }, - ], - [ - // Redeemer - gets both free and proAddOn products (both get reward) - { name: "Premium", status: CusProductStatus.Active }, - { name: "proAddOn", 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, - ); - - assert.isNotNull( - matchingProduct, - `Customer ${customer.name} should have ${expectedProduct.name} product with status ${expectedProduct.status}`, - ); - }); - }); - }); -}); From 823257c4963a9cafbdcdeb0d0f5d5451b4dcefde Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 19 Sep 2025 11:55:57 +0100 Subject: [PATCH 51/60] =?UTF-8?q?test:=20=F0=9F=92=8D=20final=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advanced/referrals/paid/referrals15.ts | 296 ++++++++++++++ .../advanced/referrals/paid/referrals16.ts | 386 ++++++++++++++++++ 2 files changed, 682 insertions(+) create mode 100644 server/tests/advanced/referrals/paid/referrals15.ts create mode 100644 server/tests/advanced/referrals/paid/referrals16.ts diff --git a/server/tests/advanced/referrals/paid/referrals15.ts b/server/tests/advanced/referrals/paid/referrals15.ts new file mode 100644 index 000000000..bd8b5843d --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals15.ts @@ -0,0 +1,296 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +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", +)}`, () => { + 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; + + 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 {} + + // 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); + + 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); + }); + + 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, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductImmediateAll.id, + }); + + 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, + }); + + // 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); + + 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); + + // 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: 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, + }), + ), + ); + + // 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( + "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 + + 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}`, + ); + } + + // 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); + + 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 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 + ); + + 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/referrals16.ts b/server/tests/advanced/referrals/paid/referrals16.ts new file mode 100644 index 000000000..25444b75a --- /dev/null +++ b/server/tests/advanced/referrals/paid/referrals16.ts @@ -0,0 +1,386 @@ +import { + type AppEnv, + CusExpand, + CusProductStatus, + ErrCode, + type Organization, + type ReferralCode, + type RewardRedemption, +} from "@autumn/shared"; +import { assert } from "chai"; +import chalk from "chalk"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +import { + advanceTestClock, + completeCheckoutForm +} from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { products, referralPrograms, rewards } from "../../../global.js"; + +export const group = "referrals16"; + +describe(`${chalk.yellowBright( + "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based", +)}`, () => { + const mainCustomerId = "main-referral-16"; + const redeemer = "referral16-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; + + 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 {} + + // 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); + + 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); + }); + + 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.paidProductCheckoutAll.id, + }); + + assert.exists(referralCode.code); + + // Get referral code again + const referralCode2 = await autumn.referrals.createCode({ + customerId: mainCustomerId, + referralId: referralPrograms.paidProductCheckoutAll.id, + }); + + 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, + }); + + // 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 and redeemer still on free tier (reward not triggered yet)", async () => { + const redemptionResult = await autumn.redemptions.get(redemption.id); + assert.equal(redemptionResult.triggered, false); // Checkout trigger not fired yet + + const mainCus = await autumn.customers.get(mainCustomerId); + const redeemerCus = await autumn.customers.get(redeemer); + const mainProds = mainCus.products; + const redeemerProds = redeemerCus.products; + + // Both customers should still only have free product + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.free.id); + + assert.equal(redeemerProds.length, 1); + assert.equal(redeemerProds[0].id, products.free.id); + + expectProductV1Attached({ + customer: mainCus, + product: products.free, + status: CusProductStatus.Active, + }); + + expectProductV1Attached({ + customer: redeemerCus, + product: products.free, + status: CusProductStatus.Active, + }); + }); + + it("should trigger reward when redeemer checks out with Premium", async () => { + // Redeemer purchases Premium product (triggers checkout reward) + const checkoutRes = await autumn.attach({ + customer_id: redeemer, + product_id: products.premium.id, + force_checkout: true, + }); + + await completeCheckoutForm(checkoutRes.checkout_url); + + // Wait a bit for webhook processing + await new Promise((resolve) => setTimeout(resolve, 10000)); + + // Now both customers should have the reward applied + const redemptionResult = await autumn.redemptions.get(redemption.id); + console.log( + "Referrer Applied:", + redemptionResult.applied, + "Redeemer Applied:", + redemptionResult.redeemer_applied, + ); + assert.equal(redemptionResult.applied, true); + + 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); + + // Redeemer should have both Premium (purchased) and the Pro price discount + assert.equal(redeemerProds.length, 1); + const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve( + redeemerProds.find((x) => x.id === products.premium.id) + ?.subscription_ids?.[0]!, + { + expand: ["discounts"], + }, + ); + console.log( + "Redeemer Stripe Discounts:", + redeemerStripeDiscounts.discounts, + ); + + const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => { + if (typeof x === "string") { + return x; + } else if (typeof x === "object") { + return x.coupon.id; + } else return null; + })!; + + assert.equal( + redeemerStripeDiscounts.discounts.length, + 1, + `Redeemer Stripe Discounts: ${JSON.stringify(redeemerStripeDiscounts.discounts, null, 4)}`, + ); + assert.equal( + typeof parsedDiscountID === "object" + ? parsedDiscountID.coupon.id + : parsedDiscountID, + rewards.paidProductWithConfig.id, + `Parsed Discount ID: ${parsedDiscountID}`, + ); + + assert.exists( + redeemerProds.find((x) => x.id === products.premium.id), + `Redeemer must have Premium product`, + ); + + expectProductV1Attached({ + customer: mainCus, + product: products.pro, + status: CusProductStatus.Active, + }); + + expectProductV1Attached({ + customer: redeemerCus, + product: products.premium, + status: CusProductStatus.Active, + }); + }); + + it("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => { + // Advance 31 days from current time to trigger next billing cycle + await Promise.all( + testClockIds.map((x) => + advanceTestClock({ + testClockId: x, + numberOfDays: 31, + waitForSeconds: 25, + stripeCli, + }), + ), + ); + + // Test that both customers' 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( + "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) Pro 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); + + assert.equal( + mainProInvoice.total, + expectedTotal, + `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}`, + ); + } + + // Check redeemer Premium invoice (should have $10 off) + const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.premium.id), + ); + if (redeemerPremiumInvoice) { + // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40) + 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 + + console.log("Redeemer expected total:", expectedTotal); + console.log( + "Redeemer Premium invoice total:", + redeemerPremiumInvoice.total, + ); + + assert.equal( + redeemerPremiumInvoice.total, + expectedTotal, + `Redeemer Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${redeemerPremiumInvoice.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 expectedProducts = [ + [ + // Main referrer - has Pro with pro_amount discount applied + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + ], + [ + // Redeemer - has both Premium (purchased) and Pro (reward) with discounts + { name: "Free", status: CusProductStatus.Expired }, + { name: "Pro", status: CusProductStatus.Active }, + { name: "Premium", 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, + ); + + 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` : ""}`, + ); + }); + }); + }); +}); From e2528194fd9be00f4ee9be3600f4091d27504fef Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:53:06 +0100 Subject: [PATCH 52/60] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20toast=20to=20alert?= =?UTF-8?q?=20of=20any=20unmigrated=20prices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/index.ts | 220 ++++++++---------- .../priceConfig/usagePriceConfig.ts | 2 + .../utils/rewardUtils/rewardMigrationUtils.ts | 177 ++++++++++++++ .../rewards/reward-config/UpdateReward.tsx | 167 ++++++++----- 4 files changed, 385 insertions(+), 181 deletions(-) create mode 100644 shared/utils/rewardUtils/rewardMigrationUtils.ts diff --git a/shared/index.ts b/shared/index.ts index 8c0e6e508..fade638f1 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -4,153 +4,129 @@ export { schemas }; // Auth Models export * from "./db/auth-schema.js"; +export * from "./enums/APIVersion.js"; +export * from "./enums/AttachErrCode.js"; +export * from "./enums/ErrCode.js"; +export * from "./enums/LoggerAction.js"; +// ENUMS +export * from "./enums/SuccessCode.js"; +export * from "./enums/WebhookEventType.js"; +// ANALYTICS MODELS +export * from "./models/analyticsModels/actionEnums.js"; +export * from "./models/analyticsModels/actionTable.js"; +export * from "./models/attachModels/attachBody.js"; +export * from "./models/attachModels/attachEnums/AttachBranch.js"; +export * from "./models/attachModels/attachEnums/AttachConfig.js"; +export * from "./models/attachModels/attachEnums/AttachFunction.js"; +// Attach Models +export * from "./models/attachModels/attachPreviewModels.js"; +export * from "./models/attachModels/checkoutModels.js"; export * from "./models/authModels/membership.js"; - -// Gen Models -export * from "./models/genModels/genEnums.js"; - -// 1. Org Models -export * from "./models/orgModels/orgTable.js"; -export * from "./models/orgModels/orgConfig.js"; -export * from "./models/orgModels/frontendOrg.js"; - -// 2. Feature Models -export * from "./models/featureModels/featureTable.js"; +export * from "./models/chatResultModels/chatResultFeature.js"; +export * from "./models/chatResultModels/chatResultFeature.js"; +// 4. Chat Result Models +export * from "./models/chatResultModels/chatResultTable.js"; +export * from "./models/checkModels/checkPreviewModels.js"; +export * from "./models/cusModels/cusExpand.js"; +// 8. Customer Models +export * from "./models/cusModels/cusModels.js"; +export * from "./models/cusModels/cusResModels/cusFeatureResponse.js"; +export * from "./models/cusModels/cusResModels/cusProductResponse.js"; +export * from "./models/cusModels/cusResModels/cusReferralsResponse.js"; +// Cus response +export * from "./models/cusModels/cusResponseModels.js"; +export * from "./models/cusModels/cusTable.js"; +export * from "./models/cusModels/entityModels/entityExpand.js"; +export * from "./models/cusModels/entityModels/entityModels.js"; +export * from "./models/cusModels/entityModels/entityResModels.js"; +export * from "./models/cusModels/entityModels/entityTable.js"; +export * from "./models/cusModels/fullCusModel.js"; +export * from "./models/cusModels/invoiceModels/invoiceModels.js"; +export * from "./models/cusModels/invoiceModels/invoiceResponseModels.js"; +export * from "./models/cusModels/invoiceModels/invoiceTable.js"; +export * from "./models/cusProductModels/cusEntModels/cusEntModels.js"; +export * from "./models/cusProductModels/cusEntModels/cusEntTable.js"; +export * from "./models/cusProductModels/cusEntModels/cusEntWithProduct.js"; +export * from "./models/cusProductModels/cusEntModels/replaceableSchema.js"; +export * from "./models/cusProductModels/cusEntModels/replaceableTable.js"; +export * from "./models/cusProductModels/cusEntModels/resetCusEnt.js"; +export * from "./models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.js"; +export * from "./models/cusProductModels/cusPriceModels/cusPriceModels.js"; +export * from "./models/cusProductModels/cusPriceModels/cusPriceTable.js"; +export * from "./models/cusProductModels/cusProductEnums.js"; +// 7. Cus Product Models +export * from "./models/cusProductModels/cusProductModels.js"; +export * from "./models/cusProductModels/cusProductTable.js"; +export * from "./models/devModels/apiKeyModels.js"; +export * from "./models/devModels/apiKeyTable.js"; +// 5. Others: events, apiKeys +export * from "./models/eventModels/eventModels.js"; +export * from "./models/eventModels/eventTable.js"; +export * from "./models/featureModels/featureConfig/creditConfig.js"; +export * from "./models/featureModels/featureConfig/meteredConfig.js"; export * from "./models/featureModels/featureEnums.js"; export * from "./models/featureModels/featureModels.js"; export * from "./models/featureModels/featureResModels.js"; -export * from "./models/featureModels/featureConfig/meteredConfig.js"; -export * from "./models/featureModels/featureConfig/creditConfig.js"; - +// 2. Feature Models +export * from "./models/featureModels/featureTable.js"; +// Gen Models +export * from "./models/genModels/genEnums.js"; +export * from "./models/migrationModels/migrationErrorTable.js"; +export * from "./models/migrationModels/migrationJobTable.js"; +export * from "./models/migrationModels/migrationModels.js"; +export * from "./models/orgModels/frontendOrg.js"; +export * from "./models/orgModels/orgConfig.js"; +// 1. Org Models +export * from "./models/orgModels/orgTable.js"; +export * from "./models/otherModels/metadataModels.js"; +export * from "./models/otherModels/metadataTable.js"; +export * from "./models/productModels/entModels/entEnums.js"; +export * from "./models/productModels/entModels/entModels.js"; // 3. Entitlement Models export * from "./models/productModels/entModels/entTable.js"; -export * from "./models/productModels/entModels/entModels.js"; -export * from "./models/productModels/entModels/entEnums.js"; - // 4. Free Trial Models export * from "./models/productModels/freeTrialModels/freeTrialEnums.js"; export * from "./models/productModels/freeTrialModels/freeTrialModels.js"; export * from "./models/productModels/freeTrialModels/freeTrialTable.js"; - -// 4. Price Models -export * from "./models/productModels/priceModels/priceEnums.js"; export * from "./models/productModels/priceModels/priceConfig/fixedPriceConfig.js"; export * from "./models/productModels/priceModels/priceConfig/usagePriceConfig.js"; -export * from "./models/productModels/priceModels/priceTable.js"; +// 4. Price Models +export * from "./models/productModels/priceModels/priceEnums.js"; export * from "./models/productModels/priceModels/priceModels.js"; - +export * from "./models/productModels/priceModels/priceTable.js"; // 5. Product Models export * from "./models/productModels/productEnums.js"; -export * from "./models/productModels/productTable.js"; export * from "./models/productModels/productModels.js"; export * from "./models/productModels/productRelations.js"; - -// 6. Product V2 Models -export * from "./models/productV2Models/productV2Models.js"; -export * from "./models/productV2Models/productResponseModels.js"; -export * from "./models/productV2Models/productItemModels/productItemModels.js"; -export * from "./models/productV2Models/productItemModels/prodItemResponseModels.js"; +export * from "./models/productModels/productTable.js"; export * from "./models/productV2Models/productItemModels/featureItem.js"; export * from "./models/productV2Models/productItemModels/featurePriceItem.js"; export * from "./models/productV2Models/productItemModels/priceItem.js"; +export * from "./models/productV2Models/productItemModels/prodItemResponseModels.js"; export * from "./models/productV2Models/productItemModels/productItemEnums.js"; - -// 7. Cus Product Models -export * from "./models/cusProductModels/cusProductModels.js"; -export * from "./models/cusProductModels/cusProductTable.js"; -export * from "./models/cusProductModels/cusProductEnums.js"; -export * from "./models/cusProductModels/cusPriceModels/cusPriceModels.js"; -export * from "./models/cusProductModels/cusPriceModels/cusPriceTable.js"; - -export * from "./models/cusProductModels/cusEntModels/cusEntModels.js"; -export * from "./models/cusProductModels/cusEntModels/cusEntWithProduct.js"; -export * from "./models/cusProductModels/cusEntModels/cusEntTable.js"; -export * from "./models/cusProductModels/cusEntModels/replaceableTable.js"; -export * from "./models/cusProductModels/cusEntModels/replaceableSchema.js"; -export * from "./models/cusProductModels/cusEntModels/rolloverModels/rolloverTable.js"; -export * from "./models/cusProductModels/cusEntModels/resetCusEnt.js"; - -// 8. Customer Models -export * from "./models/cusModels/cusModels.js"; -export * from "./models/cusModels/cusTable.js"; -export * from "./models/cusModels/fullCusModel.js"; -export * from "./models/cusModels/cusExpand.js"; -export * from "./models/cusModels/invoiceModels/invoiceResponseModels.js"; -export * from "./models/cusModels/invoiceModels/invoiceTable.js"; -// Cus response -export * from "./models/cusModels/cusResponseModels.js"; -export * from "./models/cusModels/cusResModels/cusProductResponse.js"; -export * from "./models/cusModels/cusResModels/cusFeatureResponse.js"; -export * from "./models/cusModels/cusResModels/cusReferralsResponse.js"; - -export * from "./models/cusModels/entityModels/entityModels.js"; -export * from "./models/cusModels/entityModels/entityTable.js"; -export * from "./models/cusModels/entityModels/entityExpand.js"; -export * from "./models/cusModels/entityModels/entityResModels.js"; - -// 4. Chat Result Models -export * from "./models/chatResultModels/chatResultTable.js"; -export * from "./models/chatResultModels/chatResultFeature.js"; - -// Reward Models -export * from "./models/rewardModels/rewardModels/rewardModels.js"; -export * from "./models/rewardModels/rewardModels/rewardEnums.js"; -export * from "./models/rewardModels/rewardModels/rewardTable.js"; -export * from "./models/rewardModels/rewardModels/rewardResponseModels.js"; - -export * from "./models/rewardModels/rewardProgramModels/rewardProgramModels.js"; -export * from "./models/rewardModels/rewardProgramModels/rewardProgramEnums.js"; -export * from "./models/rewardModels/rewardProgramModels/rewardProgramTable.js"; +export * from "./models/productV2Models/productItemModels/productItemModels.js"; +export * from "./models/productV2Models/productResponseModels.js"; +// 6. Product V2 Models +export * from "./models/productV2Models/productV2Models.js"; +export * from "./models/rewardModels/referralModels/referralCodeTable.js"; export * from "./models/rewardModels/referralModels/referralModels.js"; export * from "./models/rewardModels/referralModels/rewardRedemptionTable.js"; -export * from "./models/rewardModels/referralModels/referralCodeTable.js"; - -// 5. Others: events, apiKeys -export * from "./models/eventModels/eventModels.js"; -export * from "./models/eventModels/eventTable.js"; - -export * from "./models/devModels/apiKeyModels.js"; -export * from "./models/devModels/apiKeyTable.js"; - -export * from "./models/otherModels/metadataModels.js"; -export * from "./models/otherModels/metadataTable.js"; - +export * from "./models/rewardModels/rewardModels/rewardEnums.js"; +// Reward Models +export * from "./models/rewardModels/rewardModels/rewardModels.js"; +export * from "./models/rewardModels/rewardModels/rewardResponseModels.js"; +export * from "./models/rewardModels/rewardModels/rewardTable.js"; +export * from "./models/rewardModels/rewardProgramModels/rewardProgramEnums.js"; +export * from "./models/rewardModels/rewardProgramModels/rewardProgramModels.js"; +export * from "./models/rewardModels/rewardProgramModels/rewardProgramTable.js"; export * from "./models/subModels/subModels.js"; export * from "./models/subModels/subTable.js"; - -export * from "./models/cusModels/invoiceModels/invoiceModels.js"; - -export * from "./models/migrationModels/migrationModels.js"; -export * from "./models/migrationModels/migrationJobTable.js"; -export * from "./models/migrationModels/migrationErrorTable.js"; - -// ANALYTICS MODELS -export * from "./models/analyticsModels/actionEnums.js"; -export * from "./models/analyticsModels/actionTable.js"; - -// Attach Models -export * from "./models/attachModels/attachPreviewModels.js"; -export * from "./models/attachModels/attachEnums/AttachBranch.js"; -export * from "./models/attachModels/attachEnums/AttachFunction.js"; -export * from "./models/attachModels/attachEnums/AttachConfig.js"; -export * from "./models/attachModels/checkoutModels.js"; -export * from "./models/attachModels/attachBody.js"; - // Utils export * from "./utils/displayUtils.js"; -export * from "./models/checkModels/checkPreviewModels.js"; -export * from "./models/chatResultModels/chatResultFeature.js"; -export * from "./utils/productDisplayUtils/getProductItemRes.js"; -export * from "./utils/productUtils.js"; -export * from "./utils/productDisplayUtils/sortProductItems.js"; -export * from "./utils/intervalUtils.js"; -export * from "./utils/productUtils/priceToInvoiceAmount.js"; export * from "./utils/index.js"; - -// ENUMS -export * from "./enums/SuccessCode.js"; -export * from "./enums/ErrCode.js"; -export * from "./enums/LoggerAction.js"; -export * from "./enums/AttachErrCode.js"; -export * from "./enums/APIVersion.js"; -export * from "./enums/WebhookEventType.js"; +export * from "./utils/intervalUtils.js"; +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 diff --git a/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts b/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts index d24bf5bbd..319749d00 100644 --- a/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts +++ b/shared/models/productModels/priceModels/priceConfig/usagePriceConfig.ts @@ -13,6 +13,8 @@ export const UsageTierSchema = z.object({ amount: z.number(), }); +export type UsageTier = z.infer; + export const UsagePriceConfigSchema = z.object({ type: z.string(), bill_when: z.nativeEnum(BillWhen), diff --git a/shared/utils/rewardUtils/rewardMigrationUtils.ts b/shared/utils/rewardUtils/rewardMigrationUtils.ts new file mode 100644 index 000000000..372565c1d --- /dev/null +++ b/shared/utils/rewardUtils/rewardMigrationUtils.ts @@ -0,0 +1,177 @@ +import type { + 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"; + +// Helper function to check if tier structures match +const tiersMatch = (oldTiers: UsageTier[], newTiers: UsageTier[]): boolean => { + 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; + }); +}; + +// Match fixed prices by amount +const findMatchingFixedPrice = ( + oldPrice: Price, + candidates: Price[], +): Price | null => { + const oldConfig = oldPrice.config as FixedPriceConfig; + + return ( + candidates.find((candidate) => { + const newConfig = candidate.config as FixedPriceConfig; + return newConfig.amount === oldConfig.amount; + }) || null + ); +}; + +// Match usage prices by feature and billing characteristics +const findMatchingUsagePrice = ( + oldPrice: Price, + candidates: Price[], +): Price | null => { + const oldConfig = oldPrice.config as UsagePriceConfig; + + return ( + candidates.find((candidate) => { + const newConfig = candidate.config as UsagePriceConfig; + + // Match by feature + if (newConfig.feature_id !== oldConfig.feature_id) return false; + if (newConfig.internal_feature_id !== oldConfig.internal_feature_id) + return false; + + // Match by billing behavior + if (newConfig.bill_when !== oldConfig.bill_when) return false; + if (newConfig.should_prorate !== oldConfig.should_prorate) return false; + + // Optionally match by tier structure + if (!tiersMatch(oldConfig.usage_tiers, newConfig.usage_tiers)) + return false; + + return true; + }) || null + ); +}; + +// Main matching function with type-specific logic +const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => { + // First, filter by basic characteristics + const candidates = newPrices.filter( + (newPrice) => + newPrice.config.type === oldPrice.config.type && + newPrice.config.interval === oldPrice.config.interval && + newPrice.config.interval_count === oldPrice.config.interval_count, + ); + + 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); + } + + // Fallback to first candidate + return candidates[0]; +}; + +export interface RewardMigrationResult { + willMigrateCount: number; + willNotMigrateCount: number; +} + +export interface RewardPriceAnalysisResult { + validPriceCount: number; + invalidPriceCount: number; +} + +export function analyzeRewardMigration({ + rewards, + oldPrices, + newPrices, + rewardTypesToCheck, +}: { + rewards: Reward[]; + oldPrices: Price[]; + newPrices: Price[]; + rewardTypesToCheck: RewardType[]; +}): RewardMigrationResult { + let willMigrateCount = 0; + let willNotMigrateCount = 0; + + // Filter rewards to only those we care about and that have discount configs with price_ids + const relevantRewards = rewards.filter( + (reward) => + rewardTypesToCheck.includes(reward.type) && + reward.discount_config?.price_ids && + reward.discount_config.price_ids.length > 0, + ); + + for (const reward of relevantRewards) { + if (!reward.discount_config?.price_ids) continue; + + for (const priceId of reward.discount_config.price_ids) { + const oldPrice = oldPrices.find((p) => p.id === priceId); + if (!oldPrice) { + // Price not in old prices list, skip + continue; + } + + const matchingNewPrice = findBestMatch(oldPrice, newPrices); + + if (matchingNewPrice) { + willMigrateCount++; + } else { + willNotMigrateCount++; + } + } + } + + return { + willMigrateCount, + willNotMigrateCount, + }; +} + +export function analyzeRewardPrices({ + reward, + availablePriceIds, +}: { + reward: Reward; + availablePriceIds: string[]; +}): RewardPriceAnalysisResult { + let validPriceCount = 0; + let invalidPriceCount = 0; + + // Skip rewards that apply to all products + if (reward.discount_config?.apply_to_all) { + return { validPriceCount: 0, invalidPriceCount: 0 }; + } + + // Check each price ID in the reward + if (reward.discount_config?.price_ids) { + for (const priceId of reward.discount_config.price_ids) { + if (availablePriceIds.includes(priceId)) { + validPriceCount++; + } else { + invalidPriceCount++; + } + } + } + + return { + validPriceCount, + invalidPriceCount, + }; +} diff --git a/vite/src/views/products/rewards/reward-config/UpdateReward.tsx b/vite/src/views/products/rewards/reward-config/UpdateReward.tsx index c57a3cd20..63937f21d 100644 --- a/vite/src/views/products/rewards/reward-config/UpdateReward.tsx +++ b/vite/src/views/products/rewards/reward-config/UpdateReward.tsx @@ -1,79 +1,128 @@ -import React, { useState } from "react"; +import type { ProductV2, Reward } from "@autumn/shared"; +import { analyzeRewardPrices } from "@autumn/shared"; +import { useState } from "react"; +import { toast } from "sonner"; +import { WarningBox } from "@/components/general/modal-components/WarningBox"; +import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogTitle, } from "@/components/ui/dialog"; -import { Button } from "@/components/ui/button"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { toast } from "sonner"; -import { Reward } from "@autumn/shared"; -import { useEnv } from "@/utils/envUtils"; -import { RewardService } from "@/services/products/RewardService"; -import { getBackendErr } from "@/utils/genUtils"; -import { WarningBox } from "@/components/general/modal-components/WarningBox"; +import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery"; +import { RewardService } from "@/services/products/RewardService"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { useEnv } from "@/utils/envUtils"; +import { getBackendErr } from "@/utils/genUtils"; import { RewardConfig } from "./RewardConfig"; +const checkRewardMigration = ( + reward: Reward, + products: ProductV2[], +): { willMigrateCount: number; willNotMigrateCount: number } => { + // Extract all available price IDs from current products + const availablePriceIds: string[] = []; + for (const product of products) { + if (product.items) { + for (const item of product.items) { + if (item.price_id) { + availablePriceIds.push(item.price_id); + } + } + } + } + + // Use the shared utility to analyze the reward + const analysis = analyzeRewardPrices({ + reward, + availablePriceIds, + }); + + return { + willMigrateCount: analysis.validPriceCount, + willNotMigrateCount: analysis.invalidPriceCount, + }; +}; + function UpdateReward({ - open, - setOpen, - selectedReward, - setSelectedReward, + open, + setOpen, + selectedReward, + setSelectedReward, }: { - open: boolean; - setOpen: (open: boolean) => void; - selectedReward: Reward | null; - setSelectedReward: (reward: Reward) => void; + open: boolean; + setOpen: (open: boolean) => void; + selectedReward: Reward | null; + setSelectedReward: (reward: Reward) => void; }) { - const [updateLoading, setUpdateLoading] = useState(false); - const { refetch } = useRewardsQuery(); + const [updateLoading, setUpdateLoading] = useState(false); + const { refetch } = useRewardsQuery(); + const { products } = useProductsQuery(); - const env = useEnv(); - const axiosInstance = useAxiosInstance({ env }); + const env = useEnv(); + const axiosInstance = useAxiosInstance({ env }); - const handleUpdate = async () => { - setUpdateLoading(true); - try { - await RewardService.updateReward({ - axiosInstance, - internalId: selectedReward!.internal_id, - data: selectedReward!, - }); - toast.success("Reward updated successfully"); - await refetch(); - setOpen(false); - } catch (error) { - toast.error(getBackendErr(error, "Failed to update coupon")); - } - setUpdateLoading(false); - }; + if (!selectedReward) { + setOpen(false); + return; + } - return ( - - - Update Reward - - Existing customers with this coupon will not be affected - + const handleUpdate = async () => { + setUpdateLoading(true); + try { + // Check migration status and show warning if needed + if (products) { + const migrationResult = checkRewardMigration(selectedReward, products); + if (migrationResult.willNotMigrateCount > 0) { + toast.warning( + `${migrationResult.willNotMigrateCount} price${migrationResult.willNotMigrateCount === 1 ? "" : "s"} won't be migrated to the latest product version.`, + { + duration: 5000, + }, + ); + } + } - {selectedReward && ( - - )} + await RewardService.updateReward({ + axiosInstance, + internalId: selectedReward.internal_id, + data: selectedReward, + }); + toast.success("Reward updated successfully"); + await refetch(); + setOpen(false); + } catch (error) { + toast.error(getBackendErr(error, "Failed to update coupon")); + } + setUpdateLoading(false); + }; - - - - - - ); + return ( + + + Update Reward + + Existing customers with this coupon will not be affected + + + {selectedReward && ( + + )} + + + + + + + ); } export default UpdateReward; From e8ff791ace15e56956dd77bb51bd2ab2d8469ecc Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Fri, 19 Sep 2025 12:58:48 +0100 Subject: [PATCH 53/60] =?UTF-8?q?fix:=20=F0=9F=90=9B=20dont=20use=20unmatc?= =?UTF-8?q?hed=20prices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/internal/migrations/runRewardMigrationTask.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts index 67b7d0cd1..a6395fbd7 100644 --- a/server/src/internal/migrations/runRewardMigrationTask.ts +++ b/server/src/internal/migrations/runRewardMigrationTask.ts @@ -182,7 +182,9 @@ export async function runRewardMigrationTask({ duration_value: reward.discount_config!.duration_value, should_rollover: reward.discount_config!.should_rollover, apply_to_all: reward.discount_config!.apply_to_all, - price_ids: [...unmatchedPrices, ...newPriceIds], + // Note: I used to include unmatchedPrices as a safety precaution, but it seems unecessary + // price_ids: [...unmatchedPrices, ...newPriceIds], + price_ids: newPriceIds, }, }, }); From 8a77c5e05025fb37820970ce9e85d9f965a3bade Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 20 Sep 2025 08:24:32 +0100 Subject: [PATCH 54/60] fix: customer email match is case insensitive, downgrade and cancel immediately doesn't create leftover scheduled product --- .../stripe/createStripePrice/createStripePrice.ts | 6 +++--- server/src/internal/customers/CusService.ts | 4 ++-- .../upgradeFlow/handleUpgradeFlow.ts | 13 +++++++------ .../customers/cancel/handleCancelProduct.ts | 5 ++--- .../customers/handlers/handleCreateCustomer.ts | 1 - server/src/internal/products/productUtils.ts | 4 +++- .../utils/cusProductUtils/formatCusProductUtils.ts | 12 ++++++++++++ shared/utils/index.ts | 1 + 8 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 shared/utils/cusProductUtils/formatCusProductUtils.ts diff --git a/server/src/external/stripe/createStripePrice/createStripePrice.ts b/server/src/external/stripe/createStripePrice/createStripePrice.ts index dbf8c9bb7..d8a2762f1 100644 --- a/server/src/external/stripe/createStripePrice/createStripePrice.ts +++ b/server/src/external/stripe/createStripePrice/createStripePrice.ts @@ -34,8 +34,6 @@ export const checkCurStripePrice = async ({ stripeCli: Stripe; currency: string; }) => { - let priceValid = false; - let config = price.config! as UsagePriceConfig; let stripePrice: Stripe.Price | null = null; @@ -48,7 +46,9 @@ export const checkCurStripePrice = async ({ }); if (!stripePrice.active) { - stripePrice = null; + stripePrice = await stripeCli.prices.update(config.stripe_price_id!, { + active: true, + }); } if ( diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index d1d91d9f5..87a5a11cf 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -13,7 +13,7 @@ import { import RecaseError from "@/utils/errorUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; import { StatusCodes } from "http-status-codes"; -import { and, eq, or, sql } from "drizzle-orm"; +import { and, eq, ilike, or, sql } from "drizzle-orm"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { getFullCusQuery } from "./getFullCusQuery.js"; import { trace } from "@opentelemetry/api"; @@ -151,7 +151,7 @@ export class CusService { }) { const customer = await db.query.customers.findMany({ where: and( - eq(customers.email, email), + ilike(customers.email, email), eq(customers.org_id, orgId), eq(customers.env, env) ), diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index 0dffd8faf..25744e4f6 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -4,6 +4,7 @@ import { } from "@/internal/customers/cusProducts/AttachParams.js"; import { attachParamsToCurCusProduct, + attachParamsToProduct, paramsToCurSub, paramsToCurSubSchedule, } from "../../attachUtils/convertAttachParams.js"; @@ -16,6 +17,8 @@ import { AttachConfig, AttachScenario, CusProductStatus, + cusProductToProduct, + logCusProducts, ProrationBehavior, } from "@autumn/shared"; import { ExtendedRequest } from "@/utils/models/Request.js"; @@ -72,13 +75,11 @@ export const handleUpgradeFlow = async ({ const { subItems } = newItemSet; - // for (const item of subItems) { - // const { autumnPrice, ...rest } = item; - // console.log("ITEM:", rest); - // } + const products = attachParams.fromCancel + ? [cusProductToProduct({ cusProduct: attachParams.cusProduct! })] + : attachParams.products; - // Delete scheduled products if needed - for (const product of attachParams.products) { + for (const product of products) { if (product.is_add_on) continue; const { curScheduledProduct } = getExistingCusProducts({ diff --git a/server/src/internal/customers/cancel/handleCancelProduct.ts b/server/src/internal/customers/cancel/handleCancelProduct.ts index 07154048c..39d9673b5 100644 --- a/server/src/internal/customers/cancel/handleCancelProduct.ts +++ b/server/src/internal/customers/cancel/handleCancelProduct.ts @@ -4,7 +4,6 @@ import { ErrCode, FullCusProduct, FullCustomer, - APIVersion, ProrationBehavior, AttachBranch, Price, @@ -16,9 +15,9 @@ import { createStripeCli } from "@/external/stripe/utils.js"; import RecaseError from "@/utils/errorUtils.js"; import { StatusCodes } from "http-status-codes"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; -import { cancelEndOfCycle } from "./cancelEndOfCycle.js"; + import { cancelImmediately } from "./cancelImmediately.js"; -import { cancelScheduledProduct } from "./cancelScheduledProduct.js"; + import { handleRenewProduct } from "../attach/attachFunctions/handleRenewProduct.js"; import { getDefaultAttachConfig } from "../attach/attachUtils/getAttachConfig.js"; import { handleScheduleFunction2 } from "../attach/attachFunctions/scheduleFlow/handleScheduleFlow2.js"; diff --git a/server/src/internal/customers/handlers/handleCreateCustomer.ts b/server/src/internal/customers/handlers/handleCreateCustomer.ts index d8c313361..568cd3666 100644 --- a/server/src/internal/customers/handlers/handleCreateCustomer.ts +++ b/server/src/internal/customers/handlers/handleCreateCustomer.ts @@ -78,7 +78,6 @@ const handleIdIsNull = async ({ } // 2. Check if email already exists - let existingCustomers = await CusService.getByEmail({ db, email: newCus.email, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index 92a896b0f..abb5ca9cf 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -245,7 +245,9 @@ export const checkStripeProductExists = async ({ ); if (!stripeProduct.active) { - createNew = true; + await stripeCli.products.update(product.processor!.id, { + active: true, + }); } } catch (error) { createNew = true; diff --git a/shared/utils/cusProductUtils/formatCusProductUtils.ts b/shared/utils/cusProductUtils/formatCusProductUtils.ts new file mode 100644 index 000000000..0d0f68975 --- /dev/null +++ b/shared/utils/cusProductUtils/formatCusProductUtils.ts @@ -0,0 +1,12 @@ +import { FullCusProduct } from "../../models/cusProductModels/cusProductModels.js"; + +export const logCusProducts = ({ + cusProducts, +}: { + cusProducts: FullCusProduct[]; +}) => { + console.log(`CUS PRODUCTS:`); + for (const cusProduct of cusProducts) { + console.log(`${cusProduct.product.id} - ${cusProduct.status}`); + } +}; diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 10fe92537..8c5edfd47 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -7,6 +7,7 @@ export * from "./cusProductUtils/convertCusProduct.js"; export * from "./cusProductUtils/productIdToCusProduct.js"; export * from "./cusProductUtils/cusProductConstants.js"; export * from "./cusProductUtils/cusProductUtils.js"; +export * from "./cusProductUtils/formatCusProductUtils.js"; export * from "./utils.js"; // Cus ent utils From 3632178fd060dc10004644609b821bddcb8dbcb2 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 20 Sep 2025 08:48:04 +0100 Subject: [PATCH 55/60] fix: unset org stripe keys if expired response is received --- server/src/external/stripe/stripeWebhooks.ts | 36 ++++++++++++------- .../customers/handlers/handleGetCustomer.ts | 22 ++---------- server/src/internal/orgs/orgUtils.ts | 27 ++++++++++++++ .../src/internal/platform/platformRouter.ts | 7 ++-- server/src/utils/routerUtils.ts | 12 +++++++ server/tests/attach/basic/basic3.ts | 1 + server/tests/utils/stripeUtils.ts | 3 +- 7 files changed, 69 insertions(+), 39 deletions(-) diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts index fb695fccf..d05cc56c7 100644 --- a/server/src/external/stripe/stripeWebhooks.ts +++ b/server/src/external/stripe/stripeWebhooks.ts @@ -9,7 +9,11 @@ import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckout import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js"; import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js"; import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js"; -import { getStripeWebhookSecret } from "@/internal/orgs/orgUtils.js"; +import { + getStripeWebhookSecret, + isStripeConnected, + unsetOrgStripeKeys, +} from "@/internal/orgs/orgUtils.js"; import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js"; import { handleRequestError } from "@/utils/errorUtils.js"; import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js"; @@ -22,6 +26,7 @@ import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js import { CusService } from "@/internal/customers/CusService.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { handleInvoiceUpdated } from "./webhookHandlers/handleInvoiceUpdated.js"; +import { disconnectStripe } from "@/internal/orgs/handlers/handleDeleteStripe.js"; export const stripeWebhookRouter: Router = express.Router(); @@ -66,9 +71,11 @@ stripeWebhookRouter.post( request.env = env; org = data.org; - if (!org.stripe_config) { - console.log(`Org ${orgId} does not have a stripe config`); - response.status(200).send(`Org ${orgId} does not have a stripe config`); + 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; } @@ -115,15 +122,6 @@ stripeWebhookRouter.post( let logger = request.logtail; logStripeWebhook({ req: request, event }); - // const logger = createLogtailWithContext({ - // action: LoggerAction.StripeWebhook, - // event_type: event.type, - // data: event.data, - // org_id: orgId, - // org_slug: org.slug, - // env, - // }); - try { const stripeCli = createStripeCli({ org, env }); switch (event.type) { @@ -244,6 +242,18 @@ stripeWebhookRouter.post( response.status(200).json({ message: "ok" }); return; } + + if (error.message.includes("Expired API Key provided")) { + // Disconnect Stripe + await unsetOrgStripeKeys({ + db, + org, + env, + }); + + response.status(200).json({ message: "ok" }); + return; + } } handleRequestError({ diff --git a/server/src/internal/customers/handlers/handleGetCustomer.ts b/server/src/internal/customers/handlers/handleGetCustomer.ts index 7a7716b42..9b5314c1c 100644 --- a/server/src/internal/customers/handlers/handleGetCustomer.ts +++ b/server/src/internal/customers/handlers/handleGetCustomer.ts @@ -1,11 +1,5 @@ -import { CusService } from "@/internal/customers/CusService.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { - APIVersion, - CusExpand, - CusProductStatus, - ErrCode, -} from "@autumn/shared"; +import { APIVersion, CusExpand, ErrCode } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js"; import { parseCusExpand } from "../cusUtils/cusUtils.js"; @@ -30,9 +24,7 @@ export const handleGetCustomer = async (req: any, res: any) => }); let getInvoices = apiVersion < APIVersion.v1_1; - if (getInvoices) { - expandArray.push(CusExpand.Invoices); - } + if (getInvoices) expandArray.push(CusExpand.Invoices); logger.info(`getting customer ${customerId} for org ${org.slug}`); const startTime = Date.now(); @@ -46,16 +38,6 @@ export const handleGetCustomer = async (req: any, res: any) => logger, }); - // const customer = await CusService.getFull({ - // db, - // idOrInternalId: customerId, - // orgId: org.id, - // env: env, - // withEntities: true, - // expand: expandArray, - // allowNotFound: true, - // withSubs: true, - // }); logger.info(`get customer took ${Date.now() - startTime}ms`); if (!customer) { diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts index 5ba4daff1..96f23aa14 100644 --- a/server/src/internal/orgs/orgUtils.ts +++ b/server/src/internal/orgs/orgUtils.ts @@ -212,3 +212,30 @@ export const updateOrgConfig = async ({ await CacheManager.disconnect(); } }; + +export const unsetOrgStripeKeys = async ({ + org, + env, + db, +}: { + 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; + } + + await OrgService.update({ + db, + orgId: org.id, + updates: { + stripe_config: newStripeConfig, + }, + }); +}; diff --git a/server/src/internal/platform/platformRouter.ts b/server/src/internal/platform/platformRouter.ts index 9b2c26a04..a5ab46c04 100644 --- a/server/src/internal/platform/platformRouter.ts +++ b/server/src/internal/platform/platformRouter.ts @@ -12,15 +12,15 @@ import { import { ExtendedRequest } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { slugify } from "@/utils/genUtils.js"; + import { and, eq } from "drizzle-orm"; import { connectStripe } from "../orgs/handlers/handleConnectStripe.js"; import { z } from "zod"; import { createKey } from "../dev/api-keys/apiKeyUtils.js"; import { afterOrgCreated } from "@/utils/authUtils/afterOrgCreated.js"; import { Autumn } from "autumn-js"; -import { organizationSchema } from "better-auth/plugins"; -import { isStripeConnected, shouldReconnectStripe } from "../orgs/orgUtils.js"; + +import { shouldReconnectStripe } from "../orgs/orgUtils.js"; const platformRouter = Router(); @@ -215,7 +215,6 @@ platformRouter.post("/exchange", (req: any, res: any) => }); if (reconnectStripe) { - console.log("Reconnecting stripe sandbox"); let { test_api_key, test_webhook_secret, diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts index 1af70d20d..2d05cb0fd 100644 --- a/server/src/utils/routerUtils.ts +++ b/server/src/utils/routerUtils.ts @@ -280,6 +280,18 @@ export const routeHandler = async ({ 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, + }); + } + if ( originalUrl.includes("/billing_portal") && error.message.includes("Provide a configuration or create your default") diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts index 69d032bce..eba70ead9 100644 --- a/server/tests/attach/basic/basic3.ts +++ b/server/tests/attach/basic/basic3.ts @@ -126,6 +126,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on 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)" diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 85567aabd..4f15ce4fd 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -66,7 +66,6 @@ export const completeCheckoutForm = async ( await page.type("#billingPostalCode", "123456"); if (overrideQuantity) { - // console.log(" - Overriding quantity"); const quantityBtn = await page.$(".AdjustableQuantitySelector"); await quantityBtn?.evaluate((b: any) => (b as HTMLElement).click()); @@ -77,6 +76,7 @@ export const completeCheckoutForm = async ( const updateBtn = await page.$(".AdjustQuantityFooter-btn"); await updateBtn?.evaluate((b: any) => (b as HTMLElement).click()); + await timeout(1000); } @@ -88,7 +88,6 @@ export const completeCheckoutForm = async ( await timeout(5000); } - // const submitButton = await page.$(".SubmitButton-TextContainer"); const submitButton = await page.$(".SubmitButton-TextContainer"); await submitButton?.evaluate((b: any) => (b as HTMLElement).click()); await timeout(7000); From 57d915c9117d90ca6a64a0513d6d76f769811dd4 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 20 Sep 2025 08:50:42 +0100 Subject: [PATCH 56/60] chore: updated gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5e7c175f3..16fd20dfd 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,5 @@ supabase/ migration.sh stat.sh -CLAUDE.md \ No newline at end of file +CLAUDE.md +interview/ \ No newline at end of file From 0a2052e46543c73b55178c2e28d096b054f6fa1d Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sat, 20 Sep 2025 10:56:12 +0100 Subject: [PATCH 57/60] chore: ran referral tests --- server/shell/g5.sh | 7 +- .../advanced/referrals/paid/referrals13.ts | 430 ++-- .../advanced/referrals/paid/referrals14.ts | 440 ++-- .../advanced/referrals/paid/referrals15.ts | 488 ++--- .../advanced/referrals/paid/referrals16.ts | 645 +++--- server/tests/global.ts | 1812 +++++++++-------- 6 files changed, 1883 insertions(+), 1939 deletions(-) diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 1ca91d668..6396f2ab2 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -7,16 +7,13 @@ source "$(dirname "$0")/config.sh" if [[ "$1" == *"setup"* ]]; then MOCHA_PARALLEL=true $MOCHA_SETUP fi -# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' +# $MOCHA_CMD 'tests/advanced/referrals/*.ts' 'tests/advanced/coupons/*.ts' $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ 'tests/advanced/coupons/*.ts' \ 'tests/attach/updateQuantity/*.ts' \ 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/rollovers/*.ts' \ - 'tests/advanced/customInterval/*.ts' - -$MOCHA_CMD 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/referrals/paid/*.ts' \ 'tests/advanced/rollovers/*.ts' \ 'tests/advanced/customInterval/*.ts' diff --git a/server/tests/advanced/referrals/paid/referrals13.ts b/server/tests/advanced/referrals/paid/referrals13.ts index a6b35e3d5..207447704 100644 --- a/server/tests/advanced/referrals/paid/referrals13.ts +++ b/server/tests/advanced/referrals/paid/referrals13.ts @@ -1,11 +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"; @@ -14,7 +15,7 @@ import { setupBefore } from "tests/before.js"; import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; + import { CusService } from "@/internal/customers/CusService.js"; import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; @@ -23,251 +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); + }); - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductImmediateReferrer.id, - }); + it("should create redemption for redeemer and fail if redeemed again", async () => { + redemption = await autumn.referrals.redeem({ + customerId: redeemer, + code: referralCode.code, + }); - assert.equal(referralCode2.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); + } + }); - it("should create redemption for redeemer and fail if redeemed again", async () => { - redemption = await autumn.referrals.redeem({ - customerId: redeemer, - code: referralCode.code, - }); + 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); - // 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); - } - }); + const mainProds = (await autumn.customers.get(mainCustomerId)).products; + const redeemerProds = (await autumn.customers.get(redeemer)).products; - 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); + // Main customer (referrer) should have the pro product (already attached) + assert.equal(mainProds.length, 1); + assert.equal(mainProds[0].id, products.pro.id); - const mainProds = (await autumn.customers.get(mainCustomerId)).products; - const redeemerProds = (await autumn.customers.get(redeemer)).products; + // 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); - // Main customer (referrer) should have the pro product (already attached) - assert.equal(mainProds.length, 1); - assert.equal(mainProds[0].id, products.pro.id); + expectProductV1Attached({ + customer: await autumn.customers.get(mainCustomerId), + product: products.pro, + status: CusProductStatus.Active, + }); - // 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); + // Verify redeemer only has free product + expectProductV1Attached({ + customer: await autumn.customers.get(redeemer), + product: products.free, + status: CusProductStatus.Active, + }); + }); - expectProductV1Attached({ - customer: await autumn.customers.get(mainCustomerId), - product: products.pro, - 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, + }) + ) + ); - // Verify redeemer only has free product - expectProductV1Attached({ - customer: await autumn.customers.get(redeemer), - product: products.free, - status: CusProductStatus.Active, - }); - }); + // Test that main customer's Pro invoice has discount applied + const mainCustomerWithInvoices = await autumn.customers.get( + mainCustomerId, + { + expand: [CusExpand.Invoices, CusExpand.Rewards], + } + ); - 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, - }), - ), - ); + const proInvoice = mainCustomerWithInvoices.invoices.find((x) => + x.product_ids.includes(products.pro.id) + ); - // Test that main customer's Pro invoice has discount applied - const mainCustomerWithInvoices = await autumn.customers.get( - mainCustomerId, - { - expand: [CusExpand.Invoices, CusExpand.Rewards], - }, - ); + const expectedTotal = products.pro.prices[0].config.amount; - console.log( - "mainCustomerWithInvoices", - mainCustomerWithInvoices.invoices - .map( - (x) => - `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, - ) - .join("\n"), - ); - const proInvoice = mainCustomerWithInvoices.invoices.find((x) => - x.product_ids.includes(products.pro.id), - ); - console.log("proInvoice", proInvoice); + const actualTotal = proInvoice?.total; - const expectedTotal = products.pro.prices[0].config.amount; - console.log("expectedTotal", expectedTotal); - const actualTotal = proInvoice?.total; - console.log("actualTotal", actualTotal); + 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 a0f1e1bc6..12aa8246b 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,254 +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], + } + ); - console.log( - "Invoices:\n", - mainCustomerWithInvoices.invoices - .map( - (x) => - `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, - ) - .join("\n"), - ); + 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 - console.log("Expected total:", expectedTotal); - console.log("Premium invoice total:", 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}` + ); - // 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 bd8b5843d..59b884853 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 - - console.log("Main customer expected total:", expectedTotal); - console.log("Main customer Pro invoice total:", mainProInvoice.total); + // 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 - assert.equal( - mainProInvoice.total, - expectedTotal, - `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}`, - ); - } + // console.log("Main customer expected total:", expectedTotal); + // console.log("Main customer Pro invoice total:", 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 - - console.log("Redeemer expected total:", expectedTotal); - console.log("Redeemer Pro invoice total:", redeemerProInvoice.total); + assert.equal( + mainProInvoice.total, + expectedTotal, + `Main customer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${mainProInvoice.total}` + ); + } - assert.equal( - redeemerProInvoice.total, - expectedTotal, - `Redeemer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${redeemerProInvoice.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 - 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, - ], - }), - ), - ); + // console.log("Redeemer expected total:", expectedTotal); + // console.log("Redeemer Pro invoice total:", redeemerProInvoice.total); - 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 }, - ], - ]; + assert.equal( + redeemerProInvoice.total, + expectedTotal, + `Redeemer Pro invoice should be $0 (Pro $10 - Pro amount $10 discount). Got $${redeemerProInvoice.total}` + ); + } - 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 - ); + 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, + ], + }) + ) + ); - 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` : ""}`, - ); - }); - }); - }); + 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 + ); + + 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/referrals16.ts b/server/tests/advanced/referrals/paid/referrals16.ts index 25444b75a..0b56bcab2 100644 --- a/server/tests/advanced/referrals/paid/referrals16.ts +++ b/server/tests/advanced/referrals/paid/referrals16.ts @@ -1,386 +1,351 @@ -import { - type AppEnv, - CusExpand, - CusProductStatus, - ErrCode, - type Organization, - type ReferralCode, - type RewardRedemption, -} from "@autumn/shared"; -import { assert } from "chai"; -import chalk from "chalk"; -import type { Stripe } from "stripe"; -import { setupBefore } from "tests/before.js"; -import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; -import { - advanceTestClock, - completeCheckoutForm -} from "tests/utils/stripeUtils.js"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { products, referralPrograms, rewards } from "../../../global.js"; +// import { +// type AppEnv, +// CusExpand, +// CusProductStatus, +// ErrCode, +// type Organization, +// type ReferralCode, +// type RewardRedemption, +// } from "@autumn/shared"; +// import { assert } from "chai"; +// import chalk from "chalk"; +// import type { Stripe } from "stripe"; +// import { setupBefore } from "tests/before.js"; +// import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js"; +// import { +// advanceTestClock, +// completeCheckoutForm, +// } from "tests/utils/stripeUtils.js"; +// import type { DrizzleCli } from "@/db/initDrizzle.js"; +// import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +// import { CusService } from "@/internal/customers/CusService.js"; +// import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +// import { products, referralPrograms, rewards } from "../../../global.js"; -export const group = "referrals16"; +// export const group = "referrals16"; -describe(`${chalk.yellowBright( - "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based", -)}`, () => { - const mainCustomerId = "main-referral-16"; - const redeemer = "referral16-r1"; - const redeemerPM = "success"; - const autumn: AutumnInt = new AutumnInt(); - let stripeCli: Stripe; - const testClockIds: string[] = []; - let referralCode: ReferralCode; +// describe(`${chalk.yellowBright( +// "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based" +// )}`, () => { +// const mainCustomerId = "main-referral-16"; +// const redeemer = "referral16-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.paidProductCheckoutAll.id, - }); +// it("should create code once", async () => { +// referralCode = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.id, +// }); - assert.exists(referralCode.code); +// assert.exists(referralCode.code); - // Get referral code again - const referralCode2 = await autumn.referrals.createCode({ - customerId: mainCustomerId, - referralId: referralPrograms.paidProductCheckoutAll.id, - }); +// // Get referral code again +// const referralCode2 = await autumn.referrals.createCode({ +// customerId: mainCustomerId, +// referralId: referralPrograms.paidProductCheckoutAll.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 and redeemer still on free tier (reward not triggered yet)", async () => { - const redemptionResult = await autumn.redemptions.get(redemption.id); - assert.equal(redemptionResult.triggered, false); // Checkout trigger not fired yet +// it("should have referrer and redeemer still on free tier (reward not triggered yet)", async () => { +// const redemptionResult = await autumn.redemptions.get(redemption.id); +// assert.equal(redemptionResult.triggered, false); // Checkout trigger not fired yet - 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; - // Both customers should still only have free product - assert.equal(mainProds.length, 1); - assert.equal(mainProds[0].id, products.free.id); +// // Both customers should still only have free product +// assert.equal(mainProds.length, 1); +// assert.equal(mainProds[0].id, products.free.id); - assert.equal(redeemerProds.length, 1); - assert.equal(redeemerProds[0].id, products.free.id); +// assert.equal(redeemerProds.length, 1); +// assert.equal(redeemerProds[0].id, products.free.id); - expectProductV1Attached({ - customer: mainCus, - product: products.free, - status: CusProductStatus.Active, - }); +// expectProductV1Attached({ +// customer: mainCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); - expectProductV1Attached({ - customer: redeemerCus, - product: products.free, - status: CusProductStatus.Active, - }); - }); +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.free, +// status: CusProductStatus.Active, +// }); +// }); - it("should trigger reward when redeemer checks out with Premium", async () => { - // Redeemer purchases Premium product (triggers checkout reward) - const checkoutRes = await autumn.attach({ - customer_id: redeemer, - product_id: products.premium.id, - force_checkout: true, - }); +// it("should trigger reward when redeemer checks out with Premium", async () => { +// // Redeemer purchases Premium product (triggers checkout reward) +// const checkoutRes = await autumn.attach({ +// customer_id: redeemer, +// product_id: products.premium.id, +// force_checkout: true, +// }); - await completeCheckoutForm(checkoutRes.checkout_url); +// await completeCheckoutForm(checkoutRes.checkout_url); - // Wait a bit for webhook processing - await new Promise((resolve) => setTimeout(resolve, 10000)); +// // Wait a bit for webhook processing +// await new Promise((resolve) => setTimeout(resolve, 10000)); - // Now both customers should have the reward applied - const redemptionResult = await autumn.redemptions.get(redemption.id); - console.log( - "Referrer Applied:", - redemptionResult.applied, - "Redeemer Applied:", - redemptionResult.redeemer_applied, - ); - assert.equal(redemptionResult.applied, true); +// // Now both customers should have the reward applied +// const redemptionResult = await autumn.redemptions.get(redemption.id); - const mainCus = await autumn.customers.get(mainCustomerId); - const redeemerCus = await autumn.customers.get(redeemer); - const mainProds = mainCus.products; - const redeemerProds = redeemerCus.products; +// assert.equal(redemptionResult.applied, true); - // Main customer (referrer) should now have the pro product - assert.equal(mainProds.length, 1); - assert.equal(mainProds[0].id, products.pro.id); +// const mainCus = await autumn.customers.get(mainCustomerId); +// const redeemerCus = await autumn.customers.get(redeemer); +// const mainProds = mainCus.products; +// const redeemerProds = redeemerCus.products; - // Redeemer should have both Premium (purchased) and the Pro price discount - assert.equal(redeemerProds.length, 1); - const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve( - redeemerProds.find((x) => x.id === products.premium.id) - ?.subscription_ids?.[0]!, - { - expand: ["discounts"], - }, - ); - console.log( - "Redeemer Stripe Discounts:", - redeemerStripeDiscounts.discounts, - ); +// // Main customer (referrer) should now have the pro product +// assert.equal(mainProds.length, 1); +// assert.equal(mainProds[0].id, products.pro.id); - const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => { - if (typeof x === "string") { - return x; - } else if (typeof x === "object") { - return x.coupon.id; - } else return null; - })!; +// // Redeemer should have both Premium (purchased) and the Pro price discount +// assert.equal(redeemerProds.length, 1); +// const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve( +// redeemerProds.find((x) => x.id === products.premium.id) +// ?.subscription_ids?.[0]!, +// { +// expand: ["discounts"], +// } +// ); - assert.equal( - redeemerStripeDiscounts.discounts.length, - 1, - `Redeemer Stripe Discounts: ${JSON.stringify(redeemerStripeDiscounts.discounts, null, 4)}`, - ); - assert.equal( - typeof parsedDiscountID === "object" - ? parsedDiscountID.coupon.id - : parsedDiscountID, - rewards.paidProductWithConfig.id, - `Parsed Discount ID: ${parsedDiscountID}`, - ); +// const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => { +// if (typeof x === "string") { +// return x; +// } else if (typeof x === "object") { +// return x.coupon.id; +// } else return null; +// })!; - assert.exists( - redeemerProds.find((x) => x.id === products.premium.id), - `Redeemer must have Premium product`, - ); +// assert.equal( +// redeemerStripeDiscounts.discounts.length, +// 1, +// `Redeemer Stripe Discounts: ${JSON.stringify(redeemerStripeDiscounts.discounts, null, 4)}` +// ); +// assert.equal( +// typeof parsedDiscountID === "object" +// ? parsedDiscountID.coupon.id +// : parsedDiscountID, +// rewards.paidProductWithConfig.id, +// `Parsed Discount ID: ${parsedDiscountID}` +// ); - expectProductV1Attached({ - customer: mainCus, - product: products.pro, - status: CusProductStatus.Active, - }); +// assert.exists( +// redeemerProds.find((x) => x.id === products.premium.id), +// `Redeemer must have Premium product` +// ); - expectProductV1Attached({ - customer: redeemerCus, - product: products.premium, - status: CusProductStatus.Active, - }); - }); +// expectProductV1Attached({ +// customer: mainCus, +// product: products.pro, +// status: CusProductStatus.Active, +// }); - it("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => { - // Advance 31 days from current time to trigger next billing cycle - await Promise.all( - testClockIds.map((x) => - advanceTestClock({ - testClockId: x, - numberOfDays: 31, - waitForSeconds: 25, - stripeCli, - }), - ), - ); +// expectProductV1Attached({ +// customer: redeemerCus, +// product: products.premium, +// status: CusProductStatus.Active, +// }); +// }); - // Test that both customers' 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], - }), - ]); +// it("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => { +// // Advance 31 days from current time to trigger next billing cycle +// await Promise.all( +// testClockIds.map((x) => +// advanceTestClock({ +// testClockId: x, +// numberOfDays: 31, +// waitForSeconds: 25, +// stripeCli, +// }) +// ) +// ); - console.log( - "Main Customer Invoices:\n", - mainCustomerWithInvoices.invoices - .map( - (x) => - `${x.product_ids.join(", ")}: ${x.total} | ${new Date(x.created_at).toLocaleDateString()}`, - ) - .join("\n"), - ); +// // Test that both customers' 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( - "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) Pro 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) Pro 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 Premium invoice (should have $10 off) +// const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) => +// x.product_ids.includes(products.premium.id) +// ); +// if (redeemerPremiumInvoice) { +// // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40) +// 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 - // Check redeemer Premium invoice (should have $10 off) - const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) => - x.product_ids.includes(products.premium.id), - ); - if (redeemerPremiumInvoice) { - // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40) - 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 +// assert.equal( +// redeemerPremiumInvoice.total, +// expectedTotal, +// `Redeemer Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${redeemerPremiumInvoice.total}` +// ); +// } - console.log("Redeemer expected total:", expectedTotal); - console.log( - "Redeemer Premium invoice total:", - redeemerPremiumInvoice.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, +// ], +// }) +// ) +// ); - assert.equal( - redeemerPremiumInvoice.total, - expectedTotal, - `Redeemer Premium invoice should be $40 (Premium $50 - Pro amount $10 discount). Got $${redeemerPremiumInvoice.total}`, - ); - } +// const expectedProducts = [ +// [ +// // Main referrer - has Pro with pro_amount discount applied +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// ], +// [ +// // Redeemer - has both Premium (purchased) and Pro (reward) with discounts +// { name: "Free", status: CusProductStatus.Expired }, +// { name: "Pro", status: CusProductStatus.Active }, +// { name: "Premium", status: CusProductStatus.Active }, +// ], +// ]; - 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, - ], - }), - ), - ); +// 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 +// ); - const expectedProducts = [ - [ - // Main referrer - has Pro with pro_amount discount applied - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - ], - [ - // Redeemer - has both Premium (purchased) and Pro (reward) with discounts - { name: "Free", status: CusProductStatus.Expired }, - { name: "Pro", status: CusProductStatus.Active }, - { name: "Premium", 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, - ); - - 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/global.ts b/server/tests/global.ts index 22596efe6..3e8ea98ff 100644 --- a/server/tests/global.ts +++ b/server/tests/global.ts @@ -3,982 +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, - }), + 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 = { - 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(); }); From e40e5a06f9585924b9fa7d775da6fb4d7d6da1ff Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 22 Sep 2025 09:53:38 +0100 Subject: [PATCH 58/60] chore: fixing reward migrations --- .../migrations/runRewardMigrationTask.ts | 424 ++++++++++-------- .../products/internalProductRouter.ts | 12 +- 2 files changed, 249 insertions(+), 187 deletions(-) diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts index a6395fbd7..d0e45e241 100644 --- a/server/src/internal/migrations/runRewardMigrationTask.ts +++ b/server/src/internal/migrations/runRewardMigrationTask.ts @@ -1,232 +1,292 @@ import { - type AppEnv, - type FixedPriceConfig, - type FullProduct, - type Price, - RewardType, - type UsagePriceConfig, + type AppEnv, + type FixedPriceConfig, + type FullProduct, + type Price, + type UsagePriceConfig, + DiscountConfig, + PriceType, + RewardType, + getBillingType, + isFixedPrice, + isUsagePrice, } from "@autumn/shared"; -import { - isFixedPrice, - isUsagePrice, -} from "@shared/utils/productUtils/priceUtils.js"; + import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { logger as loggerType } from "@/external/logtail/logtailUtils.js"; import type { JobName } from "@/queue/JobName.js"; import type { Payloads } from "@/queue/queueUtils.js"; import { RewardService } from "../rewards/RewardService.js"; +import { tiersAreSame } from "../products/prices/priceInitUtils.js"; +import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCouponUtils.js"; +import { PriceService } from "../products/prices/PriceService.js"; +import { OrgService } from "../orgs/OrgService.js"; +import { formatPrice } from "../products/prices/priceUtils.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; - return ( - candidates.find((candidate) => { - const newConfig = candidate.config as FixedPriceConfig; - return newConfig.amount === oldConfig.amount; - }) || null - ); + const possibleCandidate = candidates.find((candidate) => { + const newConfig = candidate.config as FixedPriceConfig; + return newConfig.amount === oldConfig.amount; + }); + + 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.feature_id !== oldConfig.feature_id) return false; - 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 - if (newConfig.bill_when !== oldConfig.bill_when) return false; - if (newConfig.should_prorate !== oldConfig.should_prorate) 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)) - 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 - const candidates = newPrices.filter( - (newPrice) => - newPrice.config.type === oldPrice.config.type && - newPrice.config.interval === oldPrice.config.interval && - newPrice.config.interval_count === oldPrice.config.interval_count, - ); + // First, filter by basic characteristics - if (candidates.length === 0) return null; - if (candidates.length === 1) return candidates[0]; + const candidates = newPrices.filter((newPrice) => { + const oldConfig = oldPrice.config as UsagePriceConfig; + const newConfig = newPrice.config as UsagePriceConfig; - // If multiple candidates, use type-specific matching - if (isFixedPrice({ price: oldPrice })) { - return findMatchingFixedPrice(oldPrice, candidates); - } else if (isUsagePrice({ price: oldPrice })) { - return findMatchingUsagePrice(oldPrice, candidates); - } + 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) + ); + }); - // Fallback to first candidate - return candidates[0]; + console.log( + "Candidates: ", + candidates.map((p) => formatPrice({ price: p })) + ); + console.log("--------------------------------"); + + 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); + } + + // 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, - newPrices, - orgId, - env, - }: { - oldPrices: Price[]; - newPrices: Price[]; - product: FullProduct; - orgId: string; - env: AppEnv; - } = payload; + try { + const { + oldPrices, + newPrices, + orgId, + env, + }: { + oldPrices: Price[]; + newPrices: Price[]; + product: FullProduct; + orgId: string; + env: AppEnv; + } = payload; - const rewards = await RewardService.list({ - db, - orgId, - env, - inTypes: [ - RewardType.PercentageDiscount, - RewardType.FixedDiscount, - RewardType.InvoiceCredits, - ], - }); + // Get organization for Stripe operations + const org = await OrgService.get({ + db, + orgId, + }); - 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 rewards = await RewardService.list({ + db, + orgId, + env, + inTypes: [ + RewardType.PercentageDiscount, + RewardType.FixedDiscount, + RewardType.InvoiceCredits, + ], + }); - for (const reward of filteredRewards) { - const newPriceIds: string[] = []; - const unmatchedPrices: string[] = []; + 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) + ) + ); - if (reward.discount_config?.price_ids) { - for (const priceId of reward.discount_config.price_ids) { - const oldPrice = oldPrices.find((p) => p.id === priceId); - if (!oldPrice) { - logger.warn(`Old price ${priceId} not found in oldPrices array`); - continue; - } + // console.log( + // "New price IDs: ", + // newPrices.map((p) => formatPrice({ price: p })) + // ); + for (const reward of filteredRewards) { + const newPriceIds: string[] = []; + const unmatchedPrices: string[] = []; - const matchingNewPrice = findBestMatch(oldPrice, newPrices); + if (reward.discount_config?.price_ids) { + for (const priceId of reward.discount_config.price_ids) { + const oldPrice = oldPrices.find((p) => p.id === priceId); + if (!oldPrice) { + logger.warn(`Old price ${priceId} not found in oldPrices array`); + continue; + } - if (matchingNewPrice) { - newPriceIds.push(matchingNewPrice.id); - logger.info( - `Matched ${oldPrice.id} -> ${matchingNewPrice.id} (${oldPrice.config.type})`, - ); - } else { - unmatchedPrices.push(oldPrice.id); - logger.warn( - `No match found for price ${oldPrice.id} (${oldPrice.config.type})`, - ); - } - } - } + const matchingNewPrice = findBestMatch(oldPrice, newPrices); - // Update the reward with new price IDs - if (newPriceIds.length > 0) { - try { - await RewardService.update({ - db, - internalId: reward.internal_id!, - env, - orgId, - update: { - discount_config: { - discount_value: reward.discount_config!.discount_value, - duration_type: reward.discount_config!.duration_type, - duration_value: reward.discount_config!.duration_value, - should_rollover: reward.discount_config!.should_rollover, - apply_to_all: reward.discount_config!.apply_to_all, - // Note: I used to include unmatchedPrices as a safety precaution, but it seems unecessary - // price_ids: [...unmatchedPrices, ...newPriceIds], - price_ids: newPriceIds, - }, - }, - }); - logger.info( - `Updated reward "${reward.name}" (${reward.id}) with ${newPriceIds.length} prices`, - ); - } catch (error) { - logger.error(`Failed to update reward ${reward.id}:`, error); - } - } else { - logger.warn( - `Reward "${reward.name}" (${reward.id}) has no matching prices - skipping update`, - ); - } + if (matchingNewPrice) { + newPriceIds.push(matchingNewPrice.id); + } else { + unmatchedPrices.push(oldPrice.id); + } + } + } - if (unmatchedPrices.length > 0) { - logger.warn( - `Unmatched prices for reward ${reward.id}:`, - unmatchedPrices, - ); - } - } + console.log("New price IDs: ", newPriceIds); + throw new Error("test"); - // Migration summary - const totalRewards = filteredRewards.length; - const updatedRewards = filteredRewards.filter((r) => - r.discount_config?.price_ids?.some( - (priceId) => - oldPrices.find((p) => p.id === priceId) && - findBestMatch(oldPrices.find((p) => p.id === priceId)!, newPrices), - ), - ).length; + // Update the reward with new price IDs + if (newPriceIds.length > 0) { + try { + // Check if price IDs have actually changed + const originalPriceIds = reward.discount_config?.price_ids || []; + const priceIdsChanged = + originalPriceIds.length !== newPriceIds.length || + !originalPriceIds.every((id) => newPriceIds.includes(id)); - logger.info("================================"); - logger.info("REWARD MIGRATION SUMMARY FOR ORG: ", orgId); - logger.info("================================"); - logger.info(`Total rewards processed: ${totalRewards}`); - logger.info(`Rewards with successful matches: ${updatedRewards}`); - logger.info(`Rewards with no matches: ${totalRewards - updatedRewards}`); - logger.info("================================"); - } catch (error) { - logger.error("Error running reward migration task", { error }); - throw error; - } + // 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 Stripe coupon if price IDs have changed + if (priceIdsChanged && org) { + try { + logger.info( + `Price IDs changed for reward ${reward.id}, updating Stripe coupon...` + ); + + // 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, + }); + + logger.info( + `Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions` + ); + } catch (stripeError) { + logger.error( + `Failed to update Stripe coupon for reward ${reward.id}:`, + stripeError + ); + // Don't throw here - we want to continue with other rewards + } + } + logger.info( + `Updated reward "${reward.name}" (${reward.id}) with ${newPriceIds.length} prices` + ); + } catch (error) { + logger.error(`Failed to update reward ${reward.id}:`, error); + } + } + + if (unmatchedPrices.length > 0) { + logger.warn( + `Unmatched prices for reward ${reward.id}:`, + unmatchedPrices + ); + } + } + + // Migration summary + const totalRewards = filteredRewards.length; + const updatedRewards = filteredRewards.filter((r) => + r.discount_config?.price_ids?.some( + (priceId) => + oldPrices.find((p) => p.id === priceId) && + findBestMatch(oldPrices.find((p) => p.id === priceId)!, newPrices) + ) + ).length; + + logger.info("================================"); + logger.info("REWARD MIGRATION SUMMARY FOR ORG: ", orgId); + logger.info("================================"); + logger.info(`Total rewards processed: ${totalRewards}`); + logger.info(`Rewards with successful matches: ${updatedRewards}`); + logger.info(`Rewards with no matches: ${totalRewards - updatedRewards}`); + logger.info("================================"); + } catch (error) { + logger.error("Error running reward migration task", { error }); + throw error; + } } diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index f326aa66a..32af65272 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -14,13 +14,13 @@ import { RewardProgramService } from "../rewards/RewardProgramService.js"; import { mapToProductV2 } from "./productV2Utils.js"; import { isFeaturePriceItem } from "./product-items/productItemUtils/getItemType.js"; -import RecaseError, { - handleFrontendReqError, - handleRequestError, -} from "@/utils/errorUtils.js"; +import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; import { createOrgResponse } from "../orgs/orgUtils.js"; -import { sortFullProducts } from "./productUtils/sortProductUtils.js"; +import { + sortFullProducts, + sortProductsByPrice, +} from "./productUtils/sortProductUtils.js"; import { handleGetProductDeleteInfo } from "./handlers/handleGetProductDeleteInfo.js"; export const productRouter: Router = Router({ mergeParams: true }); @@ -35,6 +35,8 @@ productRouter.get("/products", async (req: any, res) => { env: req.env, }); + sortFullProducts({ products }); + const groupToDefaults = getGroupToDefaults({ defaultProds: products, }); From 4fd25cc1611ebff07d8befec98f3dbaca6639f28 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 22 Sep 2025 12:43:06 +0100 Subject: [PATCH 59/60] fix: ent rollover null / undefined difference --- .../createStripePrice/createStripePrepaid.ts | 1 + .../migrations/runRewardMigrationTask.ts | 130 +++---- .../products/entitlements/entitlementUtils.ts | 44 ++- .../handleUpdateProduct.ts | 355 +++++++++--------- .../productItemUtils/handleNewProductItems.ts | 1 + .../productItemUtils/itemToPriceAndEnt.ts | 4 +- server/src/queue/queueUtils.ts | 57 +-- 7 files changed, 295 insertions(+), 297 deletions(-) diff --git a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts index 83ad3ee51..4736ca35b 100644 --- a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts +++ b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts @@ -13,6 +13,7 @@ import { SupabaseClient } from "@supabase/supabase-js"; import Stripe from "stripe"; import { billingIntervalToStripe } from "../stripePriceUtils.js"; import { + formatPrice, getBillingType, getPriceEntitlement, } from "@/internal/products/prices/priceUtils.js"; diff --git a/server/src/internal/migrations/runRewardMigrationTask.ts b/server/src/internal/migrations/runRewardMigrationTask.ts index d0e45e241..d57c8fed5 100644 --- a/server/src/internal/migrations/runRewardMigrationTask.ts +++ b/server/src/internal/migrations/runRewardMigrationTask.ts @@ -22,6 +22,7 @@ import { createStripeCoupon } from "@/external/stripe/stripeCouponUtils/stripeCo import { PriceService } from "../products/prices/PriceService.js"; import { OrgService } from "../orgs/OrgService.js"; import { formatPrice } from "../products/prices/priceUtils.js"; +import { ProductService } from "../products/ProductService.js"; // Helper function to check if tier structures match const tiersMatch = (oldTiers: any[], newTiers: any[]): boolean => { @@ -83,6 +84,8 @@ const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => { // First, filter by basic characteristics const candidates = newPrices.filter((newPrice) => { + if (newPrice.id === oldPrice.id) return true; + const oldConfig = oldPrice.config as UsagePriceConfig; const newConfig = newPrice.config as UsagePriceConfig; @@ -96,12 +99,6 @@ const findBestMatch = (oldPrice: Price, newPrices: Price[]): Price | null => { ); }); - console.log( - "Candidates: ", - candidates.map((p) => formatPrice({ price: p })) - ); - console.log("--------------------------------"); - if (candidates.length === 0) return null; if (candidates.length === 1) return candidates[0]; @@ -128,17 +125,27 @@ export async function runRewardMigrationTask({ try { const { oldPrices, - newPrices, + productId, + // newPrices, orgId, env, }: { oldPrices: Price[]; - newPrices: Price[]; - product: FullProduct; + // newPrices: Price[]; + productId: string; orgId: string; env: AppEnv; } = payload; + const fullProduct = await ProductService.getFull({ + db, + idOrInternalId: productId, + orgId, + env, + }); + + const newPrices = fullProduct.prices; + // Get organization for Stripe operations const org = await OrgService.get({ db, @@ -167,10 +174,8 @@ export async function runRewardMigrationTask({ ) ); - // console.log( - // "New price IDs: ", - // newPrices.map((p) => formatPrice({ price: p })) - // ); + let shouldUpdateReward = false; + for (const reward of filteredRewards) { const newPriceIds: string[] = []; const unmatchedPrices: string[] = []; @@ -178,8 +183,10 @@ export async function runRewardMigrationTask({ 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) { - logger.warn(`Old price ${priceId} not found in oldPrices array`); + newPriceIds.push(priceId); continue; } @@ -187,23 +194,28 @@ export async function runRewardMigrationTask({ 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); } } } - console.log("New price IDs: ", newPriceIds); - throw new Error("test"); - // Update the reward with new price IDs - if (newPriceIds.length > 0) { + if (shouldUpdateReward) { try { - // Check if price IDs have actually changed - const originalPriceIds = reward.discount_config?.price_ids || []; - const priceIdsChanged = - originalPriceIds.length !== newPriceIds.length || - !originalPriceIds.every((id) => newPriceIds.includes(id)); + // 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({ @@ -219,74 +231,38 @@ export async function runRewardMigrationTask({ }, }); - // Update Stripe coupon if price IDs have changed - if (priceIdsChanged && org) { - try { - logger.info( - `Price IDs changed for reward ${reward.id}, updating Stripe coupon...` - ); + // 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, - }); - - logger.info( - `Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions` - ); - } catch (stripeError) { - logger.error( - `Failed to update Stripe coupon for reward ${reward.id}:`, - stripeError - ); - // Don't throw here - we want to continue with other rewards - } - } - logger.info( - `Updated reward "${reward.name}" (${reward.id}) with ${newPriceIds.length} prices` + console.log( + `Successfully updated Stripe coupon for reward ${reward.id} with new product restrictions` ); } catch (error) { - logger.error(`Failed to update reward ${reward.id}:`, error); + console.error(`Failed to update reward ${reward.id}:`, error); } } if (unmatchedPrices.length > 0) { - logger.warn( + console.warn( `Unmatched prices for reward ${reward.id}:`, unmatchedPrices ); } } - - // Migration summary - const totalRewards = filteredRewards.length; - const updatedRewards = filteredRewards.filter((r) => - r.discount_config?.price_ids?.some( - (priceId) => - oldPrices.find((p) => p.id === priceId) && - findBestMatch(oldPrices.find((p) => p.id === priceId)!, newPrices) - ) - ).length; - - logger.info("================================"); - logger.info("REWARD MIGRATION SUMMARY FOR ORG: ", orgId); - logger.info("================================"); - logger.info(`Total rewards processed: ${totalRewards}`); - logger.info(`Rewards with successful matches: ${updatedRewards}`); - logger.info(`Rewards with no matches: ${totalRewards - updatedRewards}`); - logger.info("================================"); } catch (error) { - logger.error("Error running reward migration task", { error }); + console.error("Error running reward migration task", { error }); throw error; } } diff --git a/server/src/internal/products/entitlements/entitlementUtils.ts b/server/src/internal/products/entitlements/entitlementUtils.ts index f621b7974..30e96d07f 100644 --- a/server/src/internal/products/entitlements/entitlementUtils.ts +++ b/server/src/internal/products/entitlements/entitlementUtils.ts @@ -14,6 +14,8 @@ import { Price, FullProduct, FullEntitlement, + Rollover, + RolloverConfig, } from "@autumn/shared"; import { addDays } from "date-fns"; @@ -76,6 +78,24 @@ export const addTrialToNextResetAt = ( return addDays(new Date(nextResetAt), freeTrial.length).getTime(); }; +export const rolloversAreSame = ({ + rollover1, + rollover2, +}: { + rollover1?: RolloverConfig | null; + rollover2?: RolloverConfig | null; +}) => { + 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 + ); +}; + 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) { @@ -121,23 +141,25 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => { message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`, }, rollover: { - condition: - JSON.stringify(ent1.rollover) !== JSON.stringify(ent2.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); - // if (entsAreDiff) { - // console.log("Entitlements different"); - // console.log( - // "Differences:", - // Object.values(diffs) - // .filter((d) => d.condition) - // .map((d) => d.message), - // ); - // } + if (entsAreDiff) { + console.log("Entitlements different"); + console.log( + "Differences:", + Object.values(diffs) + .filter((d) => d.condition) + .map((d) => d.message) + ); + } return !entsAreDiff; }; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index 233a2024e..d75744f94 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -22,202 +22,199 @@ import { } 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, + }); + } - const { prices, entitlements } = 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, + }); - if (free_trial !== undefined) { - await validateOneOffTrial({ - prices, - freeTrial: free_trial, - }); + // New full product + const newFullProduct = await ProductService.getFull({ + db, + idOrInternalId: fullProduct.id, + orgId, + env, + }); - await handleNewFreeTrial({ - db, - curFreeTrial: fullProduct.free_trial, - newFreeTrial: free_trial, - internalProductId: fullProduct.internal_id, - isCustom: false, - product: fullProduct, - }); - } + if (free_trial !== undefined) { + await validateOneOffTrial({ + prices: newFullProduct.prices, + freeTrial: free_trial, + }); - await initProductInStripe({ - db, - product: { - ...fullProduct, - prices, - entitlements, - } as FullProduct, - org, - env, - logger, - }); + await handleNewFreeTrial({ + db, + curFreeTrial: fullProduct.free_trial, + newFreeTrial: free_trial, + internalProductId: fullProduct.internal_id, + isCustom: false, + product: fullProduct, + }); + } - logger.info("Adding task to queue to detect base variant"); - await addTaskToQueue({ - jobName: JobName.DetectBaseVariant, - payload: { - curProduct: { - ...fullProduct, - prices: prices.length > 0 ? prices : fullProduct.prices, - entitlements, - }, - }, - }); + // New full product + await initProductInStripe({ + db, + product: newFullProduct, + org, + env, + logger, + }); - await addTaskToQueue({ - jobName: JobName.RewardMigration, - payload: { - oldPrices: fullProduct.prices, - newPrices: prices, - product: { - ...fullProduct, - prices, - entitlements: getEntsWithFeature({ ents: entitlements, features }), - }, - orgId: org.id, - env, - }, - }); - res.status(200).send({ message: "Product updated" }); - return; - }, - }); + 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; + }, + }); diff --git a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts index 271829697..8d3ca4e7b 100644 --- a/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts +++ b/server/src/internal/products/product-items/productItemUtils/handleNewProductItems.ts @@ -13,6 +13,7 @@ import { validateProductItems } from "../validateProductItems.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { isFeatureItem } from "./getItemType.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { formatPrice } from "../../prices/priceUtils.js"; const updateDbPricesAndEnts = async ({ db, diff --git a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts index 733f0fe09..84cbc93f9 100644 --- a/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts +++ b/server/src/internal/products/product-items/productItemUtils/itemToPriceAndEnt.ts @@ -1,3 +1,4 @@ +import RecaseError from "@/utils/errorUtils.js"; import { AllowanceType, BillingInterval, @@ -19,13 +20,12 @@ import { OnIncrease, OnDecrease, FeatureUsageType, - features, } from "@autumn/shared"; import { generateId, notNullish, nullish } from "@/utils/genUtils.js"; import { pricesAreSame } from "@/internal/products/prices/priceInitUtils.js"; import { entsAreSame } from "../../entitlements/entitlementUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; + import { isFeatureItem, isFeaturePriceItem, diff --git a/server/src/queue/queueUtils.ts b/server/src/queue/queueUtils.ts index 62aeb7073..0438010a2 100644 --- a/server/src/queue/queueUtils.ts +++ b/server/src/queue/queueUtils.ts @@ -4,40 +4,41 @@ import { JobName } from "./JobName.js"; import { QueueManager } from "./QueueManager.js"; export interface Payloads { - [JobName.RewardMigration]: { - oldPrices: Price[]; - newPrices: Price[]; - product: FullProduct; + [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, + }, + }); + } + } }; From 79cf0f5839e32b811b0848a97e62bd3a9405e790 Mon Sep 17 00:00:00 2001 From: John Yeo Date: Mon, 22 Sep 2025 13:06:09 +0100 Subject: [PATCH 60/60] fix: update feature modal scrolls --- vite/src/views/products/features/components/UpdateFeature.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite/src/views/products/features/components/UpdateFeature.tsx b/vite/src/views/products/features/components/UpdateFeature.tsx index 1b8c9646f..3f8812beb 100644 --- a/vite/src/views/products/features/components/UpdateFeature.tsx +++ b/vite/src/views/products/features/components/UpdateFeature.tsx @@ -85,7 +85,7 @@ export default function UpdateFeature({ return ( - + Update Feature